diff --git a/.github/workflows/hr-workspace-performance-goal-review-state.yml b/.github/workflows/hr-workspace-performance-goal-review-state.yml
new file mode 100644
index 000000000..2d96fe7be
--- /dev/null
+++ b/.github/workflows/hr-workspace-performance-goal-review-state.yml
@@ -0,0 +1,55 @@
+name: HR Workspace Performance Goal Review State Quality
+
+on:
+ pull_request:
+ branches:
+ - develop
+ - feat/hr-workspace-protected-read-state
+ paths:
+ - "apps/hr-workspace/performance-goal-review-state.js"
+ - "apps/hr-workspace/performance-goal-review-state.css"
+ - "apps/hr-workspace/performance-goal-review-state.stories.js"
+ - "tests/hr-workspace-performance-goal-review-state.test.mjs"
+ - "docs/traceability/hr-workspace-performance-goal-review-state.md"
+ - "docs/doctoring/hr-workspace-performance-goal-accessibility-references.md"
+ - ".github/workflows/hr-workspace-performance-goal-review-state.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: hr-workspace-performance-goal-review-state-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ performance-goal-review-state:
+ name: Performance goal 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 performance-goal 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-performance-goal-review-state.test.mjs
+ - name: Require clean checkout
+ run: |
+ git diff --exit-code
+ test -z "$(git status --porcelain)"
diff --git a/apps/hr-workspace/performance-goal-review-state.css b/apps/hr-workspace/performance-goal-review-state.css
new file mode 100644
index 000000000..4e9d719f2
--- /dev/null
+++ b/apps/hr-workspace/performance-goal-review-state.css
@@ -0,0 +1,62 @@
+.performance-goal-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);
+}
+
+.performance-goal-review-status,
+.performance-goal-review-next-action {
+ display: grid;
+ gap: var(--orgmetra-space-xs);
+ margin: 0;
+}
+
+.performance-goal-review-status span,
+.performance-goal-review-next-action span {
+ color: var(--orgmetra-text-muted);
+}
+
+.performance-goal-review-state[data-interaction-state="high-risk-confirmation"] {
+ border-color: var(--orgmetra-action-review);
+}
+
+.performance-goal-review-state[data-interaction-state="read-only"] {
+ border-color: var(--orgmetra-border-subtle);
+}
+
+.performance-goal-review-state[data-interaction-state="permission-denied"],
+.performance-goal-review-state[data-interaction-state="validation-error"],
+.performance-goal-review-state[data-interaction-state="error"] {
+ border-color: var(--orgmetra-danger);
+}
+
+.performance-goal-review-state[aria-busy="true"] {
+ cursor: progress;
+}
+
+.performance-goal-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;
+}
+
+.performance-goal-review-submit:disabled {
+ cursor: not-allowed;
+ opacity: 0.62;
+}
+
+.performance-goal-review-submit:focus-visible {
+ outline: 3px solid var(--orgmetra-focus-ring);
+ outline-offset: 3px;
+}
diff --git a/apps/hr-workspace/performance-goal-review-state.js b/apps/hr-workspace/performance-goal-review-state.js
new file mode 100644
index 000000000..21ab04992
--- /dev/null
+++ b/apps/hr-workspace/performance-goal-review-state.js
@@ -0,0 +1,86 @@
+const STATE_MODELS = Object.freeze({
+ idle: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
+ interactionState: 'default', actionLabel: 'Load goal-plan evidence',
+ label: 'Review performance-goal plan evidence',
+ message: 'Load fresh governed performance-goal plan evidence before asking a human reviewer to confirm the plan.',
+ nextAction: 'Load the current governed goal-plan evidence for this authorized HR task.',
+ }),
+ loading: Object.freeze({
+ ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
+ interactionState: 'loading', actionLabel: 'Loading goal-plan evidence',
+ label: 'Loading current goal-plan evidence',
+ message: 'Orgmetra is resolving fresh Employment, Job, performance-cycle, goal-set, measurement, cadence, actor, and chronology evidence.',
+ nextAction: 'Wait for the current governed goal-plan evidence load to finish.',
+ }),
+ review: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
+ interactionState: 'high-risk-confirmation', actionLabel: 'Record human review',
+ label: 'Human review required before goal-plan activation',
+ message: 'A human reviewer may confirm the governed plan evidence. This review does not activate the plan and does not authorize a performance rating, compensation action, or employment decision.',
+ nextAction: 'Confirm only the reviewed evidence; activation remains a separately governed operation.',
+ }),
+ recording: Object.freeze({
+ ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
+ interactionState: 'loading', actionLabel: 'Recording review evidence',
+ label: 'Recording goal-plan review evidence',
+ message: 'Orgmetra is recording the human-review evidence. Duplicate confirmation is disabled while this operation is in flight.',
+ nextAction: 'Wait for the immutable review evidence to finish recording before continuing.',
+ }),
+ recorded: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: true,
+ interactionState: 'read-only', actionLabel: 'Review evidence recorded',
+ label: 'Goal-plan review evidence recorded',
+ message: 'This is read-only review evidence. Recording it does not activate the plan and does not authorize performance rating, compensation, or an employment decision.',
+ nextAction: 'If activation is required, continue through the separately governed activation boundary using fresh authoritative evidence.',
+ }),
+ denied: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'permission-denied', actionLabel: 'Review access',
+ label: 'Goal-plan review access denied',
+ message: 'The current purpose or actor authority does not permit this performance-goal review task.',
+ nextAction: 'Check the purpose and access authority before loading or reviewing goal-plan evidence again.',
+ }),
+ stale: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'validation-error', actionLabel: 'Reload evidence',
+ label: 'Goal-plan evidence is stale',
+ message: 'The authoritative goal-plan scope changed before the review evidence could be relied on.',
+ nextAction: 'Reload authoritative Employment, Job, performance-cycle, goal-set, measurement, cadence, actor, and chronology evidence before reviewing again.',
+ }),
+ activationBlocked: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'validation-error', actionLabel: 'Review activation scope',
+ label: 'Goal-plan activation is blocked',
+ message: 'The recorded review cannot proceed to activation while authoritative scope or chronology is inconsistent.',
+ nextAction: 'Resolve Employment, Job, performance cycle, goal-set, measurement, cadence, actor, and chronology evidence through the governed owners before requesting activation.',
+ }),
+ error: Object.freeze({
+ ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
+ interactionState: 'error', actionLabel: 'Retry governed load',
+ label: 'Performance-goal review unavailable',
+ message: 'The governed performance-goal review service did not return usable authoritative evidence.',
+ nextAction: 'Do not activate or infer a performance outcome from cached or partial evidence; verify the service and authorization before retrying.',
+ }),
+});
+
+function requireExactState(value) {
+ if (typeof value !== 'string') {
+ throw new TypeError('performance-goal review state must be an exact built-in string');
+ }
+ const model = Object.hasOwn(STATE_MODELS, value) ? STATE_MODELS[value] : undefined;
+ if (!model) throw new TypeError(`unsupported performance-goal review state: ${value}`);
+ return model;
+}
+
+/** Return immutable accessibility semantics for one governed performance-goal review state. */
+export function performanceGoalReviewViewModel(state) {
+ return requireExactState(state);
+}
+
+/** Render static Storybook evidence without accepting caller-controlled HR values or identifiers. */
+export function performanceGoalReviewStateMarkup(state) {
+ const model = requireExactState(state);
+ const disabled = model.submitDisabled ? ' disabled' : '';
+ return ``;
+}
diff --git a/apps/hr-workspace/performance-goal-review-state.stories.js b/apps/hr-workspace/performance-goal-review-state.stories.js
new file mode 100644
index 000000000..8ac2a5bb9
--- /dev/null
+++ b/apps/hr-workspace/performance-goal-review-state.stories.js
@@ -0,0 +1,26 @@
+import { performanceGoalReviewStateMarkup } from './performance-goal-review-state.js';
+import './performance-goal-review-state.css';
+
+export default {
+ title: 'HR Workspace/Performance Goal Review States',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'Orgmetra Baseline — Storybook Inventory node 1:64',
+ },
+ },
+};
+
+function story(state) {
+ return () => performanceGoalReviewStateMarkup(state);
+}
+
+export const Idle = story('idle');
+export const Loading = story('loading');
+export const HumanReview = story('review');
+export const Recording = story('recording');
+export const RecordedReadOnly = story('recorded');
+export const PermissionDenied = story('denied');
+export const StaleEvidence = story('stale');
+export const ActivationBlocked = story('activationBlocked');
+export const Error = story('error');
diff --git a/docs/doctoring/hr-workspace-performance-goal-accessibility-references.md b/docs/doctoring/hr-workspace-performance-goal-accessibility-references.md
new file mode 100644
index 000000000..5c3ee79fd
--- /dev/null
+++ b/docs/doctoring/hr-workspace-performance-goal-accessibility-references.md
@@ -0,0 +1,17 @@
+# HR Workspace performance-goal review accessibility references
+
+Status: active-PR evidence for the dependency-first HR Workspace performance-goal review interaction. This document does not claim protected-main integration or release authorization.
+
+## Primary standards
+
+World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/
+
+World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/
+
+## Applied contract
+
+- In-flight load/record operations expose `aria-busy="true"` and disable duplicate action submission.
+- Denied, stale, blocked, and error states use assertive alert semantics and always explain the next safe action.
+- Interactive controls preserve the existing Orgmetra `:focus-visible` token and a minimum 44px target height.
+- Human review is visually and semantically separated from authoritative activation. A recorded review is read-only evidence and cannot be interpreted as a performance rating, compensation action, or employment decision.
+- Storybook evidence remains correlated to Figma `Orgmetra Baseline`, Storybook Inventory node `1:64`, whose required states are default / hover / focus / disabled / loading / validation-error / read-only / high-risk-confirmation.
diff --git a/docs/traceability/hr-workspace-performance-goal-review-state.md b/docs/traceability/hr-workspace-performance-goal-review-state.md
new file mode 100644
index 000000000..4ba91bd28
--- /dev/null
+++ b/docs/traceability/hr-workspace-performance-goal-review-state.md
@@ -0,0 +1,19 @@
+# HR Workspace performance-goal review state traceability
+
+Status: active stacked PR only; not protected-main truth and not release authorization.
+
+| Need / risk | Owner boundary | Executable evidence |
+| --- | --- | --- |
+| Shared protected-read interaction semantics | #130 | Existing HR Workspace design tokens, focus treatment, loading/read-only/error patterns |
+| Human-reviewed goal-plan governance evidence | #92 | Separate backend authority; this UI does not import or duplicate the unmerged implementation |
+| Authoritative goal-plan activation | #121 | Separate backend authority; this UI never activates a plan |
+| Durable goal-plan persistence | #125 | Separate persistence boundary; this UI stores no HR truth |
+| Figma / Storybook interaction-state inventory | Figma `Orgmetra Baseline`, node `1:64` | Storybook stories and `data-figma-node-id="1:64"` correlation |
+| Prevent review → activation/rating/compensation/employment-decision confusion | This PR | `review` and `recorded` state regressions explicitly deny those authorities |
+| Prevent stale or inconsistent scope from becoming consequential action | This PR | `stale` and `activationBlocked` states require fresh authoritative evidence and governed owner resolution |
+| Minimize UI-state evidence | This PR | View-model key allowlist excludes identifiers, goal text/value, ratings, compensation, assessments, credentials, prompts, and model output |
+| Accessibility and actionable failure recovery | This PR | WCAG 2.2 / WAI-ARIA 1.2 doctoring, Storybook state inventory, exact 100% line/branch/function focused gate |
+
+## Dependency-first integration
+
+Base this slice on #130. Process #53 → #130 first. After the parent actually integrates, retarget/revalidate this child against fresh `develop`, reconcile intervening HR Workspace and performance-goal changes, and rerun every applicable browser/accessibility/Foundation/Recovery/SAST/Security and central required workflow on one exact resulting head. Parent/backend checks and reviews never transfer.
diff --git a/tests/hr-workspace-performance-goal-review-state.test.mjs b/tests/hr-workspace-performance-goal-review-state.test.mjs
new file mode 100644
index 000000000..c12ca3cdf
--- /dev/null
+++ b/tests/hr-workspace-performance-goal-review-state.test.mjs
@@ -0,0 +1,132 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+import {
+ performanceGoalReviewStateMarkup,
+ performanceGoalReviewViewModel,
+} from '../apps/hr-workspace/performance-goal-review-state.js';
+
+const story = readFileSync(
+ new URL('../apps/hr-workspace/performance-goal-review-state.stories.js', import.meta.url),
+ 'utf8',
+);
+const css = readFileSync(
+ new URL('../apps/hr-workspace/performance-goal-review-state.css', import.meta.url),
+ 'utf8',
+);
+const workflow = readFileSync(
+ new URL('../.github/workflows/hr-workspace-performance-goal-review-state.yml', import.meta.url),
+ 'utf8',
+);
+
+const expectedStates = {
+ idle: ['false', 'status', false, 'default', 'Review performance-goal plan evidence'],
+ loading: ['true', 'status', true, 'loading', 'Loading current goal-plan evidence'],
+ review: ['false', 'status', false, 'high-risk-confirmation', 'Human review required before goal-plan activation'],
+ recording: ['true', 'status', true, 'loading', 'Recording goal-plan review evidence'],
+ recorded: ['false', 'status', true, 'read-only', 'Goal-plan review evidence recorded'],
+ denied: ['false', 'alert', false, 'permission-denied', 'Goal-plan review access denied'],
+ stale: ['false', 'alert', false, 'validation-error', 'Goal-plan evidence is stale'],
+ activationBlocked: ['false', 'alert', false, 'validation-error', 'Goal-plan activation is blocked'],
+ error: ['false', 'alert', false, 'error', 'Performance-goal review unavailable'],
+};
+
+const allowedViewModelKeys = [
+ 'actionLabel',
+ 'ariaBusy',
+ 'ariaLive',
+ 'interactionState',
+ 'label',
+ 'message',
+ 'nextAction',
+ 'role',
+ 'submitDisabled',
+];
+
+test('performance-goal review states are bounded, actionable, and privacy-minimized', () => {
+ for (const [state, [ariaBusy, role, submitDisabled, interactionState, label]] of Object.entries(expectedStates)) {
+ const model = performanceGoalReviewViewModel(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', 'employmentRecordId', 'jobRecordId', 'performanceCycleId',
+ 'goalText', 'goalValue', 'ratingValue', 'assessmentScore', 'compensationValue',
+ 'candidateRecordId', 'workerName', 'email', 'phone', 'credential', 'token',
+ 'prompt', 'modelOutput',
+ ]) {
+ assert.equal(Object.hasOwn(model, forbiddenKey), false);
+ }
+
+ const markup = performanceGoalReviewStateMarkup(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, /