\n ${model.label}${model.message}
\n Next action${model.nextAction}
\n \n`;
+}
diff --git a/apps/hr-workspace/qualification-rule-review-state.stories.js b/apps/hr-workspace/qualification-rule-review-state.stories.js
new file mode 100644
index 000000000..00d7e1ee1
--- /dev/null
+++ b/apps/hr-workspace/qualification-rule-review-state.stories.js
@@ -0,0 +1,26 @@
+import { qualificationRuleReviewStateMarkup } from './qualification-rule-review-state.js';
+import './qualification-rule-review-state.css';
+
+export default {
+ title: 'HR Workspace/Qualification Rule Review States',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'Orgmetra Baseline — Storybook Inventory node 1:64',
+ },
+ },
+};
+
+function story(state) {
+ return () => qualificationRuleReviewStateMarkup(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 EvidenceScopeBlocked = story('blocked');
+export const Error = story('error');
diff --git a/docs/doctoring/hr-workspace-qualification-rule-review-accessibility-references.md b/docs/doctoring/hr-workspace-qualification-rule-review-accessibility-references.md
new file mode 100644
index 000000000..ec5258e92
--- /dev/null
+++ b/docs/doctoring/hr-workspace-qualification-rule-review-accessibility-references.md
@@ -0,0 +1,21 @@
+# HR Workspace qualification-rule 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 converting the entire review surface into an application widget.
+- Keep the high-impact qualification review as an explicit human-confirmation state. The state itself does not execute screening, ranking, rejection, advancement, or an employment decision.
+- Preserve a visible keyboard focus indicator and a minimum 44 CSS-pixel action height in the workflow-specific proof.
+- Error, denial, stale-evidence, and blocked-scope states use assertive alert semantics and always give 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/
+
+## Scope note
+
+WCAG 2.2 is the current completed W3C Recommendation used here for accessibility design guidance. WAI-ARIA 1.2 is the completed Recommendation used for roles, states, and properties; later ARIA work remains draft and is not treated as the normative production baseline for this slice.
diff --git a/docs/traceability/hr-workspace-qualification-rule-review-state.md b/docs/traceability/hr-workspace-qualification-rule-review-state.md
new file mode 100644
index 000000000..f0de2fecd
--- /dev/null
+++ b/docs/traceability/hr-workspace-qualification-rule-review-state.md
@@ -0,0 +1,45 @@
+# HR Workspace qualification-rule 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`.
+- Qualification-rule governance owner: PR #104 (`JobQualificationRuleReviewPacket`). Its review evidence remains `not_authorized_for_candidate_or_employment_decision`.
+- Qualification-rule persistence owner: PR #105, dependency-first under #104. Persistence does not grant candidate-screening or employment-decision authority.
+- This PR owns only the HR Workspace presentation/interaction state contract. It does not duplicate #104/#105 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 qualification evidence human-reviewed and non-authorizing:
+
+| State | Buyer-visible meaning | Safe next action |
+| --- | --- | --- |
+| `idle` | No governed qualification evidence loaded | Load current Job/Job Analysis evidence |
+| `loading` | Fresh evidence is being resolved | Wait; duplicate action disabled |
+| `review` | High-risk human confirmation is required | Confirm reviewed Task/KSAO/source evidence |
+| `recording` | Immutable human-review evidence is being recorded | Wait; duplicate submission disabled |
+| `recorded` | Review evidence exists; rule is not activated | Return to Job Analysis or a separately authorized authoritative boundary |
+| `denied` | Purpose/reviewer authority is insufficient | Review access purpose and reviewer authority |
+| `stale` | Job/Job Analysis evidence changed | Reload authoritative evidence |
+| `blocked` | Required Task/KSAO/source scope is incomplete | Resolve governed evidence scope |
+| `error` | Governed evidence/review service is unusable | Verify service and authorization; do not rely on cached evidence |
+
+`review` and `recorded` must never imply that Orgmetra evaluated, ranked, rejected, or advanced a candidate. They do not authorize an employment decision.
+
+## Privacy and data minimization
+
+State evidence contains only bounded interaction semantics. It must not contain Person/candidate identifiers, names, contact information, raw qualification-rule text, assessment/cut scores, compensation values, credentials/tokens, prompts, or model output.
+
+## Verification
+
+The dedicated `HR Workspace Qualification Rule 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
+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-qualification-rule-review-state.test.mjs b/tests/hr-workspace-qualification-rule-review-state.test.mjs
new file mode 100644
index 000000000..95ba5e099
--- /dev/null
+++ b/tests/hr-workspace-qualification-rule-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 {
+ qualificationRuleReviewStateMarkup,
+ qualificationRuleReviewViewModel,
+} from '../apps/hr-workspace/qualification-rule-review-state.js';
+
+const story = readFileSync(
+ new URL('../apps/hr-workspace/qualification-rule-review-state.stories.js', import.meta.url),
+ 'utf8',
+);
+const css = readFileSync(
+ new URL('../apps/hr-workspace/qualification-rule-review-state.css', import.meta.url),
+ 'utf8',
+);
+const workflow = readFileSync(
+ new URL('../.github/workflows/hr-workspace-qualification-rule-review-state.yml', import.meta.url),
+ 'utf8',
+);
+
+const expectedStates = {
+ idle: ['false', 'status', false, 'default', 'Review qualification-rule evidence'],
+ loading: ['true', 'status', true, 'loading', 'Loading governed qualification evidence'],
+ review: ['false', 'status', false, 'high-risk-confirmation', 'Qualification rule requires human confirmation'],
+ recording: ['true', 'status', true, 'loading', 'Recording human qualification-rule review'],
+ recorded: ['false', 'status', true, 'read-only', 'Human qualification-rule review recorded'],
+ denied: ['false', 'alert', false, 'permission-denied', 'Qualification-rule review access denied'],
+ stale: ['false', 'alert', false, 'validation-error', 'Qualification evidence is stale'],
+ blocked: ['false', 'alert', false, 'validation-error', 'Qualification-rule review is blocked by evidence scope'],
+ error: ['false', 'alert', false, 'error', 'Qualification-rule review unavailable'],
+};
+
+const allowedViewModelKeys = [
+ 'actionLabel',
+ 'ariaBusy',
+ 'ariaLive',
+ 'interactionState',
+ 'label',
+ 'message',
+ 'nextAction',
+ 'role',
+ 'submitDisabled',
+];
+
+test('qualification-rule review states are bounded, actionable, and value-minimized', () => {
+ for (const [state, [ariaBusy, role, submitDisabled, interactionState, label]] of Object.entries(expectedStates)) {
+ const model = qualificationRuleReviewViewModel(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',
+ 'candidateName',
+ 'email',
+ 'phone',
+ 'rawQualificationRule',
+ 'assessmentScore',
+ 'cutScore',
+ 'compensationValue',
+ 'credential',
+ 'token',
+ 'prompt',
+ 'modelOutput',
+ ]) {
+ assert.equal(Object.hasOwn(model, forbiddenKey), false);
+ }
+
+ const markup = qualificationRuleReviewStateMarkup(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, /