diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 000000000..059a71c64 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +# Source validation only; this label does not attest runtime isolation or capacity. +self-hosted-runner: + labels: + - cwlab-ci-isolated diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 6b475d6f2..1f55f8fbb 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -19,7 +19,9 @@ concurrency: jobs: quality: name: Repository quality - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 60 env: ORGMETRA_POSTGRES_IMAGE: postgres:16.14@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20 @@ -46,10 +48,18 @@ jobs: - name: Compile owned Python boundaries run: >- python -m compileall -q tests packages services - - name: Prove explicit GitHub-hosted runner image contract + - name: Prove proposed isolated self-hosted runner contract run: python -m unittest tests.test_github_actions_runner_image + - name: Install Node dependencies + run: npm ci - name: Validate foundation pack run: npm run validate + - name: Build Storybook + run: npm run build-storybook + - name: Install Chromium and Linux dependencies + run: npx playwright install --with-deps chromium + - name: Run HR workspace browser E2E + run: npm run test:e2e - name: Prove Foundation CI dependency hygiene run: bash tests/test_foundation_ci_dependency_hygiene.sh - name: Install reviewed test toolchain @@ -68,6 +78,16 @@ jobs: PYTHONPATH=packages/selection-review/src COVERAGE_FILE=/tmp/orgmetra-selection-review.coverage python -m pytest -c packages/selection-review/pyproject.toml packages/selection-review/tests PYTHONPATH=services/job-analysis-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src COVERAGE_FILE=/tmp/orgmetra-job-analysis-api.coverage python -m pytest -c services/job-analysis-api/pyproject.toml services/job-analysis-api/tests PYTHONPATH=services/people-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src COVERAGE_FILE=/tmp/orgmetra-people-api.coverage python -m pytest -c services/people-api/pyproject.toml services/people-api/tests + - name: Install PostgreSQL 16 host client + run: | + sudo apt-get -o DPkg::Lock::Timeout=120 update + sudo apt-get -o DPkg::Lock::Timeout=120 install --yes --no-install-recommends postgresql-client-16 + echo /usr/lib/postgresql/16/bin >> "$GITHUB_PATH" + /usr/lib/postgresql/16/bin/psql --version | grep -E '^psql \(PostgreSQL\) 16\.' + - name: Prove PostgreSQL 16 host client selection + run: | + test "$(command -v psql)" = /usr/lib/postgresql/16/bin/psql + psql --version | grep -E '^psql \(PostgreSQL\) 16\.' - name: Run PostgreSQL contracts in isolated containers env: PGPASSWORD: orgmetra diff --git a/.github/workflows/recovery-rehearsal-quality.yml b/.github/workflows/recovery-rehearsal-quality.yml index ea0f41ad0..8e542ebe5 100644 --- a/.github/workflows/recovery-rehearsal-quality.yml +++ b/.github/workflows/recovery-rehearsal-quality.yml @@ -33,7 +33,9 @@ concurrency: jobs: restore-rehearsal: name: PostgreSQL restore rehearsal - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 15 services: source_postgres: @@ -43,7 +45,7 @@ jobs: POSTGRES_PASSWORD: orgmetra POSTGRES_DB: postgres ports: - - 5432:5432 + - 5432 options: >- --health-cmd "pg_isready -U orgmetra -d postgres" --health-interval 5s @@ -56,7 +58,7 @@ jobs: POSTGRES_PASSWORD: orgmetra POSTGRES_DB: postgres ports: - - 5433:5432 + - 5432 options: >- --health-cmd "pg_isready -U orgmetra -d postgres" --health-interval 5s @@ -72,6 +74,18 @@ jobs: env: EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: test "$(git rev-parse HEAD)" = "${EXPECTED_SHA}" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Set up Node.js LTS + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + check-latest: false + - name: Install Node dependencies + run: npm ci - name: Print diagnostic recovery provenance data run: | python - <<'PY' @@ -99,11 +113,21 @@ jobs: run: node --test tests/recovery-rehearsal.test.mjs - name: Validate repository contracts run: npm run validate + - name: Install PostgreSQL 16 host client + run: | + sudo apt-get -o DPkg::Lock::Timeout=120 update + sudo apt-get -o DPkg::Lock::Timeout=120 install --yes --no-install-recommends postgresql-client-16 + echo /usr/lib/postgresql/16/bin >> "$GITHUB_PATH" + /usr/lib/postgresql/16/bin/psql --version | grep -E '^psql \(PostgreSQL\) 16\.' + - name: Prove PostgreSQL 16 host client selection + run: | + test "$(command -v psql)" = /usr/lib/postgresql/16/bin/psql + psql --version | grep -E '^psql \(PostgreSQL\) 16\.' - name: Exercise real cross-cluster dump and restore env: RECOVERY_REHEARSAL_ALLOW_ROLE_DROP: "1" - POSTGRES_SOURCE_ADMIN_URL: postgresql://orgmetra:orgmetra@localhost:5432/postgres - POSTGRES_RESTORE_ADMIN_URL: postgresql://orgmetra:orgmetra@localhost:5433/postgres + POSTGRES_SOURCE_ADMIN_URL: postgresql://orgmetra:orgmetra@localhost:${{ job.services.source_postgres.ports[5432] }}/postgres + POSTGRES_RESTORE_ADMIN_URL: postgresql://orgmetra:orgmetra@localhost:${{ job.services.restore_postgres.ports[5432] }}/postgres POSTGRES_SOURCE_CONTAINER: ${{ job.services.source_postgres.id }} POSTGRES_RESTORE_CONTAINER: ${{ job.services.restore_postgres.id }} run: bash .github/scripts/restore-rehearsal-postgres.sh diff --git a/.gitignore b/.gitignore index 1f0b61ba5..e5d4913eb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ node_modules/ dist/ coverage/ .turbo/ +storybook-static/ +playwright-report/ +test-results/ # Rust /target/ @@ -35,3 +38,4 @@ secrets/ artifacts/ reports/ *.log +/.codegraph/ diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 000000000..87fa64439 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,8 @@ +/** @type { import('@storybook/web-components-vite').StorybookConfig } */ +const config = { + stories: ['../apps/hr-workspace/**/*.stories.@(js|mjs)'], + framework: '@storybook/web-components-vite', + docs: { autodocs: 'tag' } +}; + +export default config; diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 000000000..058f02b9b --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,13 @@ +import '../packages/design-tokens/tokens.css'; +import '../apps/hr-workspace/styles.css'; + +/** @type { import('storybook').Preview } */ +const preview = { + parameters: { + layout: 'centered', + controls: { expanded: true } + }, + decorators: [(story) => `
${story()}
`] +}; + +export default preview; diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1bdc13b5a..c645ed750 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -76,7 +76,7 @@ The initial deployment may share one physical PostgreSQL cluster. Logical isolat |---|---|---| | `people_core` | `people_core`: person, name, employment, assignment, compensation, and candidate-worker linkage records | `people_core_role` | | `organization_core` | `organization_core`: organization units and position records | `organization_core_role` | -| `job_architecture` | `job_architecture`: job profiles, publication evidence, and persisted `job_analysis_snapshot` / task / KSAO rows | `job_architecture_role` | +| `job_architecture` | `job_architecture`: job profiles and publication evidence; persistent `job_analysis_snapshot`, `job_analysis_task_item`, and `job_analysis_ksao_item` tables | `job_architecture_role` | | `talent_acquisition` | `talent_acquisition`: candidate profiles, selection decisions, and decision evidence | `talent_acquisition_role` | | `performance_management` | `performance_management`: criterion blueprints and observations | `performance_management_role` | | `workforce_validation` | `workforce_validation`: validity-study registry and external result references | `workforce_validation_role` | diff --git a/CHANGELOG.md b/CHANGELOG.md index 16454da3d..88755cebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,22 +6,41 @@ All notable changes to Orgmetra will be documented in this file. ### Added +- Active-PR isolated-runner PostgreSQL client repair: host `psql` availability on the `CWL CI isolated` runners varies by job; the logs do not show a fixed per-runner client. Restore rehearsal job 112192671013 on `orgmetra-ci-01` (attempt 3) was the first to stop at `.github/scripts/restore-rehearsal-postgres.sh` line 59 with `psql: command not found` (exit 127), and job 112199226633 on `keyverse-ci-01` (attempt 4) stopped the same way. Foundation job 112192669065 on `keyverse-ci-01` stopped after 30 failed `psql` readiness probes, while Foundation job 112199223734 on `orgmetra-ci-01` reached PostgreSQL. Both workflows now install Ubuntu `postgresql-client-16`, put `/usr/lib/postgresql/16/bin` first on the job path, and prove in a later step that the selected `psql` is that binary with major version 16 before the first host `psql` use. The source contract detects host `psql` written as a bare or absolute command, after `env` or `bash -c`, or chained after a `docker exec` segment, and host scripts that call `psql` when run through `bash` (with or without flags), `sh`, `source`, `.` or `./`; it ignores `psql` only inside a `docker exec` segment. Commands that do not name `psql` or the script path literally, such as a variable that holds the command, are not detected. Database images, contract scripts, runner selectors and all quality gates are unchanged; this source change is not a passing hosted run, approval, or release. + +- Active-PR traceability correction: distinguish unchanged baseline People business/persistence boundaries from this PR's read HTTP error handling and regression changes. Paragraph-scoped provenance contracts retain the unverified-protection limit and reject the earlier blanket unchanged attribution, including wrapped contradictory text. This correction does not change People runtime behavior or certify deployment, hosted checks, approval, or release. + +- Active-PR People read error-boundary repair: contain unexpected authentication-backend exceptions before protected reads and retain existing 400/401/403/404/405/409/500 responses while adding canonical `error_code`, `next_action`, and random `support_reference` metadata. Bounded local rejection logs share the support reference without credentials, HR values, request identities, or exception text. Existing canonical mutation error envelopes and successful payloads remain unchanged; local regression and coverage evidence do not establish hosted CI, independent approval, deployed authentication, or release. + +- Active-PR recovery port wiring repair: each PostgreSQL service exposes container `5432` with an allocated host port, and each administrator URL consumes its own service's `ports[5432]` mapping. Recording-only source contracts use distinct synthetic ports and paired fixed/swapped/shared/wrong/missing controls. PostgreSQL images, health checks, cluster-identity/destructive-operation guards and isolated runner selectors are unchanged. Actual runner capacity, service allocation, cross-cluster restore, hosted execution and independent approval remain separate obligations. + +- Active-PR credential-wait dispatch repair: People and Job Analysis submit handlers reuse their existing generation to reject an obsolete read after credential acquisition and before fetch, settling through the existing version-fenced catch without clearing a newer result. Permanent synthetic Node regressions cover authority/repeated/away-and-back notification, input/change with reversion, newer submit, late valid credentials, provider rejection, and missing credentials with nonempty fresh-read controls. Direct two-argument helper callers have no generation protection; already-dispatched transport is not cancelled. Existing explicit credentials, no-storage/cookie-free reads, authorization denial and response fences remain unchanged. Host notification, actual issuer/identity integration, native-browser successor evidence, hosted CI, independent approval, and protected release remain open. + +- Active-PR, source-only CI routing proposal: bind Foundation and Recovery to the dedicated `CWL CI isolated` group and exact labels, reject hosted or privileged fallback selectors, and declare only the custom label needed by actionlint. Runtime capacity, repository access, isolation, cleanup, canary, and managed Code Quality routing remain separate acceptance conditions. Existing quality and security gates remain required. + +- Active-PR browser harness ownership repair: refuse existing HTTP servers in every environment and validate optional `ORGMETRA_WORKSPACE_PORT` overrides as canonical decimal TCP ports 1–65535 before fixture launch. Unset overrides retain 4173; browser assertions, 15-second case deadlines, and failure recording remain unchanged. Actual config-import regressions join the existing Foundation integration test; local evidence does not authorize merge or deployment. + +- Dependency-free HR Home and Employee Profile fixture at `apps/hr-workspace/`, using the Figma role frames and shared tokens with explicit API-boundary, permission-denied, evidence-drawer, high-impact confirmation, exact-allocation, focus, and English/Korean states. The fixture is not connected or deployed evidence. +- Local Storybook `10.5.10` runtime using `@storybook/web-components-vite` and native HTML/CSS stories for the HR action states, field errors, permission denial, evidence drawer, high-impact confirmation, and exact assignment values. It is local component/state evidence, not connected People API or browser E2E evidence. +- Chromium Playwright browser E2E for the HR workspace, covering keyboard-accessible human review, locale changes, purpose-bound denial, host-injected People and Job Analysis reads, authorization headers, and no-fallback error behavior. The test is local/CI evidence and does not claim protected deployment. +- Product and technical gap baseline with protected-branch truth, full current open-PR inventory, buyer-priority acceptance evidence, Figma source ID, standards/research ledger, and central integration-loop contract. The baseline distinguishes shipped runtime from active PR, accepted architecture, planned, research-only, superseded, and out-of-scope work. +- Project-local `uv` source resolution and a checked lockfile for `services/people-api`, so local tests discover the owned Keyverse adapter without an undocumented `PYTHONPATH` workaround. +- Project-local `uv` source resolution, Python compatibility metadata, test extras, and a checked lockfile for `services/job-analysis-api`, so its full test command resolves owned packages without an undocumented `PYTHONPATH` workaround. - Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies. -- Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision. -- Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate. -- Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries. +- Protected `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate. +- Protected performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries. - Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal. -- Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. -- Stacked governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work. +- Protected governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. +- Protected governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work. - `orgmetra_hris_kernel` 0.4.0 with exclusive-versus-concurrent employment, staffable position coverage, exclusive-seat capacity, and `validate_assignment_write` at 100% statement and branch coverage. -- `POST /v1/employment-records`, `POST /v1/position-records`, and `POST /v1/assignment-records` with the same Keyverse mutation context, confirmation, and versioned evidence composition as other high-impact commands. +- Protected governed People mutation and confirmed-hire materialization runtime for `POST /v1/employment-records`, `POST /v1/position-records`, `POST /v1/assignment-records`, and the tenant-bound conversion route, with purpose-bound Keyverse mutation context, explicit confirmation, versioned evidence, atomic audit/outbox persistence, and tenant-scoped idempotency. - `employment_record_version.employment_concurrency_code` constrained to `exclusive` or `concurrent`. - ADR 0005 for exclusive employment and staffable seats. - `orgmetra_hris_kernel` 0.3.0 with identity-scoped bitemporal resolution, assignment-employment coverage, allocation-portfolio checks, and a Memorial Hospital RN correction case at 100% statement and branch coverage. - `employment_record_version` and `position_record_version` so employment and position identity stay stable across retroactive corrections. - `assignment_record.employment_record_id` bound to the same person as the covering employment. - `orgmetra_keyverse_adapter` that binds an opaque Keyverse subject to a person and rejects passwords, passkeys, and tokens. -- Design tokens for the repeating HR actions: approve, review, correct, request evidence, compare, export, and escalate. +- Design tokens for the repeating HR actions: approve, review, correct, request evidence, compare, export, escalate. - ADR 0004 for employment/position versions and assignment-employment binding. - Foundation product baseline for Orgmetra as an evidence-centered HRIS/HCM. - CWL federated integration boundary map. @@ -37,7 +56,21 @@ All notable changes to Orgmetra will be documented in this file. ### Changed +- Active-PR bibliography repair: Fugu and TRINITY references now list the registered individual authors and pin the observed arXiv versions. Explicit revision dates distinguish TRINITY's 2026 revision from its 2025 first submission. A canonical regression rejects organization-author substitution for these two records; no numerical, runtime, identity, protection, or release behavior changes. +- Active-PR restore provenance repair: recovery scope now attributes backup requirements and migration source to the immutable observed develop base, not current protected integration or enabled recovery controls. A shared scope regression rejects stale protected-main claims while retaining the actual recovery source, destructive-operation guards, and source manifest integrity. Hosted execution, deployed recovery and release remain unverified. +- Active-PR maturity evidence repair: the validator rejects obsolete `implemented_on_protected_main` through otherwise-valid matrix regressions. Local source-backed capabilities use `implemented_on_active_pr` as the current assessment bound, not a claim that their implementation is new or absent from the observed develop base. Baseline source presence and historical classifications remain explicit, while current protection, hosted execution and release evidence are unverified. The external Keyverse identity row records accepted architecture rather than claiming issuer integration. No runtime, SQL, authentication or protection policy is changed. + +- Active-PR manifest semantic repair: `target_branch: develop` identifies only the intended integration destination; `generated_source_branch: null` explicitly records that this deterministic filesystem producer does not capture generation-source authority, including in named checkouts. The parser rejects missing/non-null source claims, wrong targets, and legacy `generated_for_branch` metadata while retaining exact file/hash/byte/line/path integrity. Offline bounded Python regressions join the canonical Foundation integration module; print remains read-only, deterministic, gitless, and source-agnostic. Unknown source is not authenticated provenance, protected integration, formal approval, or release; any requirement for authenticated non-null provenance remains unmet. +- Active-PR review-document repair: ADR 0017 separates its accepted decision status from observed source provenance and unverified protection/host integration. Job Analysis traceability now names existing migration 0013 tables, API source, PostgreSQL contract source, and ADR 0014. Two regressions guard these documentation boundaries; no runtime, fresh PostgreSQL, deployed approval, or release claim is added. +- Active-PR baseline attribution repair: README identifies inspected source on the observed default-branch base; the Gap ledger labels its August 21, 2026 capability, PR inventory, and execution snapshot as historical. Existing snapshot values remain evidence at their original coordinates, not fresh execution, configured protection, deployment, or release. PR #100/#51 branches remain unchanged; browser and Node authority-event coverage are now documented separately. +- Active-PR host-authority display repair: the payload-free `orgmetra:authority-invalidated` document event reuses both read invalidators to synchronously scrub People/Job results and fence prior responses. The host must notify before another authority can use the retained document or destroy it; local regressions do not establish host integration, transport cancellation, protected merge, or release. +- Active-PR architecture documentation repair: the Job Architecture ownership row now includes the existing persisted Job Analysis snapshot, task, and KSAO tables. A migration-backed regression preserves the owning schema, role, and original job-profile/publication-evidence description. This does not add database behavior or establish protected integration or release. +- Active-PR personal-details fixture display repair: changing the access purpose clears both previous allowed and denied review states. Selecting or restoring a purpose does not grant access; a fresh explicit review is required. Node and Chromium regressions preserve fixture-only maturity, existing purpose rules, and browser deadlines. +- Active-PR protected-read display repair: editing People or Job Analysis request fields clears displayed values and invalidates pending responses, including edit-and-revert sequences. A fresh explicit read remains available. Existing authorization providers, API contracts, no-cookie transport, fixture-only maturity, and browser deadlines are unchanged; Node and Chromium regressions exercise both read views. +- Active-PR CI dependency-order repair: Foundation installs the reviewed Node lockfile before validation; Recovery provisions the same pinned Node 24 and Python 3.14 runtimes and runs `npm ci` before validation. Existing config-import regressions retain their real Playwright dependency, and integration tests guard the prerequisite order. Dependencies, lockfile, browser behavior, runner selectors, and all quality gates are unchanged; local evidence is not hosted execution or approval. - Consolidated repository-owned PR validation from twelve workflows into one Foundation CI job, while keeping the dual-cluster recovery rehearsal separately path-scoped. Central required review and security workflows remain organization-owned. +- Aligned the English/Korean locale toggle's accessible name with its visible target-language label (`한국어` / `English`) and added exact browser assertions for both language states, following WCAG 2.2 Success Criterion 2.5.3 (Label in Name); the APA 7 W3C citation remains in `docs/doctoring/REFERENCES.md`. +- Reconciled README, traceability, ADR status/index, and changelog maturity claims with capabilities already integrated on protected `develop`; open-PR capabilities remain explicitly non-shipped. - New predictive-validity membership must use one normalized worker-level case; the three independent validity-study decision/evidence/outcome link relations are historical read surfaces only and can no longer accept new rows. A case insert also rejects a criterion observation whose recorded interval is already closed at `linked_at`. - Canonicalized service identifiers as two-or-more-word `snake_case` across architecture, deployment, ACL, metrics, and client contracts. - Separated fast-mlsirm, TEPP, and Psychometrics Commons into immutable external scientific contracts. @@ -74,4 +107,4 @@ All notable changes to Orgmetra will be documented in this file. ### Notes -- Protected `develop` at `e7ddb7a78a5e1460410005d10f43ebf18c5e12e4` includes normalized validity-study and criterion integrity, bitemporal workforce composition, governed candidate-to-worker conversion, purpose-bound PII authorization, GET-only People reads, governed People mutation/idempotency API, and the accepted ADR 0001–0003 source expansion integrated by #37. Job Analysis persistence/API and the selection-review packet remain active-PR truth until their unchanged exact heads satisfy fresh gates and merge. +- Protected `develop` is the shipped repository truth for integrated capability. Open PRs and draft branches are non-shipped until they integrate and satisfy fresh exact-head gates. diff --git a/README.md b/README.md index 47bb087a3..b1b227bc3 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,17 @@ Job evidence - `docs/TEST_STRATEGY.md` - `docs/OPERABILITY.md` - `docs/TRACEABILITY.md` +- `docs/product-technical-gap-baseline.md` - `docs/adr/README.md` - `docs/doctoring/REFERENCES.md` ## Status -Protected `develop` includes the employment-truth kernel, governed candidate-to-worker conversion, purpose-bound PII authorization, normalized worker-bound validity studies, criterion-observation scope, bitemporal workforce-composition evidence, the governed Naruon intent adapter, and requisition review packets. This active PR adds durable purpose-bound People mutation and confirmed-hire materialization paths for Employment, Position, and Assignment with atomic audit/outbox evidence and tenant-scoped idempotency; treat those write paths as active-PR truth until this exact head passes all fresh protected-base gates and merges. +Active PR #53 includes local People and Job Analysis read views. Editing their +request fields clears displayed values and invalidates pending responses, even +if an edit is reverted. Changing the fixture's personal-details purpose also +clears its prior allowed or denied display; only an explicit review shows a new +outcome. Node and Chromium regressions cover these display boundaries; they do +not establish released authentication or deployment. + +Observed default `develop` base `eb9757f8649aaad026a9865508d9aad50c1a7a4f` contains the employment-truth kernel (`packages/hris-kernel/src/orgmetra_hris_kernel/employment.py`), bitemporal workforce-composition snapshot source (`packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py`) and regression source (`packages/hris-kernel/tests/test_workforce_composition.py` and `test_workforce_composition_boundaries.py`), People read/mutation/confirmed-hire source (`services/people-api/src/orgmetra_people_api/people.py`, `mutations.py`, and `hire.py`; `database/migrations/0012_people_mutation_idempotency.sql`), and Job Analysis snapshot read/write source (`services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py`; `database/migrations/0013_job_analysis_snapshot.sql`). This is repository source presence only, not configured branch-protection enforcement, newly executed tests, deployed behavior, or a release. Active PR #53 UI remains unmerged; the 2026-08-21 inventory and local execution results in `docs/product-technical-gap-baseline.md` are historical, not current all-PR or release evidence. diff --git a/apps/hr-workspace/app.js b/apps/hr-workspace/app.js new file mode 100644 index 000000000..caad79d2d --- /dev/null +++ b/apps/hr-workspace/app.js @@ -0,0 +1,480 @@ +const translations = { + en: { + productLabel: 'Evidence-centered HRIS', workspaceLabel: 'People workspace', apiBoundary: 'Protected People API boundary: not connected in this fixture', fixtureMode: 'Local fixture mode', hrHome: 'HR Home', employeeProfile: 'Employee Profile', fixtureBadge: 'Fixture evidence', hrHomeBreadcrumb: 'Orgmetra / HR Home', employeeProfileBreadcrumb: 'Orgmetra / Employee Profile', today: 'Today', hiringReview: 'hiring decisions need evidence review', positionChanges: 'position changes effective next week', validationApproval: 'validation study needs approval', workQueue: 'Work queue', nextActions: 'Next actions', reviewEvidence: 'Review evidence', reviewEvidenceDetail: 'Approve or return a decision packet', effectiveConflicts: 'Effective-date conflicts', effectiveConflictsDetail: 'Inspect the employee history timeline', validityStudy: 'Open validity study', validityStudyDetail: 'Review the supporting evidence versions', evidenceState: 'Evidence state', readyForReview: 'Ready for human review', reviewState: 'Review', reviewCopy: 'Orgmetra keeps the actor, purpose, reason, and evidence versions visible before a high-impact action.', actor: 'Actor', purpose: 'Purpose', source: 'Source', personSummary: 'Person summary', sampleEmployee: 'Sample employee', syntheticRecord: 'Synthetic record for local interaction proof', authorized: 'HR operations access', person: 'Person', personValue: 'Identity and name facts', employment: 'Employment', employmentValue: 'Worker relationship', position: 'Position', positionValue: 'AI Product Architect', assignment: 'Assignment', assignmentValue: 'Platform 60% · Governance 40%', accessPurpose: 'Access purpose', hrOperations: 'HR operations', recruiting: 'Recruiting', viewPersonalDetails: 'View personal details', correctHistory: 'Correct history', permissionDenied: 'Permission denied', permissionDeniedCopy: 'This purpose does not authorize personal details. Choose a permitted HR purpose or request access.', detailsVisible: 'Personal details visible', detailsVisibleCopy: 'Purpose-bound access was evaluated for this fixture and recorded for audit.', bitemporalHistory: 'Bitemporal history', historyTitle: 'Effective vs recorded time', readOnly: 'Read-only snapshot', historyCaption: 'Employee history with effective and recorded time', record: 'Record', effectiveTime: 'Effective time', recordedTime: 'Recorded time', state: 'State', active: 'Active', pendingReview: 'Pending review', assignmentSplit: 'Assignment split', capacityTitle: 'Visible allocation capacity', assignmentCaption: 'Exact assignment allocation values', assignmentName: 'Assignment', allocation: 'Allocation', nextAction: 'Next action', noAction: 'No action required', credentialBoundary: 'Credential boundary', credentialCopy: 'No identity credential or passkey is stored in the person record.', evidenceDrawer: 'Evidence drawer', evidenceTitle: 'Decision packet evidence', evidenceCopy: 'These references are reviewable inputs. They do not make an autonomous employment decision.', evidenceOne: 'Structured interview criterion ratings', evidenceTwo: 'SME-approved job profile version 4', evidenceThree: 'Human reviewer reason and purpose record', close: 'Close', changeLanguage: 'Change language to 한국어', requestEvidence: 'Request more evidence', highImpactConfirmation: 'High-impact confirmation', correctHistoryTitle: 'Correct history', confirmationCopy: 'A correction remains a draft until a human confirms the actor, purpose, reason, and evidence context.', reason: 'Reason', reasonPlaceholder: 'Required', draftConfirmed: 'Draft correction confirmed for local fixture evidence; no API mutation was sent.', cancel: 'Cancel', confirm: 'Confirm correction' + }, + ko: { + productLabel: '근거 중심 HRIS', workspaceLabel: '사람 워크스페이스', apiBoundary: '보호된 People API 경계: 이 픽스처에서는 연결되지 않음', fixtureMode: '로컬 픽스처 모드', hrHome: 'HR 홈', employeeProfile: '직원 프로필', fixtureBadge: '픽스처 근거', hrHomeBreadcrumb: 'Orgmetra / HR 홈', employeeProfileBreadcrumb: 'Orgmetra / 직원 프로필', today: '오늘', hiringReview: '근거 검토가 필요한 채용 결정', positionChanges: '다음 주 효력이 시작되는 직위 변경', validationApproval: '승인이 필요한 타당도 연구', workQueue: '작업 큐', nextActions: '다음 작업', reviewEvidence: '근거 검토', reviewEvidenceDetail: '결정 패킷을 승인하거나 반환', effectiveConflicts: '효력일 충돌', effectiveConflictsDetail: '직원 이력 타임라인 확인', validityStudy: '타당도 연구 열기', validityStudyDetail: '지원 근거 버전 검토', evidenceState: '근거 상태', readyForReview: '사람 검토 대기', reviewState: '검토', reviewCopy: '고위험 작업 전에 행위자, 목적, 사유, 근거 버전을 표시합니다.', actor: '행위자', purpose: '목적', source: '출처', personSummary: '사람 요약', sampleEmployee: '샘플 직원', syntheticRecord: '로컬 상호작용 증명을 위한 합성 레코드', authorized: 'HR 운영 접근', person: '사람', personValue: '식별 및 이름 사실', employment: '고용', employmentValue: '근로자 관계', position: '직위', positionValue: 'AI 제품 아키텍트', assignment: '배정', assignmentValue: '플랫폼 60% · 거버넌스 40%', accessPurpose: '접근 목적', hrOperations: 'HR 운영', recruiting: '채용', viewPersonalDetails: '개인 상세 보기', correctHistory: '이력 정정', permissionDenied: '권한 거부', permissionDeniedCopy: '이 목적에는 개인 상세 접근 권한이 없습니다. 허용된 HR 목적을 선택하거나 접근을 요청하세요.', detailsVisible: '개인 상세 표시', detailsVisibleCopy: '이 픽스처의 목적 기반 접근을 평가하고 감사 기록을 남겼습니다.', bitemporalHistory: '이중 시간 이력', historyTitle: '효력 시간과 기록 시간', readOnly: '읽기 전용 스냅샷', historyCaption: '효력 시간과 기록 시간을 포함한 직원 이력', record: '레코드', effectiveTime: '효력 시간', recordedTime: '기록 시간', state: '상태', active: '활성', pendingReview: '검토 대기', assignmentSplit: '배정 비율', capacityTitle: '표시된 할당 용량', assignmentCaption: '정확한 배정 할당 값', assignmentName: '배정', allocation: '할당', nextAction: '다음 작업', noAction: '조치 필요 없음', credentialBoundary: '자격 증명 경계', credentialCopy: '사람 레코드에 신원 자격 증명이나 패스키를 저장하지 않습니다.', evidenceDrawer: '근거 서랍', evidenceTitle: '결정 패킷 근거', evidenceCopy: '검토 가능한 입력이며 자율적인 고용 결정을 만들지 않습니다.', evidenceOne: '구조화 면접 기준 평정', evidenceTwo: 'SME 승인 직무 프로필 버전 4', evidenceThree: '사람 검토자의 사유 및 목적 기록', close: '닫기', changeLanguage: '언어를 English로 변경', requestEvidence: '추가 근거 요청', highImpactConfirmation: '고위험 작업 확인', correctHistoryTitle: '이력 정정', confirmationCopy: '행위자, 목적, 사유, 근거 맥락을 사람이 확인하기 전까지 정정은 초안으로 남습니다.', reason: '사유', reasonPlaceholder: '필수', draftConfirmed: '로컬 픽스처 근거에 정정 초안을 확인했으며 API 변경은 전송하지 않았습니다.', cancel: '취소', confirm: '정정 확인' + } +}; + +Object.assign(translations.en, { + jobAnalysis: 'Job Analysis', + jobAnalysisBadge: 'API-bound read', + jobAnalysisBreadcrumb: 'Orgmetra / Job Analysis', + jobAnalysisTitle: 'Job Analysis snapshot', + jobAnalysisCopy: 'Read one governed, bitemporal job-analysis snapshot from the protected API.', + jobAnalysisTenant: 'Tenant record ID', + jobAnalysisRecord: 'Analysis record ID', + jobAnalysisPurpose: 'Purpose code', + jobAnalysisLoad: 'Load snapshot', + jobAnalysisReady: 'Ready to request a protected snapshot.', + jobAnalysisNotConfigured: 'Not connected: the host must provide an API base URL and authorization provider.', + jobAnalysisLoading: 'Loading governed snapshot…', + jobAnalysisLoaded: 'Snapshot loaded from the protected Job Analysis API.', + jobAnalysisDenied: 'The protected API denied this request. Verify the purpose and host authorization.', + jobAnalysisFailed: 'The protected API could not return this snapshot. No local fallback was used.', + jobAnalysisSummary: 'Snapshot evidence', + jobAnalysisAnalysisId: 'Analysis record', + jobAnalysisStatus: 'Status', + jobAnalysisEffective: 'Effective from', + jobAnalysisRecorded: 'Recorded at', + jobAnalysisTasks: 'Tasks', + jobAnalysisKsaos: 'KSAO requirements', + jobAnalysisNoValues: 'No snapshot values loaded.', + peopleApiBadge: 'API-bound read', + peopleApiTitle: 'Protected People record', + peopleApiCopy: 'Read one authorized worker record from the protected People API.', + peopleApiTenant: 'Tenant record ID', + peopleApiPerson: 'Person record ID', + peopleApiEffective: 'Effective date', + peopleApiPurpose: 'Purpose code', + peopleApiFields: 'Requested fields', + peopleApiLoad: 'Load worker record', + peopleApiReady: 'Ready to request a protected worker record.', + peopleApiNotConfigured: 'Not connected: the host must provide a People API URL and authorization provider.', + peopleApiLoading: 'Loading the authorized worker record…', + peopleApiLoaded: 'Worker record loaded from the protected People API.', + peopleApiDenied: 'The protected People API denied this request. Verify purpose and authorization.', + peopleApiFailed: 'The protected People API could not return this record. No local fallback was used.', + peopleApiDisplayName: 'Authorized display name', + peopleApiEmploymentStatus: 'Employment status', + peopleApiNoValues: 'No protected People API values loaded.', +}); +Object.assign(translations.ko, { + jobAnalysis: '직무 분석', + jobAnalysisBadge: 'API 연결 읽기', + jobAnalysisBreadcrumb: 'Orgmetra / 직무 분석', + jobAnalysisTitle: '직무 분석 스냅샷', + jobAnalysisCopy: '보호된 API에서 하나의 거버넌스 적용 이중 시간 직무 분석 스냅샷을 읽습니다.', + jobAnalysisTenant: '테넌트 레코드 ID', + jobAnalysisRecord: '분석 레코드 ID', + jobAnalysisPurpose: '목적 코드', + jobAnalysisLoad: '스냅샷 불러오기', + jobAnalysisReady: '보호된 스냅샷을 요청할 준비가 되었습니다.', + jobAnalysisNotConfigured: '연결되지 않음: 호스트가 API 기본 URL과 인증 제공자를 제공해야 합니다.', + jobAnalysisLoading: '거버넌스 적용 스냅샷을 불러오는 중…', + jobAnalysisLoaded: '보호된 직무 분석 API에서 스냅샷을 불러왔습니다.', + jobAnalysisDenied: '보호된 API가 요청을 거부했습니다. 목적과 호스트 인증을 확인하세요.', + jobAnalysisFailed: '보호된 API에서 이 스냅샷을 반환하지 못했습니다. 로컬 대체 데이터는 사용하지 않았습니다.', + jobAnalysisSummary: '스냅샷 근거', + jobAnalysisAnalysisId: '분석 레코드', + jobAnalysisStatus: '상태', + jobAnalysisEffective: '효력 시작일', + jobAnalysisRecorded: '기록 시각', + jobAnalysisTasks: '과업', + jobAnalysisKsaos: 'KSAO 요구사항', + jobAnalysisNoValues: '불러온 스냅샷 값이 없습니다.', + peopleApiBadge: 'API 연결 읽기', + peopleApiTitle: '보호된 People 레코드', + peopleApiCopy: '보호된 People API에서 권한이 부여된 근로자 레코드 하나를 읽습니다.', + peopleApiTenant: '테넌트 레코드 ID', + peopleApiPerson: '사람 레코드 ID', + peopleApiEffective: '효력 기준일', + peopleApiPurpose: '목적 코드', + peopleApiFields: '요청 필드', + peopleApiLoad: '근로자 레코드 불러오기', + peopleApiReady: '보호된 근로자 레코드를 요청할 준비가 되었습니다.', + peopleApiNotConfigured: '연결되지 않음: 호스트가 People API URL과 인증 제공자를 제공해야 합니다.', + peopleApiLoading: '권한이 부여된 근로자 레코드를 불러오는 중…', + peopleApiLoaded: '보호된 People API에서 근로자 레코드를 불러왔습니다.', + peopleApiDenied: '보호된 People API가 요청을 거부했습니다. 목적과 인증을 확인하세요.', + peopleApiFailed: '보호된 People API에서 레코드를 반환하지 못했습니다. 로컬 대체 데이터는 사용하지 않았습니다.', + peopleApiDisplayName: '권한이 부여된 표시 이름', + peopleApiEmploymentStatus: '고용 상태', + peopleApiNoValues: '불러온 보호된 People API 값이 없습니다.', +}); + +/** + * Return whether the selected purpose is allowed to reveal personal details. + * @param {string} purpose Purpose code selected by the reviewer. + * @returns {boolean} True only for the fixture's HR-operations purpose. + */ +export function isPurposeAuthorized(purpose) { + return purpose === 'hr_operations'; +} + +/** + * Return the alternate supported workspace locale. + * @param {string} locale Current locale code. + * @returns {'en'|'ko'} The other supported locale; unknown values fall back to English. + */ +export function nextLocale(locale) { + return locale === 'en' ? 'ko' : 'en'; +} + +/** + * Build the protected Job Analysis snapshot URL after validating required identifiers. + * @param {{baseUrl: string, tenantRecordId: string, analysisRecordId: string}} config Request coordinates. + * @returns {string} Encoded snapshot URL. + * @throws {Error} If the base URL, tenant record ID, or analysis record ID is missing. + */ +export function jobAnalysisSnapshotUrl(config) { + if (!config || typeof config.baseUrl !== 'string' || !config.baseUrl.trim()) { + throw new Error('Job Analysis API base URL is not configured.'); + } + if (typeof config.tenantRecordId !== 'string' || !config.tenantRecordId.trim()) { + throw new Error('Tenant record ID is required.'); + } + if (typeof config.analysisRecordId !== 'string' || !config.analysisRecordId.trim()) { + throw new Error('Analysis record ID is required.'); + } + return `${config.baseUrl.replace(/\/$/, '')}/v1/tenants/${encodeURIComponent(config.tenantRecordId)}/job-analysis-snapshots/${encodeURIComponent(config.analysisRecordId)}`; +} + +/** + * Read one protected Job Analysis snapshot using a host-provided short-lived credential. + * The credential is used only for this request and is never persisted by the workspace. + * @param {{getAuthorization: Function, purposeCode?: string} & Record} config Protected-read configuration. + * @param {typeof fetch} fetchImpl Fetch implementation, injectable for tests. + * @param {() => boolean} isCurrentRequest Synchronous workspace-generation check before dispatch; omitted callers have no generation fence. + * @returns {Promise} Parsed snapshot payload. + * @throws {Error} For missing authorization providers/credentials, unavailable fetch, obsolete guarded reads, denied access, or failed requests. + */ +export async function fetchJobAnalysisSnapshot(config, fetchImpl = globalThis.fetch, isCurrentRequest = () => true) { + if (typeof config?.getAuthorization !== 'function') { + throw new Error('Job Analysis API authorization provider is not configured.'); + } + if (typeof fetchImpl !== 'function') throw new Error('Fetch is unavailable.'); + const authorization = await config.getAuthorization(); + if (typeof authorization !== 'string' || !authorization.trim()) { + throw new Error('Job Analysis API authorization provider returned no credential.'); + } + if (!isCurrentRequest()) throw new Error('OBSOLETE_WORKSPACE_READ'); + const response = await fetchImpl(jobAnalysisSnapshotUrl(config), { + headers: { + Authorization: authorization, + 'X-Purpose-Code': config.purposeCode || 'job_analysis_read', + }, + credentials: 'omit', + }); + if (response.status === 401 || response.status === 403) throw new Error('JOB_ANALYSIS_ACCESS_DENIED'); + if (!response.ok) throw new Error('JOB_ANALYSIS_REQUEST_FAILED'); + return response.json(); +} + +/** + * Build the protected People-record URL after validating required read coordinates. + * @param {{baseUrl: string, tenantRecordId: string, personRecordId: string, effectiveOn: string, purposeCode?: string, requestedFields?: string[]}} config Request coordinates. + * @returns {string} Encoded People API URL with effective-date, purpose, and field query parameters. + * @throws {Error} If required coordinates or requested fields are invalid. + */ +export function peopleRecordUrl(config) { + if (!config || typeof config.baseUrl !== 'string' || !config.baseUrl.trim()) { + throw new Error('People API base URL is not configured.'); + } + if (typeof config.tenantRecordId !== 'string' || !config.tenantRecordId.trim()) { + throw new Error('Tenant record ID is required.'); + } + if (typeof config.personRecordId !== 'string' || !config.personRecordId.trim()) { + throw new Error('Person record ID is required.'); + } + if (typeof config.effectiveOn !== 'string' || !config.effectiveOn.trim()) { + throw new Error('Effective date is required.'); + } + const fields = Array.isArray(config.requestedFields) ? config.requestedFields : ['display_name', 'employment_status_code']; + if (!fields.length || fields.some((field) => typeof field !== 'string' || !field.trim())) { + throw new Error('At least one requested People API field is required.'); + } + const query = new URLSearchParams({ + effective_on: config.effectiveOn, + purpose: config.purposeCode || 'people_read', + fields: fields.join(','), + }); + return `${config.baseUrl.replace(/\/$/, '')}/v1/tenants/${encodeURIComponent(config.tenantRecordId)}/people/${encodeURIComponent(config.personRecordId)}?${query}`; +} + +/** + * Read one protected People record using a host-provided short-lived credential. + * The credential is used only for this request and is never persisted by the workspace. + * @param {{getAuthorization: Function} & Record} config Protected-read configuration. + * @param {typeof fetch} fetchImpl Fetch implementation, injectable for tests. + * @param {() => boolean} isCurrentRequest Synchronous workspace-generation check before dispatch; omitted callers have no generation fence. + * @returns {Promise} Parsed People record payload. + * @throws {Error} For missing authorization providers/credentials, unavailable fetch, obsolete guarded reads, denied access, or failed requests. + */ +export async function fetchPeopleRecord(config, fetchImpl = globalThis.fetch, isCurrentRequest = () => true) { + if (typeof config?.getAuthorization !== 'function') { + throw new Error('People API authorization provider is not configured.'); + } + if (typeof fetchImpl !== 'function') throw new Error('Fetch is unavailable.'); + const authorization = await config.getAuthorization(); + if (typeof authorization !== 'string' || !authorization.trim()) { + throw new Error('People API authorization provider returned no credential.'); + } + if (!isCurrentRequest()) throw new Error('OBSOLETE_WORKSPACE_READ'); + const response = await fetchImpl(peopleRecordUrl(config), { + headers: { Authorization: authorization }, + credentials: 'omit', + }); + if (response.status === 401 || response.status === 403) throw new Error('PEOPLE_ACCESS_DENIED'); + if (!response.ok) throw new Error('PEOPLE_REQUEST_FAILED'); + return response.json(); +} + +function setLocale(locale) { + const dictionary = translations[locale]; + document.documentElement.lang = locale; + document.documentElement.dataset.locale = locale; + document.querySelectorAll('[data-i18n]').forEach((element) => { + const value = dictionary[element.dataset.i18n]; + if (value) element.textContent = value; + }); + document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => { + const value = dictionary[element.dataset.i18nPlaceholder]; + if (value) element.placeholder = value; + }); + document.querySelectorAll('[data-i18n-aria-label]').forEach((element) => { + const value = dictionary[element.dataset.i18nAriaLabel]; + if (value) element.setAttribute('aria-label', value); + }); + document.getElementById('locale-toggle').textContent = locale === 'en' ? '한국어' : 'English'; +} + +function activateView(viewName) { + document.querySelectorAll('[data-view]').forEach((view) => { + const active = view.dataset.view === viewName; + view.hidden = !active; + view.classList.toggle('is-hidden', !active); + }); + document.querySelectorAll('[data-view-link]').forEach((link) => { + const active = link.dataset.viewLink === viewName; + link.classList.toggle('is-active', active); + if (link.matches('button')) { + if (active) link.setAttribute('aria-current', 'page'); + else link.removeAttribute('aria-current'); + } + }); + document.getElementById('main-content').focus({ preventScroll: true }); +} + +function openDialog(id) { + const dialog = document.getElementById(id); + if (typeof dialog.showModal === 'function') dialog.showModal(); + else dialog.hidden = false; +} + +function closeDialog(id) { + const dialog = document.getElementById(id); + if (typeof dialog.close === 'function') dialog.close(); + else dialog.hidden = true; +} + +function resetConfirmationState() { + document.getElementById('confirmation-reason').value = ''; + document.getElementById('confirmation-status').hidden = true; +} + +function jobAnalysisMessage(key) { + return translations[document.documentElement.dataset.locale || 'en'][key]; +} + +function setJobAnalysisStatus(key, state = 'idle') { + const status = document.getElementById('job-analysis-status'); + status.dataset.i18n = key; + status.dataset.state = state; + status.textContent = jobAnalysisMessage(key); +} + +function clearJobAnalysisSnapshot() { + document.getElementById('job-analysis-result').hidden = true; + document.getElementById('job-analysis-analysis-id').textContent = 'unknown'; + document.getElementById('job-analysis-state').textContent = 'unknown'; + document.getElementById('job-analysis-effective').textContent = 'unknown'; + document.getElementById('job-analysis-recorded').textContent = 'unknown'; + document.getElementById('job-analysis-task-count').textContent = '0'; + document.getElementById('job-analysis-ksao-count').textContent = '0'; +} + +function renderJobAnalysisSnapshot(snapshot) { + const result = document.getElementById('job-analysis-result'); + const tasks = Array.isArray(snapshot?.tasks) ? snapshot.tasks : []; + const ksaos = Array.isArray(snapshot?.ksao_requirements) ? snapshot.ksao_requirements : []; + document.getElementById('job-analysis-analysis-id').textContent = snapshot?.analysis_record_id || 'unknown'; + document.getElementById('job-analysis-state').textContent = snapshot?.status_code || 'unknown'; + document.getElementById('job-analysis-effective').textContent = snapshot?.effective_from || 'unknown'; + document.getElementById('job-analysis-recorded').textContent = snapshot?.recorded_at || 'unknown'; + document.getElementById('job-analysis-task-count').textContent = String(tasks.length); + document.getElementById('job-analysis-ksao-count').textContent = String(ksaos.length); + result.hidden = false; +} + +function peopleApiMessage(key) { + return translations[document.documentElement.dataset.locale || 'en'][key]; +} + +function setPeopleApiStatus(key, state = 'idle') { + const status = document.getElementById('people-api-status'); + status.dataset.i18n = key; + status.dataset.state = state; + status.textContent = peopleApiMessage(key); +} + +function clearPeopleRecord() { + document.getElementById('people-api-result').hidden = true; + document.getElementById('people-api-display-name').textContent = 'unknown'; + document.getElementById('people-api-employment-status').textContent = 'unknown'; +} + +function renderPeopleRecord(payload) { + const fields = payload?.fields; + if (!fields || typeof fields !== 'object') throw new Error('PEOPLE_REQUEST_FAILED'); + document.getElementById('people-api-display-name').textContent = fields.display_name || 'unknown'; + document.getElementById('people-api-employment-status').textContent = fields.employment_status_code || 'unknown'; + document.getElementById('people-api-result').hidden = false; +} + +if (typeof document !== 'undefined') { + let locale = 'en'; + let jobAnalysisRequestVersion = 0; + let peopleApiRequestVersion = 0; + document.querySelectorAll('[data-view-link]').forEach((link) => link.addEventListener('click', () => activateView(link.dataset.viewLink))); + document.getElementById('locale-toggle').addEventListener('click', () => { + locale = nextLocale(locale); + setLocale(locale); + }); + document.querySelectorAll('[data-open-dialog]').forEach((button) => button.addEventListener('click', () => openDialog(button.dataset.openDialog))); + document.querySelectorAll('[data-close-dialog]').forEach((button) => button.addEventListener('click', () => closeDialog(button.dataset.closeDialog))); + // A changed purpose needs a new explicit review; neither prior outcome remains current. + const invalidatePersonalDetailsReview = () => { + document.getElementById('permission-panel').hidden = true; + document.getElementById('details-panel').hidden = true; + }; + document.getElementById('access-purpose').addEventListener('input', invalidatePersonalDetailsReview); + document.getElementById('access-purpose').addEventListener('change', invalidatePersonalDetailsReview); + document.querySelector('[data-action="view-personal-details"]').addEventListener('click', () => { + const authorized = isPurposeAuthorized(document.getElementById('access-purpose').value); + document.getElementById('permission-panel').hidden = authorized; + document.getElementById('details-panel').hidden = !authorized; + }); + document.querySelector('[data-action="correct-history"]').addEventListener('click', () => { + resetConfirmationState(); + openDialog('confirmation-dialog'); + }); + document.getElementById('confirmation-form').addEventListener('submit', (event) => { + if (event.submitter?.value !== 'confirm') return; + const reason = document.getElementById('confirmation-reason'); + if (!reason.value.trim()) { + event.preventDefault(); + reason.focus(); + return; + } + event.preventDefault(); + document.getElementById('confirmation-status').hidden = false; + }); + const jobAnalysisForm = document.getElementById('job-analysis-form'); + const jobAnalysisConfig = globalThis.__ORGMETRA_JOB_ANALYSIS__; + if (jobAnalysisConfig?.tenantRecordId) document.getElementById('job-analysis-tenant').value = jobAnalysisConfig.tenantRecordId; + if (jobAnalysisConfig?.analysisRecordId) document.getElementById('job-analysis-record').value = jobAnalysisConfig.analysisRecordId; + if (jobAnalysisConfig?.purposeCode) document.getElementById('job-analysis-purpose').value = jobAnalysisConfig.purposeCode; + if (!jobAnalysisConfig?.baseUrl || typeof jobAnalysisConfig?.getAuthorization !== 'function') { + setJobAnalysisStatus('jobAnalysisNotConfigured', 'not-configured'); + } else { + setJobAnalysisStatus('jobAnalysisReady'); + } + // Editing read coordinates invalidates pending and displayed evidence, even after reverting. + const invalidateJobAnalysisRead = () => { + jobAnalysisRequestVersion += 1; + clearJobAnalysisSnapshot(); + if (!jobAnalysisConfig?.baseUrl || typeof jobAnalysisConfig?.getAuthorization !== 'function') { + setJobAnalysisStatus('jobAnalysisNotConfigured', 'not-configured'); + } else { + setJobAnalysisStatus('jobAnalysisReady'); + } + }; + jobAnalysisForm.addEventListener('input', invalidateJobAnalysisRead); + jobAnalysisForm.addEventListener('change', invalidateJobAnalysisRead); + jobAnalysisForm.addEventListener('submit', async (event) => { + event.preventDefault(); + if (!jobAnalysisConfig?.baseUrl || typeof jobAnalysisConfig?.getAuthorization !== 'function') { + clearJobAnalysisSnapshot(); + setJobAnalysisStatus('jobAnalysisNotConfigured', 'not-configured'); + return; + } + const requestVersion = ++jobAnalysisRequestVersion; + clearJobAnalysisSnapshot(); + const config = { + ...(jobAnalysisConfig || {}), + tenantRecordId: document.getElementById('job-analysis-tenant').value, + analysisRecordId: document.getElementById('job-analysis-record').value, + purposeCode: document.getElementById('job-analysis-purpose').value, + }; + setJobAnalysisStatus('jobAnalysisLoading', 'loading'); + try { + const snapshot = await fetchJobAnalysisSnapshot(config, globalThis.fetch, () => requestVersion === jobAnalysisRequestVersion); + if (requestVersion !== jobAnalysisRequestVersion) return; + renderJobAnalysisSnapshot(snapshot); + setJobAnalysisStatus('jobAnalysisLoaded', 'loaded'); + } catch (error) { + if (requestVersion !== jobAnalysisRequestVersion) return; + clearJobAnalysisSnapshot(); + setJobAnalysisStatus(error.message === 'JOB_ANALYSIS_ACCESS_DENIED' ? 'jobAnalysisDenied' : 'jobAnalysisFailed', 'error'); + } + }); + const peopleApiForm = document.getElementById('people-api-form'); + const peopleApiConfig = globalThis.__ORGMETRA_PEOPLE__; + if (peopleApiConfig?.tenantRecordId) document.getElementById('people-api-tenant').value = peopleApiConfig.tenantRecordId; + if (peopleApiConfig?.personRecordId) document.getElementById('people-api-person').value = peopleApiConfig.personRecordId; + if (peopleApiConfig?.effectiveOn) document.getElementById('people-api-effective').value = peopleApiConfig.effectiveOn; + if (peopleApiConfig?.purposeCode) document.getElementById('people-api-purpose').value = peopleApiConfig.purposeCode; + if (Array.isArray(peopleApiConfig?.requestedFields)) document.getElementById('people-api-fields').value = peopleApiConfig.requestedFields.join(','); + if (!peopleApiConfig?.baseUrl || typeof peopleApiConfig?.getAuthorization !== 'function') { + setPeopleApiStatus('peopleApiNotConfigured', 'not-configured'); + } else { + setPeopleApiStatus('peopleApiReady'); + } + const invalidatePeopleRead = () => { + peopleApiRequestVersion += 1; + clearPeopleRecord(); + if (!peopleApiConfig?.baseUrl || typeof peopleApiConfig?.getAuthorization !== 'function') { + setPeopleApiStatus('peopleApiNotConfigured', 'not-configured'); + } else { + setPeopleApiStatus('peopleApiReady'); + } + }; + peopleApiForm.addEventListener('input', invalidatePeopleRead); + peopleApiForm.addEventListener('change', invalidatePeopleRead); + peopleApiForm.addEventListener('submit', async (event) => { + event.preventDefault(); + if (!peopleApiConfig?.baseUrl || typeof peopleApiConfig?.getAuthorization !== 'function') { + clearPeopleRecord(); + setPeopleApiStatus('peopleApiNotConfigured', 'not-configured'); + return; + } + const requestVersion = ++peopleApiRequestVersion; + clearPeopleRecord(); + const config = { + ...(peopleApiConfig || {}), + tenantRecordId: document.getElementById('people-api-tenant').value, + personRecordId: document.getElementById('people-api-person').value, + effectiveOn: document.getElementById('people-api-effective').value, + purposeCode: document.getElementById('people-api-purpose').value, + requestedFields: document.getElementById('people-api-fields').value.split(',').map((field) => field.trim()), + }; + setPeopleApiStatus('peopleApiLoading', 'loading'); + try { + const record = await fetchPeopleRecord(config, globalThis.fetch, () => requestVersion === peopleApiRequestVersion); + if (requestVersion !== peopleApiRequestVersion) return; + renderPeopleRecord(record); + setPeopleApiStatus('peopleApiLoaded', 'loaded'); + } catch (error) { + if (requestVersion !== peopleApiRequestVersion) return; + clearPeopleRecord(); + setPeopleApiStatus(error.message === 'PEOPLE_ACCESS_DENIED' ? 'peopleApiDenied' : 'peopleApiFailed', 'error'); + } + }); + document.addEventListener('orgmetra:authority-invalidated', () => { + invalidateJobAnalysisRead(); + invalidatePeopleRead(); + }); + setLocale(locale); +} \ No newline at end of file diff --git a/apps/hr-workspace/index.html b/apps/hr-workspace/index.html new file mode 100644 index 000000000..209bba2f4 --- /dev/null +++ b/apps/hr-workspace/index.html @@ -0,0 +1,159 @@ + + + + + + + Orgmetra HR workspace + + + + + + +
+ + +
+
+
+

People workspace

+

Protected People API boundary: not connected in this fixture

+
+ +
+ +
+
+

HR Home

+ Fixture evidence +
+
+

Today

+
    +
  • 3 hiring decisions need evidence review
  • +
  • 2 position changes effective next week
  • +
  • 1 validation study needs approval
  • +
+
+
+
+

Work queue

Next actions

3
+
+ + + +
+
+
+

Evidence state

Ready for human review

Review
+

Orgmetra keeps the actor, purpose, reason, and evidence versions visible before a high-impact action.

+
ActorHR operations reviewer
PurposeHR operations
SourceProtected People API contract
+
+
+
+ + + + +
+
+ + +

Evidence drawer

Decision packet evidence

+

These references are reviewable inputs. They do not make an autonomous employment decision.

+
  • EV-2026-014Structured interview criterion ratings
  • EV-2026-021SME-approved job profile version 4
  • EV-2026-027Human reviewer reason and purpose record
+
+
+ + +
+

High-impact confirmation

Correct history

+

A correction remains a draft until a human confirms the actor, purpose, reason, and evidence context.

+
+ +
+
+
+ + \ No newline at end of file diff --git a/apps/hr-workspace/styles.css b/apps/hr-workspace/styles.css new file mode 100644 index 000000000..adfc9d666 --- /dev/null +++ b/apps/hr-workspace/styles.css @@ -0,0 +1,122 @@ +:root { + --workspace-sidebar: #0f172a; + --workspace-sidebar-muted: #aeb9ca; + --workspace-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); +} + +* { box-sizing: border-box; } +body { margin: 0; background: var(--orgmetra-surface-page); color: var(--orgmetra-text-primary); font: 16px/1.5 "Source Sans 3", system-ui, sans-serif; } +button, input, select, textarea { font: inherit; } +button { cursor: pointer; } +button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid var(--orgmetra-focus-ring); outline-offset: 2px; } +.skip-link { position: fixed; top: var(--orgmetra-space-sm); left: var(--orgmetra-space-sm); z-index: 1000; transform: translateY(calc(-100% - var(--orgmetra-space-lg))); border: 2px solid var(--orgmetra-action-review); border-radius: var(--orgmetra-radius-md); padding: 10px 14px; background: var(--orgmetra-surface-card); color: var(--orgmetra-text-primary); font-weight: 700; text-decoration: none; } +.skip-link:focus, .skip-link:focus-visible { transform: translateY(0); } +html[lang="en"] .skip-link [lang="ko"], html[lang="ko"] .skip-link [lang="en"] { display: none; } +.app-shell { min-height: 100vh; display: grid; grid-template-columns: 232px minmax(0, 1fr); } +.sidebar { display: flex; flex-direction: column; gap: var(--orgmetra-space-md); padding: 32px 20px; background: var(--workspace-sidebar); color: white; } +.brand { color: white; font-size: 24px; font-weight: 700; text-decoration: none; letter-spacing: -0.02em; } +.eyebrow { margin: 0; color: var(--orgmetra-text-muted); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } +.sidebar .eyebrow { color: var(--workspace-sidebar-muted); } +.nav-list { display: grid; gap: var(--orgmetra-space-xs); margin-top: var(--orgmetra-space-lg); } +.nav-item { width: 100%; border: 0; border-radius: var(--orgmetra-radius-md); padding: 11px 12px; background: transparent; color: var(--workspace-sidebar-muted); text-align: left; } +.nav-item:hover, .nav-item.is-active { background: rgba(255, 255, 255, 0.12); color: white; } +.sidebar-note { display: flex; align-items: center; gap: var(--orgmetra-space-sm); margin-top: auto; color: var(--workspace-sidebar-muted); font-size: 13px; } +.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--orgmetra-action-approve); } +.main-content { min-width: 0; padding: 28px clamp(20px, 4vw, 64px) 64px; } +.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--orgmetra-space-md); max-width: 1180px; margin: 0 auto 48px; } +.topbar-status { margin: 2px 0 0; color: var(--orgmetra-text-muted); font-size: 13px; } +.view { max-width: 1180px; margin: 0 auto; } +[hidden] { display: none !important; } +.view.is-hidden { display: none; } +.view-heading, .profile-heading, .panel-heading, .dialog-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--orgmetra-space-md); } +h1, h2, p { margin-top: 0; } +h1 { margin-bottom: 0; font-size: clamp(28px, 4vw, 42px); line-height: 1.1; letter-spacing: -0.03em; } +h2 { margin-bottom: 0; font-size: 20px; line-height: 1.2; letter-spacing: -0.015em; } +.breadcrumb { margin: 0 0 var(--orgmetra-space-sm); color: var(--orgmetra-text-muted); font-size: 13px; } +.badge, .count-badge { display: inline-flex; align-items: center; min-height: 26px; border-radius: 999px; padding: 3px 10px; font-size: 12px; font-weight: 700; white-space: nowrap; } +.badge-fixture { background: #e2e8f0; color: var(--orgmetra-action-export); } +.badge-review { background: #dbeafe; color: var(--orgmetra-action-review); } +.badge-authorized { background: #dcfce7; color: var(--orgmetra-action-approve); } +.badge-neutral { background: #eef2f7; color: var(--orgmetra-text-muted); } +.count-badge { background: #e0e7ff; color: var(--orgmetra-action-review); } +.summary-card, .profile-card, .panel { border: 1px solid var(--orgmetra-border-subtle); border-radius: 14px; background: var(--orgmetra-surface-card); box-shadow: var(--workspace-shadow); } +.summary-card { margin-top: 28px; padding: 26px; } +.summary-lead { margin-bottom: 2px; font-size: 20px; font-weight: 600; } +.summary-list { display: grid; gap: 4px; margin: 0; padding-left: 20px; color: var(--orgmetra-text-primary); } +.summary-list strong { font-variant-numeric: tabular-nums; } +.section-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--orgmetra-space-lg); margin-top: var(--orgmetra-space-lg); } +.panel, .profile-card { padding: 24px; } +.panel-accent-review { border-top: 4px solid var(--orgmetra-action-review); } +.panel-heading { margin-bottom: 20px; } +.panel-heading .eyebrow { margin-bottom: 4px; } +.panel-copy { color: var(--orgmetra-text-muted); } +.action-list { display: grid; gap: var(--orgmetra-space-sm); } +.action-row { display: flex; align-items: center; justify-content: space-between; gap: var(--orgmetra-space-md); width: 100%; border: 1px solid transparent; border-radius: var(--orgmetra-radius-md); padding: 12px; background: rgba(255, 255, 255, 0.8); color: inherit; text-align: left; } +.action-row:hover { border-color: var(--orgmetra-action-review); background: white; } +.action-row strong, .action-row small { display: block; } +.action-row small { margin-top: 2px; color: var(--orgmetra-text-muted); } +.fact-list { display: grid; gap: 1px; overflow: hidden; border: 1px solid var(--orgmetra-border-subtle); border-radius: var(--orgmetra-radius-md); } +.fact-list div { display: flex; justify-content: space-between; gap: var(--orgmetra-space-md); padding: 10px 12px; background: white; } +.fact-list span { color: var(--orgmetra-text-muted); } +.profile-card { margin-top: 28px; } +.profile-heading { align-items: center; margin-bottom: 24px; } +.profile-heading h2 { margin-bottom: 4px; font-size: 28px; } +.muted, .helper-text { margin-bottom: 0; color: var(--orgmetra-text-muted); } +.concept-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--orgmetra-space-sm); } +.concept-card { display: grid; gap: 3px; min-width: 0; border: 1px solid var(--orgmetra-border-subtle); border-radius: var(--orgmetra-radius-md); padding: 14px; background: white; } +.concept-card span { color: var(--orgmetra-text-muted); font-size: 13px; } +.concept-card strong { overflow-wrap: anywhere; font-size: 14px; } +.concept-card small { color: var(--orgmetra-text-muted); font-size: 13px; } +.profile-toolbar { display: flex; align-items: end; justify-content: space-between; gap: var(--orgmetra-space-md); margin: var(--orgmetra-space-lg) 0; } +.field-label { display: grid; gap: 6px; color: var(--orgmetra-text-muted); font-size: 13px; font-weight: 700; } +.select-field, input, textarea { min-height: 42px; border: 1px solid var(--orgmetra-border-subtle); border-radius: var(--orgmetra-radius-md); padding: 8px 10px; background: white; color: var(--orgmetra-text-primary); } +.toolbar-actions, .dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--orgmetra-space-sm); } +.button { border: 1px solid transparent; border-radius: var(--orgmetra-radius-md); padding: 9px 14px; font-weight: 700; } +.button-secondary { border-color: var(--orgmetra-border-subtle); background: white; color: var(--orgmetra-text-primary); } +.button-review { background: var(--orgmetra-action-review); color: white; } +.button-review:hover { background: #163d6e; } +.notice { display: flex; gap: var(--orgmetra-space-sm); margin: var(--orgmetra-space-md) 0; border-radius: var(--orgmetra-radius-md); padding: 12px 14px; } +.notice-danger { border: 1px solid #fecaca; background: #fef2f2; color: var(--orgmetra-danger); } +.notice-success { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; } +.notice-neutral { border: 1px solid #cbd5e1; background: #f8fafc; color: var(--orgmetra-text-primary); } +.table-wrap { overflow-x: auto; } +table { width: 100%; border-collapse: collapse; background: white; font-size: 14px; } +th, td { border-bottom: 1px solid var(--orgmetra-border-subtle); padding: 12px; text-align: left; vertical-align: top; } +thead th { color: var(--orgmetra-text-muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; } +tbody th { font-weight: 700; } +tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; } +.helper-text { margin-top: var(--orgmetra-space-md); font-size: 13px; } +.dialog { width: min(560px, calc(100% - 32px)); border: 1px solid var(--orgmetra-border-subtle); border-radius: 14px; padding: 24px; color: var(--orgmetra-text-primary); box-shadow: 0 24px 80px rgba(15, 23, 42, 0.25); } +.dialog::backdrop { background: rgba(15, 23, 42, 0.5); } +.dialog-close { border: 0; background: transparent; color: var(--orgmetra-text-muted); font-size: 26px; line-height: 1; } +.evidence-list { display: grid; gap: var(--orgmetra-space-sm); margin: 20px 0 28px; padding: 0; list-style: none; } +.evidence-list li { display: grid; grid-template-columns: 120px 1fr; gap: var(--orgmetra-space-sm); border-bottom: 1px solid var(--orgmetra-border-subtle); padding-bottom: var(--orgmetra-space-sm); } +.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--orgmetra-space-md); margin: 20px 0; } +.field-wide { grid-column: 1 / -1; } +textarea { resize: vertical; } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +.storybook-preview { width: min(100%, 860px); padding: var(--orgmetra-space-lg); background: var(--orgmetra-surface-page); color: var(--orgmetra-text-primary); } +.storybook-frame { display: grid; gap: var(--orgmetra-space-lg); min-width: min(100%, 640px); } +.storybook-state-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--orgmetra-space-lg); } +.storybook-state-grid > div { display: grid; gap: var(--orgmetra-space-sm); align-content: start; } +.storybook-state-grid .eyebrow { margin-bottom: 0; } + +@media (max-width: 820px) { + .app-shell { display: block; } + .sidebar { gap: var(--orgmetra-space-sm); padding: 18px 20px; } + .nav-list { display: flex; margin-top: var(--orgmetra-space-sm); } + .nav-item { width: auto; } + .sidebar-note { display: none; } + .topbar { margin-bottom: 32px; } + .section-grid, .concept-grid { grid-template-columns: 1fr; } + .profile-toolbar { align-items: stretch; flex-direction: column; } + .toolbar-actions { justify-content: flex-start; } +} + +@media (max-width: 520px) { + .main-content { padding-inline: 16px; } + .topbar, .view-heading, .profile-heading, .panel-heading { align-items: flex-start; flex-direction: column; } + .form-grid { grid-template-columns: 1fr; } + .field-wide { grid-column: auto; } + .storybook-state-grid { grid-template-columns: 1fr; } +} diff --git a/apps/hr-workspace/workspace.stories.js b/apps/hr-workspace/workspace.stories.js new file mode 100644 index 000000000..8d813e243 --- /dev/null +++ b/apps/hr-workspace/workspace.stories.js @@ -0,0 +1,134 @@ +const actionTokens = Object.freeze({ + approve: '--orgmetra-action-approve', + review: '--orgmetra-action-review', + correct: '--orgmetra-action-correct', + 'request-evidence': '--orgmetra-action-request-evidence', + compare: '--orgmetra-action-compare', + export: '--orgmetra-action-export', + escalate: '--orgmetra-action-escalate' +}); + +const frame = (content) => `
${content}
`; + +const actionButton = ({ action, label, disabled = false, loading = false, autofocus = false }) => { + const colorToken = actionTokens[action] ?? actionTokens.review; + return ``; +}; + +const protectedReadNotConnected = ({ title, boundary }) => frame(` +
+
+
+

API-bound read

+

${title}

+
+ Not connected +
+
+ Connect the host before loading protected data. + Provide the API base URL and a short-lived authorization provider, then load the protected record. +
+

${boundary} No local fallback is used and this UI does not store the request credential.

+
+`); + +export default { + title: 'Orgmetra/HR Workspace', + tags: ['autodocs'] +}; + +export const ActionButtons = { + name: 'HrActionButton states', + render: () => frame(` +
+

Default

${actionButton({ action: 'review', label: 'Review evidence' })}
+

Focus-visible ready

${actionButton({ action: 'approve', label: 'Approve', autofocus: true })}
+

Disabled

${actionButton({ action: 'correct', label: 'Correct history', disabled: true })}
+

Loading

${actionButton({ action: 'request-evidence', label: 'Request evidence', loading: true })}
+
+ `) +}; + +export const KeyboardBypass = { + name: 'Keyboard bypass — focused', + render: () => ` +
+ +
+ +

WCAG 2.4.1 interaction state

Keyboard bypass target

The first keyboard action exposes a visible bypass control that moves focus beyond repeated navigation.

+
+
+ ` +}; + +export const FieldStates = { + name: 'Read-only and validation error', + render: () => frame(` +
+ + +
+ `) +}; + +export const PermissionDenied = { + render: () => frame(` +
+

Purpose-bound access

Permission denied

Recruiting
+ +
+ `) +}; + +export const EvidenceDrawer = { + render: () => frame(` + +

Evidence drawer

Decision packet evidence

+

Reviewable inputs remain visible before a high-impact action.

+
  • EV-2026-014Structured interview criterion ratings
  • EV-2026-021SME-approved job profile version 4
+
${actionButton({ action: 'request-evidence', label: 'Request more evidence' })}
+
+ `) +}; + +export const HighRiskConfirmation = { + render: () => frame(` + +

High-impact confirmation

Correct history

+

Preview, actor, purpose, reason, and evidence context are required before confirmation.

+
+
${actionButton({ action: 'correct', label: 'Confirm correction' })}
+
+ `) +}; + +export const AssignmentSplit = { + render: () => frame(` +
+

Assignment split

Visible allocation capacity

100.00%
+
Exact assignment allocation values
AssignmentAllocationNext action
Platform60.00%No action required
Governance40.00%No action required
+
+ `) +}; + +export const PeopleNotConnected = { + name: 'People API — not connected', + render: () => protectedReadNotConnected({ + title: 'Protected People record', + boundary: 'The host owns connection and authorization setup.' + }) +}; + +export const JobAnalysisNotConnected = { + name: 'Job Analysis API — not connected', + render: () => protectedReadNotConnected({ + title: 'Job Analysis snapshot', + boundary: 'The host owns connection and authorization setup.' + }) +}; diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index 31f3ff23e..53ce2c06b 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -1,5 +1,25 @@ # Operability +## Foundation manifest integrity and uncaptured source + +The active-PR manifest contract separates `target_branch: "develop"` (intended integration destination) from `generated_source_branch: null` (generation-source authority was not captured). Null is explicit in every filesystem export, including a named checkout; it is not an archive-only fallback or authenticated branch provenance. The producer never guesses a source from Git, environment variables, PR targets, or caller labels. Validation rejects omitted or non-null source claims, a wrong/missing target, and any legacy `generated_for_branch` field. Matching digest, byte count, line count, and unique normalized path inventory establishes artifact integrity only, not generation-event authority, protection, merge, or release. + +After completing all source changes, run `python3 tests/validate_repository.py --print-manifest` against the complete final source and write its actual output to `manifest.json`. Printing is deterministic and read-only; it does not repair the file itself. Never overlay a historical manifest onto newer source. Normal validation remains `npm run validate`; the new offline stdlib manifest regressions are in its existing `tests/workspace-foundation-integration.test.mjs` discovery path, with the unchanged maximum native-child timeout of 15 seconds and fresh temporary manifest roots. They launch no application server, database, network, or credential operation; existing browser/config tests and deadlines remain unchanged. + +This is the PR owner's implementation choice under standing user authority, not a new human approval. If an independent review or release contract requires authenticated non-null generation provenance, that remains a separate unmet producer-authority requirement; null and current-checkout observations cannot satisfy it. Independent whole-successor review, normal exact-head CI/security gates, formal approvals, protected integration, host notification, live identity/database evidence, and release obligations remain open. + +## Local browser fixture operation + +The active-PR Playwright harness refuses existing servers rather than reusing them. Leave unrelated listeners running; if port 4173 is occupied, choose an available port with `ORGMETRA_WORKSPACE_PORT=43127 npm run test:e2e`. The override accepts only canonical decimal TCP ports 1–65535 and must identify a port available to the fixture; an unset override retains 4173. Do not kill a shared service to make a test pass. Keep the configured case deadlines and recording enabled, and verify the owned fixture exits and its listener closes after the run. Local fixture success is not production, hosted-gate, or merge approval. + +## Proposed isolated CI routing + +The active PR routes the two repository-owned workflows to the proposed `CWL CI isolated` group with `[self-hosted, linux, x64, cwlab-ci-isolated]` labels. The selector matches the central PR #2565 proposal. It does not prove that the group is released, registered, online, or accessible to Orgmetra. Do not substitute a privileged central runner or add the label to an existing shared runner. + +Before these jobs execute, the existing infrastructure owners must verify repository access, effective network isolation, clean per-job state, cleanup, and a canary under `linux-cluster-ops#326` and `quarantine-sandbox-runtime#136/#137`. Foundation also requires Python 3.14, Node 24, Docker, host `psql`, Chromium dependencies, and the pinned PostgreSQL 16.14 image. Recovery retains its two pinned PostgreSQL 17.6 services and ports. Managed GitHub Code Quality uses a separate dynamic workflow; its existing settings owner must resolve that routing without creating a local duplicate. Local source checks do not replace executed exact-head jobs, independent approvals, security gates, or protected integration. + +Both active-PR workflows provision pinned Node 24 and run the reviewed-lockfile `npm ci` before their first `npm run validate`. Foundation reorders its existing install; Recovery adds the same install and copies Foundation's pinned Python 3.14 setup before its Python provenance consumer. Actual Playwright config-import regressions require the reviewed development dependencies even though the foundation validator itself is dependency-free. Do not rely on inherited or symlinked `node_modules`; local validation is not clean hosted execution or approval. + ## SLO candidates - HRIS core read availability: 99.9% for production deployments. diff --git a/docs/PRD.md b/docs/PRD.md index 674a3b96c..d16e43014 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -59,6 +59,18 @@ Current HR systems often separate job architecture, recruiting, assessment, empl - Employee profile with bitemporal assignment history. - Validation dashboard shell. +The checkout also contains a local fixture slice at `apps/hr-workspace/` for +HR Home and Employee Profile, with a local Storybook runtime for its tokenized +states. It is an interaction and accessibility contract for the protected +People API boundary, not a connected or deployed customer workflow until its +API and browser E2E evidence are merged and released. On the active UI PR, +editing a People or Job Analysis request field must clear the previously +shown protected values and invalidate a pending response, even if the user +restores the old field value. Only a fresh explicit read may display values +for the current request. Changing the fixture's personal-details access purpose +must also hide its prior allowed or denied outcome. Selecting a purpose does not +perform a review; the user must explicitly request the next outcome. + ### P2 scale - HRIS migration pipelines. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index fd6dd3ea6..fd2d8a5fc 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -39,6 +39,38 @@ Evaluation fails closed unless request, actor, resource, and policy tenants all Authorization evidence contains only governance metadata, including the opaque actor and exact target-resource references, plus field names, never protected values. A denial returns a stable reason code and next safe action. An allow decision returns only the exact requested field subset, not every field the policy could permit. Both allow and denial evidence preserve the exact target reference so immutable audit correlation cannot collapse distinct person or employment records into one resource-kind-level event. These rules implement the Orgmetra side of the NIST SP 800-162 ABAC shape and attribute-integrity principles from NIST SP 800-205; ADR 0008 records the boundary. +## Active-PR workspace display boundary + +People and Job Analysis read forms invalidate their pending response generation +and clear displayed protected values on input or change events. An edit followed +by a revert does not revive the old response. This local display fence neither +cancels the request already sent nor replaces authoritative API authorization. +A fresh explicit read still uses the existing host-provided credential and +cookie-free transport. Synthetic Node and Chromium regressions are local +boundary evidence, not released authentication or deployment acceptance. +For retained documents, a payload-free `orgmetra:authority-invalidated` document +event synchronously clears both read displays and advances both response fences. +The embedding host must emit it before exposing the document to a changed +account, tenant, or effective authority, including sign-out and revocation. +It grants no access and does not infer identity from credentials or form inputs. +Host notification timing, coherent provider/coordinates, and release integration +remain separate requirements; otherwise the host must destroy the document. +The workspace submit handlers also pass their existing generation check to the +read helpers. After credential acquisition and before invoking fetch, an obsolete +submission is rejected and settles quietly through the existing version-fenced +catch; it cannot dispatch or clear a newer read. Input/change, edit-and-revert, +authority notification (including repeated or away-and-back notification), and a +newer submit invalidate credential-waiting submissions. The exported helpers +retain their two-argument behavior: a direct caller without a generation guard +has no such dispatch protection. Already-dispatched transport is not cancelled; +its response and body outcomes retain the existing display fences. This local +Node seam does not establish host notification timing, actual issuer revocation, +identity integration, hosted CI, independent approval, or protected release. +The fixture's personal-details purpose selector also clears both prior allowed +and denied display states on input or change. Changing or restoring a purpose +never grants access or performs a new review. This fixture display rule does not +claim authoritative authorization, audit persistence, or a protected-value read. + ## Mutation security contract Every mutating HTTP operation and its server-side command handler requires one validated `Idempotency-Key` that crosses the command boundary into durable transactional replay state. The published OpenAPI employment, position, assignment, person, job-profile, and selection-decision command families require `X-Tenant-Reference`, `X-Actor-Reference`, and `X-Purpose-Code`; those values must match the authenticated Keyverse principal and the operation-specific least-privilege scope. The executable People mutation handlers added on this branch currently implement employment, position, and assignment creation with those headers. Person, job-profile, and selection-decision remain published foundation API contracts until their server handlers are integrated; their OpenAPI presence is not runtime evidence. Confirmed-hire materialization instead binds the tenant in `/v1/tenants/{tenant_record_id}/candidate-worker-conversions`, the business purpose in its exact query parameter, and the actor through the authenticated principal. It does not accept weaker duplicate actor/tenant/purpose header authorities. diff --git a/docs/STORYBOOK.md b/docs/STORYBOOK.md index bd3dc7e15..0a5aef9f6 100644 --- a/docs/STORYBOOK.md +++ b/docs/STORYBOOK.md @@ -5,6 +5,7 @@ Design tokens live in `packages/design-tokens/` and map to the Figma file at htt ## Component inventory - `HrActionButton` (tokenized approve/review/correct/request-evidence/compare/export/escalate) +- `KeyboardBypass` (focused skip-to-main state tied to Figma HR Home node `1:10`) - `Button` - `LinkButton` - `TextField` @@ -44,7 +45,63 @@ Each interactive component requires at least these stories where applicable: ## Accessibility contract +- The first keyboard focus on a role workspace exposes a visible skip-to-main control before repeated workspace navigation; activation moves focus to the `main` landmark. +- The bypass label follows the active English/Korean locale and its focus treatment consumes the shared Orgmetra focus-ring token. - All charts require exact-value tables. - Every evidence citation opens a keyboard-accessible drawer. - High-impact actions require preview, reason, actor, purpose, and confirmation. - Missing evidence is shown as `unknown`, not `failed`. + +## Local executable slice + +`apps/hr-workspace/` is the first dependency-free executable slice of the +Employee Profile and HR Home experience. It consumes the shared CSS tokens, +keeps the Figma node IDs in the markup, and exercises keyboard bypass, +evidence review, purpose-bound permission denial, high-impact confirmation, +exact allocation values, and English/Korean labels. The Job Analysis and +Employee Profile People views are API-bound read surfaces: a host may inject +an API base URL and short-lived authorization provider through +`globalThis.__ORGMETRA_JOB_ANALYSIS__` and `globalThis.__ORGMETRA_PEOPLE__`. +The fixture has no such provider and therefore makes no connected-data claim +or local-data fallback. + +For a retained document, the host must synchronously dispatch the payload-free +`orgmetra:authority-invalidated` event on `document` before another account, +tenant, or effective authority can see or use the workspace. The listener clears +both protected read displays and invalidates their pending responses; it grants +no access and performs no read. The host must keep submissions unavailable until +its credential provider, destination, and request coordinates are coherent. +Use the original configuration objects/providers; replacing bootstrap globals is +not reconfiguration. Destroy and recreate the document if those objects must be +replaced or reliable notification is unavailable. Sign-out, revocation, repeated +notifications, and away-and-back transitions must not revive prior results. +The workspace submit handlers check their existing generation after credential +acquisition and before fetch: an invalidated credential-waiting submission does +not dispatch and settles without changing the newer read. This includes input +or change with reversion, repeated or away-and-back authority notifications, and +newer submissions. Direct helper callers that omit the optional generation guard +retain the original two-argument behavior, without this dispatch protection. +Already-dispatched transport is not cancelled; existing response/body display +fences remain. The new regression evidence is a synthetic Node handler seam, +not a new native-browser run. Actual host notification, issuer revocation, +identity integration, hosted CI, independent approval, and protected release +remain unverified. + +## Local Storybook runtime + +The repository uses Storybook `10.5.10` with +`@storybook/web-components-vite` and native HTML/CSS stories in +`apps/hr-workspace/workspace.stories.js`. The stories cover tokenized action +states, the focused keyboard-bypass state, read-only and validation-error +fields, purpose-bound denial, the keyboard-accessible evidence drawer, +high-impact confirmation, and exact assignment values. Shared design tokens +and workspace CSS are imported by `.storybook/preview.js`. + +Run `npm run storybook` for the local development UI or +`npm run build-storybook` for the static build. This is local component and +state evidence. Run `npm run test:e2e` for the Chromium browser contract, which +proves the initial keyboard bypass, human-review, permission, locale, +host-injected read, authorization, and no-fallback error states against a local +static runtime. This still does not prove a protected deployment: the Job +Analysis and People read surfaces require a real protected API runtime before +they can be called released. diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index c20813b72..6042e5eae 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -7,11 +7,16 @@ This document is the canonical owned-code coverage requirement. Every production The current foundation pack is executable documentation. Its validation command is: ```text +npm ci npm run validate ``` The command runs Python repository-integrity validation, the dependency-free Node foundation validator, Node regression tests, and mutation-style OpenAPI operation-contract tests. It must fail on a missing required artifact, manifest mismatch, invalid database name, missing tenant/evidence/audit/temporal DDL contract, incomplete high-risk OpenAPI operation context, empty OpenID Connect scope requirement, internal trace identifier in a client error schema, explicit unfinished-work marker, unbalanced Markdown fence, or incomplete Apache-2.0 license. Ordinary explanatory prose may contain words such as `placeholder`; only explicit TODO/TBD/FIXME marker forms are treated as unfinished work. +The install must include the reviewed development dependencies: `npm run validate` imports the actual Playwright config through the existing integration tests. Both active-PR workflows provision pinned Node 24 and run `npm ci` before validation; Recovery also provisions Foundation's pinned Python 3.14 runtime for its Python consumers. The ordering regression is checked in `tests/workspace-foundation-integration.test.mjs`. Inherited or symlinked `node_modules` does not establish clean-checkout execution. + +Python service tests use each service's project metadata, local `uv` source mappings, and checked lockfile. The canonical Job Analysis command is `uv run --project services/job-analysis-api --extra test pytest services/job-analysis-api/tests`; it resolves the owned HRIS kernel and Keyverse adapter from the repository and does not depend on a manually supplied `PYTHONPATH`. + ## Foundation test matrix | Evidence | Execution command | @@ -34,12 +39,16 @@ The command runs Python repository-integrity validation, the dependency-free Nod | Tenant/actor/purpose authorization matrix and negative high-impact commands | service-specific unit and integration test commands recorded in each service package | | AsyncAPI/CloudEvents envelope compatibility | provider and consumer contract test commands recorded beside the versioned event schema | | External adapter timeout, malformed response, tenant mismatch, and unavailable-state handling | fake-server tests in each adapter package | -| Role-workspace keyboard, focus, exact-value, permission-denied, and confirmation states | Storybook interaction/a11y tests plus browser E2E for the owning workspace | +| Role-workspace keyboard, focus, exact-value, permission-denied, and confirmation states | Storybook interaction/a11y tests plus `npm run test:e2e` for the owning workspace | The PostgreSQL scripts apply the checked-in migration chain required by the contract under test to a fresh database. The bitemporal and evidence-sealing tests execute concurrency regressions with an observable database barrier instead of a fixed scheduling assumption. The tenant-isolation test proves both read and write enforcement with unprivileged `NOLOGIN NOBYPASSRLS` roles, so table-owner/superuser bypass cannot manufacture a passing tenant result. The evidence-sealing test compares database output with independently precomputed canonical SHA-256 fixtures and forces a membership transaction to hold the evidence-set row lock before finalization, proving the digest snapshot includes evidence that committed first. The audit/outbox contract stores exact `AuditOutboxEvent.canonical_json()` bytes, independently verifies their SHA-256 digest in PostgreSQL, rejects extra top-level PII fields even when a caller recomputes the digest, and exercises outbox lifecycle invariants separately from immutable audit facts. The outbox-claim contract proves an already-expired lease cannot be created, verifies deterministic tenant-scoped claims return the immutable event/digest while live leases are excluded, then lets a valid one-second lease expire and requires atomic takeover of that same row with attempt count 2, a new future lease, and explicit `lease_expired` evidence. The dead-letter contract applies migrations 0001 through 0007, proves the dispatcher cannot select its own terminal attempt budget, rejects direct terminal DML before matching immutable escalation evidence and the stored budget are satisfied, exercises the real retry/claim path through the database-owned default fifth attempt, rejects retry at attempt five, lets the final lease expire, proves a replacement worker cannot create attempt six, proves the row remains bound to the recorded worker identity, rejects a foreign finalizer, permits that exact recorded identity to append terminal evidence after expiry, and rejects fabricated escalation evidence for nonterminal work. The People mutation idempotency contract applies the authoritative migration chain through 0012, verifies the replay record is written in the same transaction as its authoritative fact and audit/outbox evidence, proves rollback leaves no false replay marker, and uses concurrent exact-key sessions to prove one canonical committed identity wins without duplicate business facts. Foundation CI executes every matrix entry independently; a cancelled, skipped, queued, absent, neutral, failed, stale, predecessor-head, status-only, or model-only matrix result is not database evidence for the current head. Future service packages must publish their exact test, statement-coverage, branch-coverage, docstring, typecheck, and build commands in the package manifest and CI log. +## Browser fixture ownership + +The active-PR browser harness always starts its own loopback HTTP fixture; it never reuses an existing server, even outside CI. An occupied target URL fails before browser cases execute instead of accepting a foreign HTTP 200 response. `ORGMETRA_WORKSPACE_PORT` may select a distinct available port for concurrent local fixtures; an unset variable preserves port 4173. Overrides must be canonical ASCII decimal TCP ports from 1 through 65535, without signs, leading zeroes, whitespace, or other syntax, and invalid values fail during config import before server launch. The 15-second case timeout, 5-second assertion timeout, failure recording, and original 13 browser cases remain unchanged. Two additional Chromium cases hold a protected response while the user edits and reverts its request fields; the old response must remain hidden, a fresh explicit read must render, and a later edit must clear loaded values. One further Chromium case changes the personal-details fixture purpose after allowed and denied reviews, requires both previous states to clear, and proves that only a fresh explicit review displays the next outcome. Node handler regressions exercise that purpose selector and every People and Job Analysis request coordinate for both input and change events. These synthetic transport cases prove local display ownership, not live authentication, database authorization, or released customer acceptance. Config-import regressions run through `tests/workspace-foundation-integration.test.mjs` in `npm run validate`; these local harness contracts are not hosted-gate or deployment approval. + ## High-impact decision tests Required negative and provenance tests include: @@ -133,3 +142,24 @@ A model that omits evidence for a structural feature it uses is not eligible for - Permission-denied and Keyverse-unavailable states. - High-risk review, confirmation, recording, and audit states. - Narrow viewport, 200% zoom/reflow, reduced-motion, and high-contrast review. + +The current HR workspace browser contract runs Chromium against the static +workspace with Playwright. It covers the Employee Profile permission boundary, +English/Korean accessible labels, required-reason focus behavior, host-injected +People and Job Analysis reads, authorization/purpose request data, and denied +read errors without fixture fallback. The Chromium host-authority event case +covers synchronous clearing of both surfaces, all rendered-field scrubbing, and +fresh explicit reads. Separate Node VM cases cover repeated notifications and +held success/JSON/denial/body-error outcomes with distinct fresh values and +current denial. Those Node outcomes are not browser-executed schedules. These +synthetic host and transport controls do not establish actual host notification +timing, live account changes, request cancellation, or released identity +integration. Run the browser contract with: + +```text +npm run test:e2e +``` + +The browser contract is executable local/CI evidence; it does not promote the +fixture to a connected production deployment or authorize an employment +decision. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 22a4178fe..ee44e2ffc 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -1,5 +1,11 @@ # Traceability +## Source provenance and maturity limits (2026-10-05) + +The observed default-branch base is `develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f`. The local organization-cycle, candidate-to-worker conversion, People read/write business and persistence boundaries, Job Analysis persistence, criterion-observation scope, validity-case integrity, and purpose-bound PII implementation and their existing regressions are present at that immutable base. Those business and persistence sources remain unchanged in this PR. The People read HTTP error boundary and its regression source are changed by this active PR: `services/people-api/src/orgmetra_people_api/http.py` contains unexpected authentication-backend faults before protected reads and adds the canonical client-safe error envelope; `services/people-api/tests/test_http_route.py` covers those changes, response/log reference correlation, and cancellation propagation. These repairs are not attributed to the unchanged base. Their `implemented_on_active_pr` cells bound this current source assessment because protected-branch runtime truth is not established by the available evidence. This is not a claim that implementation is new or absent from the default-branch base. Historical protected classifications remain historical; they are not substituted for current configured protection, exact-head CI/review, deployed behavior, or release evidence. Test-family descriptions identify checked-in contracts and retained historical coverage claims, not newly executed PostgreSQL, package, or coverage results. Promotion to `implemented_on_protected_develop` still requires the existing evidence policy; no canonical vocabulary or approval requirement is added or relaxed. + +The external Keyverse row instead records the accepted OIDC/API architecture. Existing Orgmetra policy evaluation and opaque-subject binding consume already-authenticated attributes; injected `TokenAuthenticator` protocols are not an implemented Keyverse issuer/discovery/signature-verification integration. Those local components do not establish a published or deployed external identity connection. + ## 2. Product traceability matrix | Requirement | Architecture | Data object | Test family | ADR | Maturity | @@ -9,28 +15,30 @@ | Tenant-qualified HRIS integrity and fail-closed isolation | Core bounded contexts / Security architecture | `tenant_record`, tenant-qualified foreign keys, forced row-level security policies, tenant-scoped kernel query parameters | PostgreSQL cross-tenant FK/application-role RLS contracts plus kernel cross-tenant reconstruction, employment coverage, position coverage, seat-capacity, portfolio, exclusivity, and organization-hierarchy regressions | ADR-0001, ADR-0003 | implemented_on_active_pr | | Reserved UUID sentinel exclusion | Persistence integrity boundary | every foundation UUID `*_id` column plus audit/outbox identifiers | PostgreSQL inventory proof plus Nil/Max foundation and audit/outbox persistence regressions | ADR-0001, RFC 9562 | implemented_on_active_pr | | Normalized bitemporal organization/job/employment/position history | Core bounded contexts | `organization_unit_version`, `job_profile_version`, `employment_record_version`, `position_record_version` | PostgreSQL non-overlap, concurrent conflict, correction, rewrite-rejection, assignment-employment binding, and single-valued historical reconstruction | ADR-0001, ADR-0003, ADR-0004 | implemented_on_active_pr | -| Acyclic organization hierarchy at historical coordinates | Organization core | `organization_unit_version.parent_organization_unit_id` | indirect A→B→C→A rejection plus future-recorded and foreign-tenant isolation in `orgmetra_hris_kernel` | ADR-0001, ADR-0003 | implemented_on_protected_main | +| Acyclic organization hierarchy at historical coordinates | Organization core | `organization_unit_version.parent_organization_unit_id` | indirect A→B→C→A rejection plus future-recorded and foreign-tenant isolation in `orgmetra_hris_kernel` | ADR-0001, ADR-0003 | implemented_on_active_pr | | Effective/system time | Bitemporal HRIS | `effective_from`, `recorded_from` | strict half-open interval and historical-coordinate tests | ADR-0003 | implemented_on_active_pr | | Evidence-backed human selection decisions | Talent Acquisition | `decision_evidence_set`, `selection_decision_evidence`, `selection_decision` | database-owned SHA-256 sealing, non-empty evidence, drift/reuse rejection, OpenAPI human-confirmation tests | ADR-0001 | implemented_on_active_pr | -| Governed candidate-to-worker conversion | Talent Acquisition / People core | `candidate_worker_conversion_record` with candidate, person, employment, selection decision, audit event and outbox evidence | PostgreSQL exact hire/evidence/audit-envelope binding, correction provenance, tenant RLS, legacy-write rejection and bitemporal history contract | ADR-0001, ADR-0003, ADR-0006 | implemented_on_protected_main | -| GET-only People API | People API / purpose-bound read boundary | `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}`, `read_worker_people_record()`, `PostgresPeopleReadPort` | People API HTTP and PostgreSQL read contracts with exact 100% owned statement/branch coverage; current conversion lineage; no mutation writes | ADR-0002, ADR-0008 | implemented_on_protected_main | -| Governed People writes and confirmed-hire materialization | People API / purpose-bound mutation boundary | `POST /v1/employment-records`, `POST /v1/position-records`, `POST /v1/assignment-records`, `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions`, `people_mutation_idempotency_record` | People command/HTTP/PostgreSQL contracts with exact owned statement/branch coverage plus PostgreSQL tenant-RLS, atomic audit/outbox/idempotency, identical-retry replay, changed-command rejection, rollback, and concurrent-key regression | ADR-0002, ADR-0006, ADR-0008 | implemented_on_protected_main | -| Evidence-grounded Job analysis with governed Task/FJA/KSAO persistence | Job Analysis / Workforce Validation | `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `FunctionalJobAnalysisProfile`, `TaskKSAOLink`, `EvidenceSource`, `job_analysis_snapshot`, `job_analysis_task_item`, `job_analysis_ksao_item`, `job_analysis_task_ksao_link`, `job_analysis_write_command`, `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots`, `GET /v1/tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}` | domain tenant/Job isolation, source/version/digest provenance, task-KSAO completeness, deterministic canonicalization, accountable human-review and LLM-draft-only regressions; migration 0013 PostgreSQL parent-scope/RLS/append-only/idempotency/audit-outbox persistence; exact route/OpenAPI/error contracts and 100% owned service statement/branch coverage | ADR-0007, ADR-0014 | implemented_on_active_pr | -| Job-, cycle-, and staffing-scoped performance criterion observations | Performance / Workforce Validation | `criterion_observation`, `criterion_blueprint`, `performance_cycle`, `assignment_record`, `employment_record_version`, `position_record`, `position_record_version` | PostgreSQL wrong-Job, pre-assignment, out-of-cycle, frozen-Position, terminated-employment, closed-recorded-time, and session-TimeZone/UTC-midnight rejection plus valid worker-Job/staffing acceptance | ADR-0009 | implemented_on_protected_main | +| Governed candidate-to-worker conversion | Talent Acquisition / People core | `candidate_worker_conversion_record` with candidate, person, employment, selection decision, audit event and outbox evidence | PostgreSQL exact hire/evidence/audit-envelope binding, correction provenance, tenant RLS, legacy-write rejection and bitemporal history contract | ADR-0001, ADR-0003, ADR-0006 | implemented_on_active_pr | +| People read API | People API / purpose-bound read boundary | `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}`, `read_worker_people_record()`, `PostgresPeopleReadPort` | People API HTTP and PostgreSQL read contracts with exact 100% owned statement/branch coverage; current conversion lineage; no protected-value read after denied authorization | ADR-0002, ADR-0008 | implemented_on_active_pr | +| Governed People writes and confirmed-hire materialization | People API / purpose-bound mutation boundary | `POST /v1/employment-records`, `POST /v1/position-records`, `POST /v1/assignment-records`, `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions`, `people_mutation_idempotency_record` | People command/HTTP/PostgreSQL contracts with exact owned statement/branch coverage plus PostgreSQL tenant-RLS, atomic audit/outbox/idempotency, identical-retry replay, changed-command rejection, rollback, and concurrent-key regression | ADR-0002, ADR-0006, ADR-0008 | implemented_on_active_pr | +| HR Home and Employee Profile fixture, Storybook state runtime, and browser contract | Product / web workspace | `apps/hr-workspace/index.html`, `apps/hr-workspace/app.js`, `apps/hr-workspace/workspace.stories.js`, `.storybook/`, `playwright.config.mjs`, `tests/e2e/hr-workspace.spec.mjs`, shared design tokens, Figma nodes `1:10` and `1:28` | `npm run validate`, `npm run build-storybook`, `npm run test:e2e`; local Chromium proves fixture/API, permission, confirmation, exact-value, focus, locale, host-authorization, no-fallback, edit-and-revert stale-response refusal, and purpose-edit invalidation of fixture review states; protected deployment remains open | ADR-0015, `docs/STORYBOOK.md` | implemented_on_active_pr | +| Evidence-grounded Job analysis with Task/FJA/KSAO linkage | Job Analysis / Workforce Validation | `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `FunctionalJobAnalysisProfile`, `TaskKSAOLink`, `EvidenceSource`; migration `0013_job_analysis_snapshot.sql`: `job_analysis_snapshot`, `job_analysis_task_item`, `job_analysis_ksao_item`, `job_analysis_task_ksao_link`, `job_analysis_write_command`; `services/job-analysis-api` | `tests/test_job_analysis_snapshot_postgres.sh` and Job Analysis API contract source; tenant/Job isolation, source/version/digest provenance, task-KSAO completeness, deterministic canonicalization, human-review and LLM-draft-only regressions with exact 100% owned statement/branch coverage | ADR-0007, ADR-0014 | implemented_on_active_pr | +| Job-, cycle-, and staffing-scoped performance criterion observations | Performance / Workforce Validation | `criterion_observation`, `criterion_blueprint`, `performance_cycle`, `assignment_record`, `employment_record_version`, `position_record`, `position_record_version` | PostgreSQL wrong-Job, pre-assignment, out-of-cycle, frozen-Position, terminated-employment, closed-recorded-time, and session-TimeZone/UTC-midnight rejection plus valid worker-Job/staffing acceptance | ADR-0009 | implemented_on_active_pr | | Governed immutable audit and transactional outbox persistence | Audit Provenance / Integration Hub | `AuditOutboxEvent.canonical_json()`, `audit_event_record`, `outbox_delivery_record`, SHA-256 envelope digest | canonical-byte/digest regression plus PostgreSQL digest, allowlist/PII, high-impact confirmation, append-only, atomicity, lease-transition, terminal-state, and reserved-UUID tests | ADR-0006 | implemented_on_active_pr | | Tenant-safe atomic outbox claiming and crash recovery | Integration Hub dispatcher boundary | `outbox_delivery_record` pending/expired-lease claim indexes plus `claim_outbox_delivery(...)` | PostgreSQL already-expired-new-lease rejection, due-order claim, live-lease exclusion, pre-exhaustion takeover with `lease_expired` evidence, retry-budget claim bound, tenant-context binding, opaque-worker validation, and bounded-lease contract | ADR-0006 | implemented_on_active_pr | | Owner-bound outbox completion, retry, and terminal dead-letter escalation | Integration Hub dispatcher boundary | immutable `outbox_delivery_record.maximum_attempt_count`, `complete_outbox_delivery(...)`, `retry_outbox_delivery(...)`, `dead_letter_outbox_delivery(...)`, `outbox_delivery_escalation_record` | PostgreSQL foreign/stale-owner denial, dispatcher-budget-signature rejection, direct-terminal-DML rejection, stored-budget exhaustion, retry-attempt-N+1 denial, exhausted expired-lease non-reclaimability, recorded-owner terminalization, nonterminal-escalation rejection, terminal non-reclaimability, and append-only escalation evidence | ADR-0006 | implemented_on_active_pr | -| Predictive-validity case integrity | Workforce Validation | `validity_study`, normalized `validity_study_case_record`, exact `selection_decision`, sealed `decision_evidence_set`, governed `candidate_worker_conversion_record`, `criterion_observation` | `test_validity_study_case_postgres.sh`: legacy loose-link write rejection; exact evidence-set ID, Job, criterion and worker mismatch rejection; study/observation system-recorded visibility boundaries; governed upstream decision/evidence/conversion lineage from the evidence-sealing and candidate-worker conversion contracts; UPDATE/DELETE/TRUNCATE protection; missing/foreign-tenant RLS denial. Statistical estimation remains subsequent work. | ADR-0001, SIOP Principles 5th ed., 29 C.F.R. Part 1607 | implemented_on_protected_main | -| Purpose-bound PII access | Security architecture / Keyverse adapter boundary | `PurposeBoundAccessPolicy`, `PurposeBoundAccessRequest.resource_reference`, `AuthorizationDecision.resource_reference` | exact tenant/actor/resource binding, exact opaque target correlation for allow/deny audit evidence, resource/purpose/operation matching, operation-specific scope, field-subset minimization, malformed-attribute rejection, reserved-UUID rejection, PII-minimized denial evidence, and exact 100% owned statement/branch coverage | ADR-0008 | implemented_on_protected_main | +| Predictive-validity case integrity | Workforce Validation | `validity_study`, normalized `validity_study_case_record`, exact `selection_decision`, sealed `decision_evidence_set`, governed `candidate_worker_conversion_record`, `criterion_observation` | `test_validity_study_case_postgres.sh`: legacy loose-link write rejection; exact evidence-set ID, Job, criterion and worker mismatch rejection; study/observation system-recorded visibility boundaries; governed upstream decision/evidence/conversion lineage from the evidence-sealing and candidate-worker conversion contracts; UPDATE/DELETE/TRUNCATE protection; missing/foreign-tenant RLS denial. Statistical estimation remains subsequent work. | ADR-0001, SIOP Principles 5th ed., 29 C.F.R. Part 1607 | implemented_on_active_pr | +| Purpose-bound PII access | Security architecture / Keyverse adapter boundary | `PurposeBoundAccessPolicy`, `PurposeBoundAccessRequest.resource_reference`, `AuthorizationDecision.resource_reference` | exact tenant/actor/resource binding, exact opaque target correlation for allow/deny audit evidence, resource/purpose/operation matching, operation-specific scope, field-subset minimization, malformed-attribute rejection, reserved-UUID rejection, PII-minimized denial evidence, and exact 100% owned statement/branch coverage | ADR-0008 | implemented_on_active_pr | | Least-privilege API capability | Keyverse gateway boundary | operation scope conceptual | structural per-operation scope and confused-deputy contract tests | ADR-0002 | implemented_on_active_pr | | Client-safe failure correlation | API error boundary | `support_reference` conceptual | error disclosure and support-lookup tests | ADR-0002 | implemented_on_active_pr | -| Foundation artifact integrity | Repository governance | deterministic `manifest.json` file inventory | SHA-256/byte/line validation plus Python/Node inventory-equivalence regression and explicit dispatcher/validity/criterion/job-analysis migration and execution-contract provenance regression | ADR-0001 | implemented_on_active_pr | +| Foundation artifact integrity | Repository governance | deterministic `manifest.json` file inventory | SHA-256/byte/line validation plus Python/Node inventory-equivalence regression and explicit dispatcher/validity/criterion migration and execution-contract provenance regression | ADR-0001 | implemented_on_active_pr | +| Product and technical gap baseline | Product / platform governance | `docs/product-technical-gap-baseline.md`, maturity vocabulary, current protected head and PR inventory | Fresh local contract/coverage evidence plus current GitHub head/review/check inventory | ADR-0026 | implemented_on_active_pr | ## 4. CWL integration traceability | External contract | Orgmetra owner boundary | Integration style | Required evidence | ADR | Maturity | |---|---|---|---|---|---| -| Keyverse identity and authorization | API Gateway / purpose-bound authorization | Published OIDC/API identity and scope contract plus Orgmetra-owned `orgmetra_keyverse_adapter` policy evaluation | tenant/actor/resource agreement, exact opaque target-resource reference, purpose, operation-specific scope, requested-field minimization, opaque subject, no stored credentials or protected values in authorization evidence | ADR-0002, ADR-0008 | implemented_on_protected_main | +| Keyverse identity and authorization | API Gateway / purpose-bound authorization | Accepted published-contract OIDC/API boundary; external issuer integration remains unverified here. Orgmetra-owned `orgmetra_keyverse_adapter` policy evaluation and opaque-subject binding exist separately; service authentication is injected through `TokenAuthenticator` protocols | External issuer/discovery/signature-verification and deployed identity/scope evidence remain open; local contracts cover tenant/actor/resource agreement, exact opaque target-resource reference, purpose, operation-specific scope, requested-field minimization, opaque subject, no stored credentials or protected values in authorization evidence | ADR-0002, ADR-0008 | accepted_architecture | | naruon communication and calendar | Integration Hub | Published API/event adapter | idempotency, delivery audit, no direct table access | ADR-0002 | planned | | Psychometrics Commons @ `cc5850a0d1eacbbf16d03075534fce460a8286e6` | Workforce Validation | Immutable response/result snapshot contract | pinned revision, model/version/provenance snapshot, immutable result linkage, no direct application-table access | ADR-0002 | accepted_architecture | | fast-mlsirm @ `fb67ced09d8ee00542c05d56374537a9a7239751` | Workforce Validation | Published `orgmetra.fast_mlsirm.v1` result contract; direct calls only from approved offline validation worker | pinned revision, contract identifier, backend/result provenance, CPU/GPU parity evidence where material, no duplicated kernel | ADR-0002 | accepted_architecture | diff --git a/docs/WIREFRAMES.md b/docs/WIREFRAMES.md index 15d4f4e9a..e4855d170 100644 --- a/docs/WIREFRAMES.md +++ b/docs/WIREFRAMES.md @@ -11,6 +11,8 @@ The Figma baseline contains six role-based wireframes: Figma file: https://www.figma.com/design/xu1ZK1zmtFcDep95R8oE9O +The Figma baseline was re-verified against the live `Orgmetra Baseline` page for this interaction slice. Figma nodes `1:10` (HR Home) and `1:28` (Employee Profile) remain the visual geometry baseline. The keyboard bypass control below is intentionally a focus-only interaction affordance rather than a persistent geometric change to those frames; its visible focus state is captured in Storybook and its behavior is enforced by browser tests. + ## HR Home Primary action cards: @@ -20,6 +22,12 @@ Primary action cards: - Resolve effective-date conflict - Open validation study +Keyboard interaction contract: + +- The first keyboard-focusable control is **Skip to main content** / **본문으로 건너뛰기**. +- It becomes visibly rendered when focused and moves focus to the `main` landmark, bypassing repeated workspace navigation. +- The control follows the active English/Korean workspace locale and uses the shared focus-ring token. + ## Job Architecture Panels: @@ -53,6 +61,8 @@ Panels: - Manager and organization timeline - Performance criteria and observations +The same page-level keyboard bypass contract applies before the Employee Profile navigation path, so repeated sidebar navigation is not a prerequisite to reaching the profile content. + ## Validate Panels: diff --git a/docs/adr/0006-governed-audit-outbox-envelope.md b/docs/adr/0006-governed-audit-outbox-envelope.md index bd86f2684..c490e9b33 100644 --- a/docs/adr/0006-governed-audit-outbox-envelope.md +++ b/docs/adr/0006-governed-audit-outbox-envelope.md @@ -1,6 +1,7 @@ # ADR-0006: Governed audit/outbox envelope and durable persistence -- **Status:** Accepted for the stacked implementation branch; not protected-main truth until merged. +- Status: Accepted +- Provenance: integrated on `develop`; enforceable branch protection is tracked by Orgmetra issue #89. - **Decision date:** 2026-08-17 - **Scope:** Orgmetra-owned audit envelope, immutable audit persistence, guarded outbox delivery state, tenant-safe atomic dispatcher claiming, expired-lease takeover, owner-bound completion/retry, database-budget-governed terminal dead-letter escalation, review hardening, and privileged recovery of an expired exhausted lease when its recorded final worker identity is permanently unavailable. Exponential retry policy, retention/export workflows, and external delivery receipts remain subsequent work. @@ -59,7 +60,7 @@ The branch contains test-first application and PostgreSQL regressions for determ Review hardening added a focused PostgreSQL RED contract before migration 0008. That contract requires statement-level audit/outbox TRUNCATE denial, trusted search paths on every audited dispatcher/recovery boundary, deterministic/session-independent immutable envelope validation, the due-work partial index, and operator terminalization of only an expired exhausted lease with immutable operator-attributed escalation evidence. The operator regression executes recovery through the externally assignable NOLOGIN capability role, proves both service-owned recovery roles are non-superuser and NOBYPASSRLS, proves neither participates in a pre-existing membership edge, proves the temporary schema CREATE grant is absent after migration, and proves the operator role cannot read/update outbox state or insert escalation evidence directly. A dedicated preflight regression creates a colliding reserved role name and requires migration 0008 to fail before creating its due-work index or recovery function, then removes the collision and runs the normal migration. Additional review regressions make every captured dispatcher/dead-letter `psql` assertion fail on SQL errors, execute audit persistence sessions with explicit tenant context, assert the exact duplicate-delivery primary-key failure for transaction rollback, prove non-UTC application timestamps normalize to UTC, discover all executable migration/PostgreSQL-contract artifacts for provenance, and validate the database contract across all migrations rather than a hardcoded prefix. -Exact-current-head hosted evidence is required after every branch mutation. Queued, cancelled, stale, predecessor, or model-only results are non-passing and do not change this ADR’s protected-main status. +Exact-current-head hosted evidence is required after every branch mutation. Queued, cancelled, stale, predecessor, or model-only results are non-passing and do not confer merge authorization while the repository protection defect tracked by issue #89 remains unresolved. ## References diff --git a/docs/adr/0007-governed-job-analysis-evidence.md b/docs/adr/0007-governed-job-analysis-evidence.md index f9aeea835..cb15b5acd 100644 --- a/docs/adr/0007-governed-job-analysis-evidence.md +++ b/docs/adr/0007-governed-job-analysis-evidence.md @@ -1,6 +1,6 @@ # ADR 0007: Governed job-analysis evidence snapshots -- Status: Accepted on stacked implementation branch +- Status: Accepted - Date: 2026-08-17 - Owners: Orgmetra Job Analysis / Workforce Validation @@ -44,7 +44,7 @@ This contract does not make hiring, promotion, termination, compensation, or oth ### Costs and limitations -- Persistence of the canonical snapshot as tenant-scoped 3NF rows is specified by ADR 0014. SME workflow, source ingestion, retention/export controls, UI, and selection-validity computation remain separate owner boundaries. +- This slice is a pure domain/evidence contract; persistence, SME workflow, source ingestion, authorization, retention/export controls, UI, and selection-validity computation remain separate owner boundaries. - The 1–5 rating scales are contract-level normalized ordinals. A production job-analysis method must document its sampling, anchors, aggregation, criticality rules, inter-rater treatment, and local validation rationale rather than infer those properties from the ordinal values alone. - O*NET provides occupation-level evidence and does not eliminate the need to verify local Job content, context, and essential requirements with appropriate job experts. diff --git a/docs/adr/0010-naruon-calendar-intent-boundary.md b/docs/adr/0010-naruon-calendar-intent-boundary.md index 6bed4dd49..405b833f5 100644 --- a/docs/adr/0010-naruon-calendar-intent-boundary.md +++ b/docs/adr/0010-naruon-calendar-intent-boundary.md @@ -2,7 +2,9 @@ ## Status -Accepted on active PR only. This document is not protected-`develop` product truth until its owning PR integrates. +Status: Accepted + +Provenance: integrated on `develop`; enforceable branch protection is tracked by Orgmetra issue #89. ## Context diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index 11f137ee4..d7ecdb644 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -2,7 +2,9 @@ ## Status -Accepted on active PR #33 only. This document is not protected-`develop` product truth until the owning PR integrates. +Status: Accepted + +Provenance: integrated on `develop`; enforceable branch protection is tracked by Orgmetra issue #89. ## Context diff --git a/docs/adr/0012-governed-migration-handoff.md b/docs/adr/0012-governed-migration-handoff.md index 90f04d6bc..bc55441a5 100644 --- a/docs/adr/0012-governed-migration-handoff.md +++ b/docs/adr/0012-governed-migration-handoff.md @@ -2,7 +2,9 @@ ## Status -Accepted on this active PR only. This is not protected-`develop` product truth until the owning PR integrates. +Status: Accepted + +Provenance: integrated on `develop`; enforceable branch protection is tracked by Orgmetra issue #89. ## Context diff --git a/docs/adr/0014-job-analysis-snapshot-persistence.md b/docs/adr/0014-job-analysis-snapshot-persistence.md index cef05e28f..6d9125812 100644 --- a/docs/adr/0014-job-analysis-snapshot-persistence.md +++ b/docs/adr/0014-job-analysis-snapshot-persistence.md @@ -1,6 +1,6 @@ # ADR 0014: Persist governed job-analysis snapshots -- Status: Accepted on active implementation branch +- Status: Accepted - Date: 2026-08-20 - Owners: Orgmetra Job Analysis / Workforce Validation diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index c662a0429..ad02cacfd 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -1,12 +1,13 @@ # ADR 0017: Governed offer approval evidence -- Status: Proposed — active PR only +- Status: Accepted +- Provenance: source present on observed `develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f`; configured protection remains a separate gap (#89). - Date: 2026-08-19 - Scope: Talent Acquisition offer review ## Context -Protected `develop` can govern candidate, requisition, selection, and employment evidence, but it does not yet expose a bounded pre-send contract proving that a proposed offer is tied to the selected candidate, authoritative Job/optional Position, reviewed selection decision, compensation-package provenance, offer-terms provenance, and accountable human approval. +The observed `develop` source contains candidate, requisition, selection, and employment evidence boundaries and the bounded pre-send offer-approval contract. Its source checks bind a proposed offer packet to candidate, Job/optional Position, selection-decision, compensation-package, offer-terms, and human-review references. Repository presence does not prove authoritative reference resolution, actual approval, configured branch protection, deployed behavior, or release. Offer review is high-impact employment workflow. A governance envelope must not become an alternate decision authority, a salary-value cache, or a channel that lets generated/model material masquerade as an approved offer. Different opaque requester/approver references also do not prove that the authoritative actor boundary resolves them to different people, and UUID syntax does not prove that the referenced candidate, requisition, Job/Position, selection decision, compensation package, or offer terms belong to the packet tenant. Packet-owned UUIDv1 references also carry timestamp/node-derived correlation metadata. The authoritative tenant identifier is different: it is issued by Orgmetra core, so this leaf package must accept the canonical non-sentinel operational UUID contract owned by that boundary rather than silently imposing a second version policy. @@ -30,7 +31,7 @@ Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, while Orgmetra keeps exact provenance references, evidence version, and human accountability. Packet-owned UUIDv1/non-v4 references fail closed before serialization without making this leaf package incompatible with authoritative Orgmetra tenant UUIDs. Cross-tenant evidence mixing is fail-closed at the host approval boundary because every packet reference must resolve in the exact tenant. Requester/approver separation is proven only after tenant-scoped authoritative actor resolution. New offer-review reason categories require an explicit contract change and regression evidence rather than accepting arbitrary caller text. -Downstream offer persistence/execution must independently enforce authorization, tenant-scoped source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains proposed active-PR truth until integrated into protected `develop`. +Downstream offer persistence/execution must independently enforce authorization, tenant-scoped source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. The packet contract source is present on the observed `develop` base; configured branch protection, authoritative host integration, connected buyer workflow, and release evidence remain separate work. ## References diff --git a/docs/adr/0025-governed-candidate-evidence-intake.md b/docs/adr/0025-governed-candidate-evidence-intake.md index ab66b3088..dd4516768 100644 --- a/docs/adr/0025-governed-candidate-evidence-intake.md +++ b/docs/adr/0025-governed-candidate-evidence-intake.md @@ -1,6 +1,7 @@ # ADR 0025: Govern candidate evidence intake as reference-only evidence -- **Status:** Proposed — active PR only +- Status: Accepted +- Provenance: integrated on `develop`; enforceable branch protection is tracked by Orgmetra issue #89. - **Date:** 2026-08-19 ## Context @@ -40,7 +41,7 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab - The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. - UUID-backed tenant and candidate references are still sensitive correlating metadata and require least-privilege handling. - Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. -- This ADR remains proposed until its exact PR head merges into protected `develop`. +- This ADR is integrated on `develop`; repository-level merge authorization still depends on the independent-review and branch-protection controls tracked by issue #89. ## References diff --git a/docs/adr/0026-product-technical-gap-baseline.md b/docs/adr/0026-product-technical-gap-baseline.md new file mode 100644 index 000000000..fea907dc3 --- /dev/null +++ b/docs/adr/0026-product-technical-gap-baseline.md @@ -0,0 +1,32 @@ +# ADR 0026: Product and technical gap baseline + +- Status: Accepted +- Date: 2026-08-20 +- Owners: Orgmetra Product / Platform + +## Context + +Orgmetra has a strong evidence and integrity foundation, but its documentation has repeatedly mixed protected `develop` truth, active pull requests, accepted architecture, and planned product work. A buyer must be able to tell which workflow can be executed today, which is review-ready but unmerged, and which is only a design promise. + +The product baseline also uses a Figma wireframe source and repeated HR actions. The source file key is `xu1ZK1zmtFcDep95R8oE9O`; it is recorded here so design-to-code work can be traced to one design source. The implementation must continue to use `packages/design-tokens/` rather than inventing one-off action colors. + +## Decision + +1. `docs/product-technical-gap-baseline.md` is the current evidence ledger for buyer-visible product gaps and technical gaps. +2. Every capability in the ledger has one maturity value: `implemented_on_protected_develop`, `implemented_on_active_pr`, `accepted_architecture`, `planned`, `research_only`, `superseded`, or `out_of_scope`. +3. Protected-default-branch claims require current `develop` evidence. An OpenAPI definition, a passing predecessor workflow, a local branch, or a PR body cannot be presented as shipped runtime behavior. +4. The authoritative People mutation and confirmed-hire paths are protected integration truth. Browser workflows must preserve tenant authorization, idempotency, bitemporal history, human confirmation, audit, and outbox atomicity rather than bypassing those boundaries. +5. Job-analysis persistence/API is protected `develop` truth through merged PR #38, migration 0013, ADR-0014, and the governed Job Analysis API. Extensions must reuse that canonical Task/FJA/KSAO model instead of introducing a second store. +6. Statistical validity computation remains a separate scientific boundary. Integrity linkage is necessary evidence hygiene, not a validity result. Any future numerical kernel is Rust-first and must publish CPU reference, multilevel/multiple-membership, temporal, uncertainty, convergence, and GPU-parity evidence where GPU execution is material. +7. Figma remains a design-system input, while the local Storybook runtime is executable component/state evidence rather than evidence of a connected customer UI. A release claim requires executable UI, keyboard/accessibility checks, interaction tests, and browser evidence for the owning workflow. + +## Consequences + +- Product and engineering planning use the same gap IDs and acceptance evidence. +- Review, merge, release, and scheduled-loop automation can consume one small status contract instead of inferring maturity from filenames or PR titles. +- Figma remains traceable without copying a design file into the repository. +- A gap can be closed only when the required runtime, documentation, tests, and protected-branch evidence all exist. + +## References + +See `docs/doctoring/REFERENCES.md` for APA 7 sources, including ISO 30405:2023, NIST AI RMF 1.0, AICPA Trust Services Criteria, WCAG 2.2, Fugu, Conductor, and TRINITY. diff --git a/docs/adr/README.md b/docs/adr/README.md index 099a21139..d474110e1 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -1,18 +1,23 @@ # ADR Index -| ADR | Title | Status | -|---|---|---| -| [0001](0001-orgmetra-authoritative-hris-record.md) | Orgmetra owns authoritative HRIS records | Accepted | -| [0002](0002-federated-cwl-integration-boundaries.md) | Federated CWL integration boundaries | Accepted | -| [0003](0003-bitemporal-hris-data-contract.md) | Bitemporal HRIS data contract | Accepted | -| [0004](0004-employment-position-version-and-assignment-binding.md) | Employment and position versions bind assignments | Accepted | -| [0005](0005-exclusive-employment-and-staffable-seats.md) | Exclusive employment and staffable seats | Accepted | -| [0006](0006-governed-audit-outbox-envelope.md) | Governed audit/outbox envelope and durable persistence | Accepted on stacked implementation branch | -| [0007](0007-governed-job-analysis-evidence.md) | Governed job-analysis evidence snapshots | Accepted on stacked implementation branch | -| [0008](0008-purpose-bound-pii-authorization.md) | Purpose-bound PII authorization | Accepted on protected `develop` | -| [0009](0009-performance-criterion-observation-scope.md) | Performance criterion observations require worker-job scope | Accepted on active implementation branch | -| [0010](0010-naruon-calendar-intent-boundary.md) | Naruon calendar intent boundary | Accepted on active implementation branch | -| [0011](0011-bitemporal-workforce-composition.md) | Bitemporal workforce composition | Accepted on active implementation branch | -| [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on active implementation branch | -| [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on active implementation branch | -| [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | Accepted on active implementation branch | +`Status` is the canonical ADR decision state validated against each ADR file. `Provenance` records where that state is evidenced and must not be folded into the status cell, because doing so would bypass the index-to-file status check. `develop` entries below are integrated repository truth, but enforceable branch protection is currently tracked separately by issue #89 and therefore is not claimed here. + +| ADR | Title | Provenance | Status | +|---|---|---|---| +| [0001](0001-orgmetra-authoritative-hris-record.md) | Orgmetra owns authoritative HRIS records | — | Accepted | +| [0002](0002-federated-cwl-integration-boundaries.md) | Federated CWL integration boundaries | — | Accepted | +| [0003](0003-bitemporal-hris-data-contract.md) | Bitemporal HRIS data contract | — | Accepted | +| [0004](0004-employment-position-version-and-assignment-binding.md) | Employment and position versions bind assignments | — | Accepted | +| [0005](0005-exclusive-employment-and-staffable-seats.md) | Exclusive employment and staffable seats | — | Accepted | +| [0006](0006-governed-audit-outbox-envelope.md) | Governed audit/outbox envelope and durable persistence | `develop`; protection gap #89 | Accepted | +| [0007](0007-governed-job-analysis-evidence.md) | Governed job-analysis evidence snapshots | `develop`; protection gap #89 | Accepted | +| [0008](0008-purpose-bound-pii-authorization.md) | Purpose-bound PII authorization | `develop`; protection gap #89 | Accepted | +| [0009](0009-performance-criterion-observation-scope.md) | Performance criterion observations require worker-job scope | `develop`; protection gap #89 | Accepted | +| [0010](0010-naruon-calendar-intent-boundary.md) | Naruon calendar intent boundary | `develop`; protection gap #89 | Accepted | +| [0011](0011-bitemporal-workforce-composition.md) | Bitemporal workforce composition | `develop`; protection gap #89 | Accepted | +| [0012](0012-governed-migration-handoff.md) | Governed migration handoff | `develop`; protection gap #89 | Accepted | +| [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | `develop`; protection gap #89 | Accepted | +| [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | `develop`; protection gap #89 | Accepted | +| [0017](0017-governed-offer-approval.md) | Governed offer approval evidence | `develop`; protection gap #89 | Accepted | +| [0025](0025-governed-candidate-evidence-intake.md) | Govern candidate evidence intake as reference-only evidence | `develop`; protection gap #89 | Accepted | +| [0026](0026-product-technical-gap-baseline.md) | Product and technical gap baseline | active PR #53 | Accepted | diff --git a/docs/doctoring/REFERENCES.md b/docs/doctoring/REFERENCES.md index 309409cc2..ddd791263 100644 --- a/docs/doctoring/REFERENCES.md +++ b/docs/doctoring/REFERENCES.md @@ -12,17 +12,17 @@ Cloud Native Computing Foundation. (2022). *CloudEvents specification v1.0.2*. h Diez-Roux, A. V. (1998). Bringing context back into epidemiology: Variables and fallacies in multilevel analysis. *American Journal of Public Health, 88*(2), 216–222. https://doi.org/10.2105/AJPH.88.2.216 -Equal Employment Opportunity Commission. (1978). *Uniform guidelines on employee selection procedures*. 29 C.F.R. Part 1607. https://www.govinfo.gov/content/pkg/CFR-2025-title29-vol4/pdf/CFR-2025-title29-vol4-part1607.pdf +Equal Employment Opportunity Commission. (1978). *Uniform guidelines on employee selection procedures*. 29 C.F.R. Part 1607. Hu, V. C., Ferraiolo, D. F., & Kuhn, D. R. (2019). *Attribute considerations for access control systems* (NIST Special Publication 800-205). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-205 Hu, V. C., Ferraiolo, D. F., Kuhn, D. R., Schnitzer, A., Sandlin, K., Miller, R., & Scarfone, K. A. (2019). *Guide to attribute based access control (ABAC) definition and considerations* (NIST Special Publication 800-162). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-162 -International Organization for Standardization. (2022). *ISO 30400:2022 Human resource management — Vocabulary*. ISO. https://www.iso.org/standard/78044.html +International Organization for Standardization. (2022). *ISO 30400:2022 Human resource management — Vocabulary*. ISO. -International Organization for Standardization. (2023). *ISO 30405:2023 Human resource management — Guidelines on recruitment*. ISO. https://www.iso.org/standard/79488.html +International Organization for Standardization. (2023). *ISO 30405:2023 Human resource management — Guidelines on recruitment*. ISO. -International Organization for Standardization. (2025). *ISO 30414:2025 Human resource management — Requirements and recommendations for human capital reporting and disclosure*. ISO. https://www.iso.org/standard/30414 +International Organization for Standardization. (2025). *ISO 30414:2025 Human resource management — Requirements and recommendations for human capital reporting and disclosure*. ISO. Internet Engineering Task Force. (2024). *Universally Unique IDentifiers (UUIDs)* (RFC 9562). https://www.rfc-editor.org/rfc/rfc9562.html @@ -56,9 +56,9 @@ PostgreSQL Global Development Group. (n.d.). *SELECT (PostgreSQL 16 documentatio Robinson, W. S. (1950). Ecological correlations and the behavior of individuals. *American Sociological Review, 15*(3), 351–357. https://doi.org/10.2307/2087176 -Snodgrass, R. T. (1999). *Developing time-oriented database applications in SQL*. Morgan Kaufmann. https://lccn.loc.gov/99014298 +Snodgrass, R. T. (1999). *Developing time-oriented database applications in SQL*. Morgan Kaufmann. -Society for Industrial and Organizational Psychology. (2018). *Principles for the validation and use of personnel selection procedures* (5th ed.). SIOP. https://doi.org/10.1017/iop.2018.195 +Society for Industrial and Organizational Psychology. (2018). *Principles for the validation and use of personnel selection procedures* (5th ed.). SIOP. U.S. Department of Labor, Office of Administrative Law Judges. (1991). *Dictionary of Occupational Titles (4th ed., rev. 1991), Appendix B: Explanation of Data, People, and Things*. https://www.dol.gov/agencies/oalj/PUBLIC/DOT/REFERENCES/DOTAPPB @@ -67,3 +67,19 @@ U.S. Office of Personnel Management. (n.d.). *Job analysis*. Retrieved August 17 World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.org/TR/prov-o/ World Wide Web Consortium. (2023, October 5). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ + +## Product and technical gap baseline sources + +American Institute of Certified Public Accountants. (2023). *2017 trust services criteria (with revised points of focus—2022).* https://www.aicpa-cima.com/resources/download/2017-trust-services-criteria-with-revised-points-of-focus-2022 + +Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H., Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., & Clanuwat, T. (2026). *Sakana Fugu technical report* (arXiv:2606.21228v2). Version 2, revised June 23, 2026. arXiv. https://arxiv.org/abs/2606.21228v2 + +International Organization for Standardization. (2023). *ISO 30405:2023: Human resource management—Guidelines on recruitment* (2nd ed.). https://www.iso.org/standard/79488.html + +National Institute of Standards and Technology. (2023). *Artificial intelligence risk management framework (AI RMF 1.0)* (NIST AI 100-1). U.S. Department of Commerce. https://doi.org/10.6028/NIST.AI.100-1 + +Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* (arXiv:2512.04388). arXiv. https://arxiv.org/abs/2512.04388 + +Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2026). *TRINITY: An evolved LLM coordinator* (arXiv:2512.04695v3). Version 3, revised April 27, 2026; first submitted December 4, 2025. arXiv. https://arxiv.org/abs/2512.04695v3 + +World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/2024/REC-WCAG22-20241212/ diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 000000000..fbd66d48f --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,170 @@ +# Orgmetra product and technical gap baseline + +**Historical snapshot:** 2026-08-21, Asia/Seoul +**Historical evidence base:** protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df`; current workspace PR #53 at `c45a8a53125a6075f5edb4a33e02661df4c5d1b5` immediately before this documentation snapshot commit; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. + +This document is the buyer-facing work queue. It separates what a customer can use from what exists only in an active PR or architecture document. It is updated when a protected merge, exact-head check, review, release, or runtime test changes the evidence boundary. + +## Current observed base source attribution (2026-10-05) + +The retained local default-branch observation identifies `develop` at `eb9757f8649aaad026a9865508d9aad50c1a7a4f`. Exact immutable Git source inspection finds `packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py` (`build_workforce_composition_snapshot`, with effective date and knowledge cutoff) and `packages/hris-kernel/tests/test_workforce_composition.py` plus `test_workforce_composition_boundaries.py`; the base README also names bitemporal workforce-composition evidence as already on `develop`. This establishes base repository source presence, not evidence of configured branch-protection enforcement, a newly executed test, deployed behavior, or a release. The historical PR #54 row below describes proposed same-cutoff composition-change evidence; it must not be read as saying all workforce-composition source is absent from this observed base. + +All remaining sections below retain the 2026-08-21 snapshot: capability/maturity and buyer-gap tables, architecture/control-plane statements, open-PR inventory, and local execution results (including “current”, “current head”, and “protected” wording) are historical claims at their recorded coordinates, not fresh enforcement, execution, deployment, or release findings. No new all-PR inventory is asserted. Active PR #53 UI remains unmerged; this attribution correction does not advance its maturity or transfer prior checks/reviews to its frozen successor. + +The retained lineage observation records overlapping Draft #100 and #51 documentation work. #100 retains the Gap ledger role and those branches remain unchanged. This correction is confined to the existing PR #53 checkout; it does not adopt either Draft source, reassign their owners, or establish approval, merge, or release. + +## Maturity vocabulary + +| Value | Meaning | +|---|---| +| `implemented_on_protected_develop` | Executable behavior and its required evidence are on the protected default branch. | +| `implemented_on_active_pr` | Code or tests exist on a current PR, but protected-branch runtime truth is not established. | +| `accepted_architecture` | An owned boundary and contract are accepted, but the buyer workflow is not executable here. | +| `planned` | Product work is named and ordered, but implementation evidence is missing. | +| `research_only` | A paper, prototype, or external product informs a decision; it is not an Orgmetra capability. | +| `superseded` | The artifact must not be revived because a newer owner or contract replaced it. | +| `out_of_scope` | The capability belongs to another product or is intentionally not an Orgmetra decision. | + +## Executive finding + +Orgmetra is an evidence-centered HRIS foundation with protected Job Analysis, candidate-evidence, People mutation, and confirmed-hire boundaries, not yet a complete commercial HCM product. The protected branch provides durable PostgreSQL integrity contracts, a Python HRIS decision kernel, purpose-bound authorization, governed candidate-to-worker lineage, and executable Job Analysis and People read/write boundaries. The largest buyer-visible gap remains the missing connected browser product surface: the current checkout has an active-PR HR Home/Employee Profile fixture plus an API-bound Job Analysis read surface and local Storybook state runtime, but protected truth has no connected or released workspace. + +The next highest-leverage gaps are a connected/released buyer path over the now-protected Job Analysis API and actual statistical validity estimation. Existing contracts are useful foundations, but they do not substitute for a running customer path or a released deployment. + +```mermaid +flowchart LR + evidence[Job and candidate evidence] --> selection[Human selection record] + selection --> mutation[People mutation boundary] + mutation --> worker[Employment, position, assignment] + worker --> outcome[Performance outcomes] + outcome --> validity[Validity estimation] + validity --> policy[Human-reviewed policy change] + worker -. browser workspace missing .-> gap1[Gap P0-1] + worker -. buyer-connected Job Analysis path missing .-> gap2[Gap P0-2] + validity -. integrity only, no estimator .-> gap3[Gap P0-3] +``` + +## Historical capability truth on `develop` (2026-08-21) + +| Capability | Current evidence | Maturity | Buyer consequence | +|---|---|---|---| +| Person, employment, organization, job, position, assignment separation | `database/migrations/0001_foundation_schema.sql`; HRIS kernel tests; bitemporal and tenant contracts | `implemented_on_protected_develop` | Employment truth can be modeled without collapsing stable identities or historical versions. | +| Tenant isolation, append-only history, evidence sealing, audit/outbox integrity | Migrations `0001`–`0012`; PostgreSQL contract suite; `npm run validate` | `implemented_on_protected_develop` | The foundation can reject cross-tenant, temporal, evidence-drift, and unsafe delivery-state writes. | +| Candidate evidence intake | Protected candidate-evidence package, ADR `0025`, reference-only packet tests, and `Candidate Evidence Quality` workflow | `implemented_on_protected_develop` | Candidate evidence can be correlated without copying raw values or PII into the governance envelope; authoritative reference resolution and selection remain separate boundaries. | +| Candidate-to-worker conversion | Migration `0009`; `test_candidate_worker_conversion_postgres.sh`; protected traceability; People API hire route | `implemented_on_protected_develop` | Confirmed-hire materialization has a governed HTTP/service boundary; deployment and browser evidence remain separate release work. | +| People read | `services/people-api` GET route, PostgreSQL read adapter, HTTP tests | `implemented_on_protected_develop` | Authorized HR users can read a worker view; responses are no-store and field-scoped. | +| People mutations | Migration `0012`; `services/people-api` hire and mutation routes; current protected service tests and PostgreSQL contract | `implemented_on_protected_develop` | Authoritative person, employment, position, assignment, and confirmed-hire writes have a governed code boundary; hosted/browser release evidence remains open. | +| Offer approval packet | Protected `packages/offer-approval` packet, actor-separation/evidence-version tests, and `Offer Approval Quality` workflow | `implemented_on_protected_develop` | Offer approval evidence is bounded and human-reviewed; a connected buyer workflow and released deployment remain open. | +| Job-analysis value objects | `orgmetra_hris_kernel.job_analysis`; exact unit coverage | `implemented_on_protected_develop` | Evidence can be validated in a protected package and reused by the persisted Job Analysis boundary; buyer workflow connectivity remains open. | +| Job-analysis persistence/API | Protected migration `0013`, `services/job-analysis-api`, ADR `0014`, PostgreSQL contract, and exact snapshot tests on `develop` | `implemented_on_protected_develop` | A buyer-facing deployment and browser workflow are still absent, but one canonical persisted Job Analysis case/API now exists in protected code. | +| Performance criterion scope and validity-study case integrity | Migration `0010`/`0011`; PostgreSQL contracts | `implemented_on_protected_develop` | Invalid worker-Job/time links are rejected, but no statistical validity estimate is produced. | +| Statistical validity estimation | Traceability explicitly says estimation is subsequent; no Rust workspace or estimator exists in this repository | `planned` | Customers cannot measure prediction, bias, RMSE/MAE, uncertainty, convergence, temporal effects, or multiple membership. | +| Workforce composition change evidence | Active PR #54 adds same-cutoff bitemporal composition-change evidence | `implemented_on_active_pr` | Buyers can review a proposed workforce change only after the exact PR earns checks and independent approval; it is not protected truth yet. | +| Organization hierarchy snapshot evidence | Active PR #56 adds bitemporal organization hierarchy snapshot evidence | `implemented_on_active_pr` | Organizational reporting remains an active integration lane, not a protected or released buyer workflow. | +| Selection-validity analysis handoff | Active PR #57 adds the governed handoff boundary for later validity estimation | `implemented_on_active_pr` | The handoff is not a statistical estimator and does not yet produce validity, bias, RMSE, coverage, or convergence evidence. | +| Role workspaces, Storybook runtime, and browser contract | Active PR #53 has the HR Home/Employee Profile fixture, host-injected People and Job Analysis API-bound read views, Storybook `10.5.10`, and a Chromium Playwright browser contract; protected `develop` still has no customer UI | `implemented_on_active_pr` | The component/state runtime, browser states, and host-injected API boundaries are reviewable, but there is no connected or released buyer workflow in protected truth. | +| Naruon calendar adapter | `packages/naruon-adapter` package tests; traceability says planned integration | `accepted_architecture` | Calendar intent is contract-tested, not an integrated customer scheduling workflow. | +| TEPP adapter | PR #52 is a non-executing request boundary; no transport contract is established | `implemented_on_active_pr` | Temporal analysis can be prepared as governed evidence but is not executed by Orgmetra. | +| Contextual Orchestrator/OpenCode model path | Named in architecture; no Orgmetra adapter or evidence-backed model evaluation in protected code | `planned` | LLM assistance cannot yet be invoked through an Orgmetra-owned, auditable draft-evidence boundary. | +| Search, semantic chunking, and image understanding | Architecture mentions derived search/vector storage, but no owned schema, chunker, OCR/object metadata, or index adapter is present | `planned` | Evidence retrieval cannot yet preserve paragraph/DOM/image location semantics for customer search. | +| Hot-partition scale strategy | No `PARTITION BY`/partition-management contract in the current migrations | `planned` | Append-heavy audit/outbox and temporal tables need a tested scale plan before high-volume production. | +| CSAP/SOC 2 evidence package | Security, threat, operability, and test documents exist; no control-evidence collection or attestation exists | `accepted_architecture` | The design is compliance-ready in intent, not a certification or audit report. | +| Release artifact | Root package is `0.1.0`; changelog remains `[Unreleased]`; no protected product release was verified | `planned` | Customers have no versioned, supportable Orgmetra product release yet. | + +## Historical local candidate artifact (2026-08-21) + +The current checkout contains `apps/hr-workspace/`, a dependency-free HR Home +and Employee Profile fixture based on Figma nodes `1:10` and `1:28`, plus +host-injected People and Job Analysis API-bound read views. It uses +the shared design tokens and proves navigation, focus-visible styling, +keyboard-accessible evidence and confirmation dialogs, purpose-bound +permission denial, exact allocation values, and English/Korean labels. The +fixture explicitly displays that the protected People API is not connected. +The Job Analysis view requires a host-injected API base URL and authorization +provider, sends the existing purpose header, and has no synthetic fallback or +browser credential storage. The same active PR includes a local Storybook runtime with tokenized stories and a three-test Chromium Playwright contract; +the historical snapshot head passed the Storybook build and browser contract, but those +tests intercept host-injected reads and do not prove a connected People API, +hosted deployment, or protected-develop truth. The artifact must be reviewed, +checked, and merged independently before P0-1 can change maturity. + +## Buyer gap backlog + +| ID | Priority and owner | Gap and smallest acceptable closure evidence | Dependency | +|---|---|---|---| +| P0-1 | Product / Web | Review and merge the active HR Home + Employee Profile fixture and local Storybook states, then connect it to the protected People API and prove keyboard/focus/permission/confirmation states, exact-value tables, i18n, and browser E2E. | Protected People API evidence | +| P0-2 | Job Architecture | Protected `develop` now contains PR #38's persisted Job Analysis case/API, migration owner, ADR, versioned source evidence, and PostgreSQL acceptance tests. Active PR #53 adds the host-injected read-only browser boundary; connect it to a real API runtime, SME approval path, deployment, and browser evidence. | Protected People API evidence; active workspace path | +| P0-3 | Workforce Validation / scientific owner | Advance active PR #57 into a Rust-first estimator boundary or versioned adapter to `fast-mlsirm`/TEPP. Publish true-parameter recovery, bias, MAE, RMSE, coverage, convergence, temporal, multilevel, multiple-membership, CPU reference, and material GPU parity evidence. | Protected P0-2 and external contract re-resolution | +| P0-4 | Release / Platform | Produce a deployable release with version, changelog, migration inventory, rollback/recovery evidence, support runbook, and exact commit provenance. | P0-1 through P0-3 | +| P1-1 | Integration Hub | Implement contextual-orchestrator adapter for draft evidence only. Pin model/provider/config/evidence digests, use `NVIDIA_NIM_API_KEY` for model-backed development, and record ablations for single-route versus multi-agent depth/access lists. | P0-2; external runtime contract | +| P1-2 | Evidence Platform | Add normalized document/image segment metadata: semantic unit, source location, OCR/object tags, image reference, sensitivity, retention, embedding model/version, and owner provenance. Query filters must run before similarity ranking. | P0-2; document owner contracts | +| P1-3 | Data Platform | Define and rehearse hot-partition strategy for append-heavy audit/outbox and temporal facts, including tenant/time key choice, partition creation, retention, reindexing, and cross-partition query tests. | P0-1; production volume evidence | +| P1-4 | Trust / Operability | Build a CSAP/SOC 2 control matrix with owner, control activity, evidence location, retention, incident path, and release approval. Label it readiness evidence, never certification. | P0-1/P0-2 | + +## Historical open PR inventory and integration order (2026-08-21) + +The following is the historical GitHub inventory checked on 2026-08-21; it is not a current all-PR inventory. All listed PRs target `develop@9e3e4847510e1e612b48474ba42b177b8ed824df`. `REVIEW_REQUIRED` means the GitHub listing reported that review gate; it is not approval evidence. No self-approval or protection bypass is permitted. + +| PR | Head branch / exact head | Scope | Current state | Next action | +|---:|---|---|---|---| +| 57 | `feat/validation-analysis-handoff` / `081942a723c2ae2cb9bb98a8ffd0b33b72a8bd74` | Governed selection-validity analysis handoff | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | +| 56 | `feat/organization-hierarchy-snapshot` / `0eaa8c940842b3ba0d491f72ca57d4333c2c4d12` | Bitemporal organization hierarchy snapshot evidence | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued | Verify tenant/time semantics and current-head checks before independent approval. | +| 55 | `fix/people-read-auth-backend-failure` / `f22225700723b46625372844a06205c3dc9b46e4` | People-read error normalization and pre-auth resource budgets | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; current workflows queued | Strix's valid MEDIUM hire-route resource-boundary finding is repaired on this head; await terminal People API/security/recovery checks and independent approval. | +| 54 | `feat/workforce-composition-change` / `7d50e77d55bd908975754739f7f5f7e9422334c5` | Same-cutoff workforce composition change evidence | Ready; merge state `BLOCKED`; historical review threads resolved/outdated; no qualifying approval; current workflows queued | Re-fetch exact head, terminal checks, and qualifying independent approval before protected merge. | +| 53 | `codex/product-gap-baseline-workspace` / `c45a8a53125a6075f5edb4a33e02661df4c5d1b5` | HR Home + Employee Profile fixture, host-injected People and Job Analysis API-bound read views, Storybook, Chromium browser contract, reproducible Python service `uv` setup, offer-approval integration, and baseline repair | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued | Keep the fixture/API boundaries explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 52 | `feat/tepp-analysis-adapter` / `3690e11516443ee82be88e69b042b5e0dbec80f5` | Governed TEPP analysis request boundary | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; historical review thread resolved/outdated; current workflows queued | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | +| 51 | `docs/protected-truth-refresh` / `b67a658cc98d58347db4995aba4858f0547ffccc` | Protected product-truth documentation repair | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; historical review threads resolved/outdated; current workflows queued | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | +| 48 | `feat/governed-compensation-change-review` / `4d40a0d75cb3bba14e388f88bb418404540970b3` | Compensation review packet | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; no current review thread; current workflows queued | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | +| 47 | `feat/governed-employment-leave-review` / `9ff50ca3eecfbd65d1b310ae783fc78ef5c1ba09` | Employment leave review packet | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; no current review thread; current workflows queued | Verify temporal and authorization contracts before protected merge. | +| 46 | `feat/governed-employment-separation-review` / `3e436f314c6fe256433db6cd3256e8eed3d6fed2` | Employment separation review packet | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; historical review thread resolved/outdated; current workflows queued | Verify irreversible-action confirmation and audit evidence before protected merge. | +| 45 | `feat/governed-assignment-change-review` / `d89f82fb919039b57ea7f42993708961b7020299` | Assignment change review packet | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; historical review threads resolved/outdated; current workflows queued | Verify tenant/person/employment/position binding before protected merge. | +| 44 | `feat/governed-performance-review` / `a8f319731f1f15f5ae391eb2178f4a642c150548` | Performance review packet | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; historical review threads resolved/outdated; current workflows queued | Verify criterion scope and human confirmation before protected merge. | +| 42 | `feat/selection-outcome-monitoring-plan` / `ea8f62ab44b535b2d71bf6fa874b757bb777b72d` | Selection outcome monitoring | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; no current review thread; current workflows queued | Verify monitoring scope and temporal cohort semantics before protected merge. | +| 40 | `feat/structured-interview-plan` / `11838bcc88fd8afcc37ab0cbe3c6d8c5d8f19344` | Governed structured interview plan | Ready; merge state `BLOCKED`; `REVIEW_REQUIRED`; historical review threads resolved/outdated; current workflows queued | Verify human review, evidence versioning, and current-head checks before protected merge. | + +No open GitHub Issue was returned by the historical 2026-08-21 `gh issue list` query. This does not mean product work is exhausted: the backlog above is derived from protected runtime gaps and is intentionally independent of issue presence. + +## Operating scheduler boundary + +The review/repair/merge sweep is centrally owned by +[`ContextualWisdomLab/.github`](https://github.com/ContextualWisdomLab/.github/blob/main/.github/workflows/pr-review-merge-scheduler.yml), not copied into Orgmetra. The live central `main` workflow currently exposes `*/15` and `*/30` GitHub Actions sweeps, which is more frequent than the hourly operating contract below. Its organization sweep dispatches target-repository scans while target checks still execute against the target repository's exact head. Orgmetra therefore has no repository-local privileged scheduler or model credential path; this is an accepted control-plane boundary, not evidence that any individual PR is merge-ready. + +## Loop contract + +Every central scheduler sweep performs the same bounded sequence: + +1. Re-fetch the protected `develop` head and current open PR list. +2. For each open PR, inspect current review threads, exact head, mergeability, and all repository/security/recovery/coverage checks. +3. If a review comment identifies a defect, reproduce it on the exact head, make the smallest root-cause repair, and re-run the full affected suite. +4. Never transfer checks or approvals from a predecessor head. Never self-approve, bypass protection, cancel a pending gate to create a result, or call an unmerged PR shipped. +5. Merge only when the current head, current base, required checks, resolved threads, and independent approval satisfy live repository rules. Record the merge SHA and continue to the next dependency-ready lane. +6. If no PR is merge-ready, advance the highest-priority unblocked product gap and leave a verifiable branch/PR artifact. +7. Re-run `npm run validate`, the affected `uv` package suite, PostgreSQL contracts, and browser evidence before release claims. + +The loop is an operating procedure, not evidence of completion. Its next customer action must always be explicit: approve, review, correct, request evidence, compare, export, or escalate. + +## Standards and research alignment + +- ISO 30405:2023 grounds the recruitment lifecycle and the requirement to connect preparation, assessment, stakeholders, review, and learning. +- EEOC Uniform Guidelines and SIOP validity guidance ground job-related selection evidence and prohibit treating a statistical link as sufficient without job scope, criterion integrity, and human governance. +- NIST AI RMF 1.0 requires continuous governance, mapping, measurement, and management of AI risks; Orgmetra therefore keeps LLM output as draft evidence and requires human authority. +- AICPA Trust Services Criteria provide the SOC 2 readiness vocabulary for security, availability, processing integrity, confidentiality, and privacy; Orgmetra has readiness documentation but no attestation claim. +- WCAG 2.2 is the target UI baseline. The existing Figma file and design tokens are inputs; the missing evidence is executable accessibility and interaction testing. +- Fugu, Conductor, and TRINITY support evaluating when a single route is enough and when role-specialized multi-agent depth, recursion, and access lists improve evidence work. Orgmetra must measure this through contextual-orchestrator artifacts rather than assuming multi-agent quality. + +The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. + +## Historical local evidence (2026-08-21) + +| Check | Result | +|---|---| +| Protected `develop` contents | `git ls-tree` at `9e3e4847510e1e612b48474ba42b177b8ed824df` contains migration `0013`, Job Analysis API, ADR `0014`, candidate-evidence package, ADR `0025`, the protected offer-approval package, and their quality workflows. This proves repository presence, not deployment. | +| `npm run validate` on active PR #53 | Passed: foundation validation and 62 Node tests on exact local head `c45a8a53125a6075f5edb4a33e02661df4c5d1b5`. | +| Earlier Python package matrix | Exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f`: HRIS kernel 171 passed; candidate-evidence 75 passed; offer-approval 84 passed. All owned statement and branch coverage reports were 100%. Historical exact-head service results are listed below. | +| Storybook and browser fixture | Storybook `10.5.10` production build and `npm run test:e2e` passed on exact local head `c45a8a53125a6075f5edb4a33e02661df4c5d1b5`; Chromium `1.62.1` covered human-review/permission/confirmation/i18n states, host-injected People and Job Analysis reads, authorization/purpose request data, and denied-read no-fallback behavior. Routes are locally intercepted, so connected protected deployment evidence remains open. | +| External pinned validity smoke | At exact read-only `fast-mlsirm` revision `04d0bc21a2a20693bcf16108cd76d394fe844d23`, a local simulation→Rust fit returned `MLS2PLM`, shape `(500, 16)`, backend `rust`, device `auto`, `max_iter_reached` at 101 iterations, and nontrivial recovery error (`parameter_rmse_mean` 2.9606; `gamma_abs_error` 0.9058). This is exploratory external evidence only: no Orgmetra estimator, acceptance study, GPU parity, or protected runtime is claimed. | +| Historical exact-head service and PostgreSQL validation | On exact local head `c45a8a53125a6075f5edb4a33e02661df4c5d1b5`, `uv run --project services/people-api --extra test pytest services/people-api/tests` passed 146 tests and `uv run --project services/job-analysis-api --extra test pytest services/job-analysis-api/tests` passed 69 tests, each with 100% owned statement and branch coverage. Both services now have project-local source mappings and checked `uv.lock` files; Job Analysis additionally has corrected Python compatibility metadata and test extras. No manual `PYTHONPATH` was required. Disposable PostgreSQL `16.14` runs passed `test_job_analysis_snapshot_postgres.sh` and `test_people_mutation_idempotency_postgres.sh`; these are local contract evidence, not hosted terminal gates or deployment proof. | +| Protected-tree preservation | `git diff --name-status origin/develop..HEAD --diff-filter=D` returned zero deleted paths; protected Job Analysis migration/API/OpenAPI/quality-contract files remain present on the active PR branch. | +| PostgreSQL contracts | `tests/test_job_analysis_snapshot_postgres.sh` passed on exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f` against disposable PostgreSQL `16.14`; the full hosted PostgreSQL matrix remains the authoritative release gate. | + +These historical results record foundation contracts and a local browser contract at their listed local coordinates; they are not execution evidence for the current observed base or frozen UI successor. They do not prove that open PRs are merged, that a connected or released browser UI exists, that a statistical estimator exists, or that Orgmetra is certified under CSAP/SOC 2. diff --git a/docs/traceability/restore-rehearsal.md b/docs/traceability/restore-rehearsal.md index cacfd5c85..1fd692541 100644 --- a/docs/traceability/restore-rehearsal.md +++ b/docs/traceability/restore-rehearsal.md @@ -2,9 +2,9 @@ ## Scope and truth status -Protected-main truth: `develop` currently requires encrypted PostgreSQL backups, point-in-time recovery, restore rehearsals, restored audit digest verification, preservation of outbox/audit evidence, and integrity checks before restored data is serviceable. This active PR adds executable recovery evidence; it does not change authoritative HRIS business semantics. +The immutable observed source base is `develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f`. Its `docs/OPERABILITY.md` defines normative requirements for encrypted PostgreSQL backups, point-in-time recovery, restore rehearsals, restored audit digest verification, preservation of outbox/audit evidence, and integrity checks before restored data is serviceable. These requirements are not evidence that the controls are enabled or executed. Configured protection, hosted execution, and deployed recovery controls remain unverified. This active PR adds executable recovery source contracts; it does not change authoritative HRIS business semantics. -The rehearsal creates an isolated source database on one PostgreSQL service, applies the protected-main migration sequence, writes a tenant-bound bitemporal Person/Name fact plus immutable audit/outbox evidence, produces a real PostgreSQL custom-format dump, and restores it into a separate exact restored database on a second independent PostgreSQL service. That separation prevents a same-cluster rehearsal from silently inheriting cluster-global roles or other state from the source. The restored state, not the source state, is then validated. +The rehearsal creates an isolated source database on one PostgreSQL service, applies the observed-base migration sources, writes a tenant-bound bitemporal Person/Name fact plus immutable audit/outbox evidence, produces a real PostgreSQL custom-format dump, and restores it into a separate exact restored database on a second independent PostgreSQL service. That separation prevents a same-cluster rehearsal from silently inheriting cluster-global roles or other state from the source. The restored state, not the source state, is then validated. PostgreSQL `pg_dump` is database-scoped and does not carry cluster-global roles. Before `pg_restore`, the clean target cluster explicitly recreates only the two least-privilege recovery principals required by migration 0008, then the database dump restores their object ownership and ACLs. The rehearsal verifies the restored owner/operator risk attributes, schema privileges, operator EXECUTE-only capability, bounded recovery-owner column privileges, escalation-table grants, and denial of direct operator transport-table DML. This models the operator prerequisite instead of relying on source-cluster residue; production deployments remain responsible for governing those cluster-global principals through their approved infrastructure/bootstrap path. @@ -14,6 +14,12 @@ The executable lives under `.github/scripts` because it is an operational recove The script is intentionally destructive only inside disposable rehearsal clusters: it drops the two cluster-global recovery principals while resetting its isolated source/restore services. It therefore refuses to proceed unless `RECOVERY_REHEARSAL_ALLOW_ROLE_DROP=1` is explicitly set. The checked-in workflow supplies that opt-in only to its two ephemeral PostgreSQL service containers; operators must not set it against a shared or production cluster. +The workflow exposes only container port `5432` for each service, allowing Docker to allocate host ports rather than reserving host ports `5432` and `5433`. The rehearsal step constructs its source and restore administrator URLs from `job.services.source_postgres.ports[5432]` and `job.services.restore_postgres.ports[5432]`, respectively, while retaining each service's container ID and the existing cluster-identity checks. This avoids fixed-host-port contention when jobs share a Docker host; it does not provision a runner or prove actual service execution. + +The source contract records the actual rehearsal step environment with distinct synthetic allocated ports and rejects fixed mappings, swapped/shared/wrong/missing port references, and absent allocations. That recording-only test neither launches PostgreSQL nor implements the GitHub runner expression engine. The required `CWL CI isolated` group and labels remain unchanged; missing runner capacity is a separate execution prerequisite. + +Host `psql` availability on the required `CWL CI isolated` runners varies by job; the logs do not show a fixed per-runner client. Restore attempt 2 (job 112137284932) failed earlier because the runner could not write its temporary work directory. Restore rehearsal job 112192671013 on `orgmetra-ci-01` (attempt 3) was the first to stop with exit code 127 (`psql: command not found`) before any database statement, and job 112199226633 on `keyverse-ci-01` (attempt 4) stopped the same way. Foundation job 112199223734 on `orgmetra-ci-01` (attempt 4) reached PostgreSQL through a host `psql` whose version the log does not record. The workflow therefore installs the Ubuntu `postgresql-client-16` package, places `/usr/lib/postgresql/16/bin` ahead of any runner-supplied client, and proves in a later step that the selected `psql` is that binary and reports major version 16 before the rehearsal script runs. The `pg_dump` and `pg_restore` commands continue to run inside the pinned PostgreSQL 17.6 service containers; host `psql` sends only SQL statements and script files. This step does not prove a successful restore; only a passing hosted run of this step does. + Administrator connection URIs are parsed rather than edited by string suffix. The script accepts only `postgres`/`postgresql` schemes with a network location, rejects fragments, replaces only the database path with the isolated rehearsal database name, and preserves supported query parameters. Before restore, the produced custom-format dump must be non-empty and parse successfully through `pg_restore --list`. ## Executable evidence diff --git a/manifest.json b/manifest.json index f7b6cf55e..700675222 100644 --- a/manifest.json +++ b/manifest.json @@ -1,19 +1,32 @@ { "package": "orgmetra-foundation-pack", "version": "0.1.0", - "generated_for_branch": "feat/audit-outbox-envelope", + "target_branch": "develop", + "generated_source_branch": null, "files": [ { "path": ".github/workflows/foundation-ci.yml", - "sha256": "b6a4365936b66803a8112f034c77d53d33301a7a798ed4f68746a4f2d8b081d7", - "bytes": 6651, - "lines": 125 + "sha256": "b66a63157208934eb459f9d6e103628757de4c51c6d7fd81c3761790b02ea5e6", + "bytes": 7621, + "lines": 145 }, { "path": ".gitignore", - "sha256": "145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21", - "bytes": 375, - "lines": 37 + "sha256": "251c57f25b9fd862b3a914aa52987474ec5bc9660d8071d1aa7f1141ea103ca1", + "bytes": 439, + "lines": 41 + }, + { + "path": ".storybook/main.js", + "sha256": "210d8108637ccea90d2c52e2eec793a9215cc512012a2292ab4abf50983a134d", + "bytes": 253, + "lines": 8 + }, + { + "path": ".storybook/preview.js", + "sha256": "0f02687d3c76745afea6402607b06b893a0e88f8414dba4d79e39f50f4d17817", + "bytes": 336, + "lines": 13 }, { "path": "AGENTS.md", @@ -23,15 +36,15 @@ }, { "path": "ARCHITECTURE.md", - "sha256": "52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850", - "bytes": 7864, + "sha256": "d974b7e6f0ec17e321887e208e6f893a557546206b367fec63e112410618c0df", + "bytes": 7908, "lines": 107 }, { "path": "CHANGELOG.md", - "sha256": "f2d2e0b488c0440533effa821808f2f17e37d92f8fb586174c2fdb594f760ca5", - "bytes": 17539, - "lines": 77 + "sha256": "0e0317bcaa066d6eeb2ff2c5169ae1e0d04d48e581dbf0f95bcac701d92fea59", + "bytes": 28871, + "lines": 110 }, { "path": "CLAUDE.md", @@ -53,9 +66,33 @@ }, { "path": "README.md", - "sha256": "1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6", - "bytes": 3785, - "lines": 81 + "sha256": "1e2ca561bca7fc90b3439cc06b9be71fac1eb7657f895fec12d095587e9b77a2", + "bytes": 4756, + "lines": 89 + }, + { + "path": "apps/hr-workspace/app.js", + "sha256": "2ed60b1e5d76592a5fab1fce978789643644894d551d001668620b95989c9929", + "bytes": 32014, + "lines": 480 + }, + { + "path": "apps/hr-workspace/index.html", + "sha256": "94688218fe12a3a2f47cb666abd90f609130f930a5f3f84319ec2ad4d2405434", + "bytes": 19897, + "lines": 159 + }, + { + "path": "apps/hr-workspace/styles.css", + "sha256": "3751c1abbd8863040cdf61adf65a4fee9d00f78a9848e4a6b01a71b06b961926", + "bytes": 10731, + "lines": 122 + }, + { + "path": "apps/hr-workspace/workspace.stories.js", + "sha256": "b4e2820c3aa5e08bde95592c3d47012eecc8d3f968fe511231f2e258124d7ee5", + "bytes": 7641, + "lines": 134 }, { "path": "database/migrations/0001_foundation_schema.sql", @@ -155,21 +192,21 @@ }, { "path": "docs/OPERABILITY.md", - "sha256": "82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62", - "bytes": 11189, - "lines": 71 + "sha256": "5382d3217306178e73585c1da1353f3159322470f2b73d63a94633e7db2495ba", + "bytes": 15597, + "lines": 91 }, { "path": "docs/PRD.md", - "sha256": "3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1", - "bytes": 5490, - "lines": 111 + "sha256": "81e3060f07cb8dd219926ad844f260d1cfd20d7b19cbcfe8d67dc3f2e69b0264", + "bytes": 6327, + "lines": 123 }, { "path": "docs/SECURITY.md", - "sha256": "01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac", - "bytes": 11185, - "lines": 64 + "sha256": "ed7fc75a07ff29ca5019f2dce0f1acf61e317b9725f89d1f07034e7e34ebcad3", + "bytes": 13509, + "lines": 96 }, { "path": "docs/STORYBOARD.md", @@ -179,15 +216,15 @@ }, { "path": "docs/STORYBOOK.md", - "sha256": "82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9", - "bytes": 1389, - "lines": 50 + "sha256": "d3d630bf1bc67664a419d28fdc3ba1e5fa1dc419747869c426f7fe196212374f", + "bytes": 5123, + "lines": 107 }, { "path": "docs/TEST_STRATEGY.md", - "sha256": "d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8", - "bytes": 16534, - "lines": 135 + "sha256": "cdd8d21294d5bfe172b2a7f4d9592449b95f18703254ef9c139a19104b63540b", + "bytes": 20163, + "lines": 165 }, { "path": "docs/THREAT_MODEL.md", @@ -197,9 +234,9 @@ }, { "path": "docs/TRACEABILITY.md", - "sha256": "dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e", - "bytes": 11462, - "lines": 40 + "sha256": "5c321cbdbe70b40557c09c63f15b03c508d94014cbcf894a5100a0e8a223d793", + "bytes": 14724, + "lines": 48 }, { "path": "docs/TRD.md", @@ -221,9 +258,9 @@ }, { "path": "docs/WIREFRAMES.md", - "sha256": "b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e", - "bytes": 2005, - "lines": 77 + "sha256": "50b9d2737ed88636fc416e1ae6c22b9258a7b5fff80e233f06c5e4cdd3f58099", + "bytes": 2994, + "lines": 87 }, { "path": "docs/adr/0001-orgmetra-authoritative-hris-record.md", @@ -257,14 +294,14 @@ }, { "path": "docs/adr/0006-governed-audit-outbox-envelope.md", - "sha256": "827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd", - "bytes": 14100, - "lines": 66 + "sha256": "58a460f3d25d125c8fc683f5a37616bb6f81eaf897d7eb70572d092eafc02a28", + "bytes": 14185, + "lines": 67 }, { "path": "docs/adr/0007-governed-job-analysis-evidence.md", - "sha256": "953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52", - "bytes": 5653, + "sha256": "be665cb6132fd8acd60c00623b1f69fce97d2b301e0c87df3a0925643459c0e3", + "bytes": 5605, "lines": 57 }, { @@ -281,21 +318,21 @@ }, { "path": "docs/adr/0010-naruon-calendar-intent-boundary.md", - "sha256": "3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9", - "bytes": 3917, - "lines": 35 + "sha256": "0d79e9cce84d3f15ffbe88c42840690616d278a0e999dcef4b7ea19ec7c813cb", + "bytes": 3921, + "lines": 37 }, { "path": "docs/adr/0011-bitemporal-workforce-composition.md", - "sha256": "dbe96dfd47066288cec835789de54cc4293f920d2ad4b0e0dba930191d7d249b", + "sha256": "a898da8335f4ea394d89063468b4e0bb00bdc586f8348e222f72f362169318d8", "bytes": 5551, - "lines": 53 + "lines": 55 }, { "path": "docs/adr/0012-governed-migration-handoff.md", - "sha256": "c7bfbda34996f717ed31f8307acc16a5d69ae464edb184ab5c8ec4b2d5763cbc", - "bytes": 5958, - "lines": 59 + "sha256": "a93fcca51799df9e8166aedc054de3ca15e6f7b8ef9f00e6c78eb964ec00519c", + "bytes": 5966, + "lines": 61 }, { "path": "docs/adr/0013-governed-requisition-review-packet.md", @@ -305,21 +342,33 @@ }, { "path": "docs/adr/0014-job-analysis-snapshot-persistence.md", - "sha256": "a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105", - "bytes": 5365, + "sha256": "a3d7e8c2234d48183433b0d06795534a8ef8b2d7b6298bed5574e088ebdae656", + "bytes": 5333, "lines": 49 }, + { + "path": "docs/adr/0026-product-technical-gap-baseline.md", + "sha256": "6d3f463e1601b0a6d5c9f404a8f866aa5ec7ea30b512b2c5251dfb5c3f42b3b4", + "bytes": 3133, + "lines": 32 + }, { "path": "docs/adr/README.md", - "sha256": "f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002", - "bytes": 1838, - "lines": 18 + "sha256": "f25a8f52936bf02182545f1a91b897fabba5debd272e80f0260757b7a33d5c41", + "bytes": 2697, + "lines": 23 }, { "path": "docs/doctoring/REFERENCES.md", - "sha256": "929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5", - "bytes": 6352, - "lines": 69 + "sha256": "63e9cbf225000be43826b5240da338946a58ff650779b5d6a0123628a1683982", + "bytes": 7652, + "lines": 85 + }, + { + "path": "docs/product-technical-gap-baseline.md", + "sha256": "fcc6a4650202dd62029eb0efe6573571d0fc5de2ba2a69fc561914d8e7b39046", + "bytes": 27509, + "lines": 170 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", @@ -333,11 +382,17 @@ "bytes": 6237, "lines": 187 }, + { + "path": "package-lock.json", + "sha256": "7f737a6adf592e4b9370abbe7de08599a7038e234f8db13aba020fbc0ba80734", + "bytes": 107967, + "lines": 3284 + }, { "path": "package.json", - "sha256": "59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5", - "bytes": 388, - "lines": 9 + "sha256": "8276ab9ab644f8fcc1a6f9ef57636d79f2e39aa8da308f08a5813f9d93fd2983", + "bytes": 809, + "lines": 19 }, { "path": "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", @@ -351,6 +406,12 @@ "bytes": 7556, "lines": 200 }, + { + "path": "playwright.config.mjs", + "sha256": "8de52e4f5a8f59f8b8814e90678c89c108fffa7ce1275e37b15b4adb02fc0d30", + "bytes": 1205, + "lines": 38 + }, { "path": "schemas/openapi.yaml", "sha256": "09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f", @@ -359,9 +420,9 @@ }, { "path": "scripts/foundation-contract-core.mjs", - "sha256": "9b03efbbdffa60a05f5924e8a61b1cbc3cd75c502df428a5920085e8d0bf3603", - "bytes": 28121, - "lines": 688 + "sha256": "79ae4f162a3bd762d16aab979da4d111416f04b9ec78797e03bf5ebb4b0b6295", + "bytes": 28722, + "lines": 704 }, { "path": "scripts/foundation-contract.mjs", @@ -371,15 +432,39 @@ }, { "path": "tests/dispatcher-inventory.test.mjs", - "sha256": "09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261", - "bytes": 1597, - "lines": 34 + "sha256": "c1b9a05c82ffdd95fa874a26819f1be47ac2646e391b0cc86d39b0549e9431c5", + "bytes": 2160, + "lines": 47 + }, + { + "path": "tests/e2e/hr-workspace-core-model.spec.mjs", + "sha256": "b4a328a7756723b0dbdca44d1b6bd2f3240c72751dc1b5f793d4b658c63da329", + "bytes": 1319, + "lines": 27 + }, + { + "path": "tests/e2e/hr-workspace.spec.mjs", + "sha256": "5b9610d26c4e7c125d89ce2b9dbdbaffd9a39fb089aa7add13028771d4fea2d6", + "bytes": 21825, + "lines": 447 }, { "path": "tests/foundation-contract.test.mjs", - "sha256": "648533b4aff8cee643df4afc06b463eda788e002e11d043971c8a16804c68501", - "bytes": 14943, - "lines": 387 + "sha256": "bf535b963debf1855fdde59bbf1da8e94cffa4ac777d891e1323a630f88b05f0", + "bytes": 29053, + "lines": 610 + }, + { + "path": "tests/hr-workspace.test.mjs", + "sha256": "4589ffd8afa5f8afcb0ed725a54b60b8fe6270b7c5ea08805ebd301aa655c446", + "bytes": 26479, + "lines": 501 + }, + { + "path": "tests/job-analysis-api-fixture.mjs", + "sha256": "7af2654ad70852509bece5e51c88111898168f224eecc501c1910b60a10c3959", + "bytes": 1728, + "lines": 48 }, { "path": "tests/openapi-contract.test.mjs", @@ -423,6 +508,12 @@ "bytes": 11349, "lines": 370 }, + { + "path": "tests/test_github_actions_runner_image.py", + "sha256": "f0962acafa377c7198c4e1228428bd4b8294e1af3f518871c1cb16420b8afe81", + "bytes": 20874, + "lines": 439 + }, { "path": "tests/test_job_analysis_snapshot_postgres.sh", "sha256": "ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f", @@ -467,9 +558,15 @@ }, { "path": "tests/validate_repository.py", - "sha256": "091836b2f68600a30b08f7da2cea8b3bef10201a123da720a7369bf10985eec2", - "bytes": 27237, - "lines": 637 + "sha256": "edd8f4e0b449e88dee5e7c5a151b158ac45ca97084880bf65147c3d530c06177", + "bytes": 28293, + "lines": 660 + }, + { + "path": "tests/workspace-foundation-integration.test.mjs", + "sha256": "2a8266435d837ba445986ee257a6de94eddf687d950a016d4d1348b48618aee7", + "bytes": 19601, + "lines": 366 } ] } diff --git a/package-lock.json b/package-lock.json index 73c9101ac..d451f61a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,3283 @@ { - "name": "@contextualwisdomlab/orgmetra", - "version": "0.0.0", + "name": "orgmetra-foundation-pack", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@contextualwisdomlab/orgmetra", - "version": "0.0.0", + "name": "orgmetra-foundation-pack", + "version": "0.1.0", + "devDependencies": { + "@playwright/test": "^1.62.1", + "@storybook/web-components-vite": "^10.5.10", + "storybook": "^10.5.10" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", + "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz", + "integrity": "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.127.0.tgz", + "integrity": "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.127.0.tgz", + "integrity": "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.127.0.tgz", + "integrity": "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.127.0.tgz", + "integrity": "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.127.0.tgz", + "integrity": "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.127.0.tgz", + "integrity": "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.127.0.tgz", + "integrity": "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.127.0.tgz", + "integrity": "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.127.0.tgz", + "integrity": "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.127.0.tgz", + "integrity": "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.127.0.tgz", + "integrity": "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.127.0.tgz", + "integrity": "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.127.0.tgz", + "integrity": "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.127.0.tgz", + "integrity": "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.127.0.tgz", + "integrity": "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.127.0.tgz", + "integrity": "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz", + "integrity": "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.127.0.tgz", + "integrity": "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.127.0.tgz", + "integrity": "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz", + "integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz", + "integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz", + "integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz", + "integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz", + "integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz", + "integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz", + "integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz", + "integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz", + "integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz", + "integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz", + "integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz", + "integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz", + "integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz", + "integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz", + "integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz", + "integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz", + "integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz", + "integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz", + "integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@storybook/builder-vite": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.10.tgz", + "integrity": "sha512-O4GgIP0tKLRueom3EmU3OaBUHKjNYj+jkOvmTIkn3PYTiWVkCuHqSKEs4ADvRyaQuLH+peHhFe4JtkNC9KbtrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "10.5.10", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.5.10", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz", + "integrity": "sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^2.3.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "esbuild": "*", + "rollup": "*", + "storybook": "^10.5.10", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "esbuild": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.1.0.tgz", + "integrity": "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@storybook/web-components": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.5.10.tgz", + "integrity": "sha512-32l6lUjrJ7SrM71C1Uqc2KvV8DU7dXh0ph1A4sPgmNkFPv7LNPoiaOPH+m0FMQ4QCk3gZHLZQU1Yb2iLvHrvWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "lit": "^2.0.0 || ^3.0.0", + "storybook": "^10.5.10" + } + }, + "node_modules/@storybook/web-components-vite": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.5.10.tgz", + "integrity": "sha512-dCsfpXCrR5DezLk1zJkN4wtZm7VoVrSsjTJ2sQCTBIGMcQZlbMq+U8FC8rZ5pNd/CxzNKABAJjE3P+jQC3lImw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/builder-vite": "10.5.10", + "@storybook/web-components": "10.5.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.5.10", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.5", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.5.tgz", + "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webcontainer/env": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", + "integrity": "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lit": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", + "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oxc-parser": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.127.0.tgz", + "integrity": "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.127.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.127.0", + "@oxc-parser/binding-android-arm64": "0.127.0", + "@oxc-parser/binding-darwin-arm64": "0.127.0", + "@oxc-parser/binding-darwin-x64": "0.127.0", + "@oxc-parser/binding-freebsd-x64": "0.127.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", + "@oxc-parser/binding-linux-arm64-musl": "0.127.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", + "@oxc-parser/binding-linux-x64-gnu": "0.127.0", + "@oxc-parser/binding-linux-x64-musl": "0.127.0", + "@oxc-parser/binding-openharmony-arm64": "0.127.0", + "@oxc-parser/binding-wasm32-wasi": "0.127.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", + "@oxc-parser/binding-win32-x64-msvc": "0.127.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz", + "integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.2", + "@oxc-resolver/binding-android-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-x64": "11.21.2", + "@oxc-resolver/binding-freebsd-x64": "11.21.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-musl": "11.21.2", + "@oxc-resolver/binding-openharmony-arm64": "11.21.2", + "@oxc-resolver/binding-wasm32-wasi": "11.21.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" + } + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/storybook": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.10.tgz", + "integrity": "sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^2.0.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/spy": "3.2.4", + "@webcontainer/env": "^1.1.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", + "jsonc-parser": "^3.3.1", + "open": "^10.2.0", + "oxc-parser": "^0.127.0", + "oxc-resolver": "11.21.2", + "recast": "^0.23.5", + "semver": "^7.7.3", + "use-sync-external-store": "^1.5.0", + "ws": "^8.21.1" + }, + "bin": { + "storybook": "dist/bin/dispatcher.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "prettier": "^2 || ^3", + "vite-plus": "^0.1.15 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, "engines": { - "node": ">=22" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package.json b/package.json index ceb2fb8fc..fae307103 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,18 @@ "name": "orgmetra-foundation-pack", "version": "0.1.0", "private": true, + "type": "module", "description": "Orgmetra evidence-centered HRIS foundation baseline.", "scripts": { - "validate": "python3 tests/validate_repository.py && node scripts/foundation-contract.mjs && node --test tests/foundation-contract.test.mjs tests/openapi-contract.test.mjs tests/dispatcher-inventory.test.mjs" + "validate": "python3 tests/validate_repository.py && node scripts/foundation-contract.mjs && node --test tests/foundation-contract.test.mjs tests/openapi-contract.test.mjs tests/dispatcher-inventory.test.mjs tests/hr-workspace.test.mjs tests/workspace-foundation-integration.test.mjs", + "test:workspace": "node --test tests/hr-workspace.test.mjs", + "test:e2e": "playwright test", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "devDependencies": { + "@playwright/test": "^1.62.1", + "@storybook/web-components-vite": "^10.5.10", + "storybook": "^10.5.10" } } diff --git a/packages/design-tokens/tokens.css b/packages/design-tokens/tokens.css index 26bc9d411..59bbbb726 100644 --- a/packages/design-tokens/tokens.css +++ b/packages/design-tokens/tokens.css @@ -7,6 +7,9 @@ --orgmetra-action-export: #334155; --orgmetra-action-escalate: #b45309; --orgmetra-surface-page: #f8fafc; + --orgmetra-surface-card: #f7faff; + --orgmetra-border-subtle: #d1d9e5; + --orgmetra-focus-ring: #1d4e89; --orgmetra-text-primary: #0f172a; --orgmetra-text-muted: #475569; --orgmetra-danger: #b91c1c; diff --git a/packages/design-tokens/tokens.json b/packages/design-tokens/tokens.json index 3ae4aa15b..2e8573b84 100644 --- a/packages/design-tokens/tokens.json +++ b/packages/design-tokens/tokens.json @@ -11,6 +11,9 @@ "action.export": "#334155", "action.escalate": "#B45309", "surface.page": "#F8FAFC", + "surface.card": "#F7FAFF", + "border.subtle": "#D1D9E5", + "focus.ring": "#1D4E89", "text.primary": "#0F172A", "text.muted": "#475569", "danger": "#B91C1C" diff --git a/playwright.config.mjs b/playwright.config.mjs new file mode 100644 index 000000000..c47a304a7 --- /dev/null +++ b/playwright.config.mjs @@ -0,0 +1,38 @@ +import { defineConfig, devices } from '@playwright/test'; + +// Reject malformed overrides before interpolating a shell command or starting a fixture. +const port = process.env.ORGMETRA_WORKSPACE_PORT ?? '4173'; +if (!/^[1-9][0-9]{0,4}$/.test(port) || Number(port) > 65535) { + throw new Error('ORGMETRA_WORKSPACE_PORT must be a canonical decimal TCP port from 1 to 65535'); +} + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 15_000, + expect: { timeout: 5_000 }, + fullyParallel: false, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI + ? [['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }]] + : 'list', + use: { + baseURL: `http://127.0.0.1:${port}`, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: `python3 -m http.server ${port} --bind 127.0.0.1`, + url: `http://127.0.0.1:${port}/apps/hr-workspace/index.html`, + reuseExistingServer: false, + timeout: 120_000, + }, +}); diff --git a/recovery-manifest.json b/recovery-manifest.json index 00deb35ad..e5da83de5 100644 --- a/recovery-manifest.json +++ b/recovery-manifest.json @@ -1 +1,30 @@ -{"package":"orgmetra-recovery-rehearsal","version":"0.1.0","files":[{"path":".github/workflows/recovery-rehearsal-quality.yml","sha256":"b2fe78ca19946aa1082d1a756fd277766908b49153d7cc8fc7184dceebdca078","bytes":4156,"lines":111},{"path":"docs/traceability/restore-rehearsal.md","sha256":"e4cf63847d03ad890918234d36b338a5f7c0b5561b86ca8bfcfb7cca416eedd4","bytes":7209,"lines":42},{"path":"tests/recovery-rehearsal.test.mjs","sha256":"0102e526fcb5f1d8809b3ffd02aec32075bf1a6e28462432e122a49c0bc9d6e6","bytes":8695,"lines":140},{"path":".github/scripts/restore-rehearsal-postgres.sh","sha256":"7b2e468898d5cae9897dc0337da5ce4e229fe9d980d2961b648a558081fdf5fe","bytes":17307,"lines":412}]} +{ + "package": "orgmetra-recovery-rehearsal", + "version": "0.1.0", + "files": [ + { + "path": ".github/workflows/recovery-rehearsal-quality.yml", + "sha256": "08abca6f783154a2cd239c131bc174d0908d14dbf3f0d4472cf4557a264ce3b6", + "bytes": 5330, + "lines": 135 + }, + { + "path": "docs/traceability/restore-rehearsal.md", + "sha256": "1ab905d5d8faea270c65ec26e53bf6883a0768f6509c999208b3db68a0c512e3", + "bytes": 9652, + "lines": 48 + }, + { + "path": "tests/recovery-rehearsal.test.mjs", + "sha256": "853cafe6aacdf0f093c518ad62d9618fcf5998e8ef863108115dcf3691bf7df1", + "bytes": 16016, + "lines": 290 + }, + { + "path": ".github/scripts/restore-rehearsal-postgres.sh", + "sha256": "7b2e468898d5cae9897dc0337da5ce4e229fe9d980d2961b648a558081fdf5fe", + "bytes": 17307, + "lines": 412 + } + ] +} diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 4aacefb3c..3615d0f29 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -20,7 +20,16 @@ export const REQUIRED_FILES = Object.freeze([ 'NOTICE', 'manifest.json', 'package.json', + 'package-lock.json', + '.storybook/main.js', + '.storybook/preview.js', + 'apps/hr-workspace/index.html', + 'apps/hr-workspace/styles.css', + 'apps/hr-workspace/app.js', + 'apps/hr-workspace/workspace.stories.js', '.github/workflows/foundation-ci.yml', + 'playwright.config.mjs', + 'tests/workspace-foundation-integration.test.mjs', 'docs/PRD.md', 'docs/TRD.md', 'docs/USER_STORIES.md', @@ -36,6 +45,7 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/TEST_STRATEGY.md', 'docs/OPERABILITY.md', 'docs/TRACEABILITY.md', + 'docs/product-technical-gap-baseline.md', 'docs/adr/README.md', 'docs/adr/0001-orgmetra-authoritative-hris-record.md', 'docs/adr/0002-federated-cwl-integration-boundaries.md', @@ -51,6 +61,7 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/adr/0012-governed-migration-handoff.md', 'docs/adr/0013-governed-requisition-review-packet.md', 'docs/adr/0014-job-analysis-snapshot-persistence.md', + 'docs/adr/0026-product-technical-gap-baseline.md', 'docs/doctoring/REFERENCES.md', 'docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md', 'docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md', @@ -74,6 +85,11 @@ export const REQUIRED_FILES = Object.freeze([ 'scripts/foundation-contract.mjs', 'tests/dispatcher-inventory.test.mjs', 'tests/foundation-contract.test.mjs', + 'tests/hr-workspace.test.mjs', + 'tests/job-analysis-api-fixture.mjs', + 'tests/test_github_actions_runner_image.py', + 'tests/e2e/hr-workspace-core-model.spec.mjs', + 'tests/e2e/hr-workspace.spec.mjs', 'tests/openapi-contract.test.mjs', 'tests/test_bitemporal_postgres.sh', 'tests/test_tenant_isolation_postgres.sh', @@ -93,7 +109,7 @@ export const REQUIRED_FILES = Object.freeze([ /** Exact maturity vocabulary accepted by traceability tables. */ export const MATURITY_VALUES = Object.freeze(new Set([ - 'implemented_on_protected_main', + 'implemented_on_protected_develop', 'implemented_on_active_pr', 'accepted_architecture', 'planned', diff --git a/services/job-analysis-api/pyproject.toml b/services/job-analysis-api/pyproject.toml index 5e27bf477..0c70550da 100644 --- a/services/job-analysis-api/pyproject.toml +++ b/services/job-analysis-api/pyproject.toml @@ -7,7 +7,7 @@ name = "orgmetra-job-analysis-api" version = "0.1.0" description = "Governed persistence boundary for Orgmetra job-analysis snapshots." readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" license = { text = "Apache-2.0" } authors = [{ name = "ContextualWisdomLab" }] dependencies = [ @@ -15,6 +15,13 @@ dependencies = [ "orgmetra-keyverse-adapter==0.1.0", ] +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.uv.sources] +orgmetra-hris-kernel = { path = "../../packages/hris-kernel" } +orgmetra-keyverse-adapter = { path = "../../packages/keyverse-adapter" } + [tool.setuptools] package-dir = {"" = "src"} diff --git a/services/job-analysis-api/uv.lock b/services/job-analysis-api/uv.lock new file mode 100644 index 000000000..0d74575fa --- /dev/null +++ b/services/job-analysis-api/uv.lock @@ -0,0 +1,225 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "orgmetra-hris-kernel" +version = "0.4.0" +source = { directory = "../../packages/hris-kernel" } + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "orgmetra-job-analysis-api" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "orgmetra-hris-kernel" }, + { name = "orgmetra-keyverse-adapter" }, +] + +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "orgmetra-hris-kernel", directory = "../../packages/hris-kernel" }, + { name = "orgmetra-keyverse-adapter", directory = "../../packages/keyverse-adapter" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "orgmetra-keyverse-adapter" +version = "0.1.0" +source = { directory = "../../packages/keyverse-adapter" } + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] diff --git a/services/people-api/README.md b/services/people-api/README.md index 548a83446..829fbaf5e 100644 --- a/services/people-api/README.md +++ b/services/people-api/README.md @@ -8,7 +8,7 @@ The service exposes a governed hire-to-employment read contract. `read_worker_pe `PostgresPeopleReadPort` is the production read-side adapter for that port. Deployment code supplies a DB-API-compatible connection factory (for example, a configured psycopg connection or pool boundary); the adapter then starts a read-only transaction, binds the exact tenant through transaction-local `orgmetra.tenant_record_id`, and issues only parameterized, fully-qualified SQL against Orgmetra-owned canonical tables. It resolves `candidate_worker_conversion_record`, `person_record`, `person_name_record`, `employment_record`, and `employment_record_version` using current recorded-time truth plus the requested business date. It fetches at most two rows and rejects duplicate current lineage instead of silently choosing one. Pooling, credentials, TLS and database-role selection remain deployment concerns rather than being embedded in the service package. -`PeopleAsgiApp` exposes that governed read use case as a dependency-light ASGI route: `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}?effective_on=YYYY-MM-DD&purpose=people_read&fields=...`. It validates the exact route and query shape before authentication, accepts exactly one ASCII Bearer credential, delegates authentication and purpose-bound authorization to injected contracts, and never reads protected worker values after a denied authorization decision. Successful responses contain only authorized fields; all HTTP responses use `Cache-Control: no-store` and `Vary: Authorization`. Authentication, authorization, missing-record, integrity-conflict, and unexpected-backend failures are mapped to stable non-disclosing responses with a useful next action, and bearer tokens are never returned in response text. +`PeopleAsgiApp` exposes that governed read use case as a dependency-light ASGI route: `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}?effective_on=YYYY-MM-DD&purpose=people_read&fields=...`. It validates the exact route and query shape before authentication, accepts exactly one ASCII Bearer credential, delegates authentication and purpose-bound authorization to injected contracts, and never reads protected worker values after a denied authorization decision. Successful responses contain only authorized fields; all HTTP responses use `Cache-Control: no-store` and `Vary: Authorization`. Authentication, authorization, missing-record, integrity-conflict, and unexpected-backend failures retain their existing HTTP statuses and legacy `error` alias, and include the shared `error_code`, actionable `next_action`, and a random `support_reference`. The same reference joins a bounded local log containing only error code, HTTP status, and support reference; no bearer token, HR field value, request identity, or exception text is logged or returned. Unexpected authentication-backend exceptions receive a safe 500 before any worker read; task cancellation is not normalized into an HTTP failure. Successful response fields and the authentication/authorization order remain unchanged. These local contracts do not establish deployed issuer authentication or database acceptance. The People API quality workflow is part of this contract and must run for pull requests to every supported protected/default integration branch, including `develop`. Its service tests enforce 100% owned statement and branch coverage and include regression coverage for the workflow dispatch boundary and HTTP security/transport behavior. diff --git a/services/people-api/pyproject.toml b/services/people-api/pyproject.toml index 3f228e11c..785f188ce 100644 --- a/services/people-api/pyproject.toml +++ b/services/people-api/pyproject.toml @@ -7,14 +7,21 @@ name = "orgmetra-people-api" version = "0.1.0" description = "Purpose-bound customer API boundary for authoritative Orgmetra HR records." readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" license = { text = "Apache-2.0" } authors = [{ name = "ContextualWisdomLab" }] dependencies = [ - "orgmetra-hris-kernel==0.1.0", + "orgmetra-hris-kernel==0.4.0", "orgmetra-keyverse-adapter==0.1.0", ] +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.uv.sources] +orgmetra-hris-kernel = { path = "../../packages/hris-kernel" } +orgmetra-keyverse-adapter = { path = "../../packages/keyverse-adapter" } + [tool.setuptools] package-dir = {"" = "src"} diff --git a/services/people-api/src/orgmetra_people_api/http.py b/services/people-api/src/orgmetra_people_api/http.py index c7a4714a0..22f4e48e9 100644 --- a/services/people-api/src/orgmetra_people_api/http.py +++ b/services/people-api/src/orgmetra_people_api/http.py @@ -10,7 +10,9 @@ from dataclasses import dataclass from datetime import date import json +import logging import re +from secrets import token_urlsafe from typing import Awaitable, Callable, Mapping, Sequence from urllib.parse import parse_qsl from uuid import UUID @@ -28,6 +30,8 @@ AsgiReceive = Callable[[], Awaitable[dict[str, object]]] AsgiSend = Callable[[dict[str, object]], Awaitable[None]] +_LOGGER = logging.getLogger(__name__) +_SUPPORT_REFERENCE_RANDOM_BYTES = 24 _ROUTE_PREFIX = ("v1", "tenants") _PURPOSE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$") _FIELD_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$") @@ -137,6 +141,16 @@ async def __call__(self, scope: Mapping[str, object], receive: AsgiReceive, send extra_headers=((b"www-authenticate", b"Bearer"),), ) return + except Exception: # noqa: BLE001 - identity backend failures must remain client-safe. + await _send_json( + send, + status=500, + payload={ + "error": "internal_error", + "message": "Retry later or contact an Orgmetra operator with non-secret request metadata; never include the bearer token.", + }, + ) + return try: view = read_worker_people_record( @@ -286,7 +300,28 @@ async def _send_json( payload: Mapping[str, object], extra_headers: tuple[tuple[bytes, bytes], ...] = (), ) -> None: - """Emit a deterministic JSON response that is never cached as shared PII.""" + """Emit no-store JSON, adding governed metadata to legacy read errors only. + + Existing canonical mutation envelopes and successful payloads stay unchanged. + Support references correlate bounded metadata without recording credentials, + HR values, request identifiers, or exception messages. + """ + if status >= 400 and "error_code" not in payload: + support_reference = f"err_{token_urlsafe(_SUPPORT_REFERENCE_RANDOM_BYTES)}" + payload = { + **payload, + "error_code": payload["error"], + "next_action": payload["message"], + "support_reference": support_reference, + } + _LOGGER.info( + "People read request rejected", + extra={ + "error_code": payload["error_code"], + "http_status": status, + "support_reference": support_reference, + }, + ) body = json.dumps(payload, ensure_ascii=True, separators=(",", ":"), sort_keys=True).encode("utf-8") headers = ( (b"content-type", b"application/json"), diff --git a/services/people-api/tests/test_http_route.py b/services/people-api/tests/test_http_route.py index 310d4bfce..9219751fd 100644 --- a/services/people-api/tests/test_http_route.py +++ b/services/people-api/tests/test_http_route.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import date import json import unittest @@ -238,6 +239,28 @@ async def test_authenticator_rejection_is_unauthorized_without_protected_read(se self.assertEqual(authenticator.tokens, ["opaque-token"]) self.assertEqual(port.calls, []) + async def test_authenticator_runtime_failure_returns_safe_500_without_read(self) -> None: + """Contain identity-backend faults before protected worker values are read.""" + authenticator = FakeAuthenticator( + self.principal, error=RuntimeError("synthetic-identity-failure-sentinel") + ) + port = FakeReadPort(worker_record()) + app = self._app(authenticator=authenticator, read_port=port) + + try: + status, headers, payload = await self._request(app) + except Exception as error: + self.assertEqual(port.calls, []) + self.fail(f"identity failure escaped ASGI as {type(error).__name__}") + + self.assertEqual((status, payload["error"]), (500, "internal_error")) + self.assertEqual(headers[b"cache-control"], b"no-store") + self.assertEqual(headers[b"vary"], b"Authorization") + self.assertNotIn("synthetic-identity-failure-sentinel", json.dumps(payload)) + self.assertNotIn("opaque-token", json.dumps(payload)) + self.assertEqual(authenticator.tokens, ["opaque-token"]) + self.assertEqual(port.calls, []) + async def test_authorization_denial_does_not_read_worker_values(self) -> None: port = FakeReadPort(worker_record()) app = self._app(read_port=port) @@ -262,6 +285,77 @@ async def test_unexpected_failure_returns_generic_500_without_secret_details(sel self.assertEqual((status, payload["error"]), (500, "internal_error")) self.assertNotIn("password", json.dumps(payload)) + async def test_read_error_envelopes_preserve_status_and_add_governed_fields(self) -> None: + """Require client-safe metadata on every existing read-error category.""" + cases = ( + (self._app(), {"method": "POST"}, 405, "method_not_allowed"), + (self._app(), {"path": "/v1/unknown"}, 404, "route_not_found"), + (self._app(), {"query": b"bogus"}, 400, "invalid_request"), + (self._app(), {"headers": []}, 401, "authentication_required"), + (self._app(), {"query": b"effective_on=2026-08-17&purpose=unapproved&fields=display_name"}, 403, "access_denied"), + (self._app(read_port=FakeReadPort(None)), {}, 404, "worker_not_found"), + (self._app(read_port=FakeReadPort(worker_record(person_record_id=OTHER_PERSON))), {}, 409, "worker_integrity_conflict"), + (self._app(read_port=ExplodingReadPort()), {}, 500, "internal_error"), + (self._app(authenticator=FakeAuthenticator(self.principal, error=RuntimeError("fixture-only-fault"))), {}, 500, "internal_error"), + ) + references: set[str] = set() + for app, request, expected_status, expected_code in cases: + with self.subTest(error=expected_code, status=expected_status): + status, headers, payload = await self._request(app, **request) + self.assertEqual(status, expected_status) + self.assertEqual(payload.get("error_code"), expected_code) + self.assertEqual(payload["error"], expected_code) + self.assertEqual(payload["next_action"], payload["message"]) + self.assertTrue(payload["next_action"]) + reference = payload["support_reference"] + self.assertRegex(reference, r"\Aerr_[A-Za-z0-9_-]{32}\Z") + self.assertNotIn(reference, references) + references.add(reference) + self.assertEqual(headers[b"cache-control"], b"no-store") + self.assertEqual(headers[b"vary"], b"Authorization") + if status == 401: + self.assertEqual(headers[b"www-authenticate"], b"Bearer") + if status == 405: + self.assertEqual(headers[b"allow"], b"GET") + serialized = json.dumps(payload) + for forbidden in ("fixture-only-fault", "do-not-leak", "opaque-token"): + self.assertNotIn(forbidden, serialized) + self.assertEqual(len(references), len(cases)) + status, _, payload = await self._request(self._app()) + self.assertEqual(status, 200) + self.assertEqual(set(payload), {"resource_reference", "fields"}) + self.assertEqual(payload["fields"], {"display_name": "Ada Lovelace", "employment_status_code": "active"}) + + async def test_read_failures_correlate_safe_support_metadata(self) -> None: + """Join random response references to bounded logs without reflecting secrets.""" + for app in ( + self._app(read_port=ExplodingReadPort()), + self._app(authenticator=FakeAuthenticator(self.principal, error=RuntimeError("fixture-only-fault"))), + ): + with self.subTest(app=type(app).__name__): + with self.assertLogs("orgmetra_people_api.http", level="INFO") as captured: + status, _, payload = await self._request(app) + self.assertEqual(status, 500) + self.assertEqual(len(captured.records), 1) + record = captured.records[0] + self.assertEqual(record.support_reference, payload["support_reference"]) + self.assertEqual(record.error_code, "internal_error") + self.assertEqual(record.http_status, 500) + serialized = json.dumps(record.__dict__, default=str) + for forbidden in ("fixture-only-fault", "do-not-leak", "opaque-token", str(TENANT), str(PERSON)): + self.assertNotIn(forbidden, serialized) + self.assertIsNone(record.exc_info) + + async def test_authentication_cancellation_preserves_identity_without_read(self) -> None: + """Let task cancellation propagate rather than publishing a false HTTP failure.""" + fault = asyncio.CancelledError("fixture cancellation") + authenticator = FakeAuthenticator(self.principal, error=fault) + port = FakeReadPort(worker_record()) + with self.assertRaises(asyncio.CancelledError) as captured: + await self._request(self._app(authenticator=authenticator, read_port=port)) + self.assertIs(captured.exception, fault) + self.assertEqual(port.calls, []) + async def test_non_http_scope_is_rejected_as_programming_error(self) -> None: app = self._app(read_port=FakeReadPort(None)) diff --git a/services/people-api/uv.lock b/services/people-api/uv.lock new file mode 100644 index 000000000..cac614b5b --- /dev/null +++ b/services/people-api/uv.lock @@ -0,0 +1,225 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "orgmetra-hris-kernel" +version = "0.4.0" +source = { directory = "../../packages/hris-kernel" } + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "orgmetra-keyverse-adapter" +version = "0.1.0" +source = { directory = "../../packages/keyverse-adapter" } + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "orgmetra-people-api" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "orgmetra-hris-kernel" }, + { name = "orgmetra-keyverse-adapter" }, +] + +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "orgmetra-hris-kernel", directory = "../../packages/hris-kernel" }, + { name = "orgmetra-keyverse-adapter", directory = "../../packages/keyverse-adapter" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] diff --git a/tests/dispatcher-inventory.test.mjs b/tests/dispatcher-inventory.test.mjs index 288a98eb4..6ee34f49e 100644 --- a/tests/dispatcher-inventory.test.mjs +++ b/tests/dispatcher-inventory.test.mjs @@ -22,6 +22,19 @@ function pythonRequiredFiles() { return new Set([...match[1].matchAll(/^\s+"([^"]+)",$/gm)].map((item) => item[1])); } +test('workspace contract support and retained integration guards are provenance-required', () => { + const nodeRequired = new Set(REQUIRED_FILES); + const pythonRequired = pythonRequiredFiles(); + for (const filePath of [ + 'tests/job-analysis-api-fixture.mjs', + 'tests/test_github_actions_runner_image.py', + 'tests/e2e/hr-workspace-core-model.spec.mjs', + ]) { + assert.equal(nodeRequired.has(filePath), true, `Node inventory omitted ${filePath}`); + assert.equal(pythonRequired.has(filePath), true, `Python inventory omitted ${filePath}`); + } +}); + test('every migration and executable PostgreSQL contract is provenance-required', () => { const nodeRequired = new Set(REQUIRED_FILES); const pythonRequired = pythonRequiredFiles(); diff --git a/tests/e2e/hr-workspace-core-model.spec.mjs b/tests/e2e/hr-workspace-core-model.spec.mjs new file mode 100644 index 000000000..2ea5d1514 --- /dev/null +++ b/tests/e2e/hr-workspace-core-model.spec.mjs @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; + +const workspacePath = '/apps/hr-workspace/index.html'; + +test('employee profile keeps Job, Position, and Assignment as distinct HRIS concepts', async ({ page }) => { + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + + const conceptGrid = page.locator('#view-employee-profile .concept-grid'); + await expect(conceptGrid.locator('.concept-card')).toHaveCount(5); + + const jobCard = conceptGrid.locator('.concept-card', { hasText: 'job_record' }); + const positionCard = conceptGrid.locator('.concept-card', { hasText: 'position_record' }); + const assignmentCard = conceptGrid.locator('.concept-card', { hasText: 'assignment_record' }); + + await expect(jobCard).toHaveCount(1); + await expect(positionCard).toHaveCount(1); + await expect(assignmentCard).toHaveCount(1); + await expect(jobCard.locator('strong')).toHaveText('job_record'); + await expect(positionCard.locator('strong')).toHaveText('position_record'); + await expect(assignmentCard.locator('strong')).toHaveText('assignment_record'); + + await page.locator('#locale-toggle').click(); + await expect(jobCard).toContainText('직무'); + await expect(positionCard).toContainText('직위'); + await expect(assignmentCard).toContainText('배정'); +}); diff --git a/tests/e2e/hr-workspace.spec.mjs b/tests/e2e/hr-workspace.spec.mjs new file mode 100644 index 000000000..d7dbb5ea3 --- /dev/null +++ b/tests/e2e/hr-workspace.spec.mjs @@ -0,0 +1,447 @@ +import { expect, test } from '@playwright/test'; +import { jobAnalysisApiFixture } from '../job-analysis-api-fixture.mjs'; + +const jobAnalysisSnapshot = jobAnalysisApiFixture(); + +const workspacePath = '/apps/hr-workspace/index.html'; + +async function injectProtectedReadConfig(page) { + await page.addInitScript((snapshot) => { + const baseUrl = window.location.origin; + const getAuthorization = () => 'Bearer e2e-host-token'; + window.__ORGMETRA_JOB_ANALYSIS__ = { + baseUrl, + tenantRecordId: snapshot.tenant_record_id, + analysisRecordId: snapshot.analysis_record_id, + purposeCode: 'job_analysis_read', + getAuthorization, + }; + window.__ORGMETRA_PEOPLE__ = { + baseUrl, + tenantRecordId: 'tenant-e2e', + personRecordId: 'person-e2e', + effectiveOn: '2026-08-21', + purposeCode: 'people_read', + requestedFields: ['display_name', 'employment_status_code'], + getAuthorization, + }; + }, jobAnalysisSnapshot); +} + +test('human-review workspace states remain keyboard-accessible and localized', async ({ page }) => { + await page.goto(workspacePath); + + await expect(page.getByRole('heading', { name: 'HR Home' })).toBeVisible(); + const localeToggle = page.locator('#locale-toggle'); + await expect(localeToggle).toHaveText('한국어'); + await expect(localeToggle).toHaveAccessibleName('Change language to 한국어'); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await expect(page.getByRole('heading', { name: 'Employee Profile' })).toBeVisible(); + + await page.locator('#access-purpose').selectOption('recruiting'); + await page.locator('[data-action="view-personal-details"]').click(); + await expect(page.locator('#permission-panel')).toBeVisible(); + await expect(page.locator('#details-panel')).toBeHidden(); + + await page.locator('#access-purpose').selectOption('hr_operations'); + await page.locator('[data-action="view-personal-details"]').click(); + await expect(page.locator('#details-panel')).toBeVisible(); + await expect(page.locator('#permission-panel')).toBeHidden(); + + await localeToggle.click(); + await expect(page.locator('html')).toHaveAttribute('lang', 'ko'); + await expect(page.getByRole('heading', { name: '직원 프로필' })).toBeVisible(); + await expect(localeToggle).toHaveText('English'); + await expect(localeToggle).toHaveAccessibleName('언어를 English로 변경'); + + await page.locator('[data-action="correct-history"]').click(); + const confirmationDialog = page.locator('#confirmation-dialog'); + await expect(confirmationDialog).toBeVisible(); + await page.locator('#confirm-correction').click(); + await expect(page.locator('#confirmation-reason')).toBeFocused(); + await page.locator('#confirmation-reason').fill('Review the effective-date evidence before correction.'); + await page.locator('#confirm-correction').click(); + await expect(page.locator('#confirmation-status')).toBeVisible(); + await expect(page.locator('#confirmation-status')).toContainText('정정 초안을 확인'); + + await page.getByRole('button', { name: '취소' }).click(); + await expect(confirmationDialog).toBeHidden(); + await page.locator('[data-action="correct-history"]').click(); + await expect(confirmationDialog).toBeVisible(); + await expect(page.locator('#confirmation-status')).toBeHidden(); + await expect(page.locator('#confirmation-reason')).toHaveValue(''); +}); + +test('Cancel dismisses correction with an empty required reason', async ({ page }) => { + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.locator('[data-action="correct-history"]').click(); + const dialog = page.locator('#confirmation-dialog'); + await expect(dialog).toBeVisible(); + await expect(page.locator('#confirmation-reason')).toHaveValue(''); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + await expect(dialog).toBeHidden(); + await expect(page.locator('#confirmation-status')).toHaveJSProperty('hidden', true); + await expect(dialog).toHaveJSProperty('returnValue', 'cancel'); +}); + +test('Close dismisses correction with an empty required reason', async ({ page }) => { + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.locator('[data-action="correct-history"]').click(); + const dialog = page.locator('#confirmation-dialog'); + await expect(dialog).toBeVisible(); + await expect(page.locator('#confirmation-reason')).toHaveValue(''); + await dialog.getByRole('button', { name: 'Close', exact: true }).click(); + await expect(dialog).toBeHidden(); + await expect(page.locator('#confirmation-status')).toHaveJSProperty('hidden', true); + await expect(dialog).toHaveJSProperty('returnValue', 'cancel'); +}); + +for (const { name, reason, valueMissing } of [ + { name: 'empty', reason: '', valueMissing: true }, + { name: 'whitespace-only', reason: ' \n\t ', valueMissing: false }, +]) { + test(`Confirmation rejects a ${name} correction reason`, async ({ page }) => { + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.locator('[data-action="correct-history"]').click(); + const dialog = page.locator('#confirmation-dialog'); + const reasonInput = page.locator('#confirmation-reason'); + const confirmButton = page.locator('#confirm-correction'); + await expect(reasonInput).toHaveJSProperty('required', true); + await expect(page.locator('#confirmation-form')).toHaveJSProperty('noValidate', false); + await expect(confirmButton).toHaveJSProperty('formNoValidate', false); + await reasonInput.fill(reason); + await expect(reasonInput).toHaveValue(reason); + expect(await reasonInput.evaluate((input) => input.validity.valueMissing)).toBe(valueMissing); + await confirmButton.click(); + await expect(dialog).toBeVisible(); + await expect(reasonInput).toBeFocused(); + await expect(page.locator('#confirmation-status')).toHaveJSProperty('hidden', true); + await expect(dialog).toHaveJSProperty('returnValue', ''); + }); +} + +test('unconfigured protected reads stay neutral and explain the host next action', async ({ page }) => { + await page.goto(workspacePath); + + await page.locator('[data-view-link="employee-profile"]').first().click(); + const peopleStatus = page.locator('#people-api-status'); + await expect(peopleStatus).toHaveAttribute('data-state', 'not-configured'); + await expect(peopleStatus).toContainText('host must provide a People API URL and authorization provider'); + await page.getByRole('button', { name: 'Load worker record' }).click(); + await expect(peopleStatus).toHaveAttribute('data-state', 'not-configured'); + await expect(page.locator('#people-api-result')).toBeHidden(); + + await page.locator('[data-view-link="job-analysis"]').first().click(); + const jobAnalysisStatus = page.locator('#job-analysis-status'); + await expect(jobAnalysisStatus).toHaveAttribute('data-state', 'not-configured'); + await expect(jobAnalysisStatus).toContainText('host must provide an API base URL and authorization provider'); + await page.getByRole('button', { name: 'Load snapshot' }).click(); + await expect(jobAnalysisStatus).toHaveAttribute('data-state', 'not-configured'); + await expect(page.locator('#job-analysis-result')).toBeHidden(); +}); + +test('connected read views use host authorization and render API evidence', async ({ page }) => { + await injectProtectedReadConfig(page); + const requests = []; + await page.route('**/v1/tenants/**/people/**', async (route) => { + const request = route.request(); + requests.push({ + kind: 'people', + url: request.url(), + authorization: (await request.headers()).authorization, + }); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ fields: { display_name: 'E2E authorized worker', employment_status_code: 'active' } }), + }); + }); + await page.route('**/v1/tenants/**/job-analysis-snapshots/**', async (route) => { + const request = route.request(); + requests.push({ + kind: 'job-analysis', + url: request.url(), + authorization: (await request.headers()).authorization, + purpose: (await request.headers())['x-purpose-code'], + }); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(jobAnalysisSnapshot), + }); + }); + + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.getByRole('button', { name: 'Load worker record' }).click(); + await expect(page.locator('#people-api-status')).toHaveAttribute('data-state', 'loaded'); + await expect(page.locator('#people-api-display-name')).toHaveText('E2E authorized worker'); + await expect(page.locator('#people-api-employment-status')).toHaveText('active'); + + await page.locator('[data-view-link="job-analysis"]').first().click(); + await page.getByRole('button', { name: 'Load snapshot' }).click(); + await expect(page.locator('#job-analysis-status')).toHaveAttribute('data-state', 'loaded'); + await expect(page.locator('#job-analysis-task-count')).toHaveText(String(jobAnalysisSnapshot.tasks.length)); + await expect(page.locator('#job-analysis-ksao-count')).toHaveText(String(jobAnalysisSnapshot.ksao_requirements.length)); + + const peopleRequest = requests.find((request) => request.kind === 'people'); + const peopleUrl = new URL(peopleRequest.url); + expect(peopleRequest.authorization).toBe('Bearer e2e-host-token'); + expect(peopleUrl.searchParams.get('effective_on')).toBe('2026-08-21'); + expect(peopleUrl.searchParams.get('purpose')).toBe('people_read'); + expect(peopleUrl.searchParams.get('fields')).toBe('display_name,employment_status_code'); + + const jobAnalysisRequest = requests.find((request) => request.kind === 'job-analysis'); + expect(jobAnalysisRequest.authorization).toBe('Bearer e2e-host-token'); + expect(jobAnalysisRequest.purpose).toBe('job_analysis_read'); +}); + +test('denied protected reads show an actionable error without local fallback', async ({ page }) => { + await injectProtectedReadConfig(page); + await page.route('**/v1/tenants/**/people/**', async (route) => { + await route.fulfill({ + status: 403, + contentType: 'application/json', + body: JSON.stringify({ error_code: 'access_denied', message: 'denied' }), + }); + }); + + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.getByRole('button', { name: 'Load worker record' }).click(); + await expect(page.locator('#people-api-status')).toHaveAttribute('data-state', 'error'); + await expect(page.locator('#people-api-status')).toContainText('denied'); + await expect(page.locator('#people-api-result')).toBeHidden(); + await expect(page.locator('#people-api-display-name')).toHaveText('unknown'); +}); + +test('a denied People reread removes previously authorized fields before the response settles', async ({ page }) => { + await injectProtectedReadConfig(page); + let requestCount = 0; + await page.route('**/v1/tenants/**/people/**', async (route) => { + requestCount += 1; + if (requestCount === 1) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ fields: { display_name: 'Previously authorized worker', employment_status_code: 'active' } }), + }); + return; + } + await new Promise((resolve) => setTimeout(resolve, 150)); + await route.fulfill({ + status: 403, + contentType: 'application/json', + body: JSON.stringify({ error_code: 'access_denied', message: 'denied' }), + }); + }); + + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + const loadButton = page.getByRole('button', { name: 'Load worker record' }); + await loadButton.click(); + await expect(page.locator('#people-api-result')).toBeVisible(); + await expect(page.locator('#people-api-display-name')).toHaveText('Previously authorized worker'); + + await loadButton.click(); + await expect(page.locator('#people-api-result')).toBeHidden(); + await expect(page.locator('#people-api-display-name')).toHaveText('unknown'); + await expect(page.locator('#people-api-status')).toHaveAttribute('data-state', 'error'); + await expect(page.locator('#people-api-result')).toBeHidden(); +}); + +test('only the latest People read may render when responses complete out of order', async ({ page }) => { + await injectProtectedReadConfig(page); + let requestCount = 0; + let markFirstRequestSeen; + const firstRequestSeen = new Promise((resolve) => { + markFirstRequestSeen = resolve; + }); + await page.route('**/v1/tenants/**/people/**', async (route) => { + requestCount += 1; + if (requestCount === 1) { + markFirstRequestSeen(); + await new Promise((resolve) => setTimeout(resolve, 200)); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ fields: { display_name: 'Stale worker', employment_status_code: 'inactive' } }), + }); + return; + } + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ fields: { display_name: 'Latest worker', employment_status_code: 'active' } }), + }); + }); + + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + const loadButton = page.getByRole('button', { name: 'Load worker record' }); + await loadButton.click(); + await firstRequestSeen; + await page.locator('#people-api-person').fill('person-latest'); + await loadButton.click(); + + await expect(page.locator('#people-api-status')).toHaveAttribute('data-state', 'loaded'); + await expect(page.locator('#people-api-display-name')).toHaveText('Latest worker'); + await expect(page.locator('#people-api-employment-status')).toHaveText('active'); + await page.waitForTimeout(250); + await expect(page.locator('#people-api-display-name')).toHaveText('Latest worker'); + await expect(page.locator('#people-api-employment-status')).toHaveText('active'); +}); + +for (const fixture of [ + { + name: 'People', view: 'employee-profile', route: '**/v1/tenants/**/people/**', + input: '#people-api-person', result: '#people-api-result', status: '#people-api-status', + field: '#people-api-display-name', button: 'Load worker record', + payload: { fields: { display_name: 'Original worker', employment_status_code: 'active' } }, + fresh: { fields: { display_name: 'Fresh worker', employment_status_code: 'active' } }, + expected: 'Fresh worker', + }, + { + name: 'Job Analysis', view: 'job-analysis', route: '**/v1/tenants/**/job-analysis-snapshots/**', + input: '#job-analysis-record', result: '#job-analysis-result', status: '#job-analysis-status', + field: '#job-analysis-analysis-id', button: 'Load snapshot', + payload: jobAnalysisSnapshot, + fresh: { ...jobAnalysisSnapshot, analysis_record_id: 'analysis-fresh' }, + expected: 'analysis-fresh', + }, +]) { + test(`${fixture.name} input edits invalidate a pending read and permit a fresh explicit request`, async ({ page }) => { + await injectProtectedReadConfig(page); + let markSeen; + const seen = new Promise((resolve) => { markSeen = resolve; }); + let release; + const held = new Promise((resolve) => { release = resolve; }); + let requests = 0; + await page.route(fixture.route, async (route) => { + requests += 1; + if (requests === 1) { + markSeen(); + await held; + } + await route.fulfill({ status: 200, contentType: 'application/json', + body: JSON.stringify(requests === 1 ? fixture.payload : fixture.fresh) }); + }); + try { + await page.goto(workspacePath); + await page.locator(`[data-view-link="${fixture.view}"]`).first().click(); + await page.getByRole('button', { name: fixture.button, exact: true }).click(); + await seen; + const original = await page.locator(fixture.input).inputValue(); + await page.locator(fixture.input).fill('different-resource'); + await page.locator(fixture.input).fill(original); + await expect(page.locator(fixture.status)).toHaveAttribute('data-state', 'idle'); + const response = page.waitForResponse((value) => value.url().includes('/v1/tenants/')); + release(); + await (await response).finished(); + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await expect(page.locator(fixture.result)).toBeHidden(); + await expect(page.locator(fixture.field)).toHaveText('unknown'); + await expect(page.locator(fixture.status)).toHaveAttribute('data-state', 'idle'); + await page.locator(fixture.input).fill('fresh-resource'); + await page.getByRole('button', { name: fixture.button, exact: true }).click(); + await expect(page.locator(fixture.field)).toHaveText(fixture.expected); + await expect(page.locator(fixture.result)).toBeVisible(); + await page.locator(fixture.input).fill('another-resource'); + await expect(page.locator(fixture.result)).toBeHidden(); + await expect(page.locator(fixture.field)).toHaveText('unknown'); + expect(requests).toBe(2); + } finally { + release(); + } + }); +} + +test('host authority invalidation clears both protected surfaces and permits explicit fresh reads', async ({ page }) => { + await injectProtectedReadConfig(page); + let requests = 0; + await page.route('**/v1/tenants/**/people/**', async (route) => { + requests += 1; + await route.fulfill({ status: 200, contentType: 'application/json', + body: JSON.stringify({ fields: { display_name: 'Authorized fixture worker', employment_status_code: 'active' } }) }); + }); + await page.route('**/v1/tenants/**/job-analysis-snapshots/**', async (route) => { + requests += 1; + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(jobAnalysisSnapshot) }); + }); + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.getByRole('button', { name: 'Load worker record', exact: true }).click(); + await expect(page.locator('#people-api-display-name')).toHaveText('Authorized fixture worker'); + await page.locator('[data-view-link="job-analysis"]').first().click(); + await page.getByRole('button', { name: 'Load snapshot', exact: true }).click(); + await expect(page.locator('#job-analysis-analysis-id')).toHaveText(jobAnalysisSnapshot.analysis_record_id); + const cleared = await page.evaluate(() => { + document.dispatchEvent(new Event('orgmetra:authority-invalidated')); + return ['people-api', 'job-analysis'].map((prefix) => ({ + hidden: document.getElementById(`${prefix}-result`).hidden, + state: document.getElementById(`${prefix}-status`).dataset.state, + })); + }); + expect(cleared).toEqual([{ hidden: true, state: 'idle' }, { hidden: true, state: 'idle' }]); + for (const id of ['people-api-display-name', 'people-api-employment-status', 'job-analysis-analysis-id', 'job-analysis-state', 'job-analysis-effective', 'job-analysis-recorded']) { + await expect(page.locator(`#${id}`)).toHaveText('unknown'); + } + for (const id of ['job-analysis-task-count', 'job-analysis-ksao-count']) await expect(page.locator(`#${id}`)).toHaveText('0'); + expect(requests).toBe(2); + await page.getByRole('button', { name: 'Load snapshot', exact: true }).click(); + await expect(page.locator('#job-analysis-result')).toBeVisible(); + await page.locator('[data-view-link="employee-profile"]').first().click(); + await page.getByRole('button', { name: 'Load worker record', exact: true }).click(); + await expect(page.locator('#people-api-result')).toBeVisible(); + expect(requests).toBe(4); +}); + +test('personal-details purpose edits clear prior review states without granting access', async ({ page }) => { + await page.goto(workspacePath); + await page.locator('[data-view-link="employee-profile"]').first().click(); + const purpose = page.locator('#access-purpose'); + const review = page.locator('[data-action="view-personal-details"]'); + const details = page.locator('#details-panel'); + const denial = page.locator('#permission-panel'); + await review.click(); + await expect(details).toBeVisible(); + await purpose.selectOption('recruiting'); + await expect(details).toBeHidden(); + await expect(denial).toBeHidden(); + await review.click(); + await expect(denial).toBeVisible(); + await expect(details).toBeHidden(); + await purpose.selectOption('hr_operations'); + await expect(denial).toBeHidden(); + await expect(details).toBeHidden(); + await review.click(); + await expect(details).toBeVisible(); + await expect(denial).toBeHidden(); +}); + +test('keyboard users can bypass repeated workspace navigation', async ({ page }) => { + await page.goto(workspacePath); + + await page.keyboard.press('Tab'); + const skipLink = page.getByRole('link', { name: 'Skip to main content' }); + await expect(skipLink).toBeFocused(); + await expect(skipLink).toBeVisible(); + + await skipLink.press('Enter'); + await expect(page.locator('#main-content')).toBeFocused(); +}); + +test('the bypass control follows the active workspace locale', async ({ page }) => { + await page.goto(workspacePath); + + const skipLink = page.locator('a.skip-link[href="#main-content"]'); + await expect(skipLink).toHaveAccessibleName('Skip to main content'); + await page.locator('#locale-toggle').click(); + await expect(page.locator('html')).toHaveAttribute('lang', 'ko'); + await expect(skipLink.locator('[lang="ko"]')).toBeVisible(); + await expect(skipLink.locator('[lang="en"]')).toBeHidden(); + await expect(skipLink).toHaveAccessibleName('본문으로 건너뛰기'); +}); diff --git a/tests/foundation-contract.test.mjs b/tests/foundation-contract.test.mjs index c2f8f7589..404942318 100644 --- a/tests/foundation-contract.test.mjs +++ b/tests/foundation-contract.test.mjs @@ -99,6 +99,113 @@ test('canonical foundation passes validation', () => { assert.deepEqual(validateFoundation(resolve('.')), []); }); +test('Job Architecture ownership documents its existing snapshot tables', () => { + const architecture = readFileSync(new URL('../ARCHITECTURE.md', import.meta.url), 'utf8'); + const migration = readFileSync(new URL('../database/migrations/0013_job_analysis_snapshot.sql', import.meta.url), 'utf8'); + const rows = architecture.split('\n').filter((line) => line.startsWith('| `job_architecture` |')); + assert.equal(rows.length, 1, 'the owning context must have one representative table row'); + assert.match(rows[0], /job profiles and publication evidence/); + assert.match(rows[0], /`job_architecture_role`/); + for (const table of ['job_analysis_snapshot', 'job_analysis_task_item', 'job_analysis_ksao_item']) { + assert.match(migration, new RegExp(`CREATE TABLE ${table} \\(`), `${table} must exist in the owned migration`); + assert.ok(rows[0].includes(`\`${table}\``), `${table} must appear in its owning architecture row`); + } +}); + +test('workspace documentation separates observed source from historical execution', () => { + const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8'); + const gap = readFileSync(new URL('../docs/product-technical-gap-baseline.md', import.meta.url), 'utf8'); + const status = readme.split('## Status\n')[1]; + assert.ok(status, 'README must retain its complete status section'); + assert.match(status, /Observed default `develop` base `eb9757f8649aaad026a9865508d9aad50c1a7a4f`/); + assert.match(status, /repository source presence only/); + assert.doesNotMatch(status, /Workforce-composition evidence remains active PR #54/); + assert.match(gap, /\*\*Historical snapshot:\*\* 2026-08-21, Asia\/Seoul/); + assert.match(gap, /## Historical open PR inventory and integration order \(2026-08-21\)/); + assert.match(gap, /## Historical local evidence \(2026-08-21\)/); + assert.match(gap, /not evidence of configured branch-protection enforcement/); + assert.match(gap, /No new all-PR inventory is asserted/); + assert.match(gap, /Historical exact-head service and PostgreSQL validation/); + assert.match(gap, /parameter_rmse_mean` 2\.9606/); +}); + +/** Check the two version-pinned bibliography records against observed arXiv metadata. */ +function requireOrchestrationReferenceAuthors(references) { + const expected = [ + { + id: '2606.21228v2', + prefix: 'Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H., Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., & Clanuwat, T. (2026).', + title: '*Sakana Fugu technical report*', + version: 'Version 2, revised June 23, 2026' + }, + { + id: '2512.04695v3', + prefix: 'Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2026).', + title: '*TRINITY: An evolved LLM coordinator*', + version: 'Version 3, revised April 27, 2026; first submitted December 4, 2025' + } + ]; + for (const record of expected) { + const entries = references.split(/\r?\n\s*\r?\n/).filter((entry) => entry.includes(record.id.split('v')[0])); + assert.equal(entries.length, 1, `${record.id}: one bibliography entry is required`); + assert.ok(entries[0].startsWith(record.prefix), `${record.id}: registered authors must replace organization attribution`); + assert.ok(entries[0].includes(record.title), `${record.id}: title must remain associated with its record`); + assert.ok(entries[0].includes(`(arXiv:${record.id})`), `${record.id}: observed version must be pinned`); + assert.ok(entries[0].includes(record.version), `${record.id}: revision year must be explicit`); + assert.ok(entries[0].includes(`https://arxiv.org/abs/${record.id}`), `${record.id}: versioned record must remain locatable`); + } +} + +test('orchestration bibliography preserves registered authors and observed revision versions', () => { + const references = readFileSync(new URL('../docs/doctoring/REFERENCES.md', import.meta.url), 'utf8'); + requireOrchestrationReferenceAuthors(references); + for (const [id, prefix, oldAttribution] of [ + ['2606.21228v2', 'Tang, Y., Cetin, E.', 'Fugu Team, Sakana AI'], + ['2512.04695v3', 'Xu, J., Sun, Q.', 'Sakana AI'] + ]) { + assert.ok(references.includes(prefix)); + const originalEntries = references.split(/\r?\n\s*\r?\n/); + const contradicted = originalEntries.map((entry) => entry.includes(`(arXiv:${id})`) ? entry.replace(prefix, oldAttribution) : entry).join('\n\n'); + const changedEntries = contradicted.split(/\r?\n\s*\r?\n/); + assert.equal(changedEntries.length, originalEntries.length); + const targetIndexes = originalEntries.flatMap((entry, index) => entry.includes(`(arXiv:${id})`) ? [index] : []); + assert.equal(targetIndexes.length, 1, `${id}: mutation target must be unique`); + const changedIndexes = originalEntries.flatMap((entry, index) => entry !== changedEntries[index] ? [index] : []); + assert.deepEqual(changedIndexes, targetIndexes, `${id}: only the identified bibliography record may change`); + assert.throws(() => requireOrchestrationReferenceAuthors(contradicted), { + code: 'ERR_ASSERTION', + message: `${id}: registered authors must replace organization attribution` + }); + } +}); + +test('offer approval ADR keeps its exact decision status separate from branch provenance', () => { + const adr = readFileSync(new URL('../docs/adr/0017-governed-offer-approval.md', import.meta.url), 'utf8'); + const index = readFileSync(new URL('../docs/adr/README.md', import.meta.url), 'utf8'); + const statuses = [...adr.matchAll(/^- Status: (.+)$/gm)].map((match) => match[1]); + assert.deepEqual(statuses, ['Accepted'], 'decision status must not include a branch or protection claim'); + const indexed = index.split('\n').find((line) => line.includes('| [0017]')); + assert.ok(indexed); + assert.equal(indexed.split('|').at(-2).trim(), statuses[0]); + assert.match(adr, /^- Provenance: .*develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f.*configured protection remains a separate gap/m); + assert.doesNotMatch(adr, /protected `develop` truth|^Protected `develop` can govern/m); +}); + +test('Job Analysis traceability includes the persisted snapshot and its contract source', () => { + const traceability = readFileSync(new URL('../docs/TRACEABILITY.md', import.meta.url), 'utf8'); + const migration = readFileSync(new URL('../database/migrations/0013_job_analysis_snapshot.sql', import.meta.url), 'utf8'); + const rows = traceability.split('\n').filter((line) => line.startsWith('| Evidence-grounded Job analysis with Task/FJA/KSAO linkage |')); + assert.equal(rows.length, 1); + for (const table of ['job_analysis_snapshot', 'job_analysis_task_item', 'job_analysis_ksao_item', 'job_analysis_task_ksao_link', 'job_analysis_write_command']) { + assert.match(migration, new RegExp(`CREATE TABLE ${table} \\(`)); + assert.ok(rows[0].includes(`\`${table}\``), `${table} must be traced to its persistence boundary`); + } + for (const path of ['services/job-analysis-api', 'tests/test_job_analysis_snapshot_postgres.sh']) { + assert.ok(rows[0].includes(`\`${path}\``), `${path} must appear in the same evidence row`); + } + assert.ok(rows[0].includes('ADR-0007, ADR-0014')); +}); + test('governance docs name the protected default branch rather than stale main', () => { const agents = readFileSync(new URL('../AGENTS.md', import.meta.url), 'utf8'); assert.doesNotMatch(agents, /protected[- ](?:`)?main(?:`)?/i); @@ -267,6 +374,15 @@ test('local links validate files and ignore anchors, web, and mail links', () => } }); +test('ADR index keeps provenance outside canonical status cells', () => { + const indexText = readFileSync(new URL('../docs/adr/README.md', import.meta.url), 'utf8'); + assert.doesNotMatch( + indexText, + /\|\s*(?:Proposed|Accepted|Superseded|Rejected)\s+on\s+[^|]+\|/, + 'ADR status cells must remain canonical; branch provenance belongs in a separate column' + ); +}); + test('ADR index reports missing files and status mismatch', () => { const root = temporaryDirectory(); try { @@ -346,6 +462,113 @@ test('foundation validator accepts ordinary prose containing placeholder vocabul } }); +test('current traceability bounds maturity to source evidence without attesting protection', () => { + const traceability = readFileSync(new URL('../docs/TRACEABILITY.md', import.meta.url), 'utf8'); + for (const section of ['2. Product traceability matrix', '4. CWL integration traceability']) { + const cells = extractMaturityCells(extractSection(traceability, section)); + assert.ok(cells.length > 0, `${section}: the actual matrix must be inspected`); + assert.equal(cells.some((cell) => cell.startsWith('implemented_on_protected_')), false, + 'a current protected classification requires evidence not captured in this source assessment'); + } + assert.match(traceability, /develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f/); + assert.match(traceability, /not a claim that implementation is new or absent from the default-branch base/); + const keyverseRow = traceability.split('\n').filter((line) => line.startsWith('| Keyverse identity and authorization |')); + assert.equal(keyverseRow.length, 1); + assert.equal(keyverseRow[0].split('|').at(-2).trim(), 'accepted_architecture'); + assert.match(keyverseRow[0], /issuer integration remains unverified here/); +}); + +/** Check only the named provenance paragraph, not arbitrary documentation prose. */ +function requirePeopleReadRepairProvenance(traceability) { + const section = extractSection(traceability, 'Source provenance and maturity limits (2026-10-05)'); + const paragraph = section.split(/\r?\n\s*\r?\n/).find((entry) => entry.includes('The observed default-branch base')); + assert.ok(paragraph, 'traceability must retain its source-provenance paragraph'); + const normalized = paragraph.replace(/\s+/g, ' '); + assert.match(normalized, /develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f/); + assert.match(normalized, /People read\/write business and persistence boundaries/, + 'unchanged business and persistence sources must remain distinct from the read HTTP repair'); + assert.match(normalized, /People read HTTP error boundary and its regression source are changed by this active PR/, + 'the read HTTP repair must be attributed to the active PR, not the unchanged base'); + for (const path of [ + 'services/people-api/src/orgmetra_people_api/http.py', + 'services/people-api/tests/test_http_route.py' + ]) { + assert.ok(normalized.includes(`\`${path}\``), `${path}: the actual changed source must be named`); + } + assert.match(normalized, /protected-branch runtime truth is not established/); + assert.doesNotMatch(normalized, /People read\/write, Job Analysis persistence, criterion-observation scope, validity-case integrity, and purpose-bound PII implementation and regression sources are present at that immutable base and unchanged in this PR/, + 'the earlier blanket unchanged claim must not survive wrapping or an appended correction'); +} + +test('People read provenance distinguishes active-PR HTTP repairs from unchanged base boundaries', () => { + const traceability = readFileSync(new URL('../docs/TRACEABILITY.md', import.meta.url), 'utf8'); + requirePeopleReadRepairProvenance(traceability); +}); + +test('People read provenance rejects the prior blanket claim and missing repair attribution', () => { + const traceability = readFileSync(new URL('../docs/TRACEABILITY.md', import.meta.url), 'utf8'); + requirePeopleReadRepairProvenance(traceability); + requirePeopleReadRepairProvenance(traceability.replace('People read HTTP error boundary', 'People read\nHTTP error boundary')); + const declaration = 'People read HTTP error boundary and its regression source are changed by this active PR'; + assert.equal(traceability.split(declaration).length - 1, 1); + const missingAttribution = traceability.replace(declaration, 'People read HTTP error boundary and its regression source are present at the base'); + assert.throws(() => requirePeopleReadRepairProvenance(missingAttribution), { + code: 'ERR_ASSERTION', + message: /the read HTTP repair must be attributed to the active PR/ + }); + const stale = 'People read/write, Job Analysis persistence, criterion-observation scope, validity-case integrity, and purpose-bound PII implementation and regression sources are present at that immutable base and unchanged in this PR.'; + const contradictory = traceability.replace('The observed default-branch base', stale + ' The observed default-branch base'); + assert.throws(() => requirePeopleReadRepairProvenance(contradictory), { + code: 'ERR_ASSERTION', + message: /the earlier blanket unchanged claim must not survive/ + }); + assert.throws(() => requirePeopleReadRepairProvenance(contradictory.replace('People read/write, Job Analysis', 'People read/write,\nJob Analysis')), { + code: 'ERR_ASSERTION', + message: /the earlier blanket unchanged claim must not survive/ + }); +}); + +test('foundation validator rejects obsolete protected-main maturity in otherwise-valid matrices', () => { + const root = temporaryDirectory(); + try { + makeMinimalValidFoundation(root); + const original = readFileSync(join(root, 'docs/TRACEABILITY.md'), 'utf8'); + const currentValues = [ + 'implemented_on_protected_develop', + 'implemented_on_active_pr', + 'accepted_architecture', + 'planned', + 'research_only', + 'superseded', + 'out_of_scope' + ]; + for (const originalValue of ['accepted_architecture', 'planned']) { + for (const currentValue of currentValues) { + write(root, 'docs/TRACEABILITY.md', original.replace( + `| ${originalValue} |`, `| ${currentValue} |` + )); + assert.deepEqual(validateFoundation(root), [], `${originalValue}: ${currentValue}`); + } + const legacy = original.replace( + `| ${originalValue} |`, '| implemented_on_protected_main |' + ); + const sectionName = originalValue === 'accepted_architecture' + ? '2. Product traceability matrix' : '4. CWL integration traceability'; + assert.deepEqual(extractMaturityCells(extractSection(legacy, sectionName)), [ + 'implemented_on_protected_main' + ], 'the legacy row must reach the canonical maturity consumer'); + write(root, 'docs/TRACEABILITY.md', legacy); + assert.deepEqual(validateFoundation(root), [ + 'docs/TRACEABILITY.md: invalid maturity value: implemented_on_protected_main' + ], `${sectionName}: obsolete maturity must be the only rejection`); + write(root, 'docs/TRACEABILITY.md', original); + assert.deepEqual(validateFoundation(root), [], 'restoring the valid row must restore admission'); + } + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('foundation validator reports missing traceability sections', () => { const root = temporaryDirectory(); try { diff --git a/tests/hr-workspace.test.mjs b/tests/hr-workspace.test.mjs new file mode 100644 index 000000000..c592f1193 --- /dev/null +++ b/tests/hr-workspace.test.mjs @@ -0,0 +1,501 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { runInNewContext } from 'node:vm'; +import { jobAnalysisApiFixture } from './job-analysis-api-fixture.mjs'; +import { + fetchJobAnalysisSnapshot, + fetchPeopleRecord, + isPurposeAuthorized, + jobAnalysisSnapshotUrl, + nextLocale, + peopleRecordUrl, +} from '../apps/hr-workspace/app.js'; + +const html = readFileSync(new URL('../apps/hr-workspace/index.html', import.meta.url), 'utf8'); +const css = readFileSync(new URL('../apps/hr-workspace/styles.css', import.meta.url), 'utf8'); +const app = readFileSync(new URL('../apps/hr-workspace/app.js', import.meta.url), 'utf8'); +const story = readFileSync(new URL('../apps/hr-workspace/workspace.stories.js', import.meta.url), 'utf8'); +const storybookConfig = readFileSync(new URL('../.storybook/main.js', import.meta.url), 'utf8'); +const storybookPreview = readFileSync(new URL('../.storybook/preview.js', import.meta.url), 'utf8'); + +test('workspace exposes the Figma role slice and existing design tokens', () => { + assert.match(html, /packages\/design-tokens\/tokens\.css/); + assert.match(html, /data-node-id="1:10"/); + assert.match(html, /data-node-id="1:28"/); + assert.match(html, /data-view-link="hr-home"/); + assert.match(html, /data-view-link="employee-profile"/); + assert.match(html, /data-view-link="job-analysis"/); + assert.match(html, /id="people-api-form"/); + assert.match(css, /var\(--orgmetra-action-review\)/); + assert.match(css, /var\(--orgmetra-focus-ring\)/); +}); + +test('workspace includes keyboard-accessible review and high-impact states', () => { + assert.match(html, /id="evidence-dialog"/); + assert.match(html, /id="confirmation-dialog"/); + assert.match(html, /role="alert"/); + assert.match(html, /role="status"/); + assert.match(html, /aria-label="Close"/); + assert.match(html, /required rows="3"/); + assert.match(html, /Exact assignment allocation values/); +}); + +test('locale-sensitive icon controls translate their accessible names', () => { + assert.match( + html, + /id="locale-toggle"[^>]*data-i18n-aria-label="changeLanguage"/, + 'language toggle needs a locale-bound accessible name', + ); + assert.equal( + (html.match(/data-i18n-aria-label="close"/g) ?? []).length, + 2, + 'both icon-only dialog close buttons need locale-bound accessible names', + ); + assert.match(app, /querySelectorAll\('\[data-i18n-aria-label\]'\)/); + assert.match(app, /dictionary\[element\.dataset\.i18nAriaLabel\]/); +}); + +test('Storybook exposes tokenized workspace states without claiming API connectivity', () => { + assert.match(storybookConfig, /@storybook\/web-components-vite/); + assert.match(storybookPreview, /design-tokens\/tokens\.css/); + for (const storyName of [ + 'ActionButtons', + 'FieldStates', + 'PermissionDenied', + 'EvidenceDrawer', + 'HighRiskConfirmation', + 'AssignmentSplit', + 'PeopleNotConnected', + 'JobAnalysisNotConnected', + ]) { + assert.match(story, new RegExp(`export const ${storyName}`)); + } + assert.match(story, /orgmetra-action-request-evidence/); + assert.match(story, /Exact assignment allocation values/); + assert.match(story, /aria-invalid="true"/); + assert.match(story, /data-figma-node-id="2:2"/); + assert.match(story, /Connect the host before loading protected data/); + assert.match(story, /No local fallback is used/); +}); + +test('purpose and locale transitions preserve the trust boundary', () => { + assert.equal(isPurposeAuthorized('hr_operations'), true); + assert.equal(isPurposeAuthorized('recruiting'), false); + assert.equal(nextLocale('en'), 'ko'); + assert.equal(nextLocale('ko'), 'en'); + assert.match(app, /no API mutation was sent/); + assert.doesNotMatch(html, /password|passkey_value|private_key/i); +}); + +test('Job Analysis uses a host authorization provider without persisting bearer material', async () => { + const config = { + baseUrl: 'https://job-analysis.example.test/', + tenantRecordId: 'tenant/alpha', + analysisRecordId: 'analysis-1', + purposeCode: 'job_analysis_read', + getAuthorization: async () => 'Bearer host-provided-token', + }; + assert.equal( + jobAnalysisSnapshotUrl(config), + 'https://job-analysis.example.test/v1/tenants/tenant%2Falpha/job-analysis-snapshots/analysis-1', + ); + let request; + const snapshot = await fetchJobAnalysisSnapshot(config, async (url, options) => { + request = { url, options }; + return { ok: true, status: 200, json: async () => ({ analysis_record_id: 'analysis-1' }) }; + }); + assert.deepEqual(snapshot, { analysis_record_id: 'analysis-1' }); + assert.equal(request.url, jobAnalysisSnapshotUrl(config)); + assert.equal(request.options.credentials, 'omit'); + assert.equal(request.options.headers.Authorization, 'Bearer host-provided-token'); + assert.equal(request.options.headers['X-Purpose-Code'], 'job_analysis_read'); + assert.doesNotMatch(html, /localStorage|sessionStorage|authorization.*input/i); +}); + +test('Job Analysis renders the authoritative API document without a whole-snapshot digest', async () => { + const snapshot = jobAnalysisApiFixture(); + assert.equal(Object.hasOwn(snapshot, 'content_digest_sha256'), false); + assert.equal(snapshot.tasks.length, 3); + assert.equal(snapshot.ksao_requirements.length, 3); + assert.match(snapshot.tasks[0].source.content_digest_sha256, /^[a-f0-9]{64}$/); + + const payload = await fetchJobAnalysisSnapshot({ + baseUrl: 'https://job-analysis.example.test', + tenantRecordId: snapshot.tenant_record_id, + analysisRecordId: snapshot.analysis_record_id, + getAuthorization: () => 'Bearer fixture-host-token', + }, async () => ({ ok: true, status: 200, json: async () => snapshot })); + const nodes = Object.fromEntries([ + 'result', 'analysis-id', 'state', 'effective', 'recorded', 'task-count', 'ksao-count', + ].map((suffix) => [`job-analysis-${suffix}`, { textContent: '', hidden: true }])); + const context = { + document: { + getElementById(id) { + assert.ok(Object.hasOwn(nodes, id), `unsupported Job Analysis field: ${id}`); + return nodes[id]; + }, + }, + snapshot: payload, + }; + for (const name of ['clearJobAnalysisSnapshot', 'renderJobAnalysisSnapshot']) { + const source = app.match(new RegExp(`^function ${name}\\([^]*?^\\}`, 'm')); + assert.ok(source, `retained ${name} function must be exercised`); + runInNewContext(`${source[0]}\n${name}(${name.startsWith('render') ? 'snapshot' : ''});`, context); + } + for (const [suffix, expected] of Object.entries({ + 'analysis-id': snapshot.analysis_record_id, + state: snapshot.status_code, + effective: snapshot.effective_from, + recorded: snapshot.recorded_at, + 'task-count': '3', + 'ksao-count': '3', + })) { + assert.match(html, new RegExp(`id="job-analysis-${suffix}"`)); + assert.equal(nodes[`job-analysis-${suffix}`].textContent, expected); + } + assert.equal(nodes['job-analysis-result'].hidden, false); + assert.doesNotMatch(html, /jobAnalysisDigest|job-analysis-digest/); + assert.doesNotMatch(app, /jobAnalysisDigest|job-analysis-digest|content_digest_sha256/); +}); + +test('browser Job Analysis mock uses the same authoritative document and retains the canonical timeout', () => { + const browser = readFileSync(new URL('./e2e/hr-workspace.spec.mjs', import.meta.url), 'utf8'); + const config = readFileSync(new URL('../playwright.config.mjs', import.meta.url), 'utf8'); + assert.match(browser, /import \{ jobAnalysisApiFixture \} from '\.\.\/job-analysis-api-fixture\.mjs'/); + assert.match(browser, /const jobAnalysisSnapshot = jobAnalysisApiFixture\(\)/); + assert.match(browser, /body: JSON\.stringify\(jobAnalysisSnapshot\)/); + assert.doesNotMatch(browser, /content_digest_sha256:|task_id:|ksao_id:/); + assert.match(config, /timeout: 15_000/); +}); + +test('gap baseline traceability names its active-PR maturity and governing ADR', () => { + const traceability = readFileSync(new URL('../docs/TRACEABILITY.md', import.meta.url), 'utf8'); + const adr = readFileSync(new URL('../docs/adr/0026-product-technical-gap-baseline.md', import.meta.url), 'utf8'); + const row = traceability.split('\n').filter((line) => line.startsWith('| Product and technical gap baseline |')); + assert.equal(row.length, 1, 'the gap baseline has one source-contract row'); + const cells = row[0].split('|').map((cell) => cell.trim()); + assert.match(adr, /^# ADR 0026: Product and technical gap baseline/m); + assert.match(adr, /`docs\/product-technical-gap-baseline\.md` is the current evidence ledger/); + assert.equal(cells[6], 'implemented_on_active_pr', 'local candidate documentation is not protected truth'); + assert.equal(cells[5], 'ADR-0026', 'the baseline is not governed by the workspace ADR'); +}); + +test('People API uses host authorization and an explicit no-storage read boundary', async () => { + const config = { + baseUrl: 'https://people.example.test/', + tenantRecordId: 'tenant/alpha', + personRecordId: 'person-1', + effectiveOn: '2026-08-21', + purposeCode: 'people_read', + requestedFields: ['display_name', 'employment_status_code'], + getAuthorization: async () => 'Bearer host-provided-token', + }; + assert.equal( + peopleRecordUrl(config), + 'https://people.example.test/v1/tenants/tenant%2Falpha/people/person-1?effective_on=2026-08-21&purpose=people_read&fields=display_name%2Cemployment_status_code', + ); + let request; + const record = await fetchPeopleRecord(config, async (url, options) => { + request = { url, options }; + return { ok: true, status: 200, json: async () => ({ fields: { display_name: 'Authorized worker', employment_status_code: 'active' } }) }; + }); + assert.deepEqual(record.fields, { display_name: 'Authorized worker', employment_status_code: 'active' }); + assert.equal(request.url, peopleRecordUrl(config)); + assert.equal(request.options.credentials, 'omit'); + assert.equal(request.options.headers.Authorization, 'Bearer host-provided-token'); + assert.doesNotMatch(html, /localStorage|sessionStorage|authorization.*input/i); +}); + +/** Execute the unchanged workspace handlers with an explicitly held synthetic transport. */ +function protectedReadWorkspace() { + const nodes = new Map(); + const node = (id) => { + if (!nodes.has(id)) nodes.set(id, { + value: '', hidden: true, textContent: '', dataset: {}, handlers: {}, + addEventListener(type, callback) { this.handlers[type] = callback; }, + setAttribute() {}, focus() {}, + }); + return nodes.get(id); + }; + const pending = []; + const context = { + URLSearchParams, + document: { + documentElement: { dataset: {}, lang: '' }, + getElementById: node, querySelector: node, querySelectorAll: () => [], + addEventListener(type, callback) { node('document').addEventListener(type, callback); }, + }, + __ORGMETRA_PEOPLE__: { + baseUrl: 'https://people.example.test', tenantRecordId: 'tenant-fixture', + personRecordId: 'person-fixture', effectiveOn: '2026-10-04', + purposeCode: 'people_read', requestedFields: ['display_name'], + getAuthorization: () => 'Bearer fixture-only', + }, + __ORGMETRA_JOB_ANALYSIS__: { + baseUrl: 'https://job.example.test', tenantRecordId: 'tenant-fixture', + analysisRecordId: 'analysis-fixture', purposeCode: 'job_analysis_read', + getAuthorization: () => 'Bearer fixture-only', + }, + fetch: () => new Promise((resolve) => pending.push(resolve)), + }; + runInNewContext(app.replace(/^export /gm, ''), context); + return { node, pending, context }; +} + +test('personal-details purpose edits clear the previous access result until an explicit review', () => { + for (const eventType of ['input', 'change']) { + const { node } = protectedReadWorkspace(); + const purpose = node('access-purpose'); + const review = node('[data-action="view-personal-details"]'); + purpose.value = 'hr_operations'; + review.handlers.click(); + assert.equal(node('details-panel').hidden, false, 'the permitted fixture review must show its result'); + purpose.value = 'recruiting'; + purpose.handlers[eventType]?.({ target: purpose }); + assert.equal(node('details-panel').hidden, true, `${eventType}: the previous purpose result must not remain visible`); + assert.equal(node('permission-panel').hidden, true, 'editing is not a new denied review'); + review.handlers.click(); + assert.equal(node('permission-panel').hidden, false, 'an explicit denied review must still explain the refusal'); + purpose.value = 'hr_operations'; + purpose.handlers[eventType]?.({ target: purpose }); + assert.equal(node('permission-panel').hidden, true, 'the previous denial must also be invalidated'); + assert.equal(node('details-panel').hidden, true, 'returning to a permitted purpose does not perform a review'); + review.handlers.click(); + assert.equal(node('details-panel').hidden, false, 'a fresh explicit permitted review must recover'); + } +}); + +test('host authority invalidation synchronously clears both loaded read surfaces', async () => { + const { node, pending } = protectedReadWorkspace(); + const people = node('people-api-form').handlers.submit({ preventDefault() {} }); + const job = node('job-analysis-form').handlers.submit({ preventDefault() {} }); + await Promise.resolve(); + assert.equal(pending.length, 2); + pending[0]({ status: 200, ok: true, json: async () => ({ fields: { display_name: 'Previous authority', employment_status_code: 'active' } }) }); + pending[1]({ status: 200, ok: true, json: async () => ({ analysis_record_id: 'previous-analysis', status_code: 'approved', effective_from: '2026-10-04', recorded_at: 'previous-cutoff', tasks: [1], ksao_requirements: [1] }) }); + await Promise.all([people, job]); + assert.equal(node('people-api-result').hidden, false); + assert.equal(node('job-analysis-result').hidden, false); + node('document').handlers['orgmetra:authority-invalidated']?.(); + for (const prefix of ['people-api', 'job-analysis']) { + assert.equal(node(`${prefix}-result`).hidden, true, `${prefix}: the old authority's result must be hidden synchronously`); + assert.equal(node(`${prefix}-status`).dataset.state, 'idle'); + } + for (const id of ['people-api-display-name', 'people-api-employment-status', 'job-analysis-analysis-id', 'job-analysis-state', 'job-analysis-effective', 'job-analysis-recorded']) { + assert.equal(node(id).textContent, 'unknown', `${id}: old values must be erased, not only hidden`); + } + for (const id of ['job-analysis-task-count', 'job-analysis-ksao-count']) assert.equal(node(id).textContent, '0'); + assert.equal(pending.length, 2, 'invalidation must not dispatch another request'); +}); + +const protectedReadCases = [ + { + kind: 'People', form: 'people-api-form', result: 'people-api-result', status: 'people-api-status', + coordinates: ['people-api-tenant', 'people-api-person', 'people-api-effective', 'people-api-purpose', 'people-api-fields'], + payload: { fields: { display_name: 'Original worker', employment_status_code: 'active' } }, + clearedField: 'people-api-display-name', + }, + { + kind: 'Job Analysis', form: 'job-analysis-form', result: 'job-analysis-result', status: 'job-analysis-status', + coordinates: ['job-analysis-tenant', 'job-analysis-record', 'job-analysis-purpose'], + payload: { analysis_record_id: 'analysis-fixture', status_code: 'approved', tasks: [], ksao_requirements: [] }, + clearedField: 'job-analysis-analysis-id', + }, +]; + +for (const fixture of protectedReadCases) { + for (const transition of [ + 'authority', 'repeated-authority', 'authority-away-back', 'newer-submit', + ...fixture.coordinates.flatMap((coordinate) => ['input', 'change'].map((eventType) => `${coordinate}/${eventType}`)), + ]) { + for (const outcome of ['credential', 'provider-rejection', 'no-credential']) { + test(`${fixture.kind} credential wait: ${transition}/${outcome} cannot dispatch or overwrite a newer read`, async () => { + const { node, context } = protectedReadWorkspace(); + const host = fixture.kind === 'People' ? context.__ORGMETRA_PEOPLE__ : context.__ORGMETRA_JOB_ANALYSIS__; + let releaseCredential; + let rejectCredential; + const heldCredential = new Promise((resolve, reject) => { + releaseCredential = resolve; + rejectCredential = reject; + }); + const providerCoordinates = []; + host.getAuthorization = function () { + providerCoordinates.push({ + tenant: this.tenantRecordId, + record: this.personRecordId || this.analysisRecordId, + }); + return providerCoordinates.length === 1 ? heldCredential : 'Bearer current-fixture-only'; + }; + const freshPayload = fixture.kind === 'People' + ? { fields: { display_name: 'Current credential worker', employment_status_code: 'active' } } + : { ...fixture.payload, analysis_record_id: 'current-credential-analysis', tasks: [1], ksao_requirements: [1] }; + const sent = []; + context.fetch = async (url, options) => { + assert.equal(options.credentials, 'omit'); + if (fixture.kind === 'Job Analysis') assert.equal(options.headers['X-Purpose-Code'], host.purposeCode); + // Synthetic headers are inspected here, never copied into retained observations. + assert.ok(['Bearer current-fixture-only', 'Bearer obsolete-fixture-only'].includes(options.headers.Authorization)); + sent.push(url); + return { status: 200, ok: true, json: async () => freshPayload }; + }; + const form = node(fixture.form); + const submit = () => form.handlers.submit({ preventDefault() {} }); + const previous = submit(); + assert.equal(providerCoordinates.length, 1, 'the real handler must enter the held provider'); + assert.equal(sent.length, 0, 'credential acquisition is not transport admission'); + const invalidate = node('document').handlers['orgmetra:authority-invalidated']; + if (transition === 'authority' || transition === 'repeated-authority') { + invalidate(); + if (transition === 'repeated-authority') invalidate(); + } else if (transition === 'authority-away-back') { + invalidate(); + host.tenantRecordId = 'away-tenant'; + invalidate(); + host.tenantRecordId = 'tenant-fixture'; + } else if (transition !== 'newer-submit') { + const [coordinate, eventType] = transition.split('/'); + const input = node(coordinate); + const original = input.value; + input.value = 'changed-coordinate'; + form.handlers[eventType]({ target: input }); + input.value = original; + form.handlers[eventType]({ target: input }); + } + if (transition !== 'newer-submit') { + assert.equal(node(fixture.result).hidden, true); + assert.equal(node(fixture.clearedField).textContent, 'unknown'); + assert.equal(node(fixture.status).dataset.state, 'idle'); + } + assert.equal(sent.length, 0, 'invalidation and coordinate reversion cannot perform a read'); + await submit(); + assert.equal(sent.length, 1, 'the new explicit current read must dispatch exactly once'); + const expectedUrl = fixture.kind === 'People' ? peopleRecordUrl(host) : jobAnalysisSnapshotUrl(host); + assert.equal(sent[0], expectedUrl, 'the original provider and request coordinates must be retained'); + assert.deepEqual(providerCoordinates, [ + { tenant: 'tenant-fixture', record: fixture.kind === 'People' ? 'person-fixture' : 'analysis-fixture' }, + { tenant: 'tenant-fixture', record: fixture.kind === 'People' ? 'person-fixture' : 'analysis-fixture' }, + ]); + const currentValue = node(fixture.clearedField).textContent; + const currentStatus = node(fixture.status).textContent; + assert.equal(currentValue, fixture.kind === 'People' ? 'Current credential worker' : 'current-credential-analysis'); + assert.equal(node(fixture.result).hidden, false, 'the nonempty current authorized result must render'); + if (fixture.kind === 'Job Analysis') assert.equal(node('job-analysis-task-count').textContent, '1'); + if (outcome === 'provider-rejection') rejectCredential(new Error('synthetic provider failure')); + else releaseCredential(outcome === 'no-credential' ? '' : 'Bearer obsolete-fixture-only'); + await previous; + assert.equal(sent.length, 1, 'obsolete credential-waiting submission must send zero requests'); + assert.equal(node(fixture.result).hidden, false, 'obsolete settlement must not clear the newest read'); + assert.equal(node(fixture.clearedField).textContent, currentValue); + assert.equal(node(fixture.status).textContent, currentStatus); + assert.equal(node(fixture.status).dataset.state, 'loaded'); + }); + } + } + + test(`${fixture.kind} authority invalidation fences old response outcomes and preserves fresh reads`, async () => { + for (const outcome of ['success', 'body', 'denial', 'body-error']) { + const { node, pending, context } = protectedReadWorkspace(); + let authority = 'previous'; + const observedAuthorities = []; + const config = fixture.kind === 'People' ? context.__ORGMETRA_PEOPLE__ : context.__ORGMETRA_JOB_ANALYSIS__; + config.getAuthorization = () => { + observedAuthorities.push(authority); + return 'Bearer fixture-only'; + }; + const submit = () => node(fixture.form).handlers.submit({ preventDefault() {} }); + const previous = submit(); + await Promise.resolve(); + assert.equal(pending.length, 1); + let releaseBody; + if (outcome === 'body') { + let markBody; + const entered = new Promise((resolve) => { markBody = resolve; }); + const body = new Promise((resolve) => { releaseBody = resolve; }); + pending[0]({ status: 200, ok: true, json: () => { markBody(); return body; } }); + await entered; + } + const invalidate = node('document').handlers['orgmetra:authority-invalidated']; + assert.equal(typeof invalidate, 'function', 'the documented host event must be wired'); + invalidate(); + invalidate(); + assert.equal(node(fixture.result).hidden, true); + assert.equal(node(fixture.clearedField).textContent, 'unknown'); + assert.equal(node(fixture.status).dataset.state, 'idle'); + assert.equal(pending.length, 1, 'repeated invalidation cannot perform a read'); + authority = 'current'; + const fresh = submit(); + await Promise.resolve(); + const freshPayload = fixture.kind === 'People' + ? { fields: { display_name: 'Current authority worker', employment_status_code: 'active' } } + : { ...fixture.payload, analysis_record_id: 'current-authority-analysis' }; + pending[1]({ status: 200, ok: true, json: async () => freshPayload }); + await fresh; + assert.deepEqual(observedAuthorities, ['previous', 'current']); + assert.equal(node(fixture.result).hidden, false, 'a new explicit authorized read must remain usable'); + const currentValue = node(fixture.clearedField).textContent; + assert.equal(currentValue, fixture.kind === 'People' ? 'Current authority worker' : 'current-authority-analysis'); + if (outcome === 'body') releaseBody(fixture.payload); + else if (outcome === 'denial') pending[0]({ status: 403, ok: false }); + else pending[0]({ status: 200, ok: true, json: async () => { + if (outcome === 'body-error') throw new Error('fixture body failure'); + return fixture.payload; + } }); + await previous; + assert.equal(node(fixture.result).hidden, false, `${outcome}: an old callback cannot hide the current read`); + assert.equal(node(fixture.clearedField).textContent, currentValue); + assert.equal(node(fixture.status).dataset.state, 'loaded'); + const denied = submit(); + await Promise.resolve(); + pending[2]({ status: 403, ok: false }); + await denied; + assert.equal(node(fixture.result).hidden, true, 'current authority denial remains authoritative'); + assert.equal(node(fixture.status).dataset.state, 'error'); + } + }); + + test(`${fixture.kind} coordinate edits invalidate pending responses even after reverting`, async () => { + for (const coordinate of fixture.coordinates) { + for (const eventType of ['input', 'change']) { + const { node, pending } = protectedReadWorkspace(); + const form = node(fixture.form); + const submitted = form.handlers.submit({ preventDefault() {} }); + await Promise.resolve(); + assert.equal(pending.length, 1, 'the actual fetch boundary must be reached'); + const input = node(coordinate); + const original = input.value; + input.value = 'changed-coordinate'; + form.handlers[eventType]?.({ target: input }); + input.value = original; + form.handlers[eventType]?.({ target: input }); + pending[0]({ status: 200, ok: true, json: async () => fixture.payload }); + await submitted; + assert.equal(node(fixture.result).hidden, true, `${coordinate}/${eventType}: stale data must remain hidden`); + assert.equal(node(fixture.clearedField).textContent, 'unknown'); + assert.equal(node(fixture.status).dataset.state, 'idle'); + } + } + }); + + test(`${fixture.kind} coordinate edits clear loaded values and allow a fresh explicit read`, async () => { + const { node, pending } = protectedReadWorkspace(); + const form = node(fixture.form); + const submit = () => form.handlers.submit({ preventDefault() {} }); + const first = submit(); + await Promise.resolve(); + pending[0]({ status: 200, ok: true, json: async () => fixture.payload }); + await first; + assert.equal(node(fixture.result).hidden, false, 'an unchanged authorized read must still render'); + node(fixture.coordinates[0]).value = 'changed-tenant'; + form.handlers.input?.({ target: node(fixture.coordinates[0]) }); + assert.equal(node(fixture.result).hidden, true, 'an edited form cannot retain the previous resource'); + assert.equal(node(fixture.clearedField).textContent, 'unknown'); + const fresh = submit(); + await Promise.resolve(); + assert.equal(pending.length, 2); + pending[1]({ status: 200, ok: true, json: async () => fixture.payload }); + await fresh; + assert.equal(node(fixture.result).hidden, false); + assert.equal(node(fixture.status).dataset.state, 'loaded'); + }); +} diff --git a/tests/job-analysis-api-fixture.mjs b/tests/job-analysis-api-fixture.mjs new file mode 100644 index 000000000..c4b3ebe59 --- /dev/null +++ b/tests/job-analysis-api-fixture.mjs @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +/** Return the real API read document from the retained kernel and service fixture. */ +export function jobAnalysisApiFixture() { + const result = spawnSync('python3', ['-B', '-c', ` +import json +import runpy +import sys +from pathlib import Path + +root = Path.cwd() +sys.path[:0] = [str(root / path) for path in ( + 'packages/hris-kernel/src', + 'packages/keyverse-adapter/src', + 'services/job-analysis-api/src', +)] +fixture = runpy.run_path(str(root / 'services/job-analysis-api/tests/fixtures.py')) +snapshot = fixture['clinical_psychologist_snapshot']() + +class FixtureReadPort: + def read_snapshot(self, *, tenant_record_id, analysis_record_id): + assert tenant_record_id == snapshot.tenant_record_id + assert analysis_record_id == snapshot.analysis_record_id + return snapshot + +from orgmetra_job_analysis_api import read_job_analysis_snapshot +view = read_job_analysis_snapshot( + principal=fixture['read_principal'](), + tenant_record_id=snapshot.tenant_record_id, + analysis_record_id=snapshot.analysis_record_id, + purpose_code='job_analysis_read', + policy=fixture['read_policy'](), + read_port=FixtureReadPort(), +) +assert view.snapshot == snapshot.to_snapshot() +print(json.dumps(view.snapshot, ensure_ascii=False)) +`], { + cwd: fileURLToPath(new URL('../', import.meta.url)), + encoding: 'utf8', + timeout: 15_000, + env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' }, + }); + assert.equal(result.error, undefined, 'the authoritative fixture must execute'); + assert.equal(result.status, 0, result.stderr); + return JSON.parse(result.stdout); +} diff --git a/tests/recovery-rehearsal.test.mjs b/tests/recovery-rehearsal.test.mjs index 981415a04..2a2f9fdd1 100644 --- a/tests/recovery-rehearsal.test.mjs +++ b/tests/recovery-rehearsal.test.mjs @@ -41,6 +41,156 @@ function requirePattern(text, pattern, message) { assert.match(text, pattern, message); } +// Parse only the workflow's existing block-mapping grammar; reject absent or +// duplicate keys instead of matching unrelated text elsewhere in the document. +function workflowBlock(text, header, indent) { + const lines = text.split(/\r?\n/); + const starts = lines.flatMap((line, index) => line === `${' '.repeat(indent)}${header}` ? [index] : []); + assert.equal(starts.length, 1, `require one ${header} block`); + const start = starts[0] + 1; + let end = start; + while (end < lines.length && (lines[end].trim() === '' || lines[end].search(/\S/) > indent)) end += 1; + return lines.slice(start, end).join('\n'); +} + +function requireRandomServicePorts(workflow) { + const services = workflowBlock(workflow, 'services:', 4); + for (const service of ['source_postgres', 'restore_postgres']) { + const body = workflowBlock(services, `${service}:`, 6); + const ports = workflowBlock(body, 'ports:', 8); + assert.equal(ports.trim(), '- 5432', `${service} must expose only container port 5432 with a random host port`); + } +} + +function recordRehearsalEnvironment(workflow, services, consume) { + const body = workflowBlock(workflow, '- name: Exercise real cross-cluster dump and restore', 6); + const envBlock = workflowBlock(body, 'env:', 8); + const environment = {}; + for (const line of envBlock.split('\n')) { + const match = /^ {10}([A-Z_]+): (.+)$/.exec(line); + assert.ok(match, 'require the existing literal rehearsal env mapping'); + assert.equal(Object.hasOwn(environment, match[1]), false, `duplicate ${match[1]}`); + const value = match[2].replace(/\$\{\{\s*job\.services\.(source_postgres|restore_postgres)\.(ports\[5432\]|id)\s*\}\}/g, + (_, service, field) => { + const resolved = field === 'id' ? services[service]?.id : services[service]?.ports?.[5432]; + assert.notEqual(resolved, undefined, `missing ${service}.${field}`); + return String(resolved); + }); + assert.doesNotMatch(value, /\$\{\{/, 'unsupported service expression'); + environment[match[1]] = value.replace(/^"(.*)"$/, '$1'); + } + const run = /^ {8}run: (.+)$/m.exec(body); + assert.ok(run, 'require the actual rehearsal command'); + // This callback records the real step mapping; it never executes Bash, Docker + // or PostgreSQL and does not model the GitHub runner's expression engine. + consume(environment, run[1]); +} + +function requireRehearsalPortBinding(workflow) { + const services = { + source_postgres: { id: 'synthetic-source', ports: { 5432: 49171 } }, + restore_postgres: { id: 'synthetic-restore', ports: { 5432: 49283 } } + }; + const records = []; + recordRehearsalEnvironment(workflow, services, (environment, command) => records.push({ environment, command })); + assert.equal(records.length, 1); + assert.deepEqual(records[0], { + environment: { + RECOVERY_REHEARSAL_ALLOW_ROLE_DROP: '1', + POSTGRES_SOURCE_ADMIN_URL: 'postgresql://orgmetra:orgmetra@localhost:49171/postgres', + POSTGRES_RESTORE_ADMIN_URL: 'postgresql://orgmetra:orgmetra@localhost:49283/postgres', + POSTGRES_SOURCE_CONTAINER: 'synthetic-source', + POSTGRES_RESTORE_CONTAINER: 'synthetic-restore' + }, + command: 'bash .github/scripts/restore-rehearsal-postgres.sh' + }); +} + +test('recovery services expose container 5432 with random host ports', () => { + requireRandomServicePorts(readFileSync(workflowPath, 'utf8')); +}); + +test('recovery step consumes each distinct allocated service port', () => { + requireRehearsalPortBinding(readFileSync(workflowPath, 'utf8')); +}); + +test('recovery port contract rejects either fixed host mapping', () => { + const workflow = readFileSync(workflowPath, 'utf8'); + requireRandomServicePorts(workflow); + for (const [service, mapping] of [['source_postgres', '5432:5432'], ['restore_postgres', '5433:5432']]) { + const body = workflowBlock(workflowBlock(workflow, 'services:', 4), `${service}:`, 6); + const mutant = workflow.replace(body, body.replace('- 5432', `- ${mapping}`)); + assert.notEqual(mutant, workflow); + assert.throws(() => requireRandomServicePorts(mutant), { code: 'ERR_ASSERTION' }); + } +}); + +test('recovery port binding rejects swapped, shared, wrong and absent references', () => { + const workflow = readFileSync(workflowPath, 'utf8'); + requireRehearsalPortBinding(workflow); + const source = '${{ job.services.source_postgres.ports[5432] }}'; + const restore = '${{ job.services.restore_postgres.ports[5432] }}'; + const mutants = [ + workflow.replace(source, '__source_slot__').replace(restore, source).replace('__source_slot__', restore), + workflow.replace(restore, source), + workflow.replace(source, restore), + workflow.replace(source, '${{ job.services.source_postgres.ports[5433] }}'), + workflow.replace(restore, '${{ job.services.restore_postgres.ports[5433] }}'), + workflow.replace(source, ''), + workflow.replace(restore, ''), + workflow.replace(/^ {10}POSTGRES_SOURCE_ADMIN_URL:.*\n/m, ''), + workflow.replace(/^ {10}POSTGRES_RESTORE_ADMIN_URL:.*\n/m, '') + ]; + for (const mutant of mutants) { + assert.notEqual(mutant, workflow); + assert.throws(() => requireRehearsalPortBinding(mutant), { code: 'ERR_ASSERTION' }); + } +}); + +test('recovery environment refuses missing allocated ports before consumer entry', () => { + const workflow = readFileSync(workflowPath, 'utf8'); + const services = { + source_postgres: { id: 'synthetic-source', ports: { 5432: 49171 } }, + restore_postgres: { id: 'synthetic-restore', ports: { 5432: 49283 } } + }; + let calls = 0; + recordRehearsalEnvironment(workflow, services, () => { calls += 1; }); + assert.equal(calls, 1); + for (const service of ['source_postgres', 'restore_postgres']) { + const missing = structuredClone(services); + delete missing[service].ports[5432]; + calls = 0; + assert.throws(() => recordRehearsalEnvironment(workflow, missing, () => { calls += 1; }), + { code: 'ERR_ASSERTION', message: `missing ${service}.ports[5432]` }); + assert.equal(calls, 0); + } +}); + +/** Check source provenance without turning backup requirements into operational proof. */ +function requireRecoverySourceProvenance(text) { + const scope = /## Scope and truth status\r?\n([\s\S]*?)(?=\r?\n## |$)/.exec(text); + assert.ok(scope, 'the recovery scope section must exist'); + assert.doesNotMatch(scope[1], /protected-main|Protected-main truth|develop` currently requires/, + 'recovery scope must not assert current protected provenance'); + assert.match(scope[1], /develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f/); + assert.match(scope[1], /normative requirements/); + assert.match(scope[1], /Configured protection, hosted execution, and deployed recovery controls remain unverified/); + assert.match(scope[1], /observed-base migration sources/); +} + +test('restore scope separates immutable source requirements from current protection', () => { + const text = readFileSync(traceabilityPath, 'utf8'); + requireRecoverySourceProvenance(text); + for (const phrase of ['Protected-main truth: ', 'the protected-main migration sequence']) { + const marker = '## Scope and truth status\n'; + assert.ok(text.includes(marker)); + const contradicted = text.replace(marker, `${marker}\n${phrase}\n`); + assert.throws(() => requireRecoverySourceProvenance(contradicted), { + code: 'ERR_ASSERTION', message: 'recovery scope must not assert current protected provenance' + }); + } +}); + test('restore rehearsal is executable exact-head recovery evidence', () => { for (const requiredPath of [workflowPath, rehearsalPath, traceabilityPath]) { assert.equal(existsSync(requiredPath), true, `${requiredPath} must exist`); @@ -98,7 +248,7 @@ test('restore rehearsal is executable exact-head recovery evidence', () => { requirePattern(rehearsal, pattern, message); } - assert.ok(traceability.includes('Protected-main truth'), 'traceability must distinguish protected-main truth'); + requireRecoverySourceProvenance(traceability); assert.ok(traceability.includes('exact restored database'), 'traceability must bind evidence to the restored database'); assert.ok(traceability.includes('No certification claim'), 'traceability must avoid unsupported certification claims'); verifyRecoveryProvenance(); diff --git a/tests/test_github_actions_runner_image.py b/tests/test_github_actions_runner_image.py index 6007c7724..799238b24 100644 --- a/tests/test_github_actions_runner_image.py +++ b/tests/test_github_actions_runner_image.py @@ -1,8 +1,10 @@ -"""Regression contract for deterministic GitHub-hosted runner image selection. +"""Regression contract for proposed isolated self-hosted CI routing. -Orgmetra uses an explicit supported Ubuntu image instead of moving aliases, -other image versions, or expression-driven selectors. Queued evidence remains -non-passing; this test only protects the repository-owned runner contract. +The canonical group and labels come from central .github PR #2565, not a +released or registered pool. This is source-only evidence: capacity, QSR, +linux-cluster-ops #326 isolation/cleanup attestation and canary remain pending. +Managed GitHub Code Quality routing is separately owned; no local CodeQL copy +is introduced. Queued evidence remains non-passing. """ from __future__ import annotations @@ -15,7 +17,10 @@ ROOT = Path(__file__).resolve().parents[1] WORKFLOWS = ROOT / ".github" / "workflows" _RUNS_ON_PATTERN = re.compile(r"^\s*runs-on\s*:\s*(.*?)\s*$") -_EXPECTED_RUNNER = "ubuntu-24.04" +_EXPECTED_RUNNER = ( + "group: CWL CI isolated\n" + "labels: [self-hosted, linux, x64, cwlab-ci-isolated]" +) _CENTRAL_WORKFLOW_NAMES = { "close-empty-pr.yml", "codeql-pr.yml", @@ -65,21 +70,41 @@ def _strip_yaml_comment(value: str) -> str: def _runner_declarations(workflow: str) -> list[tuple[int, str]]: - """Return line-numbered scalar ``runs-on`` declarations without YAML comments.""" + """Read scalars or canonical two-space block selectors, never evaluating YAML. + + This deliberately narrow source grammar fails closed on alternate mapping + forms, duplicate keys, extra fields and noncanonical child indentation. + Workflow syntax is independently checked by actionlint. + """ declarations: list[tuple[int, str]] = [] - for line_number, line in enumerate(workflow.splitlines(), start=1): + lines = workflow.splitlines() + for index, line in enumerate(lines): match = _RUNS_ON_PATTERN.match(line) if match is None: continue value = _strip_yaml_comment(match.group(1)) - if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + if not value: + indent = len(line) - len(line.lstrip(" ")) + children: list[str] = [] + for child in lines[index + 1 :]: + content = _strip_yaml_comment(child) + if not content: + continue + child_indent = len(child) - len(child.lstrip(" ")) + if child_indent <= indent: + break + if child_indent != indent + 2: + content = "noncanonical indentation: " + content + children.append(content) + value = "\n".join(children) + elif len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: value = value[1:-1] - declarations.append((line_number, value)) + declarations.append((index + 1, value)) return declarations class GitHubActionsRunnerImageContractTest(unittest.TestCase): - """Keep every repository-owned runner declaration on one explicit image.""" + """Keep every repository-owned runner on the proposed isolated group selector.""" def test_all_repository_workflow_runner_selectors_are_exact(self) -> None: """Reject aliases, expressions, other versions, and missing runner declarations.""" @@ -105,31 +130,34 @@ def test_all_repository_workflow_runner_selectors_are_exact(self) -> None: ) def test_runner_parser_rejects_dynamic_and_noncanonical_values(self) -> None: - """Keep the validator sensitive to aliases, expressions, lists, and other images.""" - sample = "\n".join( - ( - "runs-on: ubuntu-latest", - "runs-on: ${{ matrix.runner }}", - "runs-on: ubuntu-22.04", - "runs-on: [self-hosted, linux]", - "runs-on: 'ubuntu-24.04'", - ) - ) - declarations = _runner_declarations(sample) - self.assertEqual( - [ - "ubuntu-latest", - "${{ matrix.runner }}", - "ubuntu-22.04", - "[self-hosted, linux]", - "ubuntu-24.04", - ], - [value for _, value in declarations], - ) - self.assertEqual( - 1, - sum(value == _EXPECTED_RUNNER for _, value in declarations), + """Admit only a literal dedicated group plus the complete canonical labels.""" + canonical = "runs-on:\n " + _EXPECTED_RUNNER.replace("\n", "\n ") + self.assertEqual([(1, _EXPECTED_RUNNER)], _runner_declarations(canonical)) + rejected = ( + "runs-on: ubuntu-24.04", + "runs-on: ubuntu-latest", + "runs-on: ${{ matrix.runner }}", + "runs-on: [self-hosted, linux, x64, cwlab-ci-isolated]", + "runs-on:\n labels: [self-hosted, linux, x64, cwlab-ci-isolated]", + canonical.replace("CWL CI isolated", "CWL central control"), + canonical.replace("CWL CI isolated", "${{ vars.RUNNER_GROUP }}"), + canonical.replace(" labels: [self-hosted, linux, x64, cwlab-ci-isolated]", ""), + canonical.replace("[self-hosted, linux, x64, cwlab-ci-isolated]", "${{ matrix.labels }}"), + canonical.replace(", cwlab-ci-isolated", ""), + canonical.replace("cwlab-ci-isolated", "cwlab-control"), + canonical.replace("cwlab-ci-isolated]", "cwlab-ci-isolated, ubuntu-24.04]"), + canonical.replace("linux, x64", "x64, linux"), + canonical + "\n group: CWL central control", + canonical.replace(" labels:", " labels:"), + "runs-on:", + "name: no runner declaration", ) + for sample in rejected: + with self.subTest(sample=sample): + self.assertFalse( + any(value == _EXPECTED_RUNNER for _, value in _runner_declarations(sample)), + f"noncanonical selector was admitted: {sample}", + ) def test_runner_parser_only_strips_yaml_comment_tokens(self) -> None: """Do not mistake a hash inside a plain or quoted scalar for a YAML comment.""" @@ -200,5 +228,212 @@ def test_postgres_contracts_wait_on_the_dynamic_host_port(self) -> None: self.assertLess(workflow.index(host_probe), workflow.index(contract_run)) +_POSTGRES_CLIENT_STEP = "- name: Install PostgreSQL 16 host client" +_POSTGRES_CLIENT_SELECTION_STEP = "- name: Prove PostgreSQL 16 host client selection" +_POSTGRES_CLIENT_BIN = "/usr/lib/postgresql/16/bin" +_APT_LOCK = "-o DPkg::Lock::Timeout=120" +_POSTGRES_CLIENT_INSTALL = ( + f"sudo apt-get {_APT_LOCK} install --yes --no-install-recommends postgresql-client-16" +) +_POSTGRES_CLIENT_PIN = f'echo {_POSTGRES_CLIENT_BIN} >> "$GITHUB_PATH"' +_POSTGRES_CLIENT_PINNED_PROOF = ( + f"{_POSTGRES_CLIENT_BIN}/psql --version | grep -E '^psql \\(PostgreSQL\\) 16\\.'" +) +_POSTGRES_CLIENT_SELECTED = f'test "$(command -v psql)" = {_POSTGRES_CLIENT_BIN}/psql' +_POSTGRES_CLIENT_PROOF = "psql --version | grep -E '^psql \\(PostgreSQL\\) 16\\.'" +_HOST_PSQL_PATTERN = re.compile( + r"(?:(? tuple[int, int]: + """Return the start and end offsets of one named workflow step.""" + start = workflow.index(step_name) + end = workflow.find("\n - ", start) + return start, len(workflow) if end < 0 else end + + +def _script_uses_host_psql(path: str) -> bool: + """Treat dynamic script paths as psql users; read static scripts from the repository.""" + if "$" in path: + return True + script = ROOT / path + return script.is_file() and bool(_HOST_PSQL_PATTERN.search(script.read_text(encoding="utf-8"))) + + +def _first_host_psql_index(workflow: str) -> int: + """Return the first offset that runs host psql directly or through a bash script. + + The client installation and selection steps are blanked out first because + they only prove the client. Each line is split into shell segments at + ``&&``, ``||``, ``;`` and ``|``; a segment that contains ``docker exec`` runs + psql inside a PostgreSQL container, not on the runner host, so only that + segment is ignored. Scripts count when run through bash (with flags), sh, + source, ``.`` or ``./``. Commands that do not name psql or the script path + literally (for example a variable holding the command) are not detected. + """ + masked = workflow + for name in (_POSTGRES_CLIENT_STEP, _POSTGRES_CLIENT_SELECTION_STEP): + if name in masked: + start, end = _step_block(masked, name) + masked = masked[:start] + " " * (end - start) + masked[end:] + candidates: list[int] = [] + offset = 0 + for line in masked.splitlines(keepends=True): + segment_start = 0 + for segment in _SHELL_SEGMENT_SEPARATOR.split(line): + segment_offset = offset + line.index(segment, segment_start) + segment_start = segment_offset - offset + len(segment) + if "docker exec" in segment: + continue + direct = _HOST_PSQL_PATTERN.search(segment) + if direct: + candidates.append(segment_offset + direct.start()) + for script in _SCRIPT_RUN_PATTERN.finditer(segment): + if _script_uses_host_psql(script.group(1) or script.group(2)): + candidates.append(segment_offset + script.start()) + offset += len(line) + if not candidates: + raise ValueError("workflow has no host psql use") + return min(candidates) + + +class SelfHostedPostgresClientContractTest(unittest.TestCase): + """Provision and pin the host psql client; isolated runners are not uniform.""" + + def test_each_postgres_workflow_installs_and_proves_client_before_use(self) -> None: + """keyverse-ci-01 lacks psql; pin PostgreSQL 16 before first host use.""" + for name in sorted(_EXPECTED_LOCAL_WORKFLOWS): + with self.subTest(workflow=name): + workflow = (WORKFLOWS / name).read_text(encoding="utf-8") + self.assertIn(_POSTGRES_CLIENT_STEP, workflow) + self.assertIn(_POSTGRES_CLIENT_SELECTION_STEP, workflow) + step, step_end = _step_block(workflow, _POSTGRES_CLIENT_STEP) + selection, selection_end = _step_block( + workflow, _POSTGRES_CLIENT_SELECTION_STEP + ) + install = workflow.index(_POSTGRES_CLIENT_INSTALL, step, step_end) + update = workflow.index(f"sudo apt-get {_APT_LOCK} update", step, step_end) + pin = workflow.index(_POSTGRES_CLIENT_PIN, install, step_end) + workflow.index(_POSTGRES_CLIENT_PINNED_PROOF, pin, step_end) + selected = workflow.index(_POSTGRES_CLIENT_SELECTED, selection, selection_end) + proof = workflow.index(_POSTGRES_CLIENT_PROOF, selected, selection_end) + self.assertLess(update, install) + self.assertLess(step_end, selection) + self.assertLess(proof, _first_host_psql_index(workflow)) + for block in (workflow[step:step_end], workflow[selection:selection_end]): + self.assertNotIn("continue-on-error", block) + self.assertNotRegex(block, r"(?m)^\s+if\s*:") + + def test_first_host_psql_detector_rejects_late_client_install(self) -> None: + """Every host psql form placed before the client steps must be detected.""" + early_uses = ( + " - run: bash .github/scripts/restore-rehearsal-postgres.sh\n", + ' - run: if psql "$database_url" -Atqc \'SELECT 1\'; then :; fi\n', + ' - run: psql -h localhost -c "SELECT 1"\n', + ' - run: DATABASE_URL="$database_url" bash "tests/$contract"\n', + " - run: bash tests/test_job_analysis_snapshot_schema_hardening.sh\n", + ) + for early in early_uses: + with self.subTest(early=early): + late = ( + early + + f" {_POSTGRES_CLIENT_STEP}\n" + + f" run: {_POSTGRES_CLIENT_PINNED_PROOF}\n" + + f" {_POSTGRES_CLIENT_SELECTION_STEP}\n" + + f" run: {_POSTGRES_CLIENT_PROOF}\n" + ) + self.assertGreater(late.index(_POSTGRES_CLIENT_PROOF), _first_host_psql_index(late)) + + def test_detector_ignores_container_psql_and_psql_free_scripts(self) -> None: + """docker exec psql and scripts without psql are not host client users.""" + workflow = ( + ' - run: docker exec "$name" psql -U orgmetra -c "SELECT 1"\n' + " - run: bash tests/test_foundation_ci_dependency_hygiene.sh\n" + ' - run: psql "$database_url" -c "SELECT 1"\n' + ) + self.assertEqual(workflow.index('psql "$database_url"'), _first_host_psql_index(workflow)) + with self.assertRaises(ValueError): + _first_host_psql_index(' - run: docker exec "$name" psql -c "SELECT 1"\n') + + +class HostPsqlDetectorBypassTest(unittest.TestCase): + """Reviewer probe forms that previously escaped the host psql detector (PSQL-DOC-003).""" + + _CLIENT = ( + f" {_POSTGRES_CLIENT_STEP}\n run: x\n" + f" {_POSTGRES_CLIENT_SELECTION_STEP}\n run: {_POSTGRES_CLIENT_PROOF}\n" + ) + + def test_detector_rejects_every_reviewer_bypass_form_before_client_steps(self) -> None: + """sh/source/dot/flagged bash scripts, ./script, absolute psql and chained psql count.""" + bypasses = { + "sh_script": " - run: sh .github/scripts/restore-rehearsal-postgres.sh\n", + "dot_slash_script": " - run: ./.github/scripts/restore-rehearsal-postgres.sh\n", + "source_script": " - run: source .github/scripts/restore-rehearsal-postgres.sh\n", + "dot_source_script": " - run: . .github/scripts/restore-rehearsal-postgres.sh\n", + "bash_flag_script": " - run: bash -e .github/scripts/restore-rehearsal-postgres.sh\n", + "absolute_path_psql": " - run: /usr/bin/psql -c 1\n", + "docker_exec_then_host_psql": " - run: docker exec c true && psql -c 1\n", + "bash_c_inline": " - run: bash -c 'psql -c 1'\n", + "env_prefixed_psql": " - run: env PGHOST=x psql -c 1\n", + } + for name, early in bypasses.items(): + with self.subTest(form=name): + workflow = early + self._CLIENT + boundary = workflow.index(_POSTGRES_CLIENT_STEP) + self.assertLess(_first_host_psql_index(workflow), boundary) + + +def _paragraph_containing(text: str, marker: str) -> str: + """Return the blank-line separated paragraph or list item that contains a marker.""" + matches = [part for part in re.split(r"\n\s*\n", text) if marker in part] + if len(matches) != 1: + raise AssertionError(f"expected exactly one paragraph containing {marker!r}") + return matches[0] + + +class HostPsqlRunnerEvidenceDocumentationTest(unittest.TestCase): + """Bind client-repair prose to observed hosted jobs, not per-runner properties.""" + + _DOCUMENTS = ( + ROOT / "CHANGELOG.md", + ROOT / "docs" / "traceability" / "restore-rehearsal.md", + ) + _OBSERVED_JOBS = ( + "112192671013", # restore attempt 3, orgmetra-ci-01, first exit 127 + "112199226633", # restore attempt 4, keyverse-ci-01, exit 127 + "112199223734", # Foundation attempt 4, orgmetra-ci-01, reached PostgreSQL + ) + + def test_client_repair_prose_names_observed_jobs_without_runner_inference(self) -> None: + """PSQL-DOC-001/002: first failure order and job-level, not runner-level, claims.""" + for document in self._DOCUMENTS: + with self.subTest(document=document.name): + paragraph = _paragraph_containing( + document.read_text(encoding="utf-8"), "postgresql-client-16" + ) + for job in self._OBSERVED_JOBS: + self.assertIn(job, paragraph) + self.assertNotRegex(paragraph, r"first hosted attempt on `keyverse-ci-01`") + self.assertNotRegex(paragraph, r"already ha(?:d|s)\b") + self.assertRegex(paragraph, r"varies by job|differs between jobs") + + def test_changelog_claims_only_the_detector_forms_it_tests(self) -> None: + """PSQL-DOC-003: no blanket claim that every psql form is rejected.""" + paragraph = _paragraph_containing( + (ROOT / "CHANGELOG.md").read_text(encoding="utf-8"), "postgresql-client-16" + ) + self.assertNotRegex(paragraph, r"every (?:direct )?`psql` form") + self.assertNotRegex(paragraph, r"every host script that calls `psql`") + for covered in ("`sh`", "`source`", "`./`", "absolute", "`docker exec`"): + self.assertIn(covered, paragraph) + + if __name__ == "__main__": # pragma: no cover - normal execution is via unittest discovery. unittest.main() diff --git a/tests/validate_repository.py b/tests/validate_repository.py index d9d4c15a3..c2104688e 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -23,7 +23,16 @@ "NOTICE", "manifest.json", "package.json", + "package-lock.json", + ".storybook/main.js", + ".storybook/preview.js", + "apps/hr-workspace/index.html", + "apps/hr-workspace/styles.css", + "apps/hr-workspace/app.js", + "apps/hr-workspace/workspace.stories.js", ".github/workflows/foundation-ci.yml", + "playwright.config.mjs", + "tests/workspace-foundation-integration.test.mjs", "docs/PRD.md", "docs/TRD.md", "docs/USER_STORIES.md", @@ -39,6 +48,7 @@ "docs/TEST_STRATEGY.md", "docs/OPERABILITY.md", "docs/TRACEABILITY.md", + "docs/product-technical-gap-baseline.md", "docs/adr/README.md", "docs/adr/0001-orgmetra-authoritative-hris-record.md", "docs/adr/0002-federated-cwl-integration-boundaries.md", @@ -54,6 +64,7 @@ "docs/adr/0012-governed-migration-handoff.md", "docs/adr/0013-governed-requisition-review-packet.md", "docs/adr/0014-job-analysis-snapshot-persistence.md", + "docs/adr/0026-product-technical-gap-baseline.md", "docs/doctoring/REFERENCES.md", "docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md", "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", @@ -77,6 +88,11 @@ "scripts/foundation-contract.mjs", "tests/dispatcher-inventory.test.mjs", "tests/foundation-contract.test.mjs", + "tests/hr-workspace.test.mjs", + "tests/job-analysis-api-fixture.mjs", + "tests/test_github_actions_runner_image.py", + "tests/e2e/hr-workspace-core-model.spec.mjs", + "tests/e2e/hr-workspace.spec.mjs", "tests/openapi-contract.test.mjs", "tests/test_bitemporal_postgres.sh", "tests/test_tenant_isolation_postgres.sh", @@ -120,7 +136,7 @@ def _line_count(data: bytes) -> int: def _expected_manifest_document() -> dict[str, Any]: - """Build deterministic provenance for the exact active branch artifact set.""" + """Build deterministic artifact integrity for the target; source is not captured.""" files = [] for relative_path in sorted(set(REQUIRED) - {"manifest.json"}): path = ROOT / relative_path @@ -138,7 +154,8 @@ def _expected_manifest_document() -> dict[str, Any]: return { "package": "orgmetra-foundation-pack", "version": "0.1.0", - "generated_for_branch": "feat/audit-outbox-envelope", + "target_branch": "develop", + "generated_source_branch": None, "files": files, } @@ -152,10 +169,14 @@ def _manifest_entries() -> dict[str, dict[str, Any]]: if not isinstance(manifest, dict) or not isinstance(manifest.get("files"), list): _fail("manifest.json must contain a files array") - if manifest.get("generated_for_branch") != "feat/audit-outbox-envelope": + if manifest.get("target_branch") != "develop": + _fail("manifest target_branch must identify the intended integration target develop") + if "generated_for_branch" in manifest: + _fail("manifest generated_for_branch is obsolete; use target_branch") + if "generated_source_branch" not in manifest or manifest["generated_source_branch"] is not None: _fail( - "manifest generated_for_branch must identify the active generation branch " - "feat/audit-outbox-envelope" + "manifest generated_source_branch must be null; " + "this deterministic producer does not capture generation-source authority" ) entries: dict[str, dict[str, Any]] = {} @@ -596,6 +617,8 @@ def _validate_openapi_contract() -> None: def _validate_markdown() -> None: """Reject explicit unfinished-work markers with exact path/line and malformed fences.""" for path in ROOT.rglob("*.md"): + if {"node_modules", "storybook-static"}.intersection(path.parts): + continue text = path.read_text(encoding="utf-8") for line_number, line in enumerate(text.splitlines(), start=1): if UNFINISHED_MARKER_LINE_PATTERN.fullmatch(line): diff --git a/tests/workspace-foundation-integration.test.mjs b/tests/workspace-foundation-integration.test.mjs new file mode 100644 index 000000000..230f001b6 --- /dev/null +++ b/tests/workspace-foundation-integration.test.mjs @@ -0,0 +1,366 @@ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +const root = new URL('../', import.meta.url); + +/** Import the actual config in a bounded child with an isolated port/CI setting. */ +function importBrowserConfig({ port, ci } = {}) { + const env = { ...process.env }; + delete env.ORGMETRA_WORKSPACE_PORT; + delete env.CI; + if (port !== undefined) env.ORGMETRA_WORKSPACE_PORT = port; + if (ci !== undefined) env.CI = ci; + const result = spawnSync(process.execPath, ['--input-type=module', '-e', + "import config from './playwright.config.mjs'; console.log(JSON.stringify(config));"], { + cwd: fileURLToPath(root), env, encoding: 'utf8', timeout: 15_000, + }); + assert.equal(result.error, undefined, 'config import must finish within its bound'); + return result; +} + +test('browser acceptance always owns its fixture instead of reusing a foreign HTTP server', () => { + for (const ci of [undefined, 'true', '']) { + const result = importBrowserConfig({ ci }); + assert.equal(result.status, 0, result.stderr); + const config = JSON.parse(result.stdout); + assert.equal(config.webServer.reuseExistingServer, false); + assert.equal(config.use.baseURL, 'http://127.0.0.1:4173'); + assert.equal(config.webServer.url, 'http://127.0.0.1:4173/apps/hr-workspace/index.html'); + assert.equal(config.webServer.command, 'python3 -m http.server 4173 --bind 127.0.0.1'); + assert.equal(config.timeout, 15_000); + assert.equal(config.expect.timeout, 5_000); + assert.equal(config.use.trace, 'retain-on-failure'); + assert.equal(config.use.screenshot, 'only-on-failure'); + assert.equal(config.use.video, 'retain-on-failure'); + } +}); + +test('browser fixture uses a configured canonical TCP port consistently', () => { + for (const port of ['1', '43127', '65535']) { + const result = importBrowserConfig({ port }); + assert.equal(result.status, 0, result.stderr); + const config = JSON.parse(result.stdout); + assert.equal(config.use.baseURL, `http://127.0.0.1:${port}`); + assert.equal(config.webServer.url, `http://127.0.0.1:${port}/apps/hr-workspace/index.html`); + assert.equal(config.webServer.command, `python3 -m http.server ${port} --bind 127.0.0.1`); + assert.equal(config.webServer.reuseExistingServer, false); + } +}); + +test('browser fixture rejects invalid port grammar and bounds before returning a server config', () => { + const invalidPorts = ['', '0', '65536', '-1', '+4173', '04173', ' 4173', '4173 ', + '4173\n', '4.173', '4e3', '0x104d', 'NaN', 'Infinity', '999999999999999999999', '4173; echo unsafe']; + const result = spawnSync(process.execPath, ['--input-type=module', '-e', ` + const results = []; + for (const [index, port] of JSON.parse(process.argv[1]).entries()) { + process.env.ORGMETRA_WORKSPACE_PORT = port; + try { + await import('./playwright.config.mjs?invalid=' + index); + results.push({ rejected: false }); + } catch (error) { + results.push({ rejected: true, message: error.message }); + } + } + console.log(JSON.stringify(results)); + `, JSON.stringify(invalidPorts)], { + cwd: fileURLToPath(root), env: { ...process.env }, encoding: 'utf8', timeout: 15_000, + }); + assert.equal(result.error, undefined, 'invalid config imports must finish within their bound'); + assert.equal(result.status, 0, result.stderr); + const results = JSON.parse(result.stdout); + assert.equal(results.length, invalidPorts.length); + for (const [index, observation] of results.entries()) { + assert.equal(observation.rejected, true, `invalid port case ${index} must be rejected`); + assert.equal(observation.message, + 'ORGMETRA_WORKSPACE_PORT must be a canonical decimal TCP port from 1 to 65535'); + } +}); + +/** Check dependency provisioning in the owning workflows' canonical step blocks. */ +function assertValidationDependencies(workflowPath) { + const workflow = readFileSync(new URL(workflowPath, root), 'utf8'); + const steps = workflow.split(/^ - /m).slice(1); + const validateIndex = steps.findIndex((step) => /^ run: npm run validate$/m.test(step)); + const installIndex = steps.findIndex((step) => /^ run: npm ci$/m.test(step)); + assert.ok(validateIndex >= 0, `${workflowPath} must retain npm run validate`); + assert.ok(installIndex >= 0 && installIndex < validateIndex, + `${workflowPath} must provision npm ci before the first npm run validate`); + const install = steps[installIndex]; + assert.doesNotMatch(install, /^ (?:if|continue-on-error):/m, + 'dependency installation must be unconditional and fail closed'); + const setupIndex = steps.findIndex((step) => + /uses: actions\/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020\b/.test(step) + && /^ node-version: "24"$/m.test(step) + && /^ check-latest: false$/m.test(step)); + assert.ok(setupIndex >= 0 && setupIndex < installIndex, + `${workflowPath} must set up pinned Node 24 before npm ci`); +} + +test('Foundation provisions reviewed Node dependencies before repository validation', () => { + assertValidationDependencies('.github/workflows/foundation-ci.yml'); +}); + +test('Recovery provisions reviewed runtimes and Node dependencies before repository validation', () => { + const workflowPath = '.github/workflows/recovery-rehearsal-quality.yml'; + assertValidationDependencies(workflowPath); + const workflow = readFileSync(new URL(workflowPath, root), 'utf8'); + const steps = workflow.split(/^ - /m).slice(1); + const setupIndex = steps.findIndex((step) => + /uses: actions\/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97\b/.test(step) + && /^ python-version: "3\.14"$/m.test(step) + && /^ check-latest: false$/m.test(step)); + const consumerIndex = steps.findIndex((step) => /^ python - <<'PY'$/m.test(step)); + assert.ok(consumerIndex >= 0, 'Recovery must retain its Python provenance consumer'); + assert.ok(setupIndex >= 0 && setupIndex < consumerIndex, + 'Recovery must provision pinned Python 3.14 before its first Python consumer'); +}); + +test('the retained workspace executes browser acceptance in canonical Foundation without a second quality owner', () => { + const workflow = readFileSync(new URL('.github/workflows/foundation-ci.yml', root), 'utf8'); + assert.equal(existsSync(new URL('.github/workflows/hr-workspace-browser-e2e.yml', root)), false); + assert.equal(existsSync(new URL('.github/workflows/job-analysis-api-quality.yml', root)), false); + assert.match(workflow, /npx playwright install --with-deps chromium/); + assert.match(workflow, /npm run test:e2e/); + assert.match(workflow, /npm run build-storybook/); + assert.match(workflow, /Run PostgreSQL contracts in isolated containers/); + assert.match(workflow, /git diff --exit-code/); +}); + +/** Run only manifest seams from the exact Python source in an owned, bounded child. */ +function manifestDiagnostic(method) { + const diagnostic = String.raw` +import contextlib +import copy +import hashlib +import io +import json +import os +from pathlib import Path +import runpy +import shutil +import sys +import tempfile +import unittest + + +git_read_guard = False + +def deny_external(event, args): + """Keep this diagnostic offline and forbid nested native children or Git reads.""" + if event.startswith("socket.") or event in { + "subprocess.Popen", "os.system", "os.posix_spawn", "os.fork", "os.forkpty" + }: + raise RuntimeError("forbidden manifest diagnostic event: " + event) + if git_read_guard and event == "open" and isinstance(args[0], (str, bytes)): + if ".git" in Path(os.fsdecode(args[0])).parts: + raise RuntimeError("manifest producer must not read Git metadata") + + +sys.addaudithook(deny_external) +source = Path(sys.argv[1]).resolve() +module = runpy.run_path(str(source), run_name="manifest_diagnostic")["main"].__globals__ +original_root = module["ROOT"] +controls = [] + + +def printed(): + """Invoke actual main's read-only public print branch, not a substitute emitter.""" + global git_read_guard + old_argv = sys.argv + git_read_guard = True + try: + sys.argv = [str(source), "--print-manifest"] + output = io.StringIO() + with contextlib.redirect_stdout(output): + module["main"]() + return output.getvalue() + finally: + git_read_guard = False + sys.argv = old_argv + + +class ManifestContracts(unittest.TestCase): + """Pair emitted metadata and exact artifact integrity with unsupported claims.""" + + def test_emission(self): + document = json.loads(printed()) + self.assertEqual(document.get("target_branch"), "develop", + "producer must label intended target, not generation source") + self.assertIn("generated_source_branch", document) + self.assertIsNone(document["generated_source_branch"]) + self.assertNotIn("generated_for_branch", document) + controls.append({"case": "emitted_target_and_uncaptured_source", "result": "accepted"}) + + def check_document(self, label, base, mutation=None, expected=None): + """Call the real parser/integrity validator and retain its actual rejection.""" + document = copy.deepcopy(base) + if mutation is not None: + mutation(document) + module["MANIFEST_PATH"].write_text(json.dumps(document), encoding="utf-8") + try: + module["_validate_manifest"]() + except SystemExit as error: + self.assertIsNotNone(expected, f"positive rejected: {error}") + self.assertIn(expected, str(error)) + controls.append({"case": label, "result": "rejected", "message": str(error)}) + else: + self.assertIsNone(expected, f"negative incorrectly admitted: {label}") + controls.append({"case": label, "result": "accepted"}) + + def test_metadata(self): + base = module["_expected_manifest_document"]() + # Specify the parent-selected implementation contract independently of emission. + base.pop("generated_for_branch", None) + base.update(target_branch="develop", generated_source_branch=None) + cases = [ + ("valid_unknown_source", None, None), + ("wrong_target", lambda d: d.update(target_branch="main"), "target_branch"), + ("source_used_as_target", lambda d: d.update(target_branch="feature/source"), "target_branch"), + ("missing_target", lambda d: d.pop("target_branch"), "target_branch"), + ("malformed_target", lambda d: d.update(target_branch=[]), "target_branch"), + ("missing_source", lambda d: d.pop("generated_source_branch"), "generated_source_branch"), + ("unsupported_named_source", lambda d: d.update(generated_source_branch="feature/source"), "generated_source_branch"), + ("target_promoted_to_source", lambda d: d.update(generated_source_branch="develop"), "generated_source_branch"), + ("empty_source", lambda d: d.update(generated_source_branch=""), "generated_source_branch"), + ("boolean_source", lambda d: d.update(generated_source_branch=False), "generated_source_branch"), + ("object_source", lambda d: d.update(generated_source_branch={}), "generated_source_branch"), + ("array_source", lambda d: d.update(generated_source_branch=[]), "generated_source_branch"), + ("numeric_source", lambda d: d.update(generated_source_branch=0), "generated_source_branch"), + ("legacy_claim", lambda d: d.update(generated_for_branch="develop"), "generated_for_branch"), + ("legacy_null_claim", lambda d: d.update(generated_for_branch=None), "generated_for_branch"), + ] + for label, mutation, expected in cases: + with self.subTest(case=label): + self.check_document(label, base, mutation, expected) + + def test_integrity(self): + base = json.loads(printed()) + expected_paths = sorted(set(module["REQUIRED"]) - {"manifest.json"}) + self.assertEqual([entry["path"] for entry in base["files"]], expected_paths) + for entry in base["files"]: + data = (module["ROOT"] / entry["path"]).read_bytes() + self.assertEqual(entry["sha256"], hashlib.sha256(data).hexdigest()) + self.assertEqual(entry["bytes"], len(data)) + self.assertEqual(entry["lines"], len(data.decode("utf-8").splitlines())) + cases = [ + ("exact_digest_size_lines_paths", None, None), + ("wrong_digest", lambda d: d["files"][0].update(sha256="0" * 64), "sha256 mismatch"), + ("wrong_bytes", lambda d: d["files"][0].update(bytes=-1), "bytes mismatch"), + ("wrong_lines", lambda d: d["files"][0].update(lines=-1), "lines mismatch"), + ("missing_digest", lambda d: d["files"][0].pop("sha256"), "sha256 mismatch"), + ("missing_bytes", lambda d: d["files"][0].pop("bytes"), "bytes mismatch"), + ("missing_lines", lambda d: d["files"][0].pop("lines"), "lines mismatch"), + ("missing_path", lambda d: d["files"][0].pop("path"), "non-empty string"), + ("empty_path", lambda d: d["files"][0].update(path=""), "non-empty string"), + ("wrong_path_type", lambda d: d["files"][0].update(path=[]), "non-empty string"), + ("duplicate_path", lambda d: d["files"].append(copy.deepcopy(d["files"][0])), "duplicate manifest path"), + ("self_reference", lambda d: d["files"][0].update(path="manifest.json"), "exclude itself"), + ("parent_path", lambda d: d["files"][0].update(path="../README.md"), "safe normalized"), + ("absolute_path", lambda d: d["files"][0].update(path="/README.md"), "safe normalized"), + ("unnormalized_path", lambda d: d["files"][0].update(path="docs/./OPERABILITY.md"), "safe normalized"), + ("missing_entry", lambda d: d["files"].pop(), "path set mismatch"), + ("extra_entry", lambda d: d["files"].append({"path": "extra.txt"}), "path set mismatch"), + ("malformed_files", lambda d: d.update(files={}), "files array"), + ("malformed_entry", lambda d: d["files"].__setitem__(0, None), "entries must be objects"), + ] + for label, mutation, expected in cases: + with self.subTest(case=label): + self.check_document(label, base, mutation, expected) + module["MANIFEST_PATH"].write_text("{not json", encoding="utf-8") + with self.assertRaisesRegex(SystemExit, "not readable JSON") as caught: + module["_manifest_entries"]() + controls.append({"case": "malformed_json", "result": "rejected", "message": str(caught.exception)}) + # Match changed content with independently computed digest/size/line values. + changed = module["ROOT"] / base["files"][0]["path"] + changed.write_bytes(changed.read_bytes() + b"\nmanifest fixture line\n") + self.check_document("stale_disk_content", base, expected="sha256 mismatch") + repaired = copy.deepcopy(base) + data = changed.read_bytes() + repaired["files"][0].update(sha256=hashlib.sha256(data).hexdigest(), + bytes=len(data), lines=len(data.decode("utf-8").splitlines())) + self.check_document("changed_content_exact_integrity", repaired) + + def test_portability(self): + root = module["ROOT"] + self.assertFalse((root / ".git").exists()) + def inventory(): + return {p.relative_to(root).as_posix(): p.read_bytes() + for p in root.rglob("*") if p.is_file() and ".git" not in p.relative_to(root).parts} + before = inventory() + first = printed() + self.assertEqual(first, printed()) + self.assertEqual(before, inventory(), "print must not rewrite any artifact") + self.assertEqual(json.loads(first), module["_expected_manifest_document"]()) + controls.append({"case": "gitless_deterministic_readonly", "result": "accepted"}) + # Untrusted environment labels and a misleading Git pointer cannot become authority. + previous = dict(os.environ) + try: + for source_label in ("feature/source-a", "unrelated/source-b", "develop", ""): + os.environ.update(GITHUB_HEAD_REF=source_label, GITHUB_REF_NAME=source_label, + CI_COMMIT_BRANCH=source_label, GENERATED_SOURCE_BRANCH=source_label, + GITHUB_BASE_REF="wrong-target", TARGET_BRANCH="wrong-target", + GIT_DIR=str(root / "missing-git-dir")) + self.assertEqual(first, printed()) + finally: + os.environ.clear() + os.environ.update(previous) + (root / ".git").mkdir() + (root / ".git" / "HEAD").write_text("ref: refs/heads/not-generation-authority\n") + # Audit hook forbids the producer from opening that Git metadata. + without_git_reads = printed() + self.assertEqual(first, without_git_reads) + self.assertEqual(before, {k: v for k, v in inventory().items() if not k.startswith(".git/")}) + self.assertIsNone(json.loads(without_git_reads)["generated_source_branch"]) + controls.append({"case": "sourceagnostic_env_and_unread_git_metadata", "result": "accepted"}) + + +with tempfile.TemporaryDirectory(prefix="orgmetra-manifest-") as temporary: + root = Path(temporary) + for relative in module["REQUIRED"]: + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(original_root / relative, target) + module["ROOT"] = root + module["MANIFEST_PATH"] = root / "manifest.json" + suite = unittest.TestSuite([ManifestContracts(sys.argv[2])]) + outcome = unittest.TextTestRunner(verbosity=2).run(suite) + print(json.dumps({"method": sys.argv[2], "tests": outcome.testsRun, + "failures": len(outcome.failures), "errors": len(outcome.errors), + "required_count": len(set(module["REQUIRED"])), "controls": controls})) +sys.exit(0 if outcome.wasSuccessful() else 1) +`; + const env = { PATH: process.env.PATH }; + if (process.env.TMPDIR) env.TMPDIR = process.env.TMPDIR; + const result = spawnSync('python3', ['-I', '-B', '-c', diagnostic, + fileURLToPath(new URL('tests/validate_repository.py', root)), method], { + cwd: fileURLToPath(root), env, encoding: 'utf8', timeout: 15_000, + }); + const details = `status=${result.status} signal=${result.signal}\n${result.stderr}\n${result.stdout}`; + assert.equal(result.error, undefined, `manifest diagnostic must finish within its bound: ${details}`); + assert.equal(result.status, 0, details); + const report = JSON.parse(result.stdout); + assert.equal(report.tests, 1); + assert.equal(report.failures, 0); + assert.equal(report.errors, 0); + console.log(JSON.stringify({ ...report, nativeChild: { status: result.status, signal: result.signal, stderr: result.stderr } })); + return report; +} + +test('manifest honest contract: emits the target and explicit uncaptured source', () => { + manifestDiagnostic('test_emission'); +}); + +test('manifest honest contract: rejects unsupported source, target and legacy claims', () => { + manifestDiagnostic('test_metadata'); +}); + +test('manifest honest contract: preserves exact artifact integrity and path rejection', () => { + manifestDiagnostic('test_integrity'); +}); + +test('manifest honest contract: print is deterministic, read-only, gitless and source-agnostic', () => { + manifestDiagnostic('test_portability'); +});