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-qualification-rule-review-state.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: HR Workspace Qualification Rule Review State Quality

on:
pull_request:
branches:
- develop
- feat/hr-workspace-protected-read-state
paths:
- "apps/hr-workspace/qualification-rule-review-state.js"
- "apps/hr-workspace/qualification-rule-review-state.css"
- "apps/hr-workspace/qualification-rule-review-state.stories.js"
- "tests/hr-workspace-qualification-rule-review-state.test.mjs"
- "docs/traceability/hr-workspace-qualification-rule-review-state.md"
- "docs/doctoring/hr-workspace-qualification-rule-review-accessibility-references.md"
- ".github/workflows/hr-workspace-qualification-rule-review-state.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: hr-workspace-qualification-rule-review-state-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
qualification-rule-review-state:
name: Qualification-rule 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 qualification-rule 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-qualification-rule-review-state.test.mjs
- name: Require clean checkout
run: |
git diff --exit-code
test -z "$(git status --porcelain)"
58 changes: 58 additions & 0 deletions apps/hr-workspace/qualification-rule-review-state.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.qualification-rule-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);
}

.qualification-rule-review-status,
.qualification-rule-review-next-action {
display: grid;
gap: var(--orgmetra-space-xs);
margin: 0;
}

.qualification-rule-review-status span,
.qualification-rule-review-next-action span {
color: var(--orgmetra-text-muted);
}

.qualification-rule-review-state[data-interaction-state="high-risk-confirmation"] {
border-color: var(--orgmetra-action-review);
}

.qualification-rule-review-state[data-interaction-state="permission-denied"],
.qualification-rule-review-state[data-interaction-state="validation-error"],
.qualification-rule-review-state[data-interaction-state="error"] {
border-color: var(--orgmetra-danger);
}

.qualification-rule-review-state[aria-busy="true"] {
cursor: progress;
}

.qualification-rule-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;
}

.qualification-rule-review-submit:disabled {
cursor: wait;
opacity: 0.62;
}

.qualification-rule-review-submit:focus-visible {
outline: 3px solid var(--orgmetra-focus-ring);
outline-offset: 3px;
}
86 changes: 86 additions & 0 deletions apps/hr-workspace/qualification-rule-review-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const STATE_MODELS = Object.freeze({
idle: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
interactionState: 'default', actionLabel: 'Load qualification evidence',
label: 'Review qualification-rule evidence',
message: 'Load fresh Job, Job Analysis, Task, KSAO, and source evidence before making a human qualification-rule review.',
nextAction: 'Load the current governed qualification evidence for one Job.',
}),
loading: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
interactionState: 'loading', actionLabel: 'Loading qualification evidence',
label: 'Loading governed qualification evidence',
message: 'Orgmetra is waiting for fresh Job and Job Analysis evidence. No cached qualification rule is accepted.',
nextAction: 'Wait for the current evidence load to finish.',
}),
review: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
interactionState: 'high-risk-confirmation', actionLabel: 'Confirm human review',
label: 'Qualification rule requires human confirmation',
message: 'This is read-only evidence for human review. It does not evaluate, rank, reject, or advance a candidate and does not authorize an employment decision.',
nextAction: 'Confirm the reviewed Task, KSAO, and source 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 qualification-rule review',
message: 'Orgmetra is recording the human review evidence. Duplicate submission is disabled.',
nextAction: 'Wait for immutable qualification-rule review evidence to be recorded.',
}),
recorded: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: true,
interactionState: 'read-only', actionLabel: 'Review recorded',
label: 'Human qualification-rule review recorded',
message: 'The recorded review is evidence only and does not activate the rule, screen a candidate, or authorize an employment decision.',
nextAction: 'Return to Job Analysis or continue only through the separately authorized authoritative qualification-rule boundary.',
}),
denied: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'permission-denied', actionLabel: 'Review access',
label: 'Qualification-rule review access denied',
message: 'The current purpose or reviewer authority does not permit this qualification-rule 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: 'Qualification evidence is stale',
message: 'The Job, Job Analysis, Task, KSAO, or source evidence changed before review recording.',
nextAction: 'Reload authoritative Job and Job Analysis evidence before reviewing again.',
}),
blocked: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'validation-error', actionLabel: 'Review evidence scope',
label: 'Qualification-rule review is blocked by evidence scope',
message: 'The reviewed qualification rule is not fully supported by the governed Task, KSAO, and source evidence required for this Job.',
nextAction: 'Resolve the Task, KSAO, and source evidence scope before reviewing the qualification rule again.',
}),
error: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'error', actionLabel: 'Retry governed load',
label: 'Qualification-rule review unavailable',
message: 'The governed evidence or immutable review service did not return a usable result. No cached review is accepted.',
nextAction: 'Do not rely on cached qualification evidence; verify the service and authorization before retrying.',
}),
});

function requireExactState(value) {
if (typeof value !== 'string') {
throw new TypeError('qualification-rule review state must be an exact built-in string');
}
const model = Object.hasOwn(STATE_MODELS, value) ? STATE_MODELS[value] : undefined;
if (!model) throw new TypeError(`unsupported qualification-rule review state: ${value}`);
return model;
}

/** Return immutable, value-minimized accessibility semantics for one qualification-rule review state. */
export function qualificationRuleReviewViewModel(state) {
return requireExactState(state);
}

/** Render one static Storybook proof without accepting caller-controlled HR or candidate values. */
export function qualificationRuleReviewStateMarkup(state) {
const model = requireExactState(state);
const disabled = model.submitDisabled ? ' disabled' : '';
return `<section class="qualification-rule-review-state" data-figma-node-id="1:64" data-interaction-state="${model.interactionState}" aria-busy="${model.ariaBusy}">\n <p class="qualification-rule-review-status" role="${model.role}" aria-live="${model.ariaLive}"><strong>${model.label}</strong><span>${model.message}</span></p>\n <p class="qualification-rule-review-next-action"><strong>Next action</strong><span>${model.nextAction}</span></p>\n <button class="qualification-rule-review-submit" type="button"${disabled}>${model.actionLabel}</button>\n</section>`;
}
26 changes: 26 additions & 0 deletions apps/hr-workspace/qualification-rule-review-state.stories.js
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions docs/traceability/hr-workspace-qualification-rule-review-state.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading