From 78eeecb13d7280ab038fb3234c8018486d93618c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:05:36 -0700 Subject: [PATCH 1/9] test(ui): define Job Architecture workspace contract --- tests/hr-workspace-job-architecture.test.mjs | 157 +++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/hr-workspace-job-architecture.test.mjs diff --git a/tests/hr-workspace-job-architecture.test.mjs b/tests/hr-workspace-job-architecture.test.mjs new file mode 100644 index 000000000..f66ceec70 --- /dev/null +++ b/tests/hr-workspace-job-architecture.test.mjs @@ -0,0 +1,157 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + jobArchitectureMarkup, + jobArchitectureViewModel, +} from '../apps/hr-workspace/job-architecture-state.js'; + +const STATES = Object.freeze([ + 'idle', + 'loading', + 'draft', + 'review', + 'publishing', + 'published', + 'denied', + 'stale', + 'evidenceBlocked', + 'error', +]); + +const ALLOWED_KEYS = Object.freeze([ + 'actionDisabled', + 'actionLabel', + 'ariaBusy', + 'ariaLive', + 'interactionState', + 'jobProfilePublished', + 'label', + 'message', + 'nextAction', + 'role', + 'smeConfirmationRequired', +]); + +const FORBIDDEN_VALUE_KEYS = Object.freeze([ + 'tenantId', + 'jobId', + 'jobTitle', + 'jobAnalysisId', + 'jobAnalysisVersion', + 'effectiveDate', + 'taskText', + 'fjaValue', + 'ksaoValue', + 'sourceUrl', + 'sourceContent', + 'smeActorId', + 'smeName', + 'candidateId', + 'personId', + 'positionId', + 'assignmentId', + 'compensation', + 'credential', + 'token', + 'prompt', + 'modelOutput', +]); + +test('Job Architecture exposes only bounded governed workspace states', () => { + for (const state of STATES) { + const model = jobArchitectureViewModel(state); + assert.deepEqual(Object.keys(model).sort(), [...ALLOWED_KEYS].sort()); + assert.equal(typeof model.nextAction, 'string'); + assert.ok(model.nextAction.length > 0); + assert.equal(typeof model.smeConfirmationRequired, 'boolean'); + assert.equal(typeof model.jobProfilePublished, 'boolean'); + } + + assert.equal(jobArchitectureViewModel('loading').ariaBusy, 'true'); + assert.equal(jobArchitectureViewModel('loading').actionDisabled, true); + assert.equal(jobArchitectureViewModel('draft').interactionState, 'read-only'); + assert.equal(jobArchitectureViewModel('review').interactionState, 'high-risk-confirmation'); + assert.equal(jobArchitectureViewModel('review').smeConfirmationRequired, true); + assert.equal(jobArchitectureViewModel('publishing').actionDisabled, true); + assert.equal(jobArchitectureViewModel('published').interactionState, 'read-only'); + assert.equal(jobArchitectureViewModel('published').jobProfilePublished, true); + assert.equal(jobArchitectureViewModel('denied').role, 'alert'); +}); + +test('Job Architecture is value-minimized and never creates shadow Job or employment-decision authority', () => { + for (const state of STATES) { + const model = jobArchitectureViewModel(state); + for (const forbiddenKey of FORBIDDEN_VALUE_KEYS) { + assert.equal(Object.hasOwn(model, forbiddenKey), false); + } + } + + const draft = jobArchitectureViewModel('draft'); + assert.match(draft.message, /read-only draft evidence/i); + assert.match(draft.message, /Task.*FJA.*KSAO/i); + assert.match(draft.message, /not published Job truth/i); + + const review = jobArchitectureViewModel('review'); + assert.match(review.message, /accountable SME/i); + assert.match(review.message, /evidence version/i); + assert.match(review.message, /does not authorize.*candidate.*employment decision/i); + + const publishing = jobArchitectureViewModel('publishing'); + assert.match(publishing.message, /not proof that the Job profile was published/i); + + const published = jobArchitectureViewModel('published'); + assert.match(published.message, /read-only/i); + assert.match(published.message, /authoritative Job Analysis publication evidence/i); + assert.match(published.message, /does not itself change Position, Assignment, compensation, or candidate status/i); +}); + +test('Job Architecture renders Figma-correlated accessible workspace evidence', () => { + const loading = jobArchitectureMarkup('loading'); + assert.match(loading, /data-figma-node-id="1:16"/); + assert.match(loading, /data-storybook-inventory-node-id="1:64"/); + assert.match(loading, /aria-busy="true"/); + assert.match(loading, /disabled/); + + const review = jobArchitectureMarkup('review'); + assert.match(review, /data-interaction-state="high-risk-confirmation"/); + assert.match(review, /data-sme-confirmation-required="true"/); + assert.match(review, /SME confirmation/); + assert.match(review, /Evidence drawer/); + + const published = jobArchitectureMarkup('published'); + assert.match(published, /data-interaction-state="read-only"/); + assert.match(published, /data-job-profile-published="true"/); + assert.match(published, /role="status"/); + assert.match(published, /Next action/); +}); + +test('Job Architecture rejects non-string and prototype-inherited state names', () => { + for (const invalid of [null, 1, {}, [], new String('published')]) { + assert.throws(() => jobArchitectureViewModel(invalid), TypeError); + assert.throws(() => jobArchitectureMarkup(invalid), TypeError); + } + + for (const inheritedName of ['constructor', 'toString', '__proto__']) { + assert.throws(() => jobArchitectureViewModel(inheritedName), TypeError); + assert.throws(() => jobArchitectureMarkup(inheritedName), TypeError); + } + + assert.throws(() => jobArchitectureViewModel('unknown'), TypeError); +}); + +test('Job Architecture fails closed with a concrete next action', () => { + const expectations = { + denied: /check the HR purpose and Job Architecture authority/i, + stale: /reload the current governed Job Analysis evidence/i, + evidenceBlocked: /return to evidence review/i, + error: /reconcile the authoritative Job Analysis publication evidence/i, + }; + + for (const [state, pattern] of Object.entries(expectations)) { + const model = jobArchitectureViewModel(state); + assert.equal(model.actionDisabled, false); + assert.equal(model.jobProfilePublished, false); + assert.match(model.nextAction, pattern); + } +}); From 15c6328d2491009ffca40faaa28d38bb01ca0238 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:05:53 -0700 Subject: [PATCH 2/9] ci(ui): enforce Job Architecture workspace contract --- .../hr-workspace-job-architecture.yml | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/hr-workspace-job-architecture.yml diff --git a/.github/workflows/hr-workspace-job-architecture.yml b/.github/workflows/hr-workspace-job-architecture.yml new file mode 100644 index 000000000..6fafb3656 --- /dev/null +++ b/.github/workflows/hr-workspace-job-architecture.yml @@ -0,0 +1,54 @@ +name: HR Workspace Job Architecture State Quality + +on: + pull_request: + branches: + - feat/hr-workspace-protected-read-state + paths: + - "apps/hr-workspace/job-architecture-state.js" + - "apps/hr-workspace/job-architecture-state.css" + - "apps/hr-workspace/job-architecture-state.stories.js" + - "tests/hr-workspace-job-architecture.test.mjs" + - "docs/traceability/hr-workspace-job-architecture.md" + - "docs/doctoring/hr-workspace-job-architecture-accessibility-references.md" + - ".github/workflows/hr-workspace-job-architecture.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: hr-workspace-job-architecture-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + job-architecture: + name: Job Architecture workspace 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 Architecture 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-architecture.test.mjs + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From d24e1069c549f9be33b164d0ab0237b4b080b513 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:07:29 -0700 Subject: [PATCH 3/9] feat(ui): implement governed Job Architecture states --- apps/hr-workspace/job-architecture-state.js | 110 ++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 apps/hr-workspace/job-architecture-state.js diff --git a/apps/hr-workspace/job-architecture-state.js b/apps/hr-workspace/job-architecture-state.js new file mode 100644 index 000000000..7cf61e797 --- /dev/null +++ b/apps/hr-workspace/job-architecture-state.js @@ -0,0 +1,110 @@ +const STATE_MODELS = Object.freeze({ + idle: Object.freeze({ + ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: false, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'default', actionLabel: 'Load Job Architecture evidence', + label: 'Review Job Architecture evidence', + message: 'Load fresh purpose-authorized Job Analysis, source-provenance, and publication evidence before reviewing a Job profile.', + nextAction: 'Load the current governed Job Analysis evidence before opening the evidence drawer or requesting SME review.', + }), + loading: Object.freeze({ + ariaBusy: 'true', ariaLive: 'polite', role: 'status', actionDisabled: true, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'loading', actionLabel: 'Loading Job Architecture evidence', + label: 'Loading Job Architecture evidence', + message: 'Orgmetra is resolving the authorized Job Analysis snapshot, Task/FJA/KSAO lineage, source provenance, and publication scope for this purpose-bound read.', + nextAction: 'Wait for the governed Job Architecture evidence read to finish.', + }), + draft: Object.freeze({ + ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: false, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'read-only', actionLabel: 'Open evidence drawer', + label: 'Job profile draft requires review', + message: 'Read-only draft evidence shows Task → FJA → KSAO lineage and source provenance; it is not published Job truth. Model-assisted content remains untrusted draft evidence until accountable SME review.', + nextAction: 'Inspect the evidence drawer, provenance, limitations, and Job scope before requesting accountable SME review.', + }), + review: Object.freeze({ + ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false, + smeConfirmationRequired: true, jobProfilePublished: false, + interactionState: 'high-risk-confirmation', actionLabel: 'Confirm reviewed Job profile', + label: 'SME confirmation required', + message: 'An accountable SME must confirm Job scope, Task/FJA/KSAO evidence, source provenance, limitations, actor, purpose, reason, and evidence version before requesting authoritative publication. This presentation state does not authorize candidate ranking, rejection, progression, compensation, or any employment decision.', + nextAction: 'Verify the evidence and limitations, then explicitly confirm the reviewed Job profile only if the accountable SME judgment is supported.', + }), + publishing: Object.freeze({ + ariaBusy: 'true', ariaLive: 'polite', role: 'status', actionDisabled: true, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'loading', actionLabel: 'Publishing reviewed Job profile', + label: 'Submitting reviewed Job profile', + message: 'Orgmetra is submitting the SME-confirmed profile to the authoritative Job Analysis publication boundary. This in-progress state is not proof that the Job profile was published.', + nextAction: 'Do not reuse, republish, or treat the profile as authoritative until publication and immutable audit evidence are returned.', + }), + published: Object.freeze({ + ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: true, + smeConfirmationRequired: false, jobProfilePublished: true, + interactionState: 'read-only', actionLabel: 'Job profile published', + label: 'Job profile publication recorded', + message: 'Authoritative Job Analysis publication evidence and immutable audit evidence were returned. This read-only UI does not itself change Position, Assignment, compensation, or candidate status.', + nextAction: 'Use the published Job profile only through separately governed requisition, Position, Assignment, selection, or workforce boundaries that require it.', + }), + denied: Object.freeze({ + ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'permission-denied', actionLabel: 'Review access', + label: 'Job Architecture access denied', + message: 'The current actor or HR purpose does not permit this governed Job Architecture read or review action.', + nextAction: 'Check the HR purpose and Job Architecture authority before requesting this evidence or review action again.', + }), + stale: Object.freeze({ + ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'validation-error', actionLabel: 'Reload Job evidence', + label: 'Job Architecture evidence is stale', + message: 'Job Analysis scope, evidence version, source provenance, or publication truth changed before the profile could be safely reviewed or published.', + nextAction: 'Reload the current governed Job Analysis evidence before reviewing or requesting publication again.', + }), + evidenceBlocked: Object.freeze({ + ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'validation-error', actionLabel: 'Return to evidence review', + label: 'Job profile evidence is incomplete', + message: 'The governed publication boundary cannot prove the required Job scope, Task/FJA/KSAO lineage, source provenance, limitations, accountable SME context, or evidence version.', + nextAction: 'Return to evidence review, resolve the missing governed evidence, and begin a new SME confirmation from fresh authority.', + }), + error: Object.freeze({ + ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false, + smeConfirmationRequired: false, jobProfilePublished: false, + interactionState: 'error', actionLabel: 'Reconcile publication status', + label: 'Job profile publication status unavailable', + message: 'The publication request did not return usable authoritative Job Analysis publication and immutable audit evidence, so Orgmetra does not treat the Job profile as published.', + nextAction: 'Reconcile the authoritative Job Analysis publication evidence and immutable audit evidence before retrying or using the profile downstream.', + }), +}); + +function requireExactState(value) { + if (typeof value !== 'string') { + throw new TypeError('Job Architecture state must be an exact built-in string'); + } + if (!Object.hasOwn(STATE_MODELS, value)) { + throw new TypeError(`unsupported Job Architecture state: ${value}`); + } + return STATE_MODELS[value]; +} + +/** Return immutable accessibility semantics for one purpose-bound Job Architecture workspace state. */ +export function jobArchitectureViewModel(state) { + return requireExactState(state); +} + +/** Render static Storybook workflow evidence without accepting caller-controlled Job or evidence values. */ +export function jobArchitectureMarkup(state) { + const model = requireExactState(state); + const disabled = model.actionDisabled ? ' disabled' : ''; + const confirmationText = model.smeConfirmationRequired + ? 'Required before the reviewed profile may be submitted for authoritative publication.' + : 'Not available in this workflow state.'; + const publishedText = model.jobProfilePublished + ? 'Authoritative Job Analysis publication and immutable audit evidence returned.' + : 'No published Job profile is asserted by this workflow state.'; + return `
\n

${model.label}${model.message}

\n

Evidence drawerTask → FJA → KSAO and source provenance remain governed read evidence; caller-controlled evidence values are not embedded in this state model.

\n

SME confirmation${confirmationText}

\n

Publication evidence${publishedText}

\n

Next action${model.nextAction}

\n \n
`; +} From 9c18517966ec3945d5380550779bbf1f794cbe55 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:08:09 -0700 Subject: [PATCH 4/9] feat(ui): style governed Job Architecture states --- apps/hr-workspace/job-architecture-state.css | 64 ++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 apps/hr-workspace/job-architecture-state.css diff --git a/apps/hr-workspace/job-architecture-state.css b/apps/hr-workspace/job-architecture-state.css new file mode 100644 index 000000000..06856beb9 --- /dev/null +++ b/apps/hr-workspace/job-architecture-state.css @@ -0,0 +1,64 @@ +.job-architecture-state { + display: grid; + gap: var(--orgmetra-space-md); + max-width: 42rem; + 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-architecture-status, +.job-architecture-evidence, +.job-architecture-confirmation, +.job-architecture-publication, +.job-architecture-next-action { + display: grid; + gap: var(--orgmetra-space-xs); + margin: 0; +} + +.job-architecture-status span, +.job-architecture-evidence span, +.job-architecture-confirmation span, +.job-architecture-publication span, +.job-architecture-next-action span { + color: var(--orgmetra-text-muted); +} + +.job-architecture-state[data-interaction-state="high-risk-confirmation"] { + border-color: var(--orgmetra-action-review); +} + +.job-architecture-state[data-interaction-state="permission-denied"], +.job-architecture-state[data-interaction-state="validation-error"], +.job-architecture-state[data-interaction-state="error"] { + border-color: var(--orgmetra-danger); +} + +.job-architecture-state[aria-busy="true"] { + cursor: progress; +} + +.job-architecture-action { + 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-architecture-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.job-architecture-action:focus-visible { + outline: 3px solid var(--orgmetra-focus-ring); + outline-offset: 3px; +} From 6fd11b71b4846d20d52e00b93535e2da9d2313bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:08:17 -0700 Subject: [PATCH 5/9] feat(ui): add Job Architecture Storybook states --- .../job-architecture-state.stories.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/hr-workspace/job-architecture-state.stories.js diff --git a/apps/hr-workspace/job-architecture-state.stories.js b/apps/hr-workspace/job-architecture-state.stories.js new file mode 100644 index 000000000..ea41380ea --- /dev/null +++ b/apps/hr-workspace/job-architecture-state.stories.js @@ -0,0 +1,27 @@ +import { jobArchitectureMarkup } from './job-architecture-state.js'; +import './job-architecture-state.css'; + +export default { + title: 'HR Workspace/Job Architecture States', + parameters: { + design: { + type: 'figma', + url: 'Orgmetra Baseline — Job Architecture node 1:16 / Storybook Inventory node 1:64', + }, + }, +}; + +function story(state) { + return () => jobArchitectureMarkup(state); +} + +export const Idle = story('idle'); +export const Loading = story('loading'); +export const DraftReadOnly = story('draft'); +export const SmeConfirmation = story('review'); +export const Publishing = story('publishing'); +export const PublishedReadOnly = story('published'); +export const PermissionDenied = story('denied'); +export const StaleEvidence = story('stale'); +export const EvidenceBlocked = story('evidenceBlocked'); +export const Error = story('error'); From d55198862e5c5786279934f974172144b8ff451b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:10:11 -0700 Subject: [PATCH 6/9] docs(ui): doctor Job Architecture accessibility evidence --- ...b-architecture-accessibility-references.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/doctoring/hr-workspace-job-architecture-accessibility-references.md diff --git a/docs/doctoring/hr-workspace-job-architecture-accessibility-references.md b/docs/doctoring/hr-workspace-job-architecture-accessibility-references.md new file mode 100644 index 000000000..ef4e1be3b --- /dev/null +++ b/docs/doctoring/hr-workspace-job-architecture-accessibility-references.md @@ -0,0 +1,31 @@ +# Job Architecture workspace accessibility and evidence references + +Status: **active PR evidence only**. This document does not describe protected-`develop` product truth and does not authorize Job, Position, Assignment, compensation, candidate, or employment-decision mutation. + +## Product-design source + +Orgmetra's Figma file `Orgmetra Baseline` (`xu1ZK1zmtFcDep95R8oE9O`) remains the product-design authority for this presentation slice. Fresh 2026-08-28 reads identify: + +- Job Architecture frame `1:16`, which presents a versioned Job profile, Task → FJA → KSAO evidence, an evidence drawer, and publish-only-after-SME-review copy; and +- Storybook Inventory `1:64`, which requires default, hover/focus, disabled, loading, validation-error, read-only, and high-risk-confirmation behavior where applicable. + +The executable state model correlates to those nodes but deliberately does not embed caller-controlled Job titles, identifiers, Task/FJA/KSAO values, sources, SME identities, prompts, or model output in generic interaction-state evidence. + +## Accessibility decisions + +- Loading and publication-in-progress states expose `aria-busy="true"` and disable the action that would otherwise duplicate the operation. +- Consequential SME confirmation uses an assertive alert state; ordinary read-only evidence uses a polite status state. +- Denied, stale, incomplete-evidence, and indeterminate-publication states fail closed and provide a concrete next action rather than implying success. +- The action target retains the existing Orgmetra 44-pixel minimum target size and shared `:focus-visible` focus-ring token. +- Published evidence is read-only and is asserted only after the separate authoritative Job Analysis boundary returns publication and immutable-audit evidence. +- Model-assisted Task/FJA/KSAO output remains untrusted draft evidence until accountable SME review. The workspace itself never ranks, rejects, or advances candidates and never grants compensation or employment-decision authority. + +WCAG 2.2 is the current W3C accessibility Recommendation used for the interaction-level accessibility contract, while WAI-ARIA 1.2 defines the accessible-state vocabulary used here. O*NET's Content Model is used only as an external occupational-information reference corroborating the separation of task and worker-requirement evidence domains; O*NET does not become authoritative Orgmetra Job truth. + +## APA 7 references + +National Center for O*NET Development. (n.d.). *The O*NET Content Model*. O*NET Resource Center. Retrieved August 28, 2026, from https://www.onetcenter.org/content.html + +World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/ + +World Wide Web Consortium. (2024). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ From 210a659eb20ec2f77bf0b55d6897a5b9b8beab1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 05:10:39 -0700 Subject: [PATCH 7/9] docs(ui): trace Job Architecture workspace evidence --- .../hr-workspace-job-architecture.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/traceability/hr-workspace-job-architecture.md diff --git a/docs/traceability/hr-workspace-job-architecture.md b/docs/traceability/hr-workspace-job-architecture.md new file mode 100644 index 000000000..0e05b6fda --- /dev/null +++ b/docs/traceability/hr-workspace-job-architecture.md @@ -0,0 +1,41 @@ +# HR Workspace Job Architecture traceability + +Status: **active Draft PR evidence only**. Nothing in this document turns #147 into protected-`develop` truth, merge authorization, or authoritative Job mutation capability. + +## Dependency and ownership boundary + +- Parent interaction owner: #130 `feat/hr-workspace-protected-read-state@68896baa692ecf6fec8f21cfe5d981440be6071c`. +- Merged Job Analysis evidence owner: #25. +- Merged Job Analysis snapshot persistence/read owner: #38. +- Active model-assisted Task/FJA/KSAO draft owner: #117; raw model output remains untrusted draft evidence. +- Active Job-grade review/persistence owners: #101 → #109. +- This PR #147 owns only the Job Architecture workspace presentation/interaction state model, tokenized styling, Storybook stories, focused accessibility/privacy regression, and this traceability evidence. + +No parent/backend checks or reviews transfer into this child. This child does not create a shadow Job Analysis, Job-grade, Position, Assignment, candidate, compensation, or employment-decision authority. + +## Product-design correlation + +Fresh Figma `Orgmetra Baseline` evidence on 2026-08-28 identifies Job Architecture node `1:16` and Storybook Inventory node `1:64`. The executable markup pins both identifiers. The UI exposes bounded states for loading, read-only draft evidence, accountable SME confirmation, publication-in-progress, read-only published evidence, permission denial, stale evidence, incomplete evidence, and indeterminate publication. + +## Governed interaction requirements + +| Requirement | Executable evidence | +| --- | --- | +| Load fresh purpose-authorized Job evidence | `idle` → `loading` with `aria-busy` and duplicate-action prevention | +| Keep Task/FJA/KSAO/model-assisted evidence non-authoritative until review | `draft` is read-only and explicitly says it is not published Job truth | +| Require accountable human SME confirmation before publication | `review` is `high-risk-confirmation` and requires Job scope, evidence/provenance, limitations, actor, purpose, reason, and evidence version | +| Never turn UI review into candidate/compensation/employment-decision authority | `review` explicitly excludes ranking, rejection, progression, compensation, and employment-decision authority | +| Do not claim success while publishing | `publishing` disables duplicate action and explicitly says in-progress is not proof of publication | +| Claim published state only from authoritative evidence | `published` requires authoritative Job Analysis publication plus immutable audit evidence and remains read-only | +| Fail closed when authority/evidence is unavailable | `denied`, `stale`, `evidenceBlocked`, and `error` expose alert semantics and concrete next actions | +| Reject prototype-chain state confusion | exact built-in strings plus `Object.hasOwn(STATE_MODELS, state)` reject `constructor`, `toString`, and `__proto__` | +| Minimize protected values | generic state models exclude Job identifiers/titles, Job Analysis identifiers/version, effective dates, Task/FJA/KSAO values, source content/URLs, SME identity, candidate/person/position/assignment values, compensation, credentials/tokens, prompts, and model output | + +## Contract-first RED and root repair + +- Contract head `15c6328d2491009ffca40faaa28d38bb01ca0238` intentionally contained the focused regression and exact-coverage workflow before the production state module existed. +- `HR Workspace Job Architecture State Quality` run `33169736884`, job `98843660150`, checked out and proved that exact SHA, used Node 24.19.0, and terminated **FAILURE** at the focused test with `ERR_MODULE_NOT_FOUND` for the intentionally absent `apps/hr-workspace/job-architecture-state.js`. This is the genuine hosted RED. +- Root implementation commit `d24e1069c549f9be33b164d0ab0237b4b080b513` adds only the bounded presentation state model and markup contract. +- The immediate root-repair run `33169821181`, job `98843941265`, is terminal **GREEN**: exact checkout/proof, Node setup, focused contract under exact 100% line/branch/function thresholds, and clean checkout all succeeded. + +Subsequent CSS, Storybook, doctoring, and traceability commits advance the branch head, so the root GREEN above is historical repair evidence rather than passing evidence for the final branch head. The current exact head must obtain its own new terminal GREEN before this active PR can be considered internally consistent, and it remains stack-local even then. From 4ba6bbece3abb8356d154f7e07f04f8259e0399b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 03:26:58 +0900 Subject: [PATCH 8/9] fix: run job architecture contract on develop --- .github/workflows/hr-workspace-job-architecture.yml | 1 + tests/hr-workspace-job-architecture.test.mjs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/hr-workspace-job-architecture.yml b/.github/workflows/hr-workspace-job-architecture.yml index 6fafb3656..7ec0433de 100644 --- a/.github/workflows/hr-workspace-job-architecture.yml +++ b/.github/workflows/hr-workspace-job-architecture.yml @@ -3,6 +3,7 @@ name: HR Workspace Job Architecture State Quality on: pull_request: branches: + - develop - feat/hr-workspace-protected-read-state paths: - "apps/hr-workspace/job-architecture-state.js" diff --git a/tests/hr-workspace-job-architecture.test.mjs b/tests/hr-workspace-job-architecture.test.mjs index f66ceec70..35b9b0208 100644 --- a/tests/hr-workspace-job-architecture.test.mjs +++ b/tests/hr-workspace-job-architecture.test.mjs @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import { readFileSync } from 'node:fs'; import { jobArchitectureMarkup, @@ -58,6 +59,8 @@ const FORBIDDEN_VALUE_KEYS = Object.freeze([ 'modelOutput', ]); +const WORKFLOW = readFileSync(new URL('../.github/workflows/hr-workspace-job-architecture.yml', import.meta.url), 'utf8'); + test('Job Architecture exposes only bounded governed workspace states', () => { for (const state of STATES) { const model = jobArchitectureViewModel(state); @@ -155,3 +158,7 @@ test('Job Architecture fails closed with a concrete next action', () => { assert.match(model.nextAction, pattern); } }); + +test('Job Architecture contract runs on protected develop and dependency parent pull requests', () => { + assert.match(WORKFLOW, /branches:\n\s+- develop\n\s+- feat\/hr-workspace-protected-read-state/); +}); From 26e81931ec031bd9ac72f0053839cae58c21f6bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:23:40 +0900 Subject: [PATCH 9/9] fix(ui): add job architecture hover state --- apps/hr-workspace/job-architecture-state.css | 4 ++++ tests/hr-workspace-job-architecture.test.mjs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/apps/hr-workspace/job-architecture-state.css b/apps/hr-workspace/job-architecture-state.css index 06856beb9..ed36afdf3 100644 --- a/apps/hr-workspace/job-architecture-state.css +++ b/apps/hr-workspace/job-architecture-state.css @@ -53,6 +53,10 @@ cursor: pointer; } +.job-architecture-action:hover:not(:disabled) { + opacity: 0.88; +} + .job-architecture-action:disabled { cursor: not-allowed; opacity: 0.62; diff --git a/tests/hr-workspace-job-architecture.test.mjs b/tests/hr-workspace-job-architecture.test.mjs index 35b9b0208..9fc36b3b2 100644 --- a/tests/hr-workspace-job-architecture.test.mjs +++ b/tests/hr-workspace-job-architecture.test.mjs @@ -60,6 +60,7 @@ const FORBIDDEN_VALUE_KEYS = Object.freeze([ ]); const WORKFLOW = readFileSync(new URL('../.github/workflows/hr-workspace-job-architecture.yml', import.meta.url), 'utf8'); +const CSS = readFileSync(new URL('../apps/hr-workspace/job-architecture-state.css', import.meta.url), 'utf8'); test('Job Architecture exposes only bounded governed workspace states', () => { for (const state of STATES) { @@ -110,6 +111,8 @@ test('Job Architecture is value-minimized and never creates shadow Job or employ }); test('Job Architecture renders Figma-correlated accessible workspace evidence', () => { + assert.match(CSS, /:hover:not\(:disabled\)/); + assert.match(CSS, /:focus-visible/); const loading = jobArchitectureMarkup('loading'); assert.match(loading, /data-figma-node-id="1:16"/); assert.match(loading, /data-storybook-inventory-node-id="1:64"/);