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-hiring-decision-record.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: HR Workspace Hiring Decision Record State Quality

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

permissions:
contents: read

concurrency:
group: hr-workspace-hiring-decision-record-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

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

.hiring-decision-record-status,
.hiring-decision-record-confirmation,
.hiring-decision-record-evidence,
.hiring-decision-record-next-action {
display: grid;
gap: var(--orgmetra-space-xs);
margin: 0;
}

.hiring-decision-record-status span,
.hiring-decision-record-confirmation span,
.hiring-decision-record-evidence span,
.hiring-decision-record-next-action span {
color: var(--orgmetra-text-muted);
}

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

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

.hiring-decision-record-state[aria-busy="true"] {
cursor: progress;
}

.hiring-decision-record-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;
}

.hiring-decision-record-action:hover:not(:disabled) {
opacity: 0.88;
}

.hiring-decision-record-action:disabled {
cursor: not-allowed;
opacity: 0.62;
}

.hiring-decision-record-action:focus-visible {
outline: 3px solid var(--orgmetra-focus-ring);
outline-offset: 3px;
}
102 changes: 102 additions & 0 deletions apps/hr-workspace/hiring-decision-record-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const STATE_MODELS = Object.freeze({
idle: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: false,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'default', actionLabel: 'Load decision evidence',
label: 'Review hiring decision evidence',
message: 'Load fresh purpose-authorized candidate, Job, criterion, and decision evidence before recording a consequential hiring decision.',
nextAction: 'Load the current governed decision evidence before reviewing or confirming a hiring decision.',
}),
loading: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', actionDisabled: true,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'loading', actionLabel: 'Loading decision evidence',
label: 'Loading hiring decision evidence',
message: 'Orgmetra is resolving the authorized candidate, Job, criterion, evidence-version, and decision-scope references for this purpose-bound review.',
nextAction: 'Wait for the governed hiring-decision evidence read to finish.',
}),
review: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
humanConfirmationRequired: true, decisionRecorded: false,
interactionState: 'high-risk-confirmation', actionLabel: 'Confirm and record decision',
label: 'Human confirmation required',
message: 'An accountable human must confirm this hiring decision with actor, purpose, reason, and evidence version. This presentation state does not authorize worker materialization.',
nextAction: 'Verify the criterion-linked evidence and limitations, then explicitly confirm the governed decision only if the evidence supports the accountable human judgment.',
}),
recording: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', actionDisabled: true,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'loading', actionLabel: 'Recording confirmed decision',
label: 'Recording confirmed hiring decision',
message: 'Orgmetra is submitting the confirmed decision to the authoritative decision boundary. This in-progress state is not proof that the decision was recorded.',
nextAction: 'Do not resubmit or act on the outcome until the authoritative decision record and immutable audit evidence are returned.',
}),
recorded: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: true,
humanConfirmationRequired: false, decisionRecorded: true,
interactionState: 'read-only', actionLabel: 'Hiring decision recorded',
label: 'Hiring decision recorded',
message: 'The authoritative hiring decision record and immutable audit evidence were returned. This read-only UI does not itself create an offer, employment, or candidate-to-worker link.',
nextAction: 'Review the immutable decision evidence and continue only through the separately governed offer or confirmed-hire boundary that applies to this outcome.',
}),
denied: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'permission-denied', actionLabel: 'Review access',
label: 'Hiring decision access denied',
message: 'The current actor or HR purpose does not permit this governed decision-record read or confirmation.',
nextAction: 'Check the HR purpose and decision-record authority before requesting or confirming a hiring decision again.',
}),
stale: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'validation-error', actionLabel: 'Reload decision evidence',
label: 'Hiring decision evidence is stale',
message: 'Candidate, Job, criterion, evidence-version, or decision-scope truth changed before the decision could be safely confirmed.',
nextAction: 'Reload the current governed candidate, Job, and decision evidence before reviewing the hiring decision again.',
}),
evidenceBlocked: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'validation-error', actionLabel: 'Return to evidence review',
label: 'Hiring decision evidence is incomplete',
message: 'The governed decision boundary cannot prove the required criterion-linked evidence, limitations, actor context, purpose, reason, or evidence version.',
nextAction: 'Return to evidence review, resolve the missing governed evidence, and start a new accountable confirmation from fresh authority.',
}),
error: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
humanConfirmationRequired: false, decisionRecorded: false,
interactionState: 'error', actionLabel: 'Reconcile decision status',
label: 'Hiring decision status unavailable',
message: 'The decision submission did not return usable authoritative decision and audit evidence, so Orgmetra does not treat the hiring decision as recorded.',
nextAction: 'Reconcile the authoritative decision record and immutable audit evidence before retrying or taking any downstream hiring action.',
}),
});

function requireExactState(value) {
if (typeof value !== 'string') {
throw new TypeError('hiring-decision-record state must be an exact built-in string');
}
if (!Object.hasOwn(STATE_MODELS, value)) {
throw new TypeError(`unsupported hiring-decision-record state: ${value}`);
}
return STATE_MODELS[value];
}

/** Return immutable accessibility semantics for one purpose-bound hiring-decision workflow state. */
export function hiringDecisionRecordViewModel(state) {
return requireExactState(state);
}

/** Render static Storybook workflow evidence without accepting caller-controlled candidate or decision values. */
export function hiringDecisionRecordMarkup(state) {
const model = requireExactState(state);
const disabled = model.actionDisabled ? ' disabled' : '';
const confirmationText = model.humanConfirmationRequired
? 'Required before the governed decision may be submitted.'
: 'Not available in this workflow state.';
const recordedText = model.decisionRecorded
? 'Authoritative decision and immutable audit evidence returned.'
: 'No recorded decision is asserted by this workflow state.';
return `<section class="hiring-decision-record-state" data-figma-node-id="1:64" data-figma-component="DecisionRecord" data-interaction-state="${model.interactionState}" data-human-confirmation-required="${model.humanConfirmationRequired}" data-decision-recorded="${model.decisionRecorded}" aria-busy="${model.ariaBusy}">\n <p class="hiring-decision-record-status" role="${model.role}" aria-live="${model.ariaLive}"><strong>${model.label}</strong><span>${model.message}</span></p>\n <p class="hiring-decision-record-confirmation"><strong>Human confirmation</strong><span>${confirmationText}</span></p>\n <p class="hiring-decision-record-evidence"><strong>Decision evidence</strong><span>${recordedText}</span></p>\n <p class="hiring-decision-record-next-action"><strong>Next action</strong><span>${model.nextAction}</span></p>\n <button class="hiring-decision-record-action" type="button"${disabled}>${model.actionLabel}</button>\n</section>`;
}
26 changes: 26 additions & 0 deletions apps/hr-workspace/hiring-decision-record-state.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { hiringDecisionRecordMarkup } from './hiring-decision-record-state.js';
import './hiring-decision-record-state.css';

export default {
title: 'HR Workspace/Hiring Decision Record States',
parameters: {
design: {
type: 'figma',
url: 'Orgmetra Baseline — Recruiting Workspace node 1:22 / Storybook Inventory node 1:64 / DecisionRecord',
},
},
};

function story(state) {
return () => hiringDecisionRecordMarkup(state);
}

export const Idle = story('idle');
export const Loading = story('loading');
export const HumanConfirmation = story('review');
export const Recording = story('recording');
export const RecordedReadOnly = story('recorded');
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,30 @@
# Hiring decision record interaction and evidence references

Reviewed: 2026-08-28 (Asia/Seoul).

This note records primary standards used by the active Hiring decision record presentation slice. It is engineering/design evidence only. It does not claim WCAG certification, legal compliance, professional validation of a selection procedure, or authority for an autonomous hiring decision.

## Primary standards

American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/open-access-files.html

Equal Employment Opportunity Commission. (1978). *Uniform guidelines on employee selection procedures*, 29 C.F.R. Part 1607. https://www.eeoc.gov/regulations-and-guidelines

Society for Industrial and Organizational Psychology. (2018). Principles for the validation and use of personnel selection procedures. *Industrial and Organizational Psychology, 11*(S1), 1–97. https://doi.org/10.1017/iop.2018.195

World Wide Web Consortium. (2023, October 5). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/

World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/

## Design consequences

- A hiring decision is a high-impact accountable human action. The interaction requires explicit confirmation after the actor, purpose, reason, evidence version, criterion-linked evidence, and limitations are visible from authoritative governed sources.
- The presentation state never converts an assessment score, interview result, model output, or matching signal into decision authority. It also never creates an offer, Employment, or candidate-to-worker link.
- Recording is fail-closed: an in-progress submission is not treated as a recorded decision. Only the separately authoritative decision boundary plus immutable audit evidence can establish a recorded outcome.
- Recorded state is read-only. Corrections or downstream offer/hire actions must use their separately governed boundaries rather than mutating presentation state.
- Loading and recording expose `aria-busy` and disable duplicate actions. Denied, stale, evidence-blocked, and error states use alert semantics and provide a concrete next action.
- Generic interaction evidence is value-minimized and contains no candidate/Person identity, Job/requisition/application identifier, raw evidence, score, decision code/outcome, rating, compensation, credential, token, prompt, or model output.
- Interactive actions preserve visible `:focus-visible` treatment and a 44-pixel minimum target height using existing Orgmetra design tokens.
- Fresh Figma `Orgmetra Baseline` metadata was read on 2026-08-28. Recruiting Workspace node `1:22` names `Decision record with criterion evidence`; Storybook Inventory node `1:64` lists `DecisionRecord` and requires default, hover, focus, disabled, loading, validation-error, read-only, and high-risk-confirmation states.

The active PR remains dependency-first. Focused child evidence is not shipped-product, legal-compliance, or selection-validity evidence.
42 changes: 42 additions & 0 deletions docs/traceability/hr-workspace-hiring-decision-record.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# HR Workspace Hiring decision record traceability

Status: **active PR evidence only**. This document does not change protected-main shipped truth and does not authorize an employment decision.

## Ownership boundary

- Parent PR #130 owns the shared HR Workspace protected-read interaction semantics and Figma/Storybook accessibility contract.
- This dependency-first child owns only the Recruiting Workspace / `DecisionRecord` presentation and workflow-state shell.
- Existing Orgmetra selection-review/selection-decision evidence owners remain authoritative for decision evidence. Offer approval/response, confirmed-hire authority, People/Employment mutation, and candidate-to-worker conversion remain separate governed owners.
- This UI does not create a new selection algorithm, assessment owner, offer authority, hire authority, Employment mutation path, or candidate-to-worker linkage path.
- Dedicated-writer CWL repositories remain read-only dependencies and are not mutated by this slice.

## Buyer-visible contract

Protected PRD P1 names the Hiring decision record. Protected wireframes place the Selection decision record in Recruiting Workspace. Fresh Figma `Orgmetra Baseline` metadata identifies Recruiting Workspace node `1:22` with `Decision record with criterion evidence`, and Storybook Inventory node `1:64` lists `DecisionRecord`.

This slice provides bounded executable states:

`idle / loading / review / recording / recorded / denied / stale / evidenceBlocked / error`.

`review` is a high-risk-confirmation state. It requires an accountable human to verify criterion-linked evidence and limitations and explicitly confirm actor, purpose, reason, and evidence version. `recording` disables duplicate submission and is explicitly not proof of persistence. `recorded` is read-only and is asserted only after the authoritative decision boundary returns immutable audit evidence. No state itself creates an offer, Employment, or candidate-to-worker link.

Denied, stale, evidence-blocked, and error states fail closed with concrete next actions. Generic state payloads contain no candidate/Person identity, Job/requisition/application identifier, raw evidence, decision code/outcome, selection/assessment/interview score, rating, compensation, credential/token, prompt, or model output. Unsupported runtime values and prototype-inherited state names such as `constructor`, `toString`, and `__proto__` are rejected through exact primitive type plus own-key membership checks.

## Design and standards evidence

Fresh Figma `Orgmetra Baseline` metadata was read on 2026-08-28. Node `1:22` names the Recruiting Workspace decision record with criterion evidence. Storybook Inventory node `1:64` continues to require default, hover, focus, disabled, loading, validation-error, read-only, and high-risk-confirmation behavior. The implementation reuses existing Orgmetra tokens and Storybook rather than creating a parallel design system.

WCAG 2.2, WAI-ARIA 1.2, the *Standards for Educational and Psychological Testing*, the Uniform Guidelines on Employee Selection Procedures, and SIOP's fifth-edition personnel-selection principles are recorded under `docs/doctoring/hr-workspace-hiring-decision-record-accessibility-references.md`. These sources constrain interaction and evidence interpretation; they are not evidence that an individual procedure or decision is valid or lawful.

## Verification contract

`.github/workflows/hr-workspace-hiring-decision-record.yml` must:

1. check out and prove the exact candidate SHA;
2. use the reviewed Node 24 toolchain;
3. execute the focused interaction/privacy/fail-closed regression with exact 100% line, branch, and function coverage; and
4. finish with a clean checkout.

Contract-only head `b721e46b11612733026139da0a41bb16293d77b7` produced genuine hosted RED: run `33162121276`, job `98818811945` checked out and proved that exact SHA, set up Node, then failed at the focused hiring-decision record contract while production `apps/hr-workspace/hiring-decision-record-state.js` was intentionally absent. The clean-checkout step was correctly skipped after the focused failure.

Focused child GREEN remains stack-local only. After #53 and #130 integrate, this child must be retargeted/reconciled against fresh `develop` and all applicable browser/accessibility/Foundation/Recovery/SAST/Security and central required workflows must execute again on one resulting exact head. Parent or predecessor checks/reviews never transfer.
Loading
Loading