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
54 changes: 54 additions & 0 deletions .github/workflows/hr-workspace-legal-employer-history-state.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: HR Workspace Legal Employer History State Quality

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

permissions:
contents: read

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

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

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

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

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

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

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

.legal-employer-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;
}

.legal-employer-history-action:hover:not(:disabled) {
opacity: 0.88;
}

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

.legal-employer-history-action:focus-visible {
outline: 3px solid var(--orgmetra-focus-ring);
outline-offset: 3px;
}
80 changes: 80 additions & 0 deletions apps/hr-workspace/legal-employer-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 legal employer history',
label: 'Review legal employer history',
message: 'Load fresh purpose-authorized legal-employer history for the requested Employment business-time and system-knowledge coordinate.',
nextAction: 'Load the current authorized legal-employer history before relying on this Employee Profile evidence.',
}),
loading: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', actionDisabled: true,
interactionState: 'loading', actionLabel: 'Loading legal employer history',
label: 'Loading legal employer history',
message: 'Orgmetra is resolving the authorized fields and visible legal-employer versions at the requested known-at coordinate.',
nextAction: 'Wait for the governed legal-employer read to finish.',
}),
ready: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: true,
interactionState: 'read-only', actionLabel: 'Legal employer history loaded',
label: 'Legal employer history ready',
message: 'This is read-only bitemporal legal-employer history linking Employment evidence to the visible employing legal Organization. Effective time shows when the employer relationship applied; system-recorded time shows when Orgmetra knew it. Legal-employer truth is independent of Position and Assignment. This read does not authorize Employment or Organization mutation, payroll action, or statutory action.',
nextAction: 'Use only the authorized visible fields; start a separately authorized change if Employment or legal Organization truth must be updated.',
}),
empty: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', actionDisabled: false,
interactionState: 'read-only', actionLabel: 'Reload legal employer history',
label: 'No legal employer is visible here',
message: 'No employing legal Organization is visible at this authorized business-time and known-at coordinate. This is not evidence of no Employment or no legal-employer 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: 'Legal employer history access denied',
message: 'The current purpose or actor authority does not permit this legal-employer history read.',
nextAction: 'Check the HR purpose and legal-organization access authority before requesting this history again.',
}),
stale: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', actionDisabled: false,
interactionState: 'validation-error', actionLabel: 'Reload legal employer history',
label: 'Legal employer evidence is stale',
message: 'The requested known-at coordinate or authoritative Employment/legal Organization evidence changed before this view could be relied on.',
nextAction: 'Reload the purpose-authorized legal-employer 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: 'Legal employer fields are not authorized',
message: 'One or more requested legal-employer fields fall outside the current purpose-bound organization scope.',
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: 'Legal employer history unavailable',
message: 'The governed legal-employer history read did not return usable authoritative evidence.',
nextAction: 'Do not infer Employment, employer, Position, Assignment, payroll, or statutory state from cached or partial data; verify the service and authorization before retrying.',
}),
});

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

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

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

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

function story(state) {
return () => legalEmployerHistoryStateMarkup(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,23 @@
# HR Workspace legal-employer history accessibility references

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

This note records design and accessibility inputs for the active legal-employer-history interaction slice. It is engineering evidence, not a claim of accessibility certification, employment-law compliance, payroll compliance, or statutory 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, organization-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 legal-employer history remains explicitly read-only. UI evidence does not grant Employment or Organization mutation authority, payroll authority, statutory authority, or consequential employment-decision authority.
- The legal-employer relationship is described independently of Position and Assignment so presentation does not collapse distinct HRIS entities.
- Empty and stale states prohibit inference beyond the exact authorized business-time, system-knowledge, and organization-scope coordinate.
- Figma `Orgmetra Baseline` Storybook Inventory node `1:64` was freshly re-read on 2026-08-29 and continues to enumerate Timeline, EmptyState, ErrorState, PermissionDenied, and the default, hover, focus, disabled, loading, validation-error, read-only, and high-risk-confirmation interaction vocabulary. This slice reuses that design system rather than introducing parallel geometry or tokens.
44 changes: 44 additions & 0 deletions docs/traceability/hr-workspace-legal-employer-history-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# HR Workspace legal-employer history interaction traceability

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

## Buyer need

Employee Profile needs to explain which employing legal Organization is visible for an Employment at a selected business-time and system-knowledge coordinate without conflating legal-employer truth with Job, Position, Assignment, payroll, or statutory action. The read must remain useful to authorized HR users while minimizing generic UI-state evidence.

## Ownership boundary

- PR #130 owns the shared protected-read accessibility semantics and Figma/Storybook interaction system.
- PR #141 separately owns the active-PR authoritative bitemporal Employment → employing legal Organization relationship. Its backend evidence does not transfer into this UI lane and is not protected-main truth until integrated.
- This child owns **presentation/interaction only** for legal-employer history. It introduces no Employment or Organization writer, no cross-service application-table SQL, and no dedicated-writer dependency mutation.

## Governed interaction evidence

`apps/hr-workspace/legal-employer-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 legal-employer relationship applied; system-recorded time is when Orgmetra knew it. It also states that employing legal Organization truth is independent of Position and Assignment. The state is read-only and cannot authorize Employment/Organization mutation, payroll action, or statutory action. `empty`, `stale`, and failure states prohibit inference outside the exact authorized business-time, known-at, and organization-scope coordinate.

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

## Design and accessibility evidence

Figma `Orgmetra Baseline` Storybook Inventory node `1:64` was freshly re-read on 2026-08-29 (Asia/Seoul). It continues to enumerate `Timeline`, `EmptyState`, `ErrorState`, `PermissionDenied`, and the required default/hover/focus/disabled/loading/validation-error/read-only/high-risk-confirmation vocabulary. This slice uses the existing Orgmetra design tokens, visible `:focus-visible` treatment, loading semantics, read-only presentation, failure alerts, and 44px action target sizing. WCAG 2.2 and WAI-ARIA 1.2 references are recorded under `docs/doctoring/hr-workspace-legal-employer-history-accessibility-references.md`.

## Executable acceptance

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

- the full bounded state set and concrete next actions;
- read-only Employment/legal-Organization bitemporal explanation without mutation, payroll, or statutory authority;
- explicit independence from Position and Assignment;
- no unsafe inference from empty/stale/scope-blocked/error states;
- no generic-state PII, HR identifiers, legal/tax/payroll values, credentials, prompts, or model output;
- 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.

## Contract-first repair evidence

Contract head `b954b6330f5542b8c1272d4e2317c5ebf108e990` intentionally lacked the production state module. Hosted run `33219185795`, job `99009424985`, checked out and proved that exact SHA, configured Node 24.19.0, then failed at the focused contract with `ERR_MODULE_NOT_FOUND` for `apps/hr-workspace/legal-employer-history-state.js`. This is the realistic RED boundary for the buyer-visible slice; the subsequent implementation must obtain new exact-current-head GREEN evidence before the PR can advance.

After #53 and #130 integrate dependency-first, this child must be retargeted to fresh `develop`, reconciled with then-current Employee Profile and the integrated/then-current legal-employer 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