Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/hr-workspace-job-architecture.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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"
- "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)"
68 changes: 68 additions & 0 deletions apps/hr-workspace/job-architecture-state.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.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:hover:not(:disabled) {
opacity: 0.88;
}

.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;
}
110 changes: 110 additions & 0 deletions apps/hr-workspace/job-architecture-state.js
Original file line number Diff line number Diff line change
@@ -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 `<section class="job-architecture-state" data-figma-node-id="1:16" data-storybook-inventory-node-id="1:64" data-interaction-state="${model.interactionState}" data-sme-confirmation-required="${model.smeConfirmationRequired}" data-job-profile-published="${model.jobProfilePublished}" aria-busy="${model.ariaBusy}">\n <p class="job-architecture-status" role="${model.role}" aria-live="${model.ariaLive}"><strong>${model.label}</strong><span>${model.message}</span></p>\n <p class="job-architecture-evidence"><strong>Evidence drawer</strong><span>Task → FJA → KSAO and source provenance remain governed read evidence; caller-controlled evidence values are not embedded in this state model.</span></p>\n <p class="job-architecture-confirmation"><strong>SME confirmation</strong><span>${confirmationText}</span></p>\n <p class="job-architecture-publication"><strong>Publication evidence</strong><span>${publishedText}</span></p>\n <p class="job-architecture-next-action"><strong>Next action</strong><span>${model.nextAction}</span></p>\n <button class="job-architecture-action" type="button"${disabled}>${model.actionLabel}</button>\n</section>`;
}
27 changes: 27 additions & 0 deletions apps/hr-workspace/job-architecture-state.stories.js
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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/
Loading
Loading