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-assignment-history-state.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: HR Workspace Assignment History State Quality

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

permissions:
contents: read

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

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

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

.assignment-history-status span,
.assignment-history-next-action span {
color: var(--orgmetra-text-muted);
}

.assignment-history-state[data-interaction-state="read-only"] {
border-color: var(--orgmetra-border-subtle);
}

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

.assignment-history-state[aria-busy="true"] {
cursor: progress;
}

.assignment-history-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;
}

.assignment-history-action:disabled {
cursor: not-allowed;
opacity: 0.62;
}

.assignment-history-action:focus-visible {
outline: 3px solid var(--orgmetra-focus-ring);
outline-offset: 3px;
}
80 changes: 80 additions & 0 deletions apps/hr-workspace/assignment-history-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const STATE_MODELS = Object.freeze({
idle: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: false,
interactionState: 'default', actionLabel: 'Load assignment history',
label: 'Review assignment history',
message: 'Load fresh purpose-authorized Assignment history for the requested business-time and system-knowledge coordinate.',
nextAction: 'Load the current authorized Assignment history before relying on this Employee Profile evidence.',
}),
loading: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', actionDisabled: true,
interactionState: 'loading', actionLabel: 'Loading assignment history',
label: 'Loading assignment history',
message: 'Orgmetra is resolving the authorized fields and visible Assignment versions at the requested known-at coordinate.',
nextAction: 'Wait for the governed Assignment-history read to finish.',
}),
ready: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: true,
interactionState: 'read-only', actionLabel: 'Assignment history loaded',
label: 'Assignment history ready',
message: 'This is read-only bitemporal Assignment history. Effective time shows when a fact applied; system-recorded time shows when Orgmetra knew it. This evidence does not authorize Assignment mutation and does not infer current worker status beyond the exact known-at snapshot.',
nextAction: 'Use only the authorized visible fields; start a separately authorized change if Assignment truth must be updated.',
}),
empty: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: false,
interactionState: 'read-only', actionLabel: 'Reload assignment history',
label: 'No assignment history is visible here',
message: 'No Assignment version is visible at this authorized business-time and known-at coordinate. This is not evidence of no Employment or no Assignment evidence outside this coordinate.',
nextAction: 'Check the authorized time coordinate and reload if another business-time or known-at view is required.',
}),
denied: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
interactionState: 'permission-denied', actionLabel: 'Review access',
label: 'Assignment history access denied',
message: 'The current purpose or actor authority does not permit this Assignment-history read.',
nextAction: 'Check the HR purpose and access authority before requesting Assignment history again.',
}),
stale: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
interactionState: 'validation-error', actionLabel: 'Reload assignment history',
label: 'Assignment history evidence is stale',
message: 'The requested known-at coordinate or authoritative Assignment evidence changed before this view could be relied on.',
nextAction: 'Reload the purpose-authorized Assignment history at an explicit fresh known-at coordinate.',
}),
scopeBlocked: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
interactionState: 'validation-error', actionLabel: 'Narrow requested fields',
label: 'Assignment history fields are not authorized',
message: 'One or more requested Assignment-history fields fall outside the current purpose-bound authorization.',
nextAction: 'Narrow the requested fields to the authorized set or obtain the required HR access before retrying.',
}),
error: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
interactionState: 'error', actionLabel: 'Retry governed read',
label: 'Assignment history unavailable',
message: 'The governed Assignment-history read did not return usable authoritative evidence.',
nextAction: 'Do not infer Assignment or Employment status from cached or partial data; verify the service and authorization before retrying.',
}),
});

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

/** Return immutable accessibility semantics for one purpose-bound Assignment-history interaction state. */
export function assignmentHistoryViewModel(state) {
return requireExactState(state);
}

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

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

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

export const Idle = story('idle');
export const Loading = story('loading');
export const ReadyReadOnly = story('ready');
export const Empty = story('empty');
export const PermissionDenied = story('denied');
export const StaleEvidence = story('stale');
export const ScopeBlocked = story('scopeBlocked');
export const Error = story('error');
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# HR Workspace Assignment History accessibility references

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

This note records design inputs for the active Assignment-history interaction slice. It is evidence for engineering decisions, not a claim of accessibility certification or legal compliance.

## Primary standards

World Wide Web Consortium. (2024, December 12). *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/

World Wide Web Consortium. (2025, October 21). *Web Content Accessibility Guidelines (WCAG) 2.2 approved as ISO/IEC international standard*. https://www.w3.org/press-releases/2025/wcag22-iso-pas/

## Applied consequences

- Loading is perceivable through `aria-busy=true`, and transient status updates use a polite live region.
- Permission, stale-evidence, field-scope, and transport failures use assertive alert semantics and provide a concrete next action.
- Keyboard focus remains visible through the existing Orgmetra focus token; actionable controls preserve a 44px minimum target height.
- Loaded Assignment history remains explicitly read-only. UI evidence does not grant Assignment mutation authority or consequential employment-decision authority.
- Empty and stale states prohibit inference beyond the exact authorized business-time and system-knowledge coordinate.
- Figma `Orgmetra Baseline` Storybook Inventory node `1:64` was freshly re-read on 2026-08-28 and continues to require default, hover, focus, disabled, loading, validation-error, read-only, and high-risk-confirmation interaction states. This slice reuses that design system rather than introducing parallel geometry or tokens.
38 changes: 38 additions & 0 deletions docs/traceability/hr-workspace-assignment-history-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# HR Workspace Assignment History interaction traceability

Status: **active PR only; not protected-main truth**.

## Buyer need

The PRD identifies Employee Profile with bitemporal Assignment history as a P1 HRIS surface. A customer needs to understand what Assignment evidence was effective and what Orgmetra knew at a selected system-knowledge coordinate without converting that read into mutation authority or leaking fields outside the authorized HR purpose.

## Ownership boundary

- PR #142 owns the separate purpose-bound People API Assignment-history read contract. Its active-PR backend evidence does not transfer into this UI lane.
- PR #130 owns the shared protected-read accessibility semantics and Figma/Storybook interaction system.
- This child owns **presentation/interaction only** for Assignment history. It introduces no Assignment writer, no cross-service SQL, and no dedicated-writer dependency mutation.

## Governed interaction evidence

`apps/hr-workspace/assignment-history-state.js` exposes only constant, value-minimized state semantics for `idle`, `loading`, `ready`, `empty`, `denied`, `stale`, `scopeBlocked`, and `error`.

The `ready` state explains the bitemporal distinction: effective time is when the Assignment fact applied; system-recorded time is when Orgmetra knew it. The state is read-only and cannot authorize Assignment mutation. `empty`, `stale`, and failure states prohibit inference outside the exact authorized business-time and known-at coordinate.

The view model intentionally carries no Person, Employment, Assignment, Job, Position, or Organization identifiers; worker names/contact data; compensation, rating, assessment, or candidate values; credentials/tokens; prompts; or model output. Actual authorized HR values remain backend response data and must be handled through purpose-bound field authorization rather than embedded in generic UI-state evidence.

## Design and accessibility evidence

Figma `Orgmetra Baseline` Storybook Inventory node `1:64` was freshly re-read on 2026-08-28. The executable Storybook stories correlate to that node and reuse existing Orgmetra CSS tokens, visible `:focus-visible` treatment, loading semantics, read-only presentation, failure alerts, and 44px action target sizing. Current W3C WCAG 2.2 and WAI-ARIA 1.2 references are recorded under `docs/doctoring/hr-workspace-assignment-history-accessibility-references.md`.

## Executable acceptance

`tests/hr-workspace-assignment-history-state.test.mjs` requires:

- the full bounded state set and concrete next actions;
- read-only bitemporal explanation without mutation authority;
- no unsafe inference from empty/stale/scope-blocked/error states;
- exact built-in string state names and rejection of prototype-inherited names such as `constructor`, `toString`, and `__proto__`;
- Figma node correlation, Storybook inventory, existing focus/design tokens, loading/read-only/failure CSS states, and 44px action target; and
- exact 100% owned line, branch, and function coverage in the dedicated workflow.

After #53 and #130 integrate dependency-first, this child must be retargeted to fresh `develop`, reconciled with then-current Employee Profile and #142 backend truth, and rerun through applicable browser/accessibility/Foundation/Recovery/SAST/Security and central required workflows. No parent or predecessor check/review transfers.
Loading
Loading