From 8bf4d2491b9bcd8ee07777749f64df84af0e19a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:03:09 -0700 Subject: [PATCH 01/11] test(ui): define governed Job grade review states --- ...-workspace-job-grade-review-state.test.mjs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/hr-workspace-job-grade-review-state.test.mjs 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..781f91c0c --- /dev/null +++ b/tests/hr-workspace-job-grade-review-state.test.mjs @@ -0,0 +1,96 @@ +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 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'], +}; + +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, /\.$/); + + const serialized = JSON.stringify(model); + assert.doesNotMatch( + serialized, + /person|candidate|employee_name|email|phone|compensation|salary|pay|rating|assessment|credential|token|prompt|model_output/i, + ); + + 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, /]* disabled/); + else assert.doesNotMatch(markup, /]* disabled/); + } +}); + +test('review and recorded states never imply compensation or employment-decision authority', () => { + const review = jobGradeReviewViewModel('review'); + assert.match(review.message, /human review/i); + assert.match(review.message, /does not authorize compensation or an employment decision/i); + + const recorded = jobGradeReviewViewModel('recorded'); + assert.equal(recorded.submitDisabled, true); + assert.match(recorded.message, /evidence only/i); + assert.match(recorded.message, /does not authorize compensation, promotion, assignment, candidate, or employment decisions/i); +}); + +test('denial, stale evidence, and failure explain the next safe action', () => { + assert.match(jobGradeReviewViewModel('denied').nextAction, /access purpose and reviewer authority/i); + assert.match(jobGradeReviewViewModel('stale').nextAction, /Reload authoritative Job and Job Analysis evidence/i); + assert.match(jobGradeReviewViewModel('error').nextAction, /Do not rely on a cached Job grade review/i); +}); + +test('unsupported runtime input fails closed before rendering', () => { + assert.throws(() => jobGradeReviewViewModel('approved'), /unsupported Job grade review state/); + assert.throws(() => jobGradeReviewViewModel(new String('review')), /exact built-in string/); + assert.throws(() => jobGradeReviewStateMarkup(Symbol('recorded')), /exact built-in string/); +}); + +test('Storybook and CSS cover workflow-specific accessibility states', () => { + for (const storyName of [ + 'Idle', + 'Loading', + 'ReadyForHumanReview', + 'Recording', + 'RecordedReadOnly', + 'PermissionDenied', + 'StaleEvidence', + 'Error', + ]) { + assert.match(story, new RegExp(`export const ${storyName}`)); + } + assert.match(story, /jobGradeReviewStateMarkup/); + assert.match(css, /var\(--orgmetra-focus-ring\)/); + assert.match(css, /:focus-visible/); + assert.match(css, /\[aria-busy="true"\]/); +}); From 772a69fe6ce6e5e6b008c29828eb966c90326504 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:03:24 -0700 Subject: [PATCH 02/11] ci(ui): gate Job grade review interaction states --- .../hr-workspace-job-grade-review-state.yml | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/hr-workspace-job-grade-review-state.yml 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..1420788f3 --- /dev/null +++ b/.github/workflows/hr-workspace-job-grade-review-state.yml @@ -0,0 +1,54 @@ +name: HR Workspace Job Grade Review State Quality + +on: + pull_request: + branches: + - 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)" From b597758f5c5694254d913eb6dfb6d5a4e7957f13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:04:52 -0700 Subject: [PATCH 03/11] feat(ui): implement governed Job grade review states --- apps/hr-workspace/job-grade-review-state.js | 77 +++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 apps/hr-workspace/job-grade-review-state.js 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..0a1a76f8d --- /dev/null +++ b/apps/hr-workspace/job-grade-review-state.js @@ -0,0 +1,77 @@ +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'); + const model = STATE_MODELS[value]; + if (!model) throw new TypeError(`unsupported Job grade review state: ${value}`); + return model; +} + +/** 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
`; +} From 390d3caf83e4407f9600c9d0d3f149b36326cfdb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:05:04 -0700 Subject: [PATCH 04/11] style(ui): add Job grade review accessibility styles --- apps/hr-workspace/job-grade-review-state.css | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 apps/hr-workspace/job-grade-review-state.css 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; +} From adb0f77245bef24c40a948b1673f134b30d203fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:05:14 -0700 Subject: [PATCH 05/11] docs(ui): add Job grade review Storybook states --- .../job-grade-review-state.stories.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 apps/hr-workspace/job-grade-review-state.stories.js 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'); From c8af177814dc70447edf37defcb23c7108db6f71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:05:36 -0700 Subject: [PATCH 06/11] docs(traceability): map Job grade review UI boundary --- .../hr-workspace-job-grade-review-state.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/traceability/hr-workspace-job-grade-review-state.md 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..591777a2d --- /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@b3b30058a79174000919d566fbbb1fdad80c62bf` 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. From 56b759720ba6a56ec831bb16da8f313033100f47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:05:51 -0700 Subject: [PATCH 07/11] docs(doctoring): record Job grade review accessibility references --- ...b-grade-review-accessibility-references.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/doctoring/hr-workspace-job-grade-review-accessibility-references.md 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. From 83a6bdf6af86cf4a79ddbcd7e35b24f74da9322d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 09:07:02 -0700 Subject: [PATCH 08/11] test(ui): verify Job grade state shape without banning governance copy --- ...-workspace-job-grade-review-state.test.mjs | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/hr-workspace-job-grade-review-state.test.mjs b/tests/hr-workspace-job-grade-review-state.test.mjs index 781f91c0c..8bf515785 100644 --- a/tests/hr-workspace-job-grade-review-state.test.mjs +++ b/tests/hr-workspace-job-grade-review-state.test.mjs @@ -26,6 +26,18 @@ const expectedStates = { 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); @@ -36,12 +48,25 @@ test('Job grade review states are bounded, actionable, and value-minimized', () assert.equal(model.label, label); assert.equal(model.ariaLive, role === 'alert' ? 'assertive' : 'polite'); assert.match(model.nextAction, /\.$/); + assert.deepEqual(Object.keys(model).sort(), allowedViewModelKeys); - const serialized = JSON.stringify(model); - assert.doesNotMatch( - serialized, - /person|candidate|employee_name|email|phone|compensation|salary|pay|rating|assessment|credential|token|prompt|model_output/i, - ); + 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"/); From a5309b5cf2e0682135da72c4d26ac007519ba8aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 02:25:19 +0900 Subject: [PATCH 09/11] fix: harden job grade review state boundary --- apps/hr-workspace/job-grade-review-state.js | 7 ++++--- .../hr-workspace-job-grade-review-state.test.mjs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/hr-workspace/job-grade-review-state.js b/apps/hr-workspace/job-grade-review-state.js index 0a1a76f8d..121510ca6 100644 --- a/apps/hr-workspace/job-grade-review-state.js +++ b/apps/hr-workspace/job-grade-review-state.js @@ -59,9 +59,10 @@ const STATE_MODELS = Object.freeze({ function requireExactState(value) { if (typeof value !== 'string') throw new TypeError('Job grade review state must be an exact built-in string'); - const model = STATE_MODELS[value]; - if (!model) throw new TypeError(`unsupported Job grade review state: ${value}`); - return model; + 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. */ diff --git a/tests/hr-workspace-job-grade-review-state.test.mjs b/tests/hr-workspace-job-grade-review-state.test.mjs index 8bf515785..7515170c1 100644 --- a/tests/hr-workspace-job-grade-review-state.test.mjs +++ b/tests/hr-workspace-job-grade-review-state.test.mjs @@ -101,6 +101,21 @@ test('unsupported runtime input fails closed before rendering', () => { assert.throws(() => jobGradeReviewStateMarkup(Symbol('recorded')), /exact built-in string/); }); +test('prototype-inherited names cannot masquerade as governed review states', () => { + for (const inheritedName of ['constructor', 'toString', '__proto__']) { + assert.throws( + () => jobGradeReviewViewModel(inheritedName), + /unsupported Job grade review state/, + `${inheritedName} must fail closed at the view-model boundary`, + ); + assert.throws( + () => jobGradeReviewStateMarkup(inheritedName), + /unsupported Job grade review state/, + `${inheritedName} must fail closed before markup is emitted`, + ); + } +}); + test('Storybook and CSS cover workflow-specific accessibility states', () => { for (const storyName of [ 'Idle', From 40b26388527fa65596ea6875e1d3d2b025942c2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 06:56:27 +0900 Subject: [PATCH 10/11] fix(ui): rerun job grade gate on develop --- .github/workflows/hr-workspace-job-grade-review-state.yml | 1 + tests/hr-workspace-job-grade-review-state.test.mjs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/hr-workspace-job-grade-review-state.yml b/.github/workflows/hr-workspace-job-grade-review-state.yml index 1420788f3..fbbac8a25 100644 --- a/.github/workflows/hr-workspace-job-grade-review-state.yml +++ b/.github/workflows/hr-workspace-job-grade-review-state.yml @@ -3,6 +3,7 @@ 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" diff --git a/tests/hr-workspace-job-grade-review-state.test.mjs b/tests/hr-workspace-job-grade-review-state.test.mjs index 7515170c1..8930cb9a2 100644 --- a/tests/hr-workspace-job-grade-review-state.test.mjs +++ b/tests/hr-workspace-job-grade-review-state.test.mjs @@ -14,6 +14,10 @@ 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'], @@ -134,3 +138,7 @@ test('Storybook and CSS cover workflow-specific accessibility states', () => { assert.match(css, /:focus-visible/); assert.match(css, /\[aria-busy="true"\]/); }); + +test('the dedicated contract reruns on protected develop after parent integration', () => { + assert.match(workflow, /branches:\n\s+- develop\n\s+- feat\/hr-workspace-protected-read-state/); +}); From 4d72fda8e3df41e6b4a9f81e3000895a446548b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:34:41 +0900 Subject: [PATCH 11/11] docs: refresh job grade parent evidence --- docs/traceability/hr-workspace-job-grade-review-state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/hr-workspace-job-grade-review-state.md b/docs/traceability/hr-workspace-job-grade-review-state.md index 591777a2d..4559359c6 100644 --- a/docs/traceability/hr-workspace-job-grade-review-state.md +++ b/docs/traceability/hr-workspace-job-grade-review-state.md @@ -3,7 +3,7 @@ ## 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@b3b30058a79174000919d566fbbb1fdad80c62bf` owns shared protected-read accessibility semantics and Figma/Storybook correlation. +- 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.