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-employment-absence-state.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: HR Workspace Employment Absence State Quality

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

permissions:
contents: read

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

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

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

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

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

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

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

.employment-absence-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;
}

.employment-absence-submit:disabled {
cursor: not-allowed;
opacity: 0.62;
}

.employment-absence-submit:focus-visible {
outline: 3px solid var(--orgmetra-focus-ring);
outline-offset: 3px;
}
79 changes: 79 additions & 0 deletions apps/hr-workspace/employment-absence-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const STATE_MODELS = Object.freeze({
idle: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: false,
interactionState: 'default', actionLabel: 'Load absence evidence',
label: 'Review Employment absence evidence',
message: 'Load fresh reason-free Employment absence evidence for the requested business date and system-knowledge cutoff.',
nextAction: 'Load the current governed Employment and absence evidence for this authorized HR task.',
}),
loading: Object.freeze({
ariaBusy: 'true', ariaLive: 'polite', role: 'status', submitDisabled: true,
interactionState: 'loading', actionLabel: 'Loading absence evidence',
label: 'Loading current absence evidence',
message: 'Orgmetra is resolving fresh Employment and reason-free absence evidence. Cached or partial absence evidence is not accepted.',
nextAction: 'Wait for the current Employment absence evidence load to finish.',
}),
absent: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: true,
interactionState: 'read-only', actionLabel: 'Absence evidence loaded',
label: 'Employment is absent at this coordinate',
message: 'This is reason-free operational evidence that the Employment is absent at the reviewed business/system coordinate. It does not disclose why and does not authorize leave, scheduling, compensation, or an employment decision.',
nextAction: 'If a consequential action is required, continue only through the separately governed leave, work-capacity, scheduling, or Employment boundary.',
}),
notAbsent: Object.freeze({
ariaBusy: 'false', ariaLive: 'polite', role: 'status', submitDisabled: true,
interactionState: 'read-only', actionLabel: 'Absence evidence loaded',
label: 'Employment is not absent at this coordinate',
message: 'This is reason-free operational evidence that no confirmed absence is visible at the reviewed business/system coordinate. It does not prove attendance, availability, or fitness for work.',
nextAction: 'Continue only with the authorized HR task and resolve any separate attendance, scheduling, leave, or fitness evidence through its governed owner boundary.',
}),
denied: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'permission-denied', actionLabel: 'Review access',
label: 'Absence evidence access denied',
message: 'The current purpose or actor authority does not permit access to Employment absence evidence.',
nextAction: 'Check the purpose and access authority before trying to load absence evidence again.',
}),
stale: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'validation-error', actionLabel: 'Reload evidence',
label: 'Absence evidence is stale',
message: 'The Employment or absence truth changed before this view could be relied on.',
nextAction: 'Reload authoritative Employment and absence evidence at a fresh business/system coordinate before continuing.',
}),
blocked: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'validation-error', actionLabel: 'Review authoritative scope',
label: 'Absence evidence is blocked by authoritative scope',
message: 'The absence view cannot be trusted while tenant, Employment, Person binding, status, or visible-version evidence is inconsistent.',
nextAction: 'Resolve the tenant, Employment, Person binding, status, and visible-version evidence before loading absence truth again.',
}),
error: Object.freeze({
ariaBusy: 'false', ariaLive: 'assertive', role: 'alert', submitDisabled: false,
interactionState: 'error', actionLabel: 'Retry governed load',
label: 'Employment absence evidence unavailable',
message: 'The governed Employment absence service did not return usable authoritative evidence.',
nextAction: 'Do not infer absence from cached or partial evidence; verify the service and authorization before retrying.',
}),
});

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

/** Return immutable, reason-free accessibility semantics for one Employment absence evidence state. */
export function employmentAbsenceViewModel(state) {
return requireExactState(state);
}

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

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

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

export const Idle = story('idle');
export const Loading = story('loading');
export const AbsentReadOnly = story('absent');
export const NotAbsentReadOnly = story('notAbsent');
export const PermissionDenied = story('denied');
export const StaleEvidence = story('stale');
export const AuthoritativeScopeBlocked = story('blocked');
export const Error = story('error');
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# HR Workspace Employment absence accessibility references

Status: **active stacked PR research/doctoring**. These references support the interaction semantics; they are not a claim of product certification, protected-`develop` availability, or compliance attestation.

## Applied decisions

- Use native button semantics and expose `aria-busy`, status/live-region, and alert semantics without turning the evidence surface into a custom application widget.
- Keep reason-free absence truth explicitly read-only. The UI may communicate that an Employment is absent or not absent at one reviewed business/system coordinate, but it must not disclose or infer medical, family, statutory, disciplinary, benefit, or free-form reasons.
- Do not infer attendance, availability, fitness for work, leave entitlement, scheduling, compensation, or employment-decision authority from absence truth.
- Disable duplicate submission while authoritative Employment/absence evidence is loading, and make loaded evidence read-only.
- Treat stale truth and tenant/Employment/Person/status/version inconsistency as fail-closed states with an explicit safe next action.
- Preserve a visible keyboard focus indicator and a minimum 44 CSS-pixel action height.
- Keep Storybook proof data value-minimized: no Person/Employment/Assignment identifiers, worker names/contact data, absence/leave reasons, medical/family/disciplinary data, compensation/benefit/rating/assessment values, credentials/tokens, prompts, or model output.

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

## Current-status note

WCAG 2.2 and WAI-ARIA 1.2 remain completed W3C Recommendations used as the normative accessibility baseline for this slice. Editorial or draft successor work does not replace those final standards here unless a future Orgmetra decision explicitly updates the baseline. This branch claims neither WCAG/ARIA conformance certification nor broader product accessibility certification.
35 changes: 35 additions & 0 deletions docs/traceability/hr-workspace-employment-absence-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# HR Workspace Employment absence state traceability

Status: **active stacked PR**. This document records the presentation/interaction contract owned by this branch. It does not claim protected-`develop` availability, accessibility certification, leave approval, or authority to mutate Employment truth.

## Ownership boundary

- Parent presentation contract: #130 `feat/hr-workspace-protected-read-state@b3b30058a79174000919d566fbbb1fdad80c62bf`.
- HR Workspace product parent: #53.
- Reason-free bitemporal Employment absence truth: #113. This branch does not import or duplicate that unmerged HRIS-kernel implementation.
- Durable reason-free absence persistence: #114. This branch does not write its PostgreSQL relations or inherit its focused evidence.
- Employment leave review remains separately owned by #47. Absence truth must not be translated into leave reason, entitlement, schedule, benefit, compensation, fitness, or employment-decision authority.
- Figma correlation: `Orgmetra Baseline`, Storybook Inventory node `1:64`, freshly read on 2026-08-28. The node requires `default / hover / focus / disabled / loading / validation-error / read-only / high-risk-confirmation` as the shared inventory. This read-only absence slice uses the applicable default/loading/validation/read-only/focus states and deliberately does not invent a consequential confirmation action.

## Interaction contract

- `idle`: explain what fresh reason-free Employment absence evidence will be loaded and why cached evidence is insufficient.
- `loading`: mark the surface busy and disable duplicate requests while current business/system evidence is resolved.
- `absent`: read-only reason-free operational truth that the Employment is absent at the reviewed coordinate; no reason disclosure or consequential authority.
- `notAbsent`: read-only truth that no confirmed absence is visible at the reviewed coordinate; it must not be interpreted as attendance, availability, or fitness for work.
- `denied`: fail closed on purpose/authority denial and direct the user to correct authorization.
- `stale`: require a fresh authoritative Employment/absence read rather than relying on prior evidence.
- `blocked`: surface authoritative tenant/Employment/Person/status/version inconsistency and require that inconsistency to be resolved first.
- `error`: prohibit inference from cached/partial evidence and explain the safe retry prerequisite.

The interaction view model is constant and value-minimized. It carries no Person/Employment/Assignment identifier, worker name/contact data, absence or leave reason, medical/family/statutory/disciplinary/benefit fact, compensation/rating/assessment value, credential/token, prompt, or model output.

## Evidence and accessibility

The focused workflow executes the exact candidate under Node.js 24 and requires 100% line, branch, and function coverage for this interaction contract. Storybook reuses existing Orgmetra design/focus tokens and the existing Figma inventory rather than introducing a parallel design system. WCAG 2.2 and WAI-ARIA 1.2 primary final references are recorded under `docs/doctoring/hr-workspace-employment-absence-accessibility-references.md`.

## Dependency-first integration

Keep this PR Draft and process #53 -> #130 first. The focused child gate is stack-local evidence only; #53/#130/#113/#114/#47 checks or reviews never transfer. After #130 actually integrates, retarget/revalidate this child against fresh `develop`, reconcile intervening HR Workspace/absence contracts, and rerun every applicable browser/accessibility/Foundation/Recovery/SAST/Security and central required workflow on one resulting exact head.

Do not self-approve, use routine administrator bypass, race another lifecycle writer, infer a reason or consequential HR authority from the read-only absence state, transfer predecessor evidence, or mutate a dedicated-writer dependency.
Loading
Loading