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-position-lifecycle-review-state.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: HR Workspace Position Lifecycle Review State Quality

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

permissions:
contents: read

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

jobs:
position-lifecycle-review-state:
name: Position lifecycle 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 Position lifecycle 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-position-lifecycle-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/position-lifecycle-review-state.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.position-lifecycle-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);
}

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

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

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

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

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

.position-lifecycle-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;
}

.position-lifecycle-review-submit:disabled {
cursor: wait;
opacity: 0.62;
}

.position-lifecycle-review-submit:focus-visible {
outline: 3px solid var(--orgmetra-focus-ring);
outline-offset: 3px;
}
86 changes: 86 additions & 0 deletions apps/hr-workspace/position-lifecycle-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 Position evidence',
label: 'Review Position lifecycle evidence',
message: 'Load the fresh Position and Assignment evidence before making a human lifecycle review.',
nextAction: 'Load the current governed Position and staffing evidence for one lifecycle review.',
}),
loading: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
interactionState: 'loading', actionLabel: 'Loading Position evidence',
label: 'Loading governed Position evidence',
message: 'Orgmetra is waiting for fresh Position and Assignment evidence. No cached lifecycle truth is used.',
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 lifecycle review',
label: 'Position lifecycle change requires human confirmation',
message: 'The evidence is read-only input for human review. Confirming this review does not apply, freeze, close, abolish, or reactivate a Position.',
nextAction: 'Confirm the reviewed lifecycle 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 Position lifecycle 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 Position lifecycle review recorded',
message: 'The recorded review is evidence only and does not apply a Position lifecycle mutation.',
nextAction: 'Return to the Position lifecycle queue. Authoritative application is separate and must re-resolve fresh Position and Assignment truth.',
}),
denied: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'permission-denied', actionLabel: 'Review access',
label: 'Position lifecycle review access denied',
message: 'The current purpose or reviewer authority does not permit this Position lifecycle 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: 'Position lifecycle evidence is stale',
message: 'The Position or Assignment evidence changed before review recording.',
nextAction: 'Reload fresh authoritative Position and Assignment evidence before reviewing again.',
}),
blocked: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'validation-error', actionLabel: 'Reload staffing evidence',
label: 'Position lifecycle change is blocked by staffing state',
message: 'The current staffing evidence conflicts with the proposed lifecycle change. This screen cannot override that safety boundary.',
nextAction: 'Reload authoritative staffing evidence and resolve the staffing conflict before reviewing again.',
}),
error: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'error', actionLabel: 'Retry governed load',
label: 'Position lifecycle review unavailable',
message: 'The governed evidence or immutable review service did not return a usable result. No cached review is accepted.',
nextAction: 'Verify the service and authorization before retrying the governed evidence load.',
}),
});

function requireExactState(value) {
if (typeof value !== 'string') {
throw new TypeError('Position lifecycle 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 Position lifecycle review state: ${value}`);
return model;
}

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

/** Render one static Storybook proof without accepting caller-controlled HR values. */
export function positionLifecycleReviewStateMarkup(state) {
const model = requireExactState(state);
const disabled = model.submitDisabled ? ' disabled' : '';
return `<section class="position-lifecycle-review-state" data-figma-node-id="1:64" data-interaction-state="${model.interactionState}" aria-busy="${model.ariaBusy}">\n <p class="position-lifecycle-review-status" role="${model.role}" aria-live="${model.ariaLive}"><strong>${model.label}</strong><span>${model.message}</span></p>\n <p class="position-lifecycle-review-next-action"><strong>Next action</strong><span>${model.nextAction}</span></p>\n <button class="position-lifecycle-review-submit" type="button"${disabled}>${model.actionLabel}</button>\n</section>`;
}
26 changes: 26 additions & 0 deletions apps/hr-workspace/position-lifecycle-review-state.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { positionLifecycleReviewStateMarkup } from './position-lifecycle-review-state.js';
import './position-lifecycle-review-state.css';

export default {
title: 'HR Workspace/Position Lifecycle Review States',
parameters: {
design: {
type: 'figma',
url: 'Orgmetra Baseline — Storybook Inventory node 1:64',
},
},
};

function story(state) {
return () => positionLifecycleReviewStateMarkup(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 StaffingBlocked = story('blocked');
export const Error = story('error');
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# HR Workspace Position lifecycle review accessibility references

Checked against the current W3C Recommendation baselines used by the existing HR Workspace interaction stack. These references support interaction semantics only; they do not establish Position mutation, staffing, or employment-decision authority.

## Design implications

- WAI-ARIA 1.2 defines `status` as advisory live-region information and `alert` as assertive information that does not itself require focus. Orgmetra therefore uses `status` for ordinary load/review/recording progress and `alert` for denial, stale evidence, staffing conflicts, and failures.
- WAI-ARIA 1.2 defines `aria-busy` for content that is being updated. Orgmetra sets it while loading authoritative evidence or recording one human review, and the action is disabled to prevent a duplicate interaction.
- WCAG 2.2 remains the normative WCAG Recommendation baseline used here. The interaction preserves visible keyboard focus, a 44px minimum action target in the existing design-token system, actionable non-color-only state copy, and explicit next actions.
- The Figma `Orgmetra Baseline` Storybook Inventory node `1:64` explicitly requires `default / hover / focus / disabled / loading / validation-error / read-only / high-risk-confirmation` states. This slice implements those semantics for a Position lifecycle review without inventing a new geometry or application authority.

## 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

High-risk confirmation in this Storybook evidence records the need for accountable human review; it does not execute a Position lifecycle mutation. PR #111 remains the review-evidence owner and PR #112 remains the authoritative application owner. Before consequential application, fresh bitemporal Position/Assignment evidence and staffing safety must be re-established by the authoritative Orgmetra boundary.
35 changes: 35 additions & 0 deletions docs/traceability/hr-workspace-position-lifecycle-review-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# HR Workspace Position lifecycle review state traceability

## Truth boundary

- This file is **active-PR evidence only** until the dependency stack integrates and is revalidated against fresh `develop`.
- Parent UI contract: PR #130 owns shared protected-read accessibility semantics and Figma/Storybook correlation.
- Position lifecycle governance owner: PR #111 owns the human-reviewed `PositionLifecycleChangeReviewPacket`; this UI does not recreate that evidence contract.
- Authoritative application owner: dependency-first PR #112 owns the later Position lifecycle mutation boundary. This UI does not write Position/Assignment truth and does not authorize an application.

## Buyer-visible requirement → evidence

| Requirement | Active-PR evidence | Safety meaning |
| --- | --- | --- |
| Load fresh Position and Assignment evidence before review | `loading` → `review` state model and Storybook stories | Cached or locally invented lifecycle truth is not presented as sufficient evidence. |
| Show a consequential review as explicit human confirmation | `review` uses `high-risk-confirmation` and a `Confirm lifecycle review` action | Confirmation records human review only; it does not apply, freeze, close, abolish, or reactivate a Position. |
| Prevent duplicate review recording | `recording` is busy and disables the action | One in-flight review recording is visible and duplicate submission is blocked in the interaction model. |
| Recorded review remains evidence only | `recorded` is read-only and disables the action | A completed review is not mutation authority. |
| Stale authoritative evidence fails closed | `stale` is an assertive validation alert | The reviewer must reload fresh authoritative Position and Assignment evidence before another review. |
| Staffing conflict fails closed | `blocked` is an assertive validation alert | The UI cannot override a staffing-safety boundary; fresh authoritative staffing evidence must be re-resolved. |
| Denied reviewer/purpose scope fails closed | `denied` is an assertive permission alert | No local or cached downgrade path is offered. |
| Service failure names a safe next action | `error` rejects cached review and instructs service/authorization verification | Customer-facing recovery copy does not fabricate evidence. |
| Figma/Storybook state inventory stays inspectable | every rendered state carries `data-figma-node-id="1:64"`; Storybook exposes nine named states | Reviewers can inspect default, loading, high-risk-confirmation, read-only, permission-denied, validation-error, error and focus-related evidence. |
| Owned interaction logic has exact coverage | focused workflow requires Node 24 100% line/branch/function coverage | Missing owned branches are non-passing. |

## Data minimization

The view model contains only constant governed copy and interaction semantics. It accepts no Person or candidate identity, worker name/contact data, compensation or salary values, ratings, assessment results, credentials or bearer tokens, prompts, model output, free-form HR notes, or Position/Assignment payload values.

## Consequential-decision boundary

A human confirmation in this UI is not a Position mutation and is not an employment decision. Before any authoritative change, the application boundary must freshly re-resolve same-tenant bitemporal Position and Assignment truth, the reviewed lifecycle evidence, staffing safety, actor separation/authority, effective/business time, system-recorded time, and immutable audit/outbox evidence. UI state never substitutes for those checks.

## Verification and stack discipline

The dedicated workflow runs the Position lifecycle interaction contract directly on Node 24, checks out the exact candidate head, requires exact 100% line/branch/function coverage, and proves a clean checkout. Parent #130, PR #111 and PR #112 checks/reviews never transfer to this child. After #130 actually integrates, retarget this child to fresh `develop`, reconcile any UI/design changes, and rerun every applicable browser/accessibility/Foundation/Recovery/SAST/Security and central required workflow on one resulting exact head.
Loading
Loading