\n ${model.label}${model.message}
\n Next action${model.nextAction}
\n \n`;
+}
diff --git a/apps/hr-workspace/position-reporting-review-state.stories.js b/apps/hr-workspace/position-reporting-review-state.stories.js
new file mode 100644
index 000000000..6a9dde46e
--- /dev/null
+++ b/apps/hr-workspace/position-reporting-review-state.stories.js
@@ -0,0 +1,26 @@
+import { positionReportingReviewStateMarkup } from './position-reporting-review-state.js';
+import './position-reporting-review-state.css';
+
+export default {
+ title: 'HR Workspace/Position Reporting Review States',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'Orgmetra Baseline — Storybook Inventory node 1:64',
+ },
+ },
+};
+
+function story(state) {
+ return () => positionReportingReviewStateMarkup(state);
+}
+
+export const Idle = story('idle');
+export const Loading = story('loading');
+export const HighRiskHumanReview = story('review');
+export const Recording = story('recording');
+export const RecordedReadOnly = story('recorded');
+export const PermissionDenied = story('denied');
+export const StaleEvidence = story('stale');
+export const HierarchyIntegrityBlocked = story('blocked');
+export const Error = story('error');
diff --git a/docs/doctoring/hr-workspace-position-reporting-review-accessibility-references.md b/docs/doctoring/hr-workspace-position-reporting-review-accessibility-references.md
new file mode 100644
index 000000000..c7111af7e
--- /dev/null
+++ b/docs/doctoring/hr-workspace-position-reporting-review-accessibility-references.md
@@ -0,0 +1,21 @@
+# HR Workspace Position reporting review accessibility references
+
+Status: **active PR research/doctoring**. These references support the interaction semantics; they are not a claim of product certification or protected-`develop` conformance.
+
+## Applied decisions
+
+- Use native button semantics and expose busy/live state explicitly so assistive technology can receive state changes without turning the review surface into a custom application widget.
+- Keep the proposed reporting-line change behind an explicit high-risk human-confirmation state. UI confirmation records review evidence only; it does not mutate Position reporting truth or authorize an employment decision.
+- Preserve a visible keyboard focus indicator and a minimum 44 CSS-pixel action height in the workflow-specific proof.
+- Denial, stale-evidence, hierarchy-integrity, and error states use assertive alert semantics and always provide a concrete next action.
+- Loading and recording disable duplicate submission while preserving a polite status announcement.
+
+## Primary final standards
+
+World Wide Web Consortium. (2023, October 5). *Web Content Accessibility Guidelines (WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/
+
+World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2* (W3C Recommendation). https://www.w3.org/TR/wai-aria-1.2/
+
+## Current-status note
+
+WCAG 2.2 remains the completed W3C Recommendation used here and was approved as ISO/IEC 40500:2025; this slice does not claim ISO, WCAG, or accessibility certification. WAI-ARIA 1.2 remains the completed Recommendation used for roles, states, and properties. Later draft work is not treated as the normative production baseline for this slice.
diff --git a/docs/traceability/hr-workspace-position-reporting-review-state.md b/docs/traceability/hr-workspace-position-reporting-review-state.md
new file mode 100644
index 000000000..dd7d8aaca
--- /dev/null
+++ b/docs/traceability/hr-workspace-position-reporting-review-state.md
@@ -0,0 +1,47 @@
+# HR Workspace Position reporting review state traceability
+
+Status: **active PR only**. This document does not claim protected-`develop` availability.
+
+## Ownership and dependency boundary
+
+- Shared protected-read accessibility owner: PR #130, `feat/hr-workspace-protected-read-state`.
+- Authoritative Position reporting snapshot owner: PR #94.
+- Reporting-line human review evidence owner: PR #95. Its review evidence does not apply a reporting mutation or authorize an employment decision.
+- Durable reporting persistence owner: PR #106, dependency-first under #94. Persistence must independently enforce hierarchy integrity, tenant isolation, staffable Position coverage, audit/outbox evidence, and bitemporal truth.
+- Descriptive Position span-of-control evidence owner: PR #133. It is structural workforce evidence, not a target span or employment-decision rule.
+- This PR owns only the HR Workspace presentation/interaction state contract. It does not duplicate #94/#95/#106/#133 source, persistence, authorization, or decision logic.
+- Figma design authority: `Orgmetra Baseline`, Storybook Inventory node `1:64`; required interaction states include default, focus, disabled, loading, validation-error, read-only, and high-risk-confirmation.
+
+## Buyer-visible contract
+
+The UI must make the next safe action explicit while keeping reporting-line review human-confirmed and non-authorizing:
+
+| State | Buyer-visible meaning | Safe next action |
+| --- | --- | --- |
+| `idle` | No governed reporting evidence loaded | Load current Position/reporting evidence |
+| `loading` | Fresh reporting evidence is being resolved | Wait; duplicate action disabled |
+| `review` | High-risk human confirmation is required | Confirm subordinate/manager/hierarchy/staffable evidence |
+| `recording` | Immutable review evidence is being recorded | Wait; duplicate submission disabled |
+| `recorded` | Review evidence exists; reporting mutation has not occurred | Continue only through the authoritative reporting-line boundary |
+| `denied` | Purpose/reviewer authority is insufficient | Review access purpose and reviewer authority |
+| `stale` | Position/reporting evidence changed | Reload authoritative evidence |
+| `blocked` | Cycle, duplicate-manager, self-report, or staffable-Position integrity is invalid | Resolve authoritative hierarchy integrity |
+| `error` | Governed reporting/review service is unusable | Verify service and authorization; do not rely on cached evidence |
+
+`review` and `recorded` must never imply that a reporting-line change has been applied. They do not authorize an employment decision.
+
+## Privacy and data minimization
+
+State evidence contains only bounded interaction semantics. It must not contain Person, Employment, or Assignment identifiers, worker names/contact information, compensation, ratings, assessment values, credentials/tokens, prompts, or model output.
+
+## Verification
+
+The dedicated `HR Workspace Position Reporting Review State Quality` workflow:
+
+1. checks out the exact PR head;
+2. runs Node.js 24 tests with 100% line, branch, and function coverage thresholds;
+3. validates Figma correlation and Storybook state inventory;
+4. verifies fail-closed runtime state handling and non-authorizing high-risk review semantics; and
+5. requires a clean checkout.
+
+Focused child evidence is stack-local. After #130 integrates, this child must be retargeted to fresh `develop` and all applicable browser/accessibility/Foundation/Recovery/SAST/Security/central controls must execute again. Parent checks and reviews do not transfer.
diff --git a/tests/hr-workspace-position-reporting-review-state.test.mjs b/tests/hr-workspace-position-reporting-review-state.test.mjs
new file mode 100644
index 000000000..eda9ae8dd
--- /dev/null
+++ b/tests/hr-workspace-position-reporting-review-state.test.mjs
@@ -0,0 +1,146 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+import {
+ positionReportingReviewStateMarkup,
+ positionReportingReviewViewModel,
+} from '../apps/hr-workspace/position-reporting-review-state.js';
+
+const story = readFileSync(
+ new URL('../apps/hr-workspace/position-reporting-review-state.stories.js', import.meta.url),
+ 'utf8',
+);
+const css = readFileSync(
+ new URL('../apps/hr-workspace/position-reporting-review-state.css', import.meta.url),
+ 'utf8',
+);
+const workflow = readFileSync(
+ new URL('../.github/workflows/hr-workspace-position-reporting-review-state.yml', import.meta.url),
+ 'utf8',
+);
+
+const expectedStates = {
+ idle: ['false', 'status', false, 'default', 'Review reporting-line evidence'],
+ loading: ['true', 'status', true, 'loading', 'Loading current reporting-line evidence'],
+ review: ['false', 'status', false, 'high-risk-confirmation', 'Reporting-line change requires human confirmation'],
+ recording: ['true', 'status', true, 'loading', 'Recording reporting-line review'],
+ recorded: ['false', 'status', true, 'read-only', 'Reporting-line review recorded'],
+ denied: ['false', 'alert', false, 'permission-denied', 'Reporting-line review access denied'],
+ stale: ['false', 'alert', false, 'validation-error', 'Reporting-line evidence is stale'],
+ blocked: ['false', 'alert', false, 'validation-error', 'Reporting-line change is blocked by hierarchy integrity'],
+ error: ['false', 'alert', false, 'error', 'Reporting-line review unavailable'],
+};
+
+const allowedViewModelKeys = [
+ 'actionLabel',
+ 'ariaBusy',
+ 'ariaLive',
+ 'interactionState',
+ 'label',
+ 'message',
+ 'nextAction',
+ 'role',
+ 'submitDisabled',
+];
+
+test('reporting-line review states are bounded, actionable, and value-minimized', () => {
+ for (const [state, [ariaBusy, role, submitDisabled, interactionState, label]] of Object.entries(expectedStates)) {
+ const model = positionReportingReviewViewModel(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',
+ 'assignmentRecordId',
+ 'workerName',
+ 'email',
+ 'phone',
+ 'compensationValue',
+ 'ratingValue',
+ 'assessmentScore',
+ 'credential',
+ 'token',
+ 'prompt',
+ 'modelOutput',
+ ]) {
+ assert.equal(Object.hasOwn(model, forbiddenKey), false);
+ }
+
+ const markup = positionReportingReviewStateMarkup(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, /