diff --git a/.github/workflows/hr-workspace-job-grade-review-state.yml b/.github/workflows/hr-workspace-job-grade-review-state.yml
new file mode 100644
index 000000000..fbbac8a25
--- /dev/null
+++ b/.github/workflows/hr-workspace-job-grade-review-state.yml
@@ -0,0 +1,55 @@
+name: HR Workspace Job Grade Review State Quality
+
+on:
+ pull_request:
+ branches:
+ - develop
+ - feat/hr-workspace-protected-read-state
+ paths:
+ - "apps/hr-workspace/job-grade-review-state.js"
+ - "apps/hr-workspace/job-grade-review-state.css"
+ - "apps/hr-workspace/job-grade-review-state.stories.js"
+ - "tests/hr-workspace-job-grade-review-state.test.mjs"
+ - "docs/traceability/hr-workspace-job-grade-review-state.md"
+ - "docs/doctoring/hr-workspace-job-grade-review-accessibility-references.md"
+ - ".github/workflows/hr-workspace-job-grade-review-state.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: hr-workspace-job-grade-review-state-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ job-grade-review-state:
+ name: Job grade review state contract
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - name: Checkout exact candidate
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ persist-credentials: false
+ - name: Prove exact candidate checkout
+ env:
+ ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA"
+ - name: Set up Node.js LTS
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: "24"
+ check-latest: false
+ - name: Run Job grade review accessibility contract with exact coverage
+ run: >-
+ node --test --experimental-test-coverage
+ --test-coverage-lines=100
+ --test-coverage-branches=100
+ --test-coverage-functions=100
+ tests/hr-workspace-job-grade-review-state.test.mjs
+ - name: Require clean checkout
+ run: |
+ git diff --exit-code
+ test -z "$(git status --porcelain)"
diff --git a/apps/hr-workspace/job-grade-review-state.css b/apps/hr-workspace/job-grade-review-state.css
new file mode 100644
index 000000000..2c65d501e
--- /dev/null
+++ b/apps/hr-workspace/job-grade-review-state.css
@@ -0,0 +1,54 @@
+.job-grade-review-state {
+ display: grid;
+ gap: var(--orgmetra-space-md);
+ max-width: 40rem;
+ padding: var(--orgmetra-space-lg);
+ border: 1px solid var(--orgmetra-border-subtle);
+ border-radius: var(--orgmetra-radius-md);
+ background: var(--orgmetra-surface-card);
+ color: var(--orgmetra-text-primary);
+}
+
+.job-grade-review-status,
+.job-grade-review-next-action {
+ display: grid;
+ gap: var(--orgmetra-space-xs);
+ margin: 0;
+}
+
+.job-grade-review-status span,
+.job-grade-review-next-action span {
+ color: var(--orgmetra-text-muted);
+}
+
+.job-grade-review-state[data-interaction-state="permission-denied"],
+.job-grade-review-state[data-interaction-state="validation-error"],
+.job-grade-review-state[data-interaction-state="error"] {
+ border-color: var(--orgmetra-danger);
+}
+
+.job-grade-review-state[aria-busy="true"] {
+ cursor: progress;
+}
+
+.job-grade-review-submit {
+ justify-self: start;
+ min-height: 44px;
+ padding: var(--orgmetra-space-sm) var(--orgmetra-space-md);
+ border: 0;
+ border-radius: var(--orgmetra-radius-sm);
+ background: var(--orgmetra-action-review);
+ color: #fff;
+ font: inherit;
+ cursor: pointer;
+}
+
+.job-grade-review-submit:disabled {
+ cursor: wait;
+ opacity: 0.62;
+}
+
+.job-grade-review-submit:focus-visible {
+ outline: 3px solid var(--orgmetra-focus-ring);
+ outline-offset: 3px;
+}
diff --git a/apps/hr-workspace/job-grade-review-state.js b/apps/hr-workspace/job-grade-review-state.js
new file mode 100644
index 000000000..121510ca6
--- /dev/null
+++ b/apps/hr-workspace/job-grade-review-state.js
@@ -0,0 +1,78 @@
+const STATE_MODELS = Object.freeze({
+ idle: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
+ interactionState: 'default', actionLabel: 'Load Job grade evidence',
+ label: 'Review Job grade evidence',
+ message: 'Load the authoritative Job, Job Analysis, and grade-design evidence before making a human review.',
+ nextAction: 'Load the current governed evidence for one Job grade review.',
+ }),
+ loading: Object.freeze({
+ ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
+ interactionState: 'loading', actionLabel: 'Loading Job grade evidence',
+ label: 'Loading governed Job grade evidence',
+ message: 'Orgmetra is waiting for authoritative Job and Job Analysis evidence. No cached grade decision is used.',
+ nextAction: 'Wait for the current evidence load to finish.',
+ }),
+ review: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
+ interactionState: 'read-only', actionLabel: 'Record human review',
+ label: 'Job grade evidence ready for human review',
+ message: 'The evidence is read-only input for human review and does not authorize compensation or an employment decision.',
+ nextAction: 'Confirm the governed grade-design evidence before recording the human review.',
+ }),
+ recording: Object.freeze({
+ ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
+ interactionState: 'loading', actionLabel: 'Recording human review',
+ label: 'Recording human Job grade review',
+ message: 'Orgmetra is recording the human review evidence. Duplicate submission is disabled.',
+ nextAction: 'Wait for immutable review evidence to be recorded.',
+ }),
+ recorded: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: true,
+ interactionState: 'read-only', actionLabel: 'Review recorded',
+ label: 'Human Job grade review recorded',
+ message: 'The recorded review is evidence only and does not authorize compensation, promotion, assignment, candidate, or employment decisions.',
+ nextAction: 'Return to the Job architecture queue or start a separately authorized workflow.',
+ }),
+ denied: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'permission-denied', actionLabel: 'Review access',
+ label: 'Job grade review access denied',
+ message: 'The current purpose or reviewer authority does not permit this Job grade review.',
+ nextAction: 'Check the access purpose and reviewer authority before trying again.',
+ }),
+ stale: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'validation-error', actionLabel: 'Reload evidence',
+ label: 'Job grade evidence is stale',
+ message: 'The Job, Job Analysis, or grade-design evidence changed before review recording.',
+ nextAction: 'Reload authoritative Job and Job Analysis evidence before reviewing again.',
+ }),
+ error: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'error', actionLabel: 'Retry governed load',
+ label: 'Job grade review unavailable',
+ message: 'The governed evidence or immutable review service did not return a usable result.',
+ nextAction: 'Do not rely on a cached Job grade review; verify the service and authorization before retrying.',
+ }),
+});
+
+function requireExactState(value) {
+ if (typeof value !== 'string') throw new TypeError('Job grade review state must be an exact built-in string');
+ if (!Object.hasOwn(STATE_MODELS, value)) {
+ throw new TypeError(`unsupported Job grade review state: ${value}`);
+ }
+ return STATE_MODELS[value];
+}
+
+/** Return immutable, value-minimized accessibility semantics for one Job grade review state. */
+export function jobGradeReviewViewModel(state) {
+ return requireExactState(state);
+}
+
+/** Render one static Storybook proof without accepting caller-controlled HR values. */
+export function jobGradeReviewStateMarkup(state) {
+ const model = requireExactState(state);
+ const disabled = model.submitDisabled ? ' disabled' : '';
+ return `\n
${model.label}${model.message}
\n
Next action${model.nextAction}
\n \n`;
+}
diff --git a/apps/hr-workspace/job-grade-review-state.stories.js b/apps/hr-workspace/job-grade-review-state.stories.js
new file mode 100644
index 000000000..72eebaac1
--- /dev/null
+++ b/apps/hr-workspace/job-grade-review-state.stories.js
@@ -0,0 +1,25 @@
+import { jobGradeReviewStateMarkup } from './job-grade-review-state.js';
+import './job-grade-review-state.css';
+
+export default {
+ title: 'HR Workspace/Job Grade Review States',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'Orgmetra Baseline — Storybook Inventory node 1:64',
+ },
+ },
+};
+
+function story(state) {
+ return () => jobGradeReviewStateMarkup(state);
+}
+
+export const Idle = story('idle');
+export const Loading = story('loading');
+export const ReadyForHumanReview = story('review');
+export const Recording = story('recording');
+export const RecordedReadOnly = story('recorded');
+export const PermissionDenied = story('denied');
+export const StaleEvidence = story('stale');
+export const Error = story('error');
diff --git a/docs/doctoring/hr-workspace-job-grade-review-accessibility-references.md b/docs/doctoring/hr-workspace-job-grade-review-accessibility-references.md
new file mode 100644
index 000000000..a4e938da4
--- /dev/null
+++ b/docs/doctoring/hr-workspace-job-grade-review-accessibility-references.md
@@ -0,0 +1,20 @@
+# HR Workspace Job grade review accessibility references
+
+Checked against current primary W3C publications on 2026-08-28. These references support interaction semantics only; they do not establish HR, compensation, or employment-decision authority.
+
+## Design implications
+
+- WAI-ARIA 1.2 defines `status` as advisory live-region information with implicit polite announcements and `alert` as assertive information that does not itself require focus. Orgmetra therefore uses `status` for ordinary progress/completion and `alert` for denial, stale evidence, and failure.
+- WAI-ARIA 1.2 defines `aria-busy` as the state indicating that an element is being modified and assistive technologies may wait to expose updates. Orgmetra sets it only while loading or recording one governed review.
+- WCAG 2.2 remains the current WCAG 2 Recommendation baseline. The UI preserves visible keyboard focus, a 44px minimum action target in the existing design-token system, actionable status copy, and no color-only state communication.
+- The current 2026 WCAG 2 working-group activity includes proposed editorial/non-normative updates; those proposals are not treated as a replacement normative specification in this PR.
+
+## APA 7 references
+
+World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2* (W3C Recommendation). https://www.w3.org/TR/wai-aria-1.2/
+
+World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/
+
+## Product boundary
+
+The Storybook evidence correlates with the existing Figma `Orgmetra Baseline` Storybook Inventory node `1:64`. It is an active-PR design/test artifact and must not be presented as integrated protected-branch runtime until its dependency stack is merged and revalidated.
diff --git a/docs/traceability/hr-workspace-job-grade-review-state.md b/docs/traceability/hr-workspace-job-grade-review-state.md
new file mode 100644
index 000000000..4559359c6
--- /dev/null
+++ b/docs/traceability/hr-workspace-job-grade-review-state.md
@@ -0,0 +1,29 @@
+# HR Workspace Job grade review state traceability
+
+## Truth boundary
+
+- Protected `develop` truth remains `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` for this stack's base history; this child is **active-PR evidence only** until its dependencies are integrated.
+- Parent UI contract: PR #130 `feat/hr-workspace-protected-read-state@c92749cf5889a39de1ba8036742f96fd3451f459` owns shared protected-read accessibility semantics and Figma/Storybook correlation.
+- Job grade governance owner: PR #101 owns `JobGradeDesignReviewPacket`; this UI does not import that unmerged package and does not claim its review is protected-branch runtime.
+- Durable Job grade persistence is separately owned by dependency-first PR #109. This UI neither writes those tables nor treats persistence evidence as integrated.
+
+## Buyer-visible requirement → evidence
+
+| Requirement | Active-PR evidence | Safety meaning |
+| --- | --- | --- |
+| Load governed Job/Job Analysis/grade-design evidence before review | `loading` → `review` state model and Storybook stories | No cached or locally invented grade truth is treated as authoritative. |
+| Human review remains distinct from compensation/employment authority | `review` and `recorded` messages explicitly deny compensation/employment-decision authority | Reviewing a Job grade design does not authorize pay, promotion, assignment, selection, or another employment action. |
+| Prevent duplicate review submission while immutable evidence is being recorded | `recording` has `aria-busy=true` and a disabled action | One in-flight review recording is visible and duplicate activation is blocked in the interaction model. |
+| Stale upstream evidence fails closed | `stale` is an assertive alert with a reload action | A changed Job/Job Analysis/grade-design context must be reloaded before another review. |
+| Denied purpose/reviewer authority fails closed | `denied` is an assertive alert | The UI does not silently downgrade to a local or cached review path. |
+| Service failure provides a safe next action | `error` is an assertive alert and explicitly rejects cached review evidence | Customer-facing copy guides recovery without fabricating governance evidence. |
+| Storybook and Figma correlation remain discoverable | Every rendered state carries `data-figma-node-id="1:64"`; Storybook exposes eight named states | Reviewers can inspect loading, read-only, permission-denied, validation-error, error, and focus-related UI evidence. |
+| Owned interaction logic has exact coverage | `.github/workflows/hr-workspace-job-grade-review-state.yml` runs Node 24 test coverage at 100% lines/branches/functions | No missing owned branch is accepted as complete evidence. |
+
+## Data minimization
+
+The state model carries only constant governed copy and interaction semantics. It does **not** accept or serialize Person/candidate identity, employee names, contact data, compensation or salary values, ratings, assessment results, credentials/tokens, prompts, or model output.
+
+## Integration discipline
+
+Keep this PR Draft while #53 and #130 remain unintegrated. After those dependencies merge, retarget this child to fresh `develop`, reconcile intervening HR Workspace changes, and rerun all applicable browser/accessibility/Foundation/SAST/Security/Recovery and central exact-head gates. No predecessor check, review, or focused stack-local GREEN result transfers across that retarget.
diff --git a/tests/hr-workspace-job-grade-review-state.test.mjs b/tests/hr-workspace-job-grade-review-state.test.mjs
new file mode 100644
index 000000000..8930cb9a2
--- /dev/null
+++ b/tests/hr-workspace-job-grade-review-state.test.mjs
@@ -0,0 +1,144 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+import {
+ jobGradeReviewStateMarkup,
+ jobGradeReviewViewModel,
+} from '../apps/hr-workspace/job-grade-review-state.js';
+
+const story = readFileSync(
+ new URL('../apps/hr-workspace/job-grade-review-state.stories.js', import.meta.url),
+ 'utf8',
+);
+const css = readFileSync(
+ new URL('../apps/hr-workspace/job-grade-review-state.css', import.meta.url),
+ 'utf8',
+);
+const workflow = readFileSync(
+ new URL('../.github/workflows/hr-workspace-job-grade-review-state.yml', import.meta.url),
+ 'utf8',
+);
+
+const expectedStates = {
+ idle: ['false', 'status', false, 'default', 'Review Job grade evidence'],
+ loading: ['true', 'status', true, 'loading', 'Loading governed Job grade evidence'],
+ review: ['false', 'status', false, 'read-only', 'Job grade evidence ready for human review'],
+ recording: ['true', 'status', true, 'loading', 'Recording human Job grade review'],
+ recorded: ['false', 'status', true, 'read-only', 'Human Job grade review recorded'],
+ denied: ['false', 'alert', false, 'permission-denied', 'Job grade review access denied'],
+ stale: ['false', 'alert', false, 'validation-error', 'Job grade evidence is stale'],
+ error: ['false', 'alert', false, 'error', 'Job grade review unavailable'],
+};
+
+const allowedViewModelKeys = [
+ 'actionLabel',
+ 'ariaBusy',
+ 'ariaLive',
+ 'interactionState',
+ 'label',
+ 'message',
+ 'nextAction',
+ 'role',
+ 'submitDisabled',
+];
+
+test('Job grade review states are bounded, actionable, and value-minimized', () => {
+ for (const [state, [ariaBusy, role, submitDisabled, interactionState, label]] of Object.entries(expectedStates)) {
+ const model = jobGradeReviewViewModel(state);
+ assert.equal(model.ariaBusy, ariaBusy);
+ assert.equal(model.role, role);
+ assert.equal(model.submitDisabled, submitDisabled);
+ assert.equal(model.interactionState, interactionState);
+ assert.equal(model.label, label);
+ assert.equal(model.ariaLive, role === 'alert' ? 'assertive' : 'polite');
+ assert.match(model.nextAction, /\.$/);
+ assert.deepEqual(Object.keys(model).sort(), allowedViewModelKeys);
+
+ for (const forbiddenKey of [
+ 'personRecordId',
+ 'candidateReference',
+ 'employeeName',
+ 'email',
+ 'phone',
+ 'compensationValue',
+ 'salaryValue',
+ 'ratingValue',
+ 'assessmentResult',
+ 'credential',
+ 'token',
+ 'prompt',
+ 'modelOutput',
+ ]) {
+ assert.equal(Object.hasOwn(model, forbiddenKey), false);
+ }
+
+ const markup = jobGradeReviewStateMarkup(state);
+ assert.match(markup, /data-figma-node-id="1:64"/);
+ assert.match(markup, new RegExp(`data-interaction-state="${interactionState}"`));
+ assert.match(markup, new RegExp(`aria-busy="${ariaBusy}"`));
+ assert.match(markup, /Next action/);
+ if (submitDisabled) assert.match(markup, /