From 9794c0d7ab33a5ebb71d0b39b6bb024cd1118e7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:07:42 +0900 Subject: [PATCH 001/110] feat: add evidence-centered HR workspace slice --- .gitignore | 1 + CHANGELOG.md | 3 + README.md | 3 +- apps/hr-workspace/app.js | 89 ++++ apps/hr-workspace/index.html | 113 +++++ apps/hr-workspace/styles.css | 111 +++++ docs/PRD.md | 5 + docs/STORYBOOK.md | 10 + docs/TRACEABILITY.md | 7 +- .../0015-product-technical-gap-baseline.md | 32 ++ docs/adr/README.md | 5 + docs/doctoring/REFERENCES.md | 16 + docs/product-technical-gap-baseline.md | 147 ++++++ manifest.json | 464 +++++++++++++++++- package.json | 4 +- packages/design-tokens/tokens.css | 3 + packages/design-tokens/tokens.json | 3 + scripts/foundation-contract-core.mjs | 7 + services/people-api/pyproject.toml | 9 +- tests/hr-workspace.test.mjs | 36 ++ tests/validate_repository.py | 12 +- 21 files changed, 1070 insertions(+), 10 deletions(-) create mode 100644 apps/hr-workspace/app.js create mode 100644 apps/hr-workspace/index.html create mode 100644 apps/hr-workspace/styles.css create mode 100644 docs/adr/0015-product-technical-gap-baseline.md create mode 100644 docs/product-technical-gap-baseline.md create mode 100644 tests/hr-workspace.test.mjs diff --git a/.gitignore b/.gitignore index 1f0b61ba5..9f2c96bec 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ secrets/ artifacts/ reports/ *.log +/.codegraph/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b45fd56e7..47f7f3026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to Orgmetra will be documented in this file. ### Added +- 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. +- 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 hourly 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 for `services/people-api`, so local tests discover the owned Keyverse adapter without an undocumented `PYTHONPATH` workaround. - 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. - Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal. diff --git a/README.md b/README.md index 47bb087a3..8246f9f7a 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,10 @@ 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. +Protected `develop` at `39d3c15e7c47306ec2173d14afcd1c6e1a0139c9` 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, requisition review packets, and the governed People mutation/confirmed-hire implementation with tenant-scoped idempotency and atomic audit/outbox evidence. The next protected product gaps are a runnable browser workspace, canonical persisted Job Analysis, statistical validity estimation, and a versioned release; see `docs/product-technical-gap-baseline.md` for exact evidence boundaries and current PR state. diff --git a/apps/hr-workspace/app.js b/apps/hr-workspace/app.js new file mode 100644 index 000000000..ee9cd0547 --- /dev/null +++ b/apps/hr-workspace/app.js @@ -0,0 +1,89 @@ +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', 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: '닫기', requestEvidence: '추가 근거 요청', highImpactConfirmation: '고위험 작업 확인', correctHistoryTitle: '이력 정정', confirmationCopy: '행위자, 목적, 사유, 근거 맥락을 사람이 확인하기 전까지 정정은 초안으로 남습니다.', reason: '사유', reasonPlaceholder: '필수', draftConfirmed: '로컬 픽스처 근거에 정정 초안을 확인했으며 API 변경은 전송하지 않았습니다.', cancel: '취소', confirm: '정정 확인' + } +}; + +export function isPurposeAuthorized(purpose) { + return purpose === 'hr_operations'; +} + +export function nextLocale(locale) { + return locale === 'en' ? 'ko' : 'en'; +} + +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.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; +} + +if (typeof document !== 'undefined') { + let locale = 'en'; + 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))); + 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', () => 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; + }); + setLocale(locale); +} diff --git a/apps/hr-workspace/index.html b/apps/hr-workspace/index.html new file mode 100644 index 000000000..5c8072aaf --- /dev/null +++ b/apps/hr-workspace/index.html @@ -0,0 +1,113 @@ + + + + + + + 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.

+ +
+
+ + +
+

High-impact confirmation

Correct history

+

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

+
+ +
+
+
+ + diff --git a/apps/hr-workspace/styles.css b/apps/hr-workspace/styles.css new file mode 100644 index 000000000..816869987 --- /dev/null +++ b/apps/hr-workspace/styles.css @@ -0,0 +1,111 @@ +: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; } +.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; } +.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; } +.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: rect(0, 0, 0, 0); white-space: nowrap; } + +@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; } +} diff --git a/docs/PRD.md b/docs/PRD.md index 674a3b96c..61124b175 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -59,6 +59,11 @@ 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. 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. + ### P2 scale - HRIS migration pipelines. diff --git a/docs/STORYBOOK.md b/docs/STORYBOOK.md index bd3dc7e15..3d74fece6 100644 --- a/docs/STORYBOOK.md +++ b/docs/STORYBOOK.md @@ -48,3 +48,13 @@ Each interactive component requires at least these stories where applicable: - 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 evidence review, +purpose-bound permission denial, high-impact confirmation, exact allocation +values, and English/Korean labels. It is fixture mode: it does not claim a +connected API or a deployed Storybook runtime. A full Storybook runner is +deferred until a second workspace justifies extracting shared components. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 707051d22..7480a0ff8 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -13,9 +13,9 @@ | 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_active_pr | -| Evidence-grounded Job analysis with Task/FJA/KSAO linkage | Job Analysis / Workforce Validation | `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `FunctionalJobAnalysisProfile`, `TaskKSAOLink`, `EvidenceSource` | 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 | 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_protected_develop | +| 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_develop | +| Evidence-grounded Job analysis with Task/FJA/KSAO linkage | Job Analysis / Workforce Validation | `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `FunctionalJobAnalysisProfile`, `TaskKSAOLink`, `EvidenceSource` | 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 | implemented_on_protected_develop | | 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 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 | @@ -25,6 +25,7 @@ | 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 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-0015 | implemented_on_protected_develop | ## 4. CWL integration traceability diff --git a/docs/adr/0015-product-technical-gap-baseline.md b/docs/adr/0015-product-technical-gap-baseline.md new file mode 100644 index 000000000..803927d4b --- /dev/null +++ b/docs/adr/0015-product-technical-gap-baseline.md @@ -0,0 +1,32 @@ +# ADR 0015: Product and technical gap baseline + +- Status: Accepted on active development branch +- 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 now the first protected integration lane. They remain the dependency for browser workflows and must preserve tenant authorization, idempotency, bitemporal history, human confirmation, audit, and outbox atomicity. +5. Job-analysis persistence/API work now converges on the single current PR #38 model, with migration 0013 and its own ADR; a second Task/FJA/KSAO store must not be introduced. +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 and Storybook remain design-system inputs, not evidence that a customer UI exists. 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 27c190794..1c3a08933 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -11,3 +11,8 @@ | [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 protected `develop` | +| [0011](0011-bitemporal-workforce-composition.md) | Bitemporal workforce composition | Accepted on protected `develop` | +| [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on protected `develop` | +| [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on protected `develop` | +| [0015](0015-product-technical-gap-baseline.md) | Product and technical gap baseline | Accepted on active development branch | diff --git a/docs/doctoring/REFERENCES.md b/docs/doctoring/REFERENCES.md index cc6e12dba..9208acf3d 100644 --- a/docs/doctoring/REFERENCES.md +++ b/docs/doctoring/REFERENCES.md @@ -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 + +Fugu Team, Sakana AI. (2026). *Sakana Fugu technical report* (arXiv:2606.21228). arXiv. https://arxiv.org/abs/2606.21228 + +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 + +Sakana AI. (2026). *TRINITY: An evolved LLM coordinator* (arXiv:2512.04695). arXiv. https://arxiv.org/abs/2512.04695 + +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..405489423 --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,147 @@ +# Orgmetra product and technical gap baseline + +**Snapshot:** 2026-08-20, Asia/Seoul +**Evidence base:** local `develop` at `39d3c15e7c47306ec2173d14afcd1c6e1a0139c9`, current protected default branch; GitHub PR metadata, exact-head workflow results, review/thread results, and fresh local validation. + +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. + +## 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 a protected People mutation and confirmed-hire implementation, 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 People read/write boundaries. The largest buyer-visible gap is now the missing browser product surface: the repository has contracts and service code, but no runnable Employee Profile, HR Home, recruiting, or validation workspace with browser evidence. + +The next highest-leverage gaps are one canonical persisted Job Analysis case/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 -. persisted Job Analysis API missing .-> gap2[Gap P0-2] + validity -. integrity only, no estimator .-> gap3[Gap P0-3] +``` + +## Capability truth on protected `develop` + +| 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-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. | +| Job-analysis value objects | `orgmetra_hris_kernel.job_analysis`; exact unit coverage | `implemented_on_protected_develop` | Evidence can be validated in a protected package, but no protected persisted Job Analysis case/workspace exists. | +| Job-analysis persistence/API | PR #38 is the current canonical persistence lane, rebuilt on protected `39d3c15`; PR #30 is closed as superseded | `implemented_on_active_pr` | A buyer cannot yet create, approve, version, or retrieve one canonical persisted Job Analysis case. | +| 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. | +| Role workspaces and Storybook runtime | Figma wireframes, design tokens, and Storybook inventory only; no web application package or stories | `planned` | There is no buyer-clickable HR Home, Employee Profile, recruiting, validation, or admin workflow in this checkout. | +| 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. | + +## Local candidate artifact outside protected truth + +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`. 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; +it is not browser E2E, hosted release, protected-develop truth, or evidence of +completed Storybook runtime integration. 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 | Ship one Employee Profile + HR Home vertical slice backed by the protected People API, using existing tokens and Figma source. Prove keyboard/focus/permission/confirmation states, exact-value tables, i18n, and browser E2E. | Protected People API evidence | +| P0-2 | Job Architecture | Merge and verify PR #38 as one persisted Job Analysis case: one migration owner, one ADR, versioned source evidence, SME approval, retrieval API, and protected PostgreSQL acceptance test. | Protected People API evidence | +| P0-3 | Workforce Validation / scientific owner | Add a Rust-first estimator boundary or a 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. | 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 | + +## Current open PR inventory and integration order + +The following is the current GitHub inventory checked on 2026-08-20. All listed PRs target `develop`; the protected base observed by the connector is `39d3c15e7c47306ec2173d14afcd1c6e1a0139c9`. `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 | +|---:|---|---|---|---| +| 38 | `cursor/job-analysis-snapshot-5eef` / `21949fba6671e24fae2f719ea53cda0e363e8874` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; two unresolved threads are outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | +| 41 | `feat/candidate-evidence-intake` / `b64c4118905861ca8bf8b220764a411e5e92e5f7` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; all sampled checks green; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | +| 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 46 | `feat/governed-employment-separation-review` / `ac1cc7cf15ed5c430e7e3ad42d416bd35318fbc1` | Employment separation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 45 | `feat/governed-assignment-change-review` / `16d07b62bff8c01a8eff5cedb2a85fb75ac4fb56` | Assignment change review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 44 | `feat/governed-performance-review` / `752b4d0a5a09ede2c601fac899065d373bd288ec` | Performance review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 43 | `feat/governed-offer-approval` / `2975c0c606fa009db35787de5d8104f16db2a3f5` | Offer approval packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 42 | `feat/selection-outcome-monitoring-plan` / `28b5e9e096c057fd825cf9ff2aa8e9ec5611401f` | Selection outcome monitoring | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 40 | `feat/structured-interview-plan` / `9751d45f215582c722f036fa12f6f0ebe2c7e8f8` | Structured interview plan | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green; no qualifying approval | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 52 | `feat/tepp-analysis-adapter` / `b23c15f548b7ba77dfe43a41bb110b2216cff859` | Governed TEPP request boundary | Draft; not mergeable; stale base `ebad8c1`; all five exact-head runs queued | Keep non-executing; rebase only when the integration contract owner is ready, then reacquire checks and review. | +| 51 | `docs/protected-truth-refresh` / `26fc8c886eea72476ba4e79aaea9ed67db344c47` | Protected truth/documentation repair | Draft; not mergeable; stale base `ebad8c1`; two runs queued and five successful predecessor/current runs | Reconcile this baseline and rebase on `39d3c15`; rerun every applicable gate before review. | +| 37 | `cursor/expand-adr-0001-0003-f381` / `240d2770e5b3109cd1e93c9493d8b6ba685244db` | ADR/reference expansion | Draft; not mergeable; base `f815feed`; only partial checks returned | Close or rebase only after comparing its statements with current protected truth; avoid ADR-number collisions. | + +No open GitHub Issue was returned by the current `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. + +## Loop contract + +Every hourly 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`. + +## Fresh local evidence + +| Check | Result | +|---|---| +| `npm run validate` | Passed: repository validation and 55 Node tests on protected `develop` `39d3c15`. | +| `packages/hris-kernel` | 171 passed; 100% statement/branch coverage. | +| `packages/keyverse-adapter` | 41 passed; 100% statement/branch coverage. | +| `packages/migration-adapter` | 65 passed; 100% statement/branch coverage. | +| `packages/naruon-adapter` | 41 passed; 100% statement/branch coverage. | +| `packages/requisition-review` | 61 passed; 100% statement/branch coverage. | +| `packages/selection-review` | 68 passed; 100% statement/branch coverage. | +| `services/people-api` | 146 passed; 100% statement/branch coverage. Project-local `tool.uv.sources` now resolves both `orgmetra-hris-kernel` and `orgmetra-keyverse-adapter`; test extras are declared in the service project. | +| PostgreSQL contract suite | 12/12 current protected contracts passed on a dedicated PostgreSQL 16 container, including `test_people_mutation_idempotency_postgres.sh`; the temporary container was removed after the run. | + +These results prove the current foundation contracts. They do not prove that open PRs are merged, that a browser UI exists, that a statistical estimator exists, or that Orgmetra is certified under CSAP/SOC 2. diff --git a/manifest.json b/manifest.json index fc727e5de..67babf3d7 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1,463 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"bdc700b6b9e1054b5a89d6e6e91826e213ead3a9d8e0e098078aaa72b03b06e8","bytes":4328,"lines":122},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"bddc0023259306f2b9de2a67160aa5523d140270691ba42816c34c362e342d7d","bytes":7809,"lines":107},{"path":"CHANGELOG.md","sha256":"5094cfedd961eace3f6e0ef3bff498fc8e90c1b3219ab5c4c63dcc1871c1aa61","bytes":15928,"lines":74},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"1e488eefc544353749232d2ce1619c520031e1422ed977ee34c94f6b964302a5","bytes":10989,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"d6e6c51bef662ce456b5ceef9854ad2fc6292edda7e7626c981301f56af92028","bytes":1995,"lines":24},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"5886a5ac960273e2c99dadce6dfab04a33d19693ebff52152d0adb04e8f90b61","bytes":724,"lines":20},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"9e3653c7eab86996ea6b53cc7e34dc562ce9dfd1dadc5c5f4260c8cadb691c1d","bytes":1877,"lines":28},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"455f5a1cca6c96d2cfd0cf63a95c5ac30ab449efeb068a45223ad5a77c86d05f","bytes":1688,"lines":26},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"a01ade272ecc05666e6319d9597006f0a5282696ec0c1380e867023770d98fe9","bytes":1895,"lines":30},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/README.md","sha256":"88ebcd33c59fe50d027fee4f547be7b926bbc1a0fcfd6225da5dc99fdf3757e8","bytes":1185,"lines":13},{"path":"docs/doctoring/REFERENCES.md","sha256":"052c11574d523c58e646e85223d953413d4e1dcb540ad21c22d8c0acd9ff674f","bytes":6073,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"3e4e5d3f896a874eb152fe7252672cd576e993c0755fb33ec303d5dc4a317f01","bytes":18988,"lines":644},{"path":"scripts/foundation-contract-core.mjs","sha256":"e5d0ae908df567b3c5d441d72cdd7f4c6f71c3dbb22c7e41ddf6788189e10a5a","bytes":25393,"lines":642},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"e9633d28ff7d66db2930b6c6e199dd9aaaacbae48c4b9aa939029f2a19fbacac","bytes":14101,"lines":368},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"cff2e294bd6cfb86df31936683b45e6196be51cf2eeafe4a83dc2a03eea0a506","bytes":24429,"lines":578}]} \ No newline at end of file +{ + "package": "orgmetra-foundation-pack", + "version": "0.1.0", + "generated_for_branch": "develop", + "files": [ + { + "path": ".github/workflows/foundation-ci.yml", + "sha256": "bdc700b6b9e1054b5a89d6e6e91826e213ead3a9d8e0e098078aaa72b03b06e8", + "bytes": 4328, + "lines": 122 + }, + { + "path": ".gitignore", + "sha256": "f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3", + "bytes": 388, + "lines": 38 + }, + { + "path": "AGENTS.md", + "sha256": "28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16", + "bytes": 2246, + "lines": 34 + }, + { + "path": "ARCHITECTURE.md", + "sha256": "bddc0023259306f2b9de2a67160aa5523d140270691ba42816c34c362e342d7d", + "bytes": 7809, + "lines": 107 + }, + { + "path": "CHANGELOG.md", + "sha256": "15bfd1a01653f4c2f654f91c0a75e1e8124f0050ff031f86eb3dc73b16ac7d1e", + "bytes": 16770, + "lines": 77 + }, + { + "path": "CLAUDE.md", + "sha256": "add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f", + "bytes": 1229, + "lines": 20 + }, + { + "path": "LICENSE", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "bytes": 11358, + "lines": 202 + }, + { + "path": "NOTICE", + "sha256": "34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042", + "bytes": 305, + "lines": 4 + }, + { + "path": "README.md", + "sha256": "bc6ea6939bb94841c59c238a720b2540724305435915f06f7f754260e85e759c", + "bytes": 3933, + "lines": 82 + }, + { + "path": "apps/hr-workspace/app.js", + "sha256": "32b55bfb34b0081aa5b2a712a1b248c8fe86650871b69cbafa758ba7bff6cf45", + "bytes": 10708, + "lines": 89 + }, + { + "path": "apps/hr-workspace/index.html", + "sha256": "037beda6f1ed1690f81eb620718d5b9a3ba4b16f205009dfb267673ef1496b14", + "bytes": 13408, + "lines": 113 + }, + { + "path": "apps/hr-workspace/styles.css", + "sha256": "f352dde5dcffccbed2f96ea0ab7c3198f945a16a0aba4d9c59e55aec6b618ef2", + "bytes": 9424, + "lines": 111 + }, + { + "path": "database/migrations/0001_foundation_schema.sql", + "sha256": "ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd", + "bytes": 38747, + "lines": 916 + }, + { + "path": "database/migrations/0002_sealed_evidence_digest.sql", + "sha256": "93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c", + "bytes": 6649, + "lines": 202 + }, + { + "path": "database/migrations/0003_audit_outbox_persistence.sql", + "sha256": "2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc", + "bytes": 15417, + "lines": 423 + }, + { + "path": "database/migrations/0004_outbox_delivery_claim.sql", + "sha256": "d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef", + "bytes": 9451, + "lines": 234 + }, + { + "path": "database/migrations/0005_outbox_delivery_finalization.sql", + "sha256": "b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961", + "bytes": 6125, + "lines": 170 + }, + { + "path": "database/migrations/0006_outbox_delivery_dead_letter.sql", + "sha256": "c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7", + "bytes": 24919, + "lines": 628 + }, + { + "path": "database/migrations/0007_outbox_retry_exhaustion.sql", + "sha256": "812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5", + "bytes": 19081, + "lines": 476 + }, + { + "path": "database/migrations/0008_audit_outbox_review_hardening.sql", + "sha256": "c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b", + "bytes": 17562, + "lines": 448 + }, + { + "path": "database/migrations/0009_candidate_worker_conversion_governance.sql", + "sha256": "4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9", + "bytes": 11537, + "lines": 281 + }, + { + "path": "database/migrations/0010_validity_study_case_integrity.sql", + "sha256": "3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1", + "bytes": 11979, + "lines": 313 + }, + { + "path": "database/migrations/0011_criterion_observation_scope.sql", + "sha256": "f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9", + "bytes": 7444, + "lines": 165 + }, + { + "path": "database/migrations/0012_people_mutation_idempotency.sql", + "sha256": "52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69", + "bytes": 3162, + "lines": 76 + }, + { + "path": "docs/API_CONTRACT.md", + "sha256": "63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589", + "bytes": 4555, + "lines": 76 + }, + { + "path": "docs/DATA_MODEL.md", + "sha256": "6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a", + "bytes": 13366, + "lines": 85 + }, + { + "path": "docs/ERD.md", + "sha256": "546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe", + "bytes": 6964, + "lines": 70 + }, + { + "path": "docs/OPERABILITY.md", + "sha256": "82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62", + "bytes": 11189, + "lines": 71 + }, + { + "path": "docs/PRD.md", + "sha256": "b1c271bcbfc8885e275c43fee1a0516e87a655016fd11412abcfd8ea3037f34a", + "bytes": 5795, + "lines": 116 + }, + { + "path": "docs/SECURITY.md", + "sha256": "01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac", + "bytes": 11185, + "lines": 64 + }, + { + "path": "docs/STORYBOARD.md", + "sha256": "6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2", + "bytes": 1342, + "lines": 28 + }, + { + "path": "docs/STORYBOOK.md", + "sha256": "264d2d3afc303feb35743928fc9c964921504185b789d73cb68d55e37700aef8", + "bytes": 1937, + "lines": 60 + }, + { + "path": "docs/TEST_STRATEGY.md", + "sha256": "d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8", + "bytes": 16534, + "lines": 135 + }, + { + "path": "docs/THREAT_MODEL.md", + "sha256": "f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252", + "bytes": 6736, + "lines": 23 + }, + { + "path": "docs/TRACEABILITY.md", + "sha256": "732682f1c3a459a1903b1c8b473070f8dc3f306fdc32aa0deda36f218e0d0281", + "bytes": 11347, + "lines": 41 + }, + { + "path": "docs/TRD.md", + "sha256": "23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077", + "bytes": 9064, + "lines": 101 + }, + { + "path": "docs/UML.md", + "sha256": "fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9", + "bytes": 5528, + "lines": 122 + }, + { + "path": "docs/USER_STORIES.md", + "sha256": "5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f", + "bytes": 2670, + "lines": 37 + }, + { + "path": "docs/WIREFRAMES.md", + "sha256": "b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e", + "bytes": 2005, + "lines": 77 + }, + { + "path": "docs/adr/0001-orgmetra-authoritative-hris-record.md", + "sha256": "d6e6c51bef662ce456b5ceef9854ad2fc6292edda7e7626c981301f56af92028", + "bytes": 1995, + "lines": 24 + }, + { + "path": "docs/adr/0002-federated-cwl-integration-boundaries.md", + "sha256": "5886a5ac960273e2c99dadce6dfab04a33d19693ebff52152d0adb04e8f90b61", + "bytes": 724, + "lines": 20 + }, + { + "path": "docs/adr/0003-bitemporal-hris-data-contract.md", + "sha256": "9e3653c7eab86996ea6b53cc7e34dc562ce9dfd1dadc5c5f4260c8cadb691c1d", + "bytes": 1877, + "lines": 28 + }, + { + "path": "docs/adr/0004-employment-position-version-and-assignment-binding.md", + "sha256": "455f5a1cca6c96d2cfd0cf63a95c5ac30ab449efeb068a45223ad5a77c86d05f", + "bytes": 1688, + "lines": 26 + }, + { + "path": "docs/adr/0005-exclusive-employment-and-staffable-seats.md", + "sha256": "a01ade272ecc05666e6319d9597006f0a5282696ec0c1380e867023770d98fe9", + "bytes": 1895, + "lines": 30 + }, + { + "path": "docs/adr/0006-governed-audit-outbox-envelope.md", + "sha256": "827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd", + "bytes": 14100, + "lines": 66 + }, + { + "path": "docs/adr/0008-purpose-bound-pii-authorization.md", + "sha256": "c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7", + "bytes": 5988, + "lines": 55 + }, + { + "path": "docs/adr/0009-performance-criterion-observation-scope.md", + "sha256": "1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64", + "bytes": 7057, + "lines": 57 + }, + { + "path": "docs/adr/0015-product-technical-gap-baseline.md", + "sha256": "9d4227bd712a12c4dd7b0d3d408a3e1b464d535816bebb28cba6797cf62fdbb0", + "bytes": 3037, + "lines": 32 + }, + { + "path": "docs/adr/README.md", + "sha256": "54c0930cedb31b0a227eed46ccfe220975a37ede79939c4fafc6e9bacc0a9120", + "bytes": 1791, + "lines": 18 + }, + { + "path": "docs/doctoring/REFERENCES.md", + "sha256": "ad91cae6bb2d8b1fc3f89ffc1657e234173fffa4f7205c6824d9b21da0e77031", + "bytes": 7344, + "lines": 85 + }, + { + "path": "docs/product-technical-gap-baseline.md", + "sha256": "d4901696b3e687ff1cb9c44c6947bb12ea10e573ceeaac09be8f4f512068e344", + "bytes": 18954, + "lines": 147 + }, + { + "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", + "sha256": "b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd", + "bytes": 8227, + "lines": 226 + }, + { + "path": "docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md", + "sha256": "4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d", + "bytes": 6237, + "lines": 187 + }, + { + "path": "package.json", + "sha256": "e73b544d08bbf619305d972b811f9b0c541992258e247874517c97a115d2b3e5", + "bytes": 501, + "lines": 11 + }, + { + "path": "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", + "sha256": "3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190", + "bytes": 7707, + "lines": 160 + }, + { + "path": "packages/hris-kernel/tests/test_audit_outbox.py", + "sha256": "5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c", + "bytes": 7556, + "lines": 200 + }, + { + "path": "schemas/openapi.yaml", + "sha256": "3e4e5d3f896a874eb152fe7252672cd576e993c0755fb33ec303d5dc4a317f01", + "bytes": 18988, + "lines": 644 + }, + { + "path": "scripts/foundation-contract-core.mjs", + "sha256": "1ff70813bba29fb426f38d81a499e32adc7ab429b1788d1da3a30dd52b0e4d73", + "bytes": 25659, + "lines": 649 + }, + { + "path": "scripts/foundation-contract.mjs", + "sha256": "5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a", + "bytes": 218, + "lines": 6 + }, + { + "path": "tests/dispatcher-inventory.test.mjs", + "sha256": "09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261", + "bytes": 1597, + "lines": 34 + }, + { + "path": "tests/foundation-contract.test.mjs", + "sha256": "e9633d28ff7d66db2930b6c6e199dd9aaaacbae48c4b9aa939029f2a19fbacac", + "bytes": 14101, + "lines": 368 + }, + { + "path": "tests/hr-workspace.test.mjs", + "sha256": "857dae13d6c70d42bfd19971b4f0354c078c88965a693cb016055fd0af142e77", + "bytes": 1718, + "lines": 36 + }, + { + "path": "tests/openapi-contract.test.mjs", + "sha256": "80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc", + "bytes": 6438, + "lines": 195 + }, + { + "path": "tests/test_audit_outbox_hardening_postgres.sh", + "sha256": "518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0", + "bytes": 13396, + "lines": 333 + }, + { + "path": "tests/test_audit_outbox_postgres.sh", + "sha256": "e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2", + "bytes": 13443, + "lines": 357 + }, + { + "path": "tests/test_bitemporal_postgres.sh", + "sha256": "7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc", + "bytes": 8209, + "lines": 230 + }, + { + "path": "tests/test_candidate_worker_conversion_postgres.sh", + "sha256": "681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90", + "bytes": 14673, + "lines": 344 + }, + { + "path": "tests/test_criterion_observation_scope_postgres.sh", + "sha256": "0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d", + "bytes": 17811, + "lines": 469 + }, + { + "path": "tests/test_evidence_sealing_postgres.sh", + "sha256": "57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7", + "bytes": 11349, + "lines": 370 + }, + { + "path": "tests/test_operational_uuid_postgres.sh", + "sha256": "7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7", + "bytes": 3346, + "lines": 101 + }, + { + "path": "tests/test_outbox_claim_postgres.sh", + "sha256": "1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b", + "bytes": 14817, + "lines": 429 + }, + { + "path": "tests/test_outbox_dead_letter_postgres.sh", + "sha256": "0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d", + "bytes": 14008, + "lines": 377 + }, + { + "path": "tests/test_people_mutation_idempotency_postgres.sh", + "sha256": "3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5", + "bytes": 16191, + "lines": 381 + }, + { + "path": "tests/test_tenant_isolation_postgres.sh", + "sha256": "dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a", + "bytes": 15134, + "lines": 388 + }, + { + "path": "tests/test_validity_study_case_postgres.sh", + "sha256": "0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02", + "bytes": 14708, + "lines": 301 + }, + { + "path": "tests/validate_repository.py", + "sha256": "eeccebdf6a5d07340571a1ecc42e21dd2fb241b59e614fed4b4151e8e43810c1", + "bytes": 24612, + "lines": 584 + } + ] +} diff --git a/package.json b/package.json index ceb2fb8fc..dcef5c127 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,10 @@ "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", + "test:workspace": "node --test tests/hr-workspace.test.mjs" } } 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/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 6bf66ba36..904360ade 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -20,6 +20,9 @@ export const REQUIRED_FILES = Object.freeze([ 'NOTICE', 'manifest.json', 'package.json', + 'apps/hr-workspace/index.html', + 'apps/hr-workspace/styles.css', + 'apps/hr-workspace/app.js', '.github/workflows/foundation-ci.yml', 'docs/PRD.md', 'docs/TRD.md', @@ -36,6 +39,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', @@ -45,6 +49,7 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/adr/0006-governed-audit-outbox-envelope.md', 'docs/adr/0008-purpose-bound-pii-authorization.md', 'docs/adr/0009-performance-criterion-observation-scope.md', + 'docs/adr/0015-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', @@ -67,6 +72,7 @@ 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/openapi-contract.test.mjs', 'tests/test_bitemporal_postgres.sh', 'tests/test_tenant_isolation_postgres.sh', @@ -85,6 +91,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_develop', 'implemented_on_protected_main', 'implemented_on_active_pr', 'accepted_architecture', diff --git a/services/people-api/pyproject.toml b/services/people-api/pyproject.toml index 3f228e11c..6502d2e54 100644 --- a/services/people-api/pyproject.toml +++ b/services/people-api/pyproject.toml @@ -7,7 +7,7 @@ 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 = [ @@ -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/tests/hr-workspace.test.mjs b/tests/hr-workspace.test.mjs new file mode 100644 index 000000000..bca768bd6 --- /dev/null +++ b/tests/hr-workspace.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { isPurposeAuthorized, nextLocale } 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'); + +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(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('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(readFileSync(new URL('../apps/hr-workspace/app.js', import.meta.url), 'utf8'), /no API mutation was sent/); + assert.doesNotMatch(html, /password|passkey_value|private_key/i); +}); diff --git a/tests/validate_repository.py b/tests/validate_repository.py index 3b7eecc83..51173df0f 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -23,6 +23,9 @@ "NOTICE", "manifest.json", "package.json", + "apps/hr-workspace/index.html", + "apps/hr-workspace/styles.css", + "apps/hr-workspace/app.js", ".github/workflows/foundation-ci.yml", "docs/PRD.md", "docs/TRD.md", @@ -39,6 +42,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", @@ -48,6 +52,7 @@ "docs/adr/0006-governed-audit-outbox-envelope.md", "docs/adr/0008-purpose-bound-pii-authorization.md", "docs/adr/0009-performance-criterion-observation-scope.md", + "docs/adr/0015-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", @@ -70,6 +75,7 @@ "scripts/foundation-contract.mjs", "tests/dispatcher-inventory.test.mjs", "tests/foundation-contract.test.mjs", + "tests/hr-workspace.test.mjs", "tests/openapi-contract.test.mjs", "tests/test_bitemporal_postgres.sh", "tests/test_tenant_isolation_postgres.sh", @@ -130,7 +136,7 @@ def _expected_manifest_document() -> dict[str, Any]: return { "package": "orgmetra-foundation-pack", "version": "0.1.0", - "generated_for_branch": "feat/audit-outbox-envelope", + "generated_for_branch": "develop", "files": files, } @@ -144,10 +150,10 @@ 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("generated_for_branch") != "develop": _fail( "manifest generated_for_branch must identify the active generation branch " - "feat/audit-outbox-envelope" + "develop" ) entries: dict[str, dict[str, Any]] = {} From 09a4743c950b17360c6b95e9706478ed8c6c6989 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:09:28 +0900 Subject: [PATCH 002/110] docs: track workspace slice in product gaps --- docs/TRACEABILITY.md | 1 + docs/product-technical-gap-baseline.md | 3 ++- manifest.json | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 7480a0ff8..e7dbef3ac 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -15,6 +15,7 @@ | 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 | | 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_protected_develop | | 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_develop | +| HR Home and Employee Profile fixture | Product / web workspace | `apps/hr-workspace/index.html`, `apps/hr-workspace/app.js`, shared design tokens, Figma nodes `1:10` and `1:28` | `npm run validate`; local HTTP smoke; explicit fixture/API, permission, confirmation, exact-value, focus, and locale states; browser and connected API evidence remain 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` | 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 | implemented_on_protected_develop | | 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 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 | diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 405489423..ec7fd64cb 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -74,7 +74,7 @@ checked, and merged independently before P0-1 can change maturity. | ID | Priority and owner | Gap and smallest acceptable closure evidence | Dependency | |---|---|---|---| -| P0-1 | Product / Web | Ship one Employee Profile + HR Home vertical slice backed by the protected People API, using existing tokens and Figma source. Prove keyboard/focus/permission/confirmation states, exact-value tables, i18n, and browser E2E. | Protected People API evidence | +| P0-1 | Product / Web | Review and merge the active HR Home + Employee Profile fixture, 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 | Merge and verify PR #38 as one persisted Job Analysis case: one migration owner, one ADR, versioned source evidence, SME approval, retrieval API, and protected PostgreSQL acceptance test. | Protected People API evidence | | P0-3 | Workforce Validation / scientific owner | Add a Rust-first estimator boundary or a 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. | 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 | @@ -89,6 +89,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| +| 53 | `codex/product-gap-baseline-workspace` / see live PR #53 for current exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs queued; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | | 38 | `cursor/job-analysis-snapshot-5eef` / `21949fba6671e24fae2f719ea53cda0e363e8874` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; two unresolved threads are outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `b64c4118905861ca8bf8b220764a411e5e92e5f7` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; all sampled checks green; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index 67babf3d7..d03d77d33 100644 --- a/manifest.json +++ b/manifest.json @@ -209,9 +209,9 @@ }, { "path": "docs/TRACEABILITY.md", - "sha256": "732682f1c3a459a1903b1c8b473070f8dc3f306fdc32aa0deda36f218e0d0281", - "bytes": 11347, - "lines": 41 + "sha256": "c3e9e94816069a90eac7677b13cd6961e19c2a088c76c560638edca0c78ec857", + "bytes": 11761, + "lines": 42 }, { "path": "docs/TRD.md", @@ -305,9 +305,9 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "d4901696b3e687ff1cb9c44c6947bb12ea10e573ceeaac09be8f4f512068e344", - "bytes": 18954, - "lines": 147 + "sha256": "d44b4e7fb383da1e498c0cabc99ac5e07a107f772bd2df632827be6914f8f47d", + "bytes": 19347, + "lines": 148 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", From 7a57731c2a6862a0561f4d51b1e07fb21ecc123f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:18:37 +0900 Subject: [PATCH 003/110] docs: refresh current job-analysis head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ec7fd64cb..aaf852d81 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -90,7 +90,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| | 53 | `codex/product-gap-baseline-workspace` / see live PR #53 for current exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs queued; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | -| 38 | `cursor/job-analysis-snapshot-5eef` / `21949fba6671e24fae2f719ea53cda0e363e8874` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; two unresolved threads are outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | +| 38 | `cursor/job-analysis-snapshot-5eef` / `6440e0b6d2c50263a411cfea39ce2038c7d5b7ac` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; current review threads are resolved or outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `b64c4118905861ca8bf8b220764a411e5e92e5f7` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; all sampled checks green; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index d03d77d33..07898a3bf 100644 --- a/manifest.json +++ b/manifest.json @@ -305,8 +305,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "d44b4e7fb383da1e498c0cabc99ac5e07a107f772bd2df632827be6914f8f47d", - "bytes": 19347, + "sha256": "12eba2c4a8390d29beb7ed61fabab34ac4a64fa2adb0be9565d5a27eb2225ec2", + "bytes": 19359, "lines": 148 }, { From dcc52834850d89892446339bc81ede2cdabd2586 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:28:24 +0900 Subject: [PATCH 004/110] feat: add Storybook workspace state runtime --- .gitignore | 1 + .storybook/main.js | 8 + .storybook/preview.js | 13 + CHANGELOG.md | 1 + apps/hr-workspace/styles.css | 6 + apps/hr-workspace/workspace.stories.js | 88 + docs/PRD.md | 7 +- docs/STORYBOOK.md | 18 +- docs/TRACEABILITY.md | 2 +- .../0015-product-technical-gap-baseline.md | 2 +- docs/product-technical-gap-baseline.md | 15 +- manifest.json | 92 +- package-lock.json | 3214 ++++++++++++++++- package.json | 8 +- scripts/foundation-contract-core.mjs | 4 + tests/hr-workspace.test.mjs | 14 + tests/validate_repository.py | 6 + 17 files changed, 3445 insertions(+), 54 deletions(-) create mode 100644 .storybook/main.js create mode 100644 .storybook/preview.js create mode 100644 apps/hr-workspace/workspace.stories.js diff --git a/.gitignore b/.gitignore index 9f2c96bec..dfc9555ba 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ node_modules/ dist/ coverage/ .turbo/ +storybook-static/ # Rust /target/ 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/CHANGELOG.md b/CHANGELOG.md index 47f7f3026..e17c0e04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to Orgmetra will be documented in this file. ### Added - 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. - 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 hourly 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 for `services/people-api`, so local tests discover the owned Keyverse adapter without an undocumented `PYTHONPATH` workaround. - 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. diff --git a/apps/hr-workspace/styles.css b/apps/hr-workspace/styles.css index 816869987..cf1a608c1 100644 --- a/apps/hr-workspace/styles.css +++ b/apps/hr-workspace/styles.css @@ -90,6 +90,11 @@ tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; } .field-wide { grid-column: 1 / -1; } textarea { resize: vertical; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); 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; } @@ -108,4 +113,5 @@ textarea { resize: vertical; } .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..8f1958983 --- /dev/null +++ b/apps/hr-workspace/workspace.stories.js @@ -0,0 +1,88 @@ +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 ``; +}; + +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 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
+
+ `) +}; diff --git a/docs/PRD.md b/docs/PRD.md index 61124b175..579ddbb52 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -60,9 +60,10 @@ Current HR systems often separate job architecture, recruiting, assessment, empl - Validation dashboard shell. The checkout also contains a local fixture slice at `apps/hr-workspace/` for -HR Home and Employee Profile. 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. +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. ### P2 scale diff --git a/docs/STORYBOOK.md b/docs/STORYBOOK.md index 3d74fece6..ba1d5e999 100644 --- a/docs/STORYBOOK.md +++ b/docs/STORYBOOK.md @@ -56,5 +56,19 @@ Employee Profile and HR Home experience. It consumes the shared CSS tokens, keeps the Figma node IDs in the markup, and exercises evidence review, purpose-bound permission denial, high-impact confirmation, exact allocation values, and English/Korean labels. It is fixture mode: it does not claim a -connected API or a deployed Storybook runtime. A full Storybook runner is -deferred until a second workspace justifies extracting shared components. +connected API or a deployed customer workflow. + +## 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, 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; it does not replace connected People API or browser E2E +evidence. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index e7dbef3ac..630ae96c9 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -15,7 +15,7 @@ | 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 | | 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_protected_develop | | 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_develop | -| HR Home and Employee Profile fixture | Product / web workspace | `apps/hr-workspace/index.html`, `apps/hr-workspace/app.js`, shared design tokens, Figma nodes `1:10` and `1:28` | `npm run validate`; local HTTP smoke; explicit fixture/API, permission, confirmation, exact-value, focus, and locale states; browser and connected API evidence remain open | ADR-0015, `docs/STORYBOOK.md` | implemented_on_active_pr | +| HR Home and Employee Profile fixture and Storybook state runtime | Product / web workspace | `apps/hr-workspace/index.html`, `apps/hr-workspace/app.js`, `apps/hr-workspace/workspace.stories.js`, `.storybook/`, shared design tokens, Figma nodes `1:10` and `1:28` | `npm run validate`, `npm run build-storybook`; local HTTP smoke; explicit fixture/API, permission, confirmation, exact-value, focus, and locale states; browser and connected API evidence remain 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` | 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 | implemented_on_protected_develop | | 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 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 | diff --git a/docs/adr/0015-product-technical-gap-baseline.md b/docs/adr/0015-product-technical-gap-baseline.md index 803927d4b..70457db4e 100644 --- a/docs/adr/0015-product-technical-gap-baseline.md +++ b/docs/adr/0015-product-technical-gap-baseline.md @@ -18,7 +18,7 @@ The product baseline also uses a Figma wireframe source and repeated HR actions. 4. The authoritative People mutation and confirmed-hire paths are now the first protected integration lane. They remain the dependency for browser workflows and must preserve tenant authorization, idempotency, bitemporal history, human confirmation, audit, and outbox atomicity. 5. Job-analysis persistence/API work now converges on the single current PR #38 model, with migration 0013 and its own ADR; a second Task/FJA/KSAO store must not be introduced. 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 and Storybook remain design-system inputs, not evidence that a customer UI exists. A release claim requires executable UI, keyboard/accessibility checks, interaction tests, and browser evidence for the owning workflow. +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 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index aaf852d81..404b7eab5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -19,7 +19,7 @@ This document is the buyer-facing work queue. It separates what a customer can u ## Executive finding -Orgmetra is an evidence-centered HRIS foundation with a protected People mutation and confirmed-hire implementation, 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 People read/write boundaries. The largest buyer-visible gap is now the missing browser product surface: the repository has contracts and service code, but no runnable Employee Profile, HR Home, recruiting, or validation workspace with browser evidence. +Orgmetra is an evidence-centered HRIS foundation with a protected People mutation and confirmed-hire implementation, 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 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 and local Storybook state runtime, but protected truth has no connected or browser-verified workspace. The next highest-leverage gaps are one canonical persisted Job Analysis case/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. @@ -49,7 +49,7 @@ flowchart LR | Job-analysis persistence/API | PR #38 is the current canonical persistence lane, rebuilt on protected `39d3c15`; PR #30 is closed as superseded | `implemented_on_active_pr` | A buyer cannot yet create, approve, version, or retrieve one canonical persisted Job Analysis case. | | 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. | -| Role workspaces and Storybook runtime | Figma wireframes, design tokens, and Storybook inventory only; no web application package or stories | `planned` | There is no buyer-clickable HR Home, Employee Profile, recruiting, validation, or admin workflow in this checkout. | +| Role workspaces and Storybook runtime | Protected `develop` still has no customer UI; active PR #53 has the HR Home/Employee Profile fixture plus a local Storybook `10.5.10` build | `planned` | The local component/state runtime is 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. | @@ -65,16 +65,17 @@ and Employee Profile fixture based on Figma nodes `1:10` and `1:28`. 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; -it is not browser E2E, hosted release, protected-develop truth, or evidence of -completed Storybook runtime integration. The artifact must be reviewed, -checked, and merged independently before P0-1 can change maturity. +fixture explicitly displays that the protected People API is not connected. +The same active PR includes a local Storybook runtime with tokenized stories; +`npm run build-storybook` is a build check, not browser E2E, hosted release, 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, 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-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 | Merge and verify PR #38 as one persisted Job Analysis case: one migration owner, one ADR, versioned source evidence, SME approval, retrieval API, and protected PostgreSQL acceptance test. | Protected People API evidence | | P0-3 | Workforce Validation / scientific owner | Add a Rust-first estimator boundary or a 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. | 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 | diff --git a/manifest.json b/manifest.json index 07898a3bf..2425acf8e 100644 --- a/manifest.json +++ b/manifest.json @@ -11,9 +11,21 @@ }, { "path": ".gitignore", - "sha256": "f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3", - "bytes": 388, - "lines": 38 + "sha256": "80c3bfec4d01d6992e41ac1dc63d1656e72279273764a42f70174838f09ff2cd", + "bytes": 406, + "lines": 39 + }, + { + "path": ".storybook/main.js", + "sha256": "210d8108637ccea90d2c52e2eec793a9215cc512012a2292ab4abf50983a134d", + "bytes": 253, + "lines": 8 + }, + { + "path": ".storybook/preview.js", + "sha256": "0f02687d3c76745afea6402607b06b893a0e88f8414dba4d79e39f50f4d17817", + "bytes": 336, + "lines": 13 }, { "path": "AGENTS.md", @@ -29,9 +41,9 @@ }, { "path": "CHANGELOG.md", - "sha256": "15bfd1a01653f4c2f654f91c0a75e1e8124f0050ff031f86eb3dc73b16ac7d1e", - "bytes": 16770, - "lines": 77 + "sha256": "3e175e662a9052837fa75a524ca00ab8aeb7a624f6255420f4b2f2cb6156b8e4", + "bytes": 17092, + "lines": 78 }, { "path": "CLAUDE.md", @@ -71,9 +83,15 @@ }, { "path": "apps/hr-workspace/styles.css", - "sha256": "f352dde5dcffccbed2f96ea0ab7c3198f945a16a0aba4d9c59e55aec6b618ef2", - "bytes": 9424, - "lines": 111 + "sha256": "fb44828e958e164e515da587256966f805ea115b172e4b584c4fef8e6b26322a", + "bytes": 10013, + "lines": 117 + }, + { + "path": "apps/hr-workspace/workspace.stories.js", + "sha256": "2a01f2846485fe9501877da2a8e45ad4363312bfc45ab51d9b57e520250cd689", + "bytes": 5372, + "lines": 88 }, { "path": "database/migrations/0001_foundation_schema.sql", @@ -173,9 +191,9 @@ }, { "path": "docs/PRD.md", - "sha256": "b1c271bcbfc8885e275c43fee1a0516e87a655016fd11412abcfd8ea3037f34a", - "bytes": 5795, - "lines": 116 + "sha256": "bf73c0ae68f19a24beebd8a43b2e2888ad2e6844697aab8ab5ada59f6c8d87d5", + "bytes": 5852, + "lines": 117 }, { "path": "docs/SECURITY.md", @@ -191,9 +209,9 @@ }, { "path": "docs/STORYBOOK.md", - "sha256": "264d2d3afc303feb35743928fc9c964921504185b789d73cb68d55e37700aef8", - "bytes": 1937, - "lines": 60 + "sha256": "ddfbddea1a4cb82936ee72cd489195e271cc01556516f00d1ab369d8e372f388", + "bytes": 2511, + "lines": 74 }, { "path": "docs/TEST_STRATEGY.md", @@ -209,8 +227,8 @@ }, { "path": "docs/TRACEABILITY.md", - "sha256": "c3e9e94816069a90eac7677b13cd6961e19c2a088c76c560638edca0c78ec857", - "bytes": 11761, + "sha256": "0452bd2bd28e1e236b622742de745afb7ae9b886f5cb1517e3a603a46e7fd84b", + "bytes": 11873, "lines": 42 }, { @@ -287,8 +305,8 @@ }, { "path": "docs/adr/0015-product-technical-gap-baseline.md", - "sha256": "9d4227bd712a12c4dd7b0d3d408a3e1b464d535816bebb28cba6797cf62fdbb0", - "bytes": 3037, + "sha256": "49430c953798c23c453ac71e41705e216c95b737f9488c934f6329b46e6ae6ff", + "bytes": 3107, "lines": 32 }, { @@ -305,9 +323,9 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "12eba2c4a8390d29beb7ed61fabab34ac4a64fa2adb0be9565d5a27eb2225ec2", - "bytes": 19359, - "lines": 148 + "sha256": "32cb5d4a0d5b2522ac22c090e2d5952dc36679e2725a26e67668b8d6c0a00f31", + "bytes": 19531, + "lines": 149 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", @@ -321,11 +339,17 @@ "bytes": 6237, "lines": 187 }, + { + "path": "package-lock.json", + "sha256": "69089cfedb463784e053e70ec1af456bb4a0f2bbbd5c8cb7c79e75e90c279e81", + "bytes": 105932, + "lines": 3220 + }, { "path": "package.json", - "sha256": "e73b544d08bbf619305d972b811f9b0c541992258e247874517c97a115d2b3e5", - "bytes": 501, - "lines": 11 + "sha256": "94d134e202416fc50298fed8812aed02fb59f9023458da32dbf2b5208ae5da54", + "bytes": 691, + "lines": 17 }, { "path": "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", @@ -347,9 +371,9 @@ }, { "path": "scripts/foundation-contract-core.mjs", - "sha256": "1ff70813bba29fb426f38d81a499e32adc7ab429b1788d1da3a30dd52b0e4d73", - "bytes": 25659, - "lines": 649 + "sha256": "3c575fa99f3f4b0303d8586cdfd1cfbcf2ab4902dcc31b610ca6a17acba1f1c5", + "bytes": 25777, + "lines": 653 }, { "path": "scripts/foundation-contract.mjs", @@ -371,9 +395,9 @@ }, { "path": "tests/hr-workspace.test.mjs", - "sha256": "857dae13d6c70d42bfd19971b4f0354c078c88965a693cb016055fd0af142e77", - "bytes": 1718, - "lines": 36 + "sha256": "ff233f8228c1cc80bc54cfc8c030bff79334e461eadcd3497f5a632b2b3c5e67", + "bytes": 2634, + "lines": 50 }, { "path": "tests/openapi-contract.test.mjs", @@ -455,9 +479,9 @@ }, { "path": "tests/validate_repository.py", - "sha256": "eeccebdf6a5d07340571a1ecc42e21dd2fb241b59e614fed4b4151e8e43810c1", - "bytes": 24612, - "lines": 584 + "sha256": "b0fcbd17ae7f2ba0a9112e6f5045f68b1508de3ff15ad3cfffc4eaac901d26d1", + "bytes": 24833, + "lines": 590 } ] } diff --git a/package-lock.json b/package-lock.json index 73c9101ac..ec90f6a78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,3219 @@ { - "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": { + "@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/@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/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 dcef5c127..a16694d40 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,12 @@ "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 tests/hr-workspace.test.mjs", - "test:workspace": "node --test tests/hr-workspace.test.mjs" + "test:workspace": "node --test tests/hr-workspace.test.mjs", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "devDependencies": { + "@storybook/web-components-vite": "^10.5.10", + "storybook": "^10.5.10" } } diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 904360ade..3f26a4f54 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -20,9 +20,13 @@ 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', 'docs/PRD.md', 'docs/TRD.md', diff --git a/tests/hr-workspace.test.mjs b/tests/hr-workspace.test.mjs index bca768bd6..67e5a3bc7 100644 --- a/tests/hr-workspace.test.mjs +++ b/tests/hr-workspace.test.mjs @@ -5,6 +5,9 @@ import { isPurposeAuthorized, nextLocale } 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 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/); @@ -26,6 +29,17 @@ test('workspace includes keyboard-accessible review and high-impact states', () assert.match(html, /Exact assignment allocation values/); }); +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']) { + 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"/); +}); + test('purpose and locale transitions preserve the trust boundary', () => { assert.equal(isPurposeAuthorized('hr_operations'), true); assert.equal(isPurposeAuthorized('recruiting'), false); diff --git a/tests/validate_repository.py b/tests/validate_repository.py index 51173df0f..fe12721b1 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -23,9 +23,13 @@ "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", "docs/PRD.md", "docs/TRD.md", @@ -543,6 +547,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): From 3b41f61f8e91b378bdb28fbf579a9093df8b801b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:34:24 +0900 Subject: [PATCH 005/110] ci: verify Storybook build --- .github/workflows/foundation-ci.yml | 4 ++++ manifest.json | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 97efb015a..2afddc80d 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -50,6 +50,10 @@ jobs: run: python -m compileall -q tests - name: Validate foundation pack run: npm run validate + - name: Install Node dependencies + run: npm ci + - name: Build Storybook + run: npm run build-storybook - name: Prove Foundation CI dependency hygiene run: bash tests/test_foundation_ci_dependency_hygiene.sh - name: Prove HRIS kernel diff --git a/manifest.json b/manifest.json index 2425acf8e..886459a51 100644 --- a/manifest.json +++ b/manifest.json @@ -5,9 +5,9 @@ "files": [ { "path": ".github/workflows/foundation-ci.yml", - "sha256": "bdc700b6b9e1054b5a89d6e6e91826e213ead3a9d8e0e098078aaa72b03b06e8", - "bytes": 4328, - "lines": 122 + "sha256": "76e9707b902946d9f3953e76aa647b87f572273be3c222df9b82c47ca022688b", + "bytes": 4455, + "lines": 126 }, { "path": ".gitignore", From 58d6d30a7a843ecd61960107506e5a263a777111 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:36:53 +0900 Subject: [PATCH 006/110] docs: refresh job-analysis exact head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 404b7eab5..c7f95c855 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -91,7 +91,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| | 53 | `codex/product-gap-baseline-workspace` / see live PR #53 for current exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs queued; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | -| 38 | `cursor/job-analysis-snapshot-5eef` / `6440e0b6d2c50263a411cfea39ce2038c7d5b7ac` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; current review threads are resolved or outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | +| 38 | `cursor/job-analysis-snapshot-5eef` / `7ece07e3b929ecb24a7509767c582d1a52437f12` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; local exact-head suite is 63 passed with 100% statement/branch coverage plus PostgreSQL 16.14 contract; current review threads are resolved or outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `b64c4118905861ca8bf8b220764a411e5e92e5f7` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; all sampled checks green; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index 886459a51..c52f723bb 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "32cb5d4a0d5b2522ac22c090e2d5952dc36679e2725a26e67668b8d6c0a00f31", - "bytes": 19531, + "sha256": "c150d17ca3adce43c30a7ba7b35dee6e933c24b462dcc91e2cfea9751c06ad30", + "bytes": 19635, "lines": 149 }, { From dbee54de13dc7efeea64590dfae66deeff18df30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:48:27 +0900 Subject: [PATCH 007/110] docs: refresh protected truth exact head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c7f95c855..8197aae61 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -102,7 +102,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | 42 | `feat/selection-outcome-monitoring-plan` / `28b5e9e096c057fd825cf9ff2aa8e9ec5611401f` | Selection outcome monitoring | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 40 | `feat/structured-interview-plan` / `9751d45f215582c722f036fa12f6f0ebe2c7e8f8` | Structured interview plan | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green; no qualifying approval | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 52 | `feat/tepp-analysis-adapter` / `b23c15f548b7ba77dfe43a41bb110b2216cff859` | Governed TEPP request boundary | Draft; not mergeable; stale base `ebad8c1`; all five exact-head runs queued | Keep non-executing; rebase only when the integration contract owner is ready, then reacquire checks and review. | -| 51 | `docs/protected-truth-refresh` / `26fc8c886eea72476ba4e79aaea9ed67db344c47` | Protected truth/documentation repair | Draft; not mergeable; stale base `ebad8c1`; two runs queued and five successful predecessor/current runs | Reconcile this baseline and rebase on `39d3c15`; rerun every applicable gate before review. | +| 51 | `docs/protected-truth-refresh` / `4dc8aa58b5d5b6ab0c69da51ba402e92ec6bb35f` | Protected truth/documentation repair | Draft; not mergeable; stale base `ebad8c1`; exact-head local `npm run validate` passed with 56 Node tests, hosted runs not yet materialized | Reconcile this baseline and rebase on `39d3c15`; rerun every applicable gate before review. | | 37 | `cursor/expand-adr-0001-0003-f381` / `240d2770e5b3109cd1e93c9493d8b6ba685244db` | ADR/reference expansion | Draft; not mergeable; base `f815feed`; only partial checks returned | Close or rebase only after comparing its statements with current protected truth; avoid ADR-number collisions. | No open GitHub Issue was returned by the current `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. diff --git a/manifest.json b/manifest.json index c52f723bb..fc5313c15 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "c150d17ca3adce43c30a7ba7b35dee6e933c24b462dcc91e2cfea9751c06ad30", - "bytes": 19635, + "sha256": "041dd8e23f65f3d6d50d89c0cbca897e944e68ac17d44da2b9d906645d44e146", + "bytes": 19670, "lines": 149 }, { From fa10e78fba6131ca72d63686780aa1b89f0c7a64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:57:41 +0900 Subject: [PATCH 008/110] docs: refresh current job analysis and candidate heads --- docs/product-technical-gap-baseline.md | 4 ++-- manifest.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8197aae61..f188d44a5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -91,8 +91,8 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| | 53 | `codex/product-gap-baseline-workspace` / see live PR #53 for current exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs queued; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | -| 38 | `cursor/job-analysis-snapshot-5eef` / `7ece07e3b929ecb24a7509767c582d1a52437f12` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued; local exact-head suite is 63 passed with 100% statement/branch coverage plus PostgreSQL 16.14 contract; current review threads are resolved or outdated | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | -| 41 | `feat/candidate-evidence-intake` / `b64c4118905861ca8bf8b220764a411e5e92e5f7` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; all sampled checks green; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | +| 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | +| 41 | `feat/candidate-evidence-intake` / `b0889f5bf4f4d1b67d2ffcb05151cad86e6bdf2f` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five hosted checks queued; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 46 | `feat/governed-employment-separation-review` / `ac1cc7cf15ed5c430e7e3ad42d416bd35318fbc1` | Employment separation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index fc5313c15..31b946657 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "041dd8e23f65f3d6d50d89c0cbca897e944e68ac17d44da2b9d906645d44e146", - "bytes": 19670, + "sha256": "bd120a93ee32c77a3f6721b62a218396dcf263fa234f0f7ce75ed04788bd2c4f", + "bytes": 19761, "lines": 149 }, { From e38e77863f79a58d0e80db3a0e5efb8b00ca512b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:58:24 +0900 Subject: [PATCH 009/110] docs: record exact workspace baseline head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index f188d44a5..04908386e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -90,7 +90,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| -| 53 | `codex/product-gap-baseline-workspace` / see live PR #53 for current exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs queued; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / `fa10e78fba6131ca72d63686780aa1b89f0c7a64` | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs pending/queued; local validation passes with 59 Node tests; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | | 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `b0889f5bf4f4d1b67d2ffcb05151cad86e6bdf2f` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five hosted checks queued; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index 31b946657..ea3d51520 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "bd120a93ee32c77a3f6721b62a218396dcf263fa234f0f7ce75ed04788bd2c4f", - "bytes": 19761, + "sha256": "a1db78bbdfd922aa56b1422846db4ad7b68d03000a44092871d2c9efdf313a7c", + "bytes": 19817, "lines": 149 }, { From fd5d97bb9bca8f81865b462b2d6e0f5c4015bfba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:59:22 +0900 Subject: [PATCH 010/110] docs: keep workspace baseline self-referentially safe --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 04908386e..2b0a672de 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -90,7 +90,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| -| 53 | `codex/product-gap-baseline-workspace` / `fa10e78fba6131ca72d63686780aa1b89f0c7a64` | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs pending/queued; local validation passes with 59 Node tests; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / live PR #53 exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs pending/queued; local validation passes with 59 Node tests; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | | 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `b0889f5bf4f4d1b67d2ffcb05151cad86e6bdf2f` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five hosted checks queued; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index ea3d51520..47701cc07 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "a1db78bbdfd922aa56b1422846db4ad7b68d03000a44092871d2c9efdf313a7c", - "bytes": 19817, + "sha256": "eeeade2768d37a5b79b30aafc08d904d175799d87ad2f99d5e43318e742f8427", + "bytes": 19797, "lines": 149 }, { From 73b79e5b73ed0b2bf6664370dafa63222fbff057 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:02:02 +0900 Subject: [PATCH 011/110] docs: record current TEPP adapter head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2b0a672de..fe77c80d6 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -101,7 +101,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | 43 | `feat/governed-offer-approval` / `2975c0c606fa009db35787de5d8104f16db2a3f5` | Offer approval packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 42 | `feat/selection-outcome-monitoring-plan` / `28b5e9e096c057fd825cf9ff2aa8e9ec5611401f` | Selection outcome monitoring | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 40 | `feat/structured-interview-plan` / `9751d45f215582c722f036fa12f6f0ebe2c7e8f8` | Structured interview plan | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green; no qualifying approval | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 52 | `feat/tepp-analysis-adapter` / `b23c15f548b7ba77dfe43a41bb110b2216cff859` | Governed TEPP request boundary | Draft; not mergeable; stale base `ebad8c1`; all five exact-head runs queued | Keep non-executing; rebase only when the integration contract owner is ready, then reacquire checks and review. | +| 52 | `feat/tepp-analysis-adapter` / `447ef27fc79792bc55245d18a7d449945b0e63e4` | Governed TEPP request boundary | Draft; mergeable; current base `39d3c15`; five fresh exact-head runs queued; local TEPP suite 46 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Keep non-executing; wait for terminal exact-head checks and qualifying review before protected merge. | | 51 | `docs/protected-truth-refresh` / `4dc8aa58b5d5b6ab0c69da51ba402e92ec6bb35f` | Protected truth/documentation repair | Draft; not mergeable; stale base `ebad8c1`; exact-head local `npm run validate` passed with 56 Node tests, hosted runs not yet materialized | Reconcile this baseline and rebase on `39d3c15`; rerun every applicable gate before review. | | 37 | `cursor/expand-adr-0001-0003-f381` / `240d2770e5b3109cd1e93c9493d8b6ba685244db` | ADR/reference expansion | Draft; not mergeable; base `f815feed`; only partial checks returned | Close or rebase only after comparing its statements with current protected truth; avoid ADR-number collisions. | diff --git a/manifest.json b/manifest.json index 47701cc07..a3b0c0be9 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "eeeade2768d37a5b79b30aafc08d904d175799d87ad2f99d5e43318e742f8427", - "bytes": 19797, + "sha256": "fb4613260953f60cbd4068804ac0f62db0beba5da07fc5dd005c6b369d8423d0", + "bytes": 19905, "lines": 149 }, { From 2a11d5bb25a10b4e9b7a07e79c012339d24a10ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:03:36 +0900 Subject: [PATCH 012/110] docs: record current candidate evidence head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index fe77c80d6..14751eb44 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -92,7 +92,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed |---:|---|---|---|---| | 53 | `codex/product-gap-baseline-workspace` / live PR #53 exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs pending/queued; local validation passes with 59 Node tests; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | | 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | -| 41 | `feat/candidate-evidence-intake` / `b0889f5bf4f4d1b67d2ffcb05151cad86e6bdf2f` | Candidate evidence intake | Ready; mergeable; base is stale `ebad8c1`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five hosted checks queued; no qualifying approval | Rebase or close after comparing with current protected contracts; do not merge predecessor-base evidence. | +| 41 | `feat/candidate-evidence-intake` / `c7934273f6887daf2f222753fdf9cca46734ad88` | Candidate evidence intake | Ready; mergeable; current base `39d3c15`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five fresh hosted checks queued; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | | 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 46 | `feat/governed-employment-separation-review` / `ac1cc7cf15ed5c430e7e3ad42d416bd35318fbc1` | Employment separation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index a3b0c0be9..74e603c8a 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "fb4613260953f60cbd4068804ac0f62db0beba5da07fc5dd005c6b369d8423d0", - "bytes": 19905, + "sha256": "69153dc1c193eca5bd509b662b841efc211f706c155c594955a4be0f58a2f318", + "bytes": 19889, "lines": 149 }, { From 422e9c7143373bd05fde08f426f62e386fb05d40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:06:38 +0900 Subject: [PATCH 013/110] docs: record current compensation review head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 14751eb44..d09c1616a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -93,7 +93,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | 53 | `codex/product-gap-baseline-workspace` / live PR #53 exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs pending/queued; local validation passes with 59 Node tests; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | | 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `c7934273f6887daf2f222753fdf9cca46734ad88` | Candidate evidence intake | Ready; mergeable; current base `39d3c15`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five fresh hosted checks queued; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 48 | `feat/governed-compensation-change-review` / `e57369fefda046acb2bf2df108316c1b303f99af` | Compensation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 48 | `feat/governed-compensation-change-review` / `fcefb33c12b0d139077fd693f745871aa1f686e0` | Compensation review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 57 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Wait for terminal exact-head checks and qualifying independent approval before protected merge. | | 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 46 | `feat/governed-employment-separation-review` / `ac1cc7cf15ed5c430e7e3ad42d416bd35318fbc1` | Employment separation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | | 45 | `feat/governed-assignment-change-review` / `16d07b62bff8c01a8eff5cedb2a85fb75ac4fb56` | Assignment change review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | diff --git a/manifest.json b/manifest.json index 74e603c8a..14664f07c 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "69153dc1c193eca5bd509b662b841efc211f706c155c594955a4be0f58a2f318", - "bytes": 19889, + "sha256": "f2d7f05bdbcf9fb7958d9e1d5fc22a3b68f99b3d55ac495491608f3c44112aa6", + "bytes": 20025, "lines": 149 }, { From 79f638220d0bc21eeca98a2cbcf765f7f4213549 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:13:06 +0900 Subject: [PATCH 014/110] docs: refresh current review packet heads --- docs/product-technical-gap-baseline.md | 14 +++++++------- manifest.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d09c1616a..0ffda6ca0 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -94,13 +94,13 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | | 41 | `feat/candidate-evidence-intake` / `c7934273f6887daf2f222753fdf9cca46734ad88` | Candidate evidence intake | Ready; mergeable; current base `39d3c15`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five fresh hosted checks queued; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | | 48 | `feat/governed-compensation-change-review` / `fcefb33c12b0d139077fd693f745871aa1f686e0` | Compensation review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 57 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Wait for terminal exact-head checks and qualifying independent approval before protected merge. | -| 47 | `feat/governed-employment-leave-review` / `55cfbb8511e9aaf0d0c9ee9438c2874a3b1070b4` | Employment leave review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 46 | `feat/governed-employment-separation-review` / `ac1cc7cf15ed5c430e7e3ad42d416bd35318fbc1` | Employment separation review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 45 | `feat/governed-assignment-change-review` / `16d07b62bff8c01a8eff5cedb2a85fb75ac4fb56` | Assignment change review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 44 | `feat/governed-performance-review` / `752b4d0a5a09ede2c601fac899065d373bd288ec` | Performance review packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 43 | `feat/governed-offer-approval` / `2975c0c606fa009db35787de5d8104f16db2a3f5` | Offer approval packet | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 42 | `feat/selection-outcome-monitoring-plan` / `28b5e9e096c057fd825cf9ff2aa8e9ec5611401f` | Selection outcome monitoring | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | -| 40 | `feat/structured-interview-plan` / `9751d45f215582c722f036fa12f6f0ebe2c7e8f8` | Structured interview plan | Ready; not mergeable; stale base `ebad8c1`; all sampled checks green; no qualifying approval | Rebase on current develop, reacquire exact-head checks, then obtain independent approval. | +| 47 | `feat/governed-employment-leave-review` / `2ed00cedff4652d30dae7f2cde691c0c296f3a91` | Employment leave review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 62 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | +| 46 | `feat/governed-employment-separation-review` / `008feafb68f39b10f89f37152f10622331e77a4d` | Employment separation review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 87 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | +| 45 | `feat/governed-assignment-change-review` / `b41992e666057dba363e3dde3b4a1f3fca87fb19` | Assignment change review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 84 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | +| 44 | `feat/governed-performance-review` / `693bcdf7573f230c0548fa37f0d92d6f63792927` | Performance review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 63 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | +| 43 | `feat/governed-offer-approval` / `f2d80817ec415cfe2728878e09fff92905e7b11f` | Offer approval packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 84 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | +| 42 | `feat/selection-outcome-monitoring-plan` / `f7d6a3e4fea8fe8f26564e90644192203815cc91` | Selection outcome monitoring | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 96 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | +| 40 | `feat/structured-interview-plan` / `b4a0a564c8b078d48b917014b97b7e8893a537ad` | Structured interview plan | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 72 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | | 52 | `feat/tepp-analysis-adapter` / `447ef27fc79792bc55245d18a7d449945b0e63e4` | Governed TEPP request boundary | Draft; mergeable; current base `39d3c15`; five fresh exact-head runs queued; local TEPP suite 46 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Keep non-executing; wait for terminal exact-head checks and qualifying review before protected merge. | | 51 | `docs/protected-truth-refresh` / `4dc8aa58b5d5b6ab0c69da51ba402e92ec6bb35f` | Protected truth/documentation repair | Draft; not mergeable; stale base `ebad8c1`; exact-head local `npm run validate` passed with 56 Node tests, hosted runs not yet materialized | Reconcile this baseline and rebase on `39d3c15`; rerun every applicable gate before review. | | 37 | `cursor/expand-adr-0001-0003-f381` / `240d2770e5b3109cd1e93c9493d8b6ba685244db` | ADR/reference expansion | Draft; not mergeable; base `f815feed`; only partial checks returned | Close or rebase only after comparing its statements with current protected truth; avoid ADR-number collisions. | diff --git a/manifest.json b/manifest.json index 14664f07c..50355deac 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "f2d7f05bdbcf9fb7958d9e1d5fc22a3b68f99b3d55ac495491608f3c44112aa6", - "bytes": 20025, + "sha256": "769a2fb40c3a093d910f12e5d3858202746804333de53f8b5c45ceada85dc7a4", + "bytes": 20666, "lines": 149 }, { From f211148e1f3b9bb7e265fa494bca7530c02e4dab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:24:40 +0900 Subject: [PATCH 015/110] docs: record current protected truth PR head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0ffda6ca0..a6c5b3a9e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -102,7 +102,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | 42 | `feat/selection-outcome-monitoring-plan` / `f7d6a3e4fea8fe8f26564e90644192203815cc91` | Selection outcome monitoring | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 96 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | | 40 | `feat/structured-interview-plan` / `b4a0a564c8b078d48b917014b97b7e8893a537ad` | Structured interview plan | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 72 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | | 52 | `feat/tepp-analysis-adapter` / `447ef27fc79792bc55245d18a7d449945b0e63e4` | Governed TEPP request boundary | Draft; mergeable; current base `39d3c15`; five fresh exact-head runs queued; local TEPP suite 46 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Keep non-executing; wait for terminal exact-head checks and qualifying review before protected merge. | -| 51 | `docs/protected-truth-refresh` / `4dc8aa58b5d5b6ab0c69da51ba402e92ec6bb35f` | Protected truth/documentation repair | Draft; not mergeable; stale base `ebad8c1`; exact-head local `npm run validate` passed with 56 Node tests, hosted runs not yet materialized | Reconcile this baseline and rebase on `39d3c15`; rerun every applicable gate before review. | +| 51 | `docs/protected-truth-refresh` / `77133885941d1884d44fdd080ee378c5f5bd8b13` | Protected truth/documentation repair | Draft; mergeable; current base `39d3c15`; seven fresh exact-head checks queued; local exact-head `npm run validate` passed with 58 Node tests, recovery rehearsal 3/3, and compileall | Wait for terminal exact-head checks and qualifying independent review before protected merge. | | 37 | `cursor/expand-adr-0001-0003-f381` / `240d2770e5b3109cd1e93c9493d8b6ba685244db` | ADR/reference expansion | Draft; not mergeable; base `f815feed`; only partial checks returned | Close or rebase only after comparing its statements with current protected truth; avoid ADR-number collisions. | No open GitHub Issue was returned by the current `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. diff --git a/manifest.json b/manifest.json index 50355deac..2db09c50f 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "769a2fb40c3a093d910f12e5d3858202746804333de53f8b5c45ceada85dc7a4", - "bytes": 20666, + "sha256": "58b21ca147e6dd43dcd4193d97bf56ac6feda966b6103dcbe30fc87fdf9c63d8", + "bytes": 20710, "lines": 149 }, { From 19a655af3bdf4e4fa2736da9680d87817cfe546d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:30:36 +0900 Subject: [PATCH 016/110] docs: record current ADR evidence head --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a6c5b3a9e..dc1b0945b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -103,7 +103,7 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed | 40 | `feat/structured-interview-plan` / `b4a0a564c8b078d48b917014b97b7e8893a537ad` | Structured interview plan | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 72 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | | 52 | `feat/tepp-analysis-adapter` / `447ef27fc79792bc55245d18a7d449945b0e63e4` | Governed TEPP request boundary | Draft; mergeable; current base `39d3c15`; five fresh exact-head runs queued; local TEPP suite 46 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Keep non-executing; wait for terminal exact-head checks and qualifying review before protected merge. | | 51 | `docs/protected-truth-refresh` / `77133885941d1884d44fdd080ee378c5f5bd8b13` | Protected truth/documentation repair | Draft; mergeable; current base `39d3c15`; seven fresh exact-head checks queued; local exact-head `npm run validate` passed with 58 Node tests, recovery rehearsal 3/3, and compileall | Wait for terminal exact-head checks and qualifying independent review before protected merge. | -| 37 | `cursor/expand-adr-0001-0003-f381` / `240d2770e5b3109cd1e93c9493d8b6ba685244db` | ADR/reference expansion | Draft; not mergeable; base `f815feed`; only partial checks returned | Close or rebase only after comparing its statements with current protected truth; avoid ADR-number collisions. | +| 37 | `cursor/expand-adr-0001-0003-f381` / `cdb5d8c17103bc8dad264ea4e8cbf1ef04d4c288` | ADR/reference expansion | Draft; mergeable; current base `39d3c15`; six fresh exact-head checks queued; local exact-head `npm run validate` passed with 55 Node tests, recovery rehearsal 3/3, and compileall | Wait for terminal exact-head checks and qualifying independent review; avoid ADR-number collisions. | No open GitHub Issue was returned by the current `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. diff --git a/manifest.json b/manifest.json index 2db09c50f..d647fc18e 100644 --- a/manifest.json +++ b/manifest.json @@ -323,8 +323,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "58b21ca147e6dd43dcd4193d97bf56ac6feda966b6103dcbe30fc87fdf9c63d8", - "bytes": 20710, + "sha256": "d2c7d8742fb74e24525d85f87720d158f2f07e6abe94e18115638a442aa038fc", + "bytes": 20811, "lines": 149 }, { From 9624b23bfb6c58831a626f1e84e453af790f5001 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:43:11 +0900 Subject: [PATCH 017/110] docs: record central scheduler ownership --- docs/product-technical-gap-baseline.md | 7 ++++++- manifest.json | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index dc1b0945b..02435d0a0 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -107,9 +107,14 @@ The following is the current GitHub inventory checked on 2026-08-20. All listed No open GitHub Issue was returned by the current `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 hourly sweep performs the same bounded sequence: +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. diff --git a/manifest.json b/manifest.json index d647fc18e..4e23a3da6 100644 --- a/manifest.json +++ b/manifest.json @@ -323,9 +323,9 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "d2c7d8742fb74e24525d85f87720d158f2f07e6abe94e18115638a442aa038fc", - "bytes": 20811, - "lines": 149 + "sha256": "4609b2d6fcc52c490f7271cef163c3c58bbb280125ce8330ce7a5757fcff33fd", + "bytes": 21552, + "lines": 154 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", From f9527e58ede50850c158e35f1006bacda056eaef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:19:03 -0700 Subject: [PATCH 018/110] test(ui): require localized accessible names --- tests/hr-workspace.test.mjs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/hr-workspace.test.mjs b/tests/hr-workspace.test.mjs index 67e5a3bc7..28bbaca49 100644 --- a/tests/hr-workspace.test.mjs +++ b/tests/hr-workspace.test.mjs @@ -5,6 +5,7 @@ import { isPurposeAuthorized, nextLocale } 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'); @@ -29,6 +30,21 @@ test('workspace includes keyboard-accessible review and high-impact states', () 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/); @@ -45,6 +61,6 @@ test('purpose and locale transitions preserve the trust boundary', () => { assert.equal(isPurposeAuthorized('recruiting'), false); assert.equal(nextLocale('en'), 'ko'); assert.equal(nextLocale('ko'), 'en'); - assert.match(readFileSync(new URL('../apps/hr-workspace/app.js', import.meta.url), 'utf8'), /no API mutation was sent/); + assert.match(app, /no API mutation was sent/); assert.doesNotMatch(html, /password|passkey_value|private_key/i); }); From 5ca323379387d47ec29ee1249ef6d4d56210c53c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:19:43 -0700 Subject: [PATCH 019/110] fix(ui): localize icon control accessible names --- apps/hr-workspace/app.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/hr-workspace/app.js b/apps/hr-workspace/app.js index ee9cd0547..17659825f 100644 --- a/apps/hr-workspace/app.js +++ b/apps/hr-workspace/app.js @@ -1,9 +1,9 @@ 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', 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' + 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', 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: '닫기', requestEvidence: '추가 근거 요청', highImpactConfirmation: '고위험 작업 확인', correctHistoryTitle: '이력 정정', confirmationCopy: '행위자, 목적, 사유, 근거 맥락을 사람이 확인하기 전까지 정정은 초안으로 남습니다.', reason: '사유', reasonPlaceholder: '필수', draftConfirmed: '로컬 픽스처 근거에 정정 초안을 확인했으며 API 변경은 전송하지 않았습니다.', cancel: '취소', confirm: '정정 확인' + 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: '언어 변경', requestEvidence: '추가 근거 요청', highImpactConfirmation: '고위험 작업 확인', correctHistoryTitle: '이력 정정', confirmationCopy: '행위자, 목적, 사유, 근거 맥락을 사람이 확인하기 전까지 정정은 초안으로 남습니다.', reason: '사유', reasonPlaceholder: '필수', draftConfirmed: '로컬 픽스처 근거에 정정 초안을 확인했으며 API 변경은 전송하지 않았습니다.', cancel: '취소', confirm: '정정 확인' } }; @@ -27,6 +27,10 @@ function setLocale(locale) { 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'; } From cb0e4b1be1867a4a5263b15cbe50355412d07b55 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:20:27 -0700 Subject: [PATCH 020/110] fix(ui): bind accessible names to locale changes --- apps/hr-workspace/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/hr-workspace/index.html b/apps/hr-workspace/index.html index 5c8072aaf..03fb4e238 100644 --- a/apps/hr-workspace/index.html +++ b/apps/hr-workspace/index.html @@ -27,7 +27,7 @@

People workspace

Protected People API boundary: not connected in this fixture

- +
@@ -94,7 +94,7 @@ -

Evidence drawer

Decision packet evidence

+

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
@@ -102,7 +102,7 @@
-

High-impact confirmation

Correct history

+

High-impact confirmation

Correct history

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

From f647a3dbc0cc0e1bd0f5e928d670b09cc9d2ece0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:27:29 -0700 Subject: [PATCH 021/110] fix(ui): refresh accessibility manifest evidence --- manifest.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index fae33e298..3837db8e0 100644 --- a/manifest.json +++ b/manifest.json @@ -71,14 +71,14 @@ }, { "path": "apps/hr-workspace/app.js", - "sha256": "32b55bfb34b0081aa5b2a712a1b248c8fe86650871b69cbafa758ba7bff6cf45", - "bytes": 10708, - "lines": 89 + "sha256": "d2c069ebeaccbd344b10af52960f7ae23dbffb2a3c9d7650c6c26bee12022c38", + "bytes": 10978, + "lines": 93 }, { "path": "apps/hr-workspace/index.html", - "sha256": "037beda6f1ed1690f81eb620718d5b9a3ba4b16f205009dfb267673ef1496b14", - "bytes": 13408, + "sha256": "fc0e31865d114834d214a86fc1b16e7d4f424ceb8f4919a2fc6700bb19bcfb8c", + "bytes": 13504, "lines": 113 }, { @@ -395,9 +395,9 @@ }, { "path": "tests/hr-workspace.test.mjs", - "sha256": "ff233f8228c1cc80bc54cfc8c030bff79334e461eadcd3497f5a632b2b3c5e67", - "bytes": 2634, - "lines": 50 + "sha256": "03d395e3f62ab9178e7d9f9979926b186ca0f224c71e4368b23beffb9aecd940", + "bytes": 3206, + "lines": 66 }, { "path": "tests/openapi-contract.test.mjs", From e496a69a5d53fa24c7022bddaf3b4f0eb17d3359 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:22:19 -0700 Subject: [PATCH 022/110] fix(workspace): reserve unique ADR and refresh job-analysis truth --- .../0026-product-technical-gap-baseline.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/adr/0026-product-technical-gap-baseline.md 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..4e09995fe --- /dev/null +++ b/docs/adr/0026-product-technical-gap-baseline.md @@ -0,0 +1,32 @@ +# ADR 0026: Product and technical gap baseline + +- Status: Accepted on active development branch +- 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. From b16381792da7e68733ce1b65ab62704cc136f664 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:22:30 -0700 Subject: [PATCH 023/110] fix(workspace): remove conflicting ADR number --- .../0015-product-technical-gap-baseline.md | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 docs/adr/0015-product-technical-gap-baseline.md diff --git a/docs/adr/0015-product-technical-gap-baseline.md b/docs/adr/0015-product-technical-gap-baseline.md deleted file mode 100644 index 70457db4e..000000000 --- a/docs/adr/0015-product-technical-gap-baseline.md +++ /dev/null @@ -1,32 +0,0 @@ -# ADR 0015: Product and technical gap baseline - -- Status: Accepted on active development branch -- 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 now the first protected integration lane. They remain the dependency for browser workflows and must preserve tenant authorization, idempotency, bitemporal history, human confirmation, audit, and outbox atomicity. -5. Job-analysis persistence/API work now converges on the single current PR #38 model, with migration 0013 and its own ADR; a second Task/FJA/KSAO store must not be introduced. -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. From fc00897f6fbddd70b942a37bb9eb74f03b1a8ee1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:22:42 -0700 Subject: [PATCH 024/110] fix(workspace): reconcile ADR index with protected job analysis --- docs/adr/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 1c3a08933..f8e447878 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -7,12 +7,13 @@ | [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 | +| [0006](0006-governed-audit-outbox-envelope.md) | Governed audit/outbox envelope and durable persistence | Accepted on protected `develop` | +| [0007](0007-governed-job-analysis-evidence.md) | Governed job-analysis evidence snapshots | Accepted on protected `develop` | | [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 | +| [0009](0009-performance-criterion-observation-scope.md) | Performance criterion observations require worker-job scope | Accepted on protected `develop` | | [0010](0010-naruon-calendar-intent-boundary.md) | Naruon calendar intent boundary | Accepted on protected `develop` | | [0011](0011-bitemporal-workforce-composition.md) | Bitemporal workforce composition | Accepted on protected `develop` | | [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on protected `develop` | | [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on protected `develop` | -| [0015](0015-product-technical-gap-baseline.md) | Product and technical gap baseline | Accepted on active development branch | +| [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | Accepted on protected `develop` | +| [0026](0026-product-technical-gap-baseline.md) | Product and technical gap baseline | Accepted on active development branch | From 2b3f5b02271a0fb7de3d5250dca42a038a07d2d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:42:48 -0700 Subject: [PATCH 025/110] fix(workspace): index merged candidate evidence ADR --- docs/adr/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/adr/README.md b/docs/adr/README.md index f8e447878..39468cca1 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -16,4 +16,5 @@ | [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on protected `develop` | | [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on protected `develop` | | [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | Accepted on protected `develop` | +| [0025](0025-governed-candidate-evidence-intake.md) | Govern candidate evidence intake as reference-only evidence | Accepted on protected `develop` | | [0026](0026-product-technical-gap-baseline.md) | Product and technical gap baseline | Accepted on active development branch | From eedcf1c34bd6dc6e94fc0c70802c6f7d43090034 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:58:51 +0900 Subject: [PATCH 026/110] fix: reconcile ADR foundation manifest --- .../0014-job-analysis-snapshot-persistence.md | 49 +++++++++++++++++++ manifest.json | 30 +++++++----- scripts/foundation-contract-core.mjs | 3 +- tests/validate_repository.py | 3 +- 4 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 docs/adr/0014-job-analysis-snapshot-persistence.md diff --git a/docs/adr/0014-job-analysis-snapshot-persistence.md b/docs/adr/0014-job-analysis-snapshot-persistence.md new file mode 100644 index 000000000..cef05e28f --- /dev/null +++ b/docs/adr/0014-job-analysis-snapshot-persistence.md @@ -0,0 +1,49 @@ +# ADR 0014: Persist governed job-analysis snapshots + +- Status: Accepted on active implementation branch +- Date: 2026-08-20 +- Owners: Orgmetra Job Analysis / Workforce Validation + +## Context + +ADR 0007 defines the canonical in-process `JobAnalysisSnapshot` evidence contract. Protected `develop` now also contains the governed People mutation/idempotency slice through migration 0012, so the next persistence migration is 0013. The previous draft persistence lane used historical migration/ADR numbers and overlapped a second proposed job-analysis store; that competing store has been closed rather than shipping two Task/KSAO authorities. + +A snapshot that exists only in memory cannot be reread, audited, or bound to the Job, Position, or criterion identities already present in Orgmetra. Job analysis is a systematic examination of work tasks and the competencies required to perform them, with explicit task-to-competency linkage and evidence provenance. When later selection procedures depend on work behaviors or job knowledge, the analysis must remain reviewable evidence rather than an opaque model assertion. + +This slice therefore persists one immutable snapshot without making a hiring, promotion, termination, compensation, or other high-impact employment decision. LLM-origin material remains untrusted draft evidence under ADR 0007 and cannot become validated occupational evidence without accountable human review. + +## Decision + +Orgmetra will persist the canonical `JobAnalysisSnapshot` in migration `0013_job_analysis_snapshot.sql` as tenant-scoped 3NF relations: + +- `job_analysis_snapshot` stores the version header, optional Position and criterion scope, accountable review metadata, content digest, and the 1:1 Functional Job Analysis compatibility profile; +- `job_analysis_task_item` stores observable duties and their versioned evidence source; +- `job_analysis_ksao_item` stores knowledge, skill, ability, and other-characteristic requirements and their evidence source; +- `job_analysis_task_ksao_link` stores explicit task-to-KSAO relationships; +- `job_analysis_write_command` stores the caller `Idempotency-Key`, request digest, actor reference, and purpose code so idempotency and authorization context reach the write boundary. + +`job_record_id` from the kernel maps to `job_profile.job_profile_id`. Tenant-qualified foreign keys on optional `position_record_id` and `criterion_blueprint_id` enforce tenant and parent identity. Same-Job binding is a separate service-layer invariant: the PostgreSQL adapter resolves `_POSITION_SCOPE_SQL` and `_CRITERION_SCOPE_SQL` and compares each returned `job_profile_id` with the snapshot Job before inserts. Missing parents, cross-tenant parents, or parents bound to a different Job therefore fail closed through the layer that owns that invariant. Writes require purpose-bound authorization (`job_analysis_write` / `orgmetra.job_architecture.write`) and persist audit/outbox evidence in the same database transaction. The snapshot is occupational evidence rather than person PII, so fields are not indiscriminately masked. The read API returns the same governed snapshot document that was persisted. + +The stronger provenance and sealing ideas from the superseded parallel case model—source version identity, digest-addressable evidence, explicit human approval, and richer FJA evidence—must be evolved by extending this canonical snapshot contract rather than introducing a second Task/KSAO persistence authority. + +## Consequences + +### Positive + +- Buyers can persist duties, KSAOs, Task–KSAO links, and FJA evidence and read the same snapshot back. +- Later criterion and selection-validity work can bind to an exact digest-addressable job-analysis version. +- Missing Job, Position, or criterion identities cannot silently create orphan evidence. +- Orgmetra retains one canonical Job Analysis storage and API boundary. + +### Costs and limitations + +- SME workflow, governed O*NET ingestion, retention/export policy, richer source-version registries, and selection-validity computation remain later bounded slices. +- The 1–5 ratings remain contract-level ordinals; local sampling design, rater aggregation, and uncertainty estimation require explicit methodological evidence before consequential use. + +## Verification + +Tests must prove that the persisted snapshot document equals the posted payload, `Idempotency-Key` is bound at the write port and stored on `job_analysis_write_command`, missing parents fail closed for the expected foreign-key or same-Job scope reason, snapshot UPDATE and DELETE operations are rejected by the append-only guard, cross-tenant snapshot reads return no rows under a `NOSUPERUSER NOBYPASSRLS` role, and `record_audit_outbox_event(...)` is persisted in the same governed write path. The service boundary requires exact 100% owned production statement and branch coverage where the pinned toolchain exposes those metrics, plus a PostgreSQL integration test that applies protected migrations 0001 through 0012 before migration 0013. + +## References + +The APA 7 bibliography is maintained in `docs/doctoring/REFERENCES.md`, including SIOP Principles, the Uniform Guidelines on Employee Selection Procedures, current O*NET technical documentation, and current OPM job-analysis guidance. Material standards claims must remain tied to the authoritative version recorded there. diff --git a/manifest.json b/manifest.json index 3837db8e0..c64105361 100644 --- a/manifest.json +++ b/manifest.json @@ -304,16 +304,22 @@ "lines": 57 }, { - "path": "docs/adr/0015-product-technical-gap-baseline.md", - "sha256": "49430c953798c23c453ac71e41705e216c95b737f9488c934f6329b46e6ae6ff", - "bytes": 3107, + "path": "docs/adr/0014-job-analysis-snapshot-persistence.md", + "sha256": "a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105", + "bytes": 5365, + "lines": 49 + }, + { + "path": "docs/adr/0026-product-technical-gap-baseline.md", + "sha256": "8e5e4fdd3bdd0a3a2e29517de8346148bb4780cfb7c568844bc034a9593aab6d", + "bytes": 3162, "lines": 32 }, { "path": "docs/adr/README.md", - "sha256": "54c0930cedb31b0a227eed46ccfe220975a37ede79939c4fafc6e9bacc0a9120", - "bytes": 1791, - "lines": 18 + "sha256": "a03dac39c365e21c6fc7273a4cd483239d9f765f1472a6dfb7480ab60541f539", + "bytes": 2043, + "lines": 20 }, { "path": "docs/doctoring/REFERENCES.md", @@ -371,9 +377,9 @@ }, { "path": "scripts/foundation-contract-core.mjs", - "sha256": "3c575fa99f3f4b0303d8586cdfd1cfbcf2ab4902dcc31b610ca6a17acba1f1c5", - "bytes": 25777, - "lines": 653 + "sha256": "2bfbe6f00aa5246c9ccbc973b0858e7b950bc835d64b039b516f442592802f18", + "bytes": 25833, + "lines": 654 }, { "path": "scripts/foundation-contract.mjs", @@ -479,9 +485,9 @@ }, { "path": "tests/validate_repository.py", - "sha256": "b0fcbd17ae7f2ba0a9112e6f5045f68b1508de3ff15ad3cfffc4eaac901d26d1", - "bytes": 24833, - "lines": 590 + "sha256": "546485bb47e18f7dc0b0090f401b0c4491183529ea08489bf993cb13fcbd6796", + "bytes": 24891, + "lines": 591 } ] } diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 3f26a4f54..2c5dbc1db 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -53,7 +53,8 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/adr/0006-governed-audit-outbox-envelope.md', 'docs/adr/0008-purpose-bound-pii-authorization.md', 'docs/adr/0009-performance-criterion-observation-scope.md', - 'docs/adr/0015-product-technical-gap-baseline.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', diff --git a/tests/validate_repository.py b/tests/validate_repository.py index fe12721b1..d29ad86d9 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -56,7 +56,8 @@ "docs/adr/0006-governed-audit-outbox-envelope.md", "docs/adr/0008-purpose-bound-pii-authorization.md", "docs/adr/0009-performance-criterion-observation-scope.md", - "docs/adr/0015-product-technical-gap-baseline.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", From d4bb687a1c22921fdeddea94a64e22b5fe6f5dd1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:10:59 +0900 Subject: [PATCH 027/110] docs: refresh current product gap evidence --- docs/product-technical-gap-baseline.md | 72 +++++++++++++------------- manifest.json | 6 +-- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 02435d0a0..1c9c16fa3 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline -**Snapshot:** 2026-08-20, Asia/Seoul -**Evidence base:** local `develop` at `39d3c15e7c47306ec2173d14afcd1c6e1a0139c9`, current protected default branch; GitHub PR metadata, exact-head workflow results, review/thread results, and fresh local validation. +**Snapshot:** 2026-08-21, Asia/Seoul +**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `eedcf1c34bd6dc6e94fc0c70802c6f7d43090034`; 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. @@ -19,9 +19,9 @@ This document is the buyer-facing work queue. It separates what a customer can u ## Executive finding -Orgmetra is an evidence-centered HRIS foundation with a protected People mutation and confirmed-hire implementation, 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 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 and local Storybook state runtime, but protected truth has no connected or browser-verified workspace. +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 and local Storybook state runtime, but protected truth has no connected or released workspace. -The next highest-leverage gaps are one canonical persisted Job Analysis case/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. +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 @@ -32,7 +32,7 @@ flowchart LR outcome --> validity[Validity estimation] validity --> policy[Human-reviewed policy change] worker -. browser workspace missing .-> gap1[Gap P0-1] - worker -. persisted Job Analysis API missing .-> gap2[Gap P0-2] + worker -. buyer-connected Job Analysis path missing .-> gap2[Gap P0-2] validity -. integrity only, no estimator .-> gap3[Gap P0-3] ``` @@ -42,14 +42,18 @@ flowchart LR |---|---|---|---| | 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. | -| Job-analysis value objects | `orgmetra_hris_kernel.job_analysis`; exact unit coverage | `implemented_on_protected_develop` | Evidence can be validated in a protected package, but no protected persisted Job Analysis case/workspace exists. | -| Job-analysis persistence/API | PR #38 is the current canonical persistence lane, rebuilt on protected `39d3c15`; PR #30 is closed as superseded | `implemented_on_active_pr` | A buyer cannot yet create, approve, version, or retrieve one canonical persisted Job Analysis case. | +| 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. | -| Role workspaces and Storybook runtime | Protected `develop` still has no customer UI; active PR #53 has the HR Home/Employee Profile fixture plus a local Storybook `10.5.10` build | `planned` | The local component/state runtime is reviewable, but there is no connected or released buyer workflow in protected truth. | +| 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 and Storybook runtime | Active PR #53 has the HR Home/Employee Profile fixture plus a local Storybook `10.5.10` build; protected `develop` still has no customer UI | `implemented_on_active_pr` | The local component/state runtime is 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. | @@ -67,7 +71,8 @@ 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 same active PR includes a local Storybook runtime with tokenized stories; -`npm run build-storybook` is a build check, not browser E2E, hosted release, or +the current head has a passed Storybook build and local Playwright browser +smoke, but neither is connected People API integration, hosted deployment, or protected-develop truth. The artifact must be reviewed, checked, and merged independently before P0-1 can change maturity. @@ -76,8 +81,8 @@ independently before P0-1 can change maturity. | 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 | Merge and verify PR #38 as one persisted Job Analysis case: one migration owner, one ADR, versioned source evidence, SME approval, retrieval API, and protected PostgreSQL acceptance test. | Protected People API evidence | -| P0-3 | Workforce Validation / scientific owner | Add a Rust-first estimator boundary or a 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. | P0-2 and external contract re-resolution | +| 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. Connect it to the buyer workflow, 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 | @@ -86,24 +91,25 @@ independently before P0-1 can change maturity. ## Current open PR inventory and integration order -The following is the current GitHub inventory checked on 2026-08-20. All listed PRs target `develop`; the protected base observed by the connector is `39d3c15e7c47306ec2173d14afcd1c6e1a0139c9`. `REVIEW_REQUIRED` means the GitHub listing reported that review gate; it is not approval evidence. No self-approval or protection bypass is permitted. +The following is the current GitHub inventory checked on 2026-08-21. All listed PRs target `develop@33eff439df5c2ef58805c975108d156fd73799df`. `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 | |---:|---|---|---|---| -| 53 | `codex/product-gap-baseline-workspace` / live PR #53 exact head | HR Home + Employee Profile fixture and baseline repair | Draft; current base is protected `39d3c15`; seven exact-head runs pending/queued; local validation passes with 59 Node tests; no independent approval | Review the fixture/API boundary, wait for every current-head check, obtain independent approval, then decide whether to merge as a local product-surface slice. | -| 38 | `cursor/job-analysis-snapshot-5eef` / `f7fb7f7f1a160e0c3685a3105f7cb8c7fb34835b` | Canonical Job Analysis persistence/API | Draft; mergeable; base is current `39d3c15`; seven exact-head runs queued/pending; local exact-head suite is 67 passed with 100% statement/branch coverage, root validation 55 tests, and PostgreSQL 16.14 snapshot/schema contracts passed | Wait for terminal exact-head checks, then obtain independent approval; no predecessor evidence transfers. | -| 41 | `feat/candidate-evidence-intake` / `c7934273f6887daf2f222753fdf9cca46734ad88` | Candidate evidence intake | Ready; mergeable; current base `39d3c15`; local exact-head package suite is 75 passed with 100% statement/branch coverage; five fresh hosted checks queued; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 48 | `feat/governed-compensation-change-review` / `fcefb33c12b0d139077fd693f745871aa1f686e0` | Compensation review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 57 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Wait for terminal exact-head checks and qualifying independent approval before protected merge. | -| 47 | `feat/governed-employment-leave-review` / `2ed00cedff4652d30dae7f2cde691c0c296f3a91` | Employment leave review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 62 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 46 | `feat/governed-employment-separation-review` / `008feafb68f39b10f89f37152f10622331e77a4d` | Employment separation review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 87 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 45 | `feat/governed-assignment-change-review` / `b41992e666057dba363e3dde3b4a1f3fca87fb19` | Assignment change review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 84 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 44 | `feat/governed-performance-review` / `693bcdf7573f230c0548fa37f0d92d6f63792927` | Performance review packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 63 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 43 | `feat/governed-offer-approval` / `f2d80817ec415cfe2728878e09fff92905e7b11f` | Offer approval packet | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 84 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 42 | `feat/selection-outcome-monitoring-plan` / `f7d6a3e4fea8fe8f26564e90644192203815cc91` | Selection outcome monitoring | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 96 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 40 | `feat/structured-interview-plan` / `b4a0a564c8b078d48b917014b97b7e8893a537ad` | Structured interview plan | Ready; mergeable; current base `39d3c15`; five fresh exact-head checks queued; local package suite 72 passed with 100% statement/branch coverage; no qualifying approval | Wait for terminal exact-head checks and independent approval before protected merge. | -| 52 | `feat/tepp-analysis-adapter` / `447ef27fc79792bc55245d18a7d449945b0e63e4` | Governed TEPP request boundary | Draft; mergeable; current base `39d3c15`; five fresh exact-head runs queued; local TEPP suite 46 passed with 100% statement/branch coverage and root validation 55 tests; no independent approval | Keep non-executing; wait for terminal exact-head checks and qualifying review before protected merge. | -| 51 | `docs/protected-truth-refresh` / `77133885941d1884d44fdd080ee378c5f5bd8b13` | Protected truth/documentation repair | Draft; mergeable; current base `39d3c15`; seven fresh exact-head checks queued; local exact-head `npm run validate` passed with 58 Node tests, recovery rehearsal 3/3, and compileall | Wait for terminal exact-head checks and qualifying independent review before protected merge. | -| 37 | `cursor/expand-adr-0001-0003-f381` / `cdb5d8c17103bc8dad264ea4e8cbf1ef04d4c288` | ADR/reference expansion | Draft; mergeable; current base `39d3c15`; six fresh exact-head checks queued; local exact-head `npm run validate` passed with 55 Node tests, recovery rehearsal 3/3, and compileall | Wait for terminal exact-head checks and qualifying independent review; avoid ADR-number collisions. | +| 57 | `feat/validation-analysis-handoff` / `a872938b508a42a7010c059789e012beffd5ef60` | Governed selection-validity analysis handoff | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | +| 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | +| 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | +| 54 | `feat/workforce-composition-change` / `8d4958e4215ba5c168b93b820bed9b384d87f0cf` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | +| 53 | `codex/product-gap-baseline-workspace` / `eedcf1c34bd6dc6e94fc0c70802c6f7d43090034` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | +| 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | +| 48 | `feat/governed-compensation-change-review` / `f6cefeb64d214e020ed82840b64d7d0cca70ec6e` | Compensation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | +| 47 | `feat/governed-employment-leave-review` / `088abb13c1f1b2aa69c6e68b1814ce13fb08e4d9` | Employment leave review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify temporal and authorization contracts before protected merge. | +| 46 | `feat/governed-employment-separation-review` / `add78f169a01d72ad2e027a3fae9f83ce795721f` | Employment separation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify irreversible-action confirmation and audit evidence before protected merge. | +| 45 | `feat/governed-assignment-change-review` / `18ac3a733f775b67dbe0260b5b59699bc0d10301` | Assignment change review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/person/employment/position binding before protected merge. | +| 44 | `feat/governed-performance-review` / `b867f4cf1efdec198f34199e7e8c382338e94505` | Performance review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify criterion scope and human confirmation before protected merge. | +| 43 | `feat/governed-offer-approval` / `8af2e3bf31bb9352033bded6530b9357405782af` | Offer approval packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify evidence-backed offer approval and audit/outbox behavior before protected merge. | +| 42 | `feat/selection-outcome-monitoring-plan` / `a306d8199be475ba07682e31c1f9b4d9c701a50b` | Selection outcome monitoring | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify monitoring scope and temporal cohort semantics before protected merge. | +| 40 | `feat/structured-interview-plan` / `eddc92bf059b2ecb7f286754325b801e93a2042a` | Governed structured interview plan | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify human review, evidence versioning, and current-head checks before protected merge. | No open GitHub Issue was returned by the current `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. @@ -141,14 +147,10 @@ The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. | Check | Result | |---|---| -| `npm run validate` | Passed: repository validation and 55 Node tests on protected `develop` `39d3c15`. | -| `packages/hris-kernel` | 171 passed; 100% statement/branch coverage. | -| `packages/keyverse-adapter` | 41 passed; 100% statement/branch coverage. | -| `packages/migration-adapter` | 65 passed; 100% statement/branch coverage. | -| `packages/naruon-adapter` | 41 passed; 100% statement/branch coverage. | -| `packages/requisition-review` | 61 passed; 100% statement/branch coverage. | -| `packages/selection-review` | 68 passed; 100% statement/branch coverage. | -| `services/people-api` | 146 passed; 100% statement/branch coverage. Project-local `tool.uv.sources` now resolves both `orgmetra-hris-kernel` and `orgmetra-keyverse-adapter`; test extras are declared in the service project. | -| PostgreSQL contract suite | 12/12 current protected contracts passed on a dedicated PostgreSQL 16 container, including `test_people_mutation_idempotency_postgres.sh`; the temporary container was removed after the run. | +| Protected `develop` contents | `git ls-tree` at `33eff439df5c2ef58805c975108d156fd73799df` contains migration `0013`, Job Analysis API, ADR `0014`, candidate-evidence package, ADR `0025`, and their quality workflows. This proves repository presence, not deployment. | +| `npm run validate` on active PR #53 | Passed: foundation validation and 60 Node tests on exact local head `eedcf1c34bd6dc6e94fc0c70802c6f7d43090034`. | +| `packages/candidate-evidence` on active PR #53 | 75 passed; owned statement and branch coverage 100%. | +| Storybook and browser fixture | Storybook `10.5.10` production build passed; Playwright browser smoke passed for four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors. This is local fixture evidence, not protected deployment or People API integration. | +| PostgreSQL and full Python package matrix | Protected-branch contract evidence remains recorded in the merged package/workflow artifacts; a complete fresh matrix was not rerun in this documentation-only update and must be required by the corresponding hosted workflows. | These results prove the current foundation contracts. They do not prove that open PRs are merged, that a browser UI exists, that a statistical estimator exists, or that Orgmetra is certified under CSAP/SOC 2. diff --git a/manifest.json b/manifest.json index c64105361..878841cee 100644 --- a/manifest.json +++ b/manifest.json @@ -329,9 +329,9 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "4609b2d6fcc52c490f7271cef163c3c58bbb280125ce8330ce7a5757fcff33fd", - "bytes": 21552, - "lines": 154 + "sha256": "fe3ec42c234fb051053d06d466f5a63859da78dd253e87e3db33b72bfca277bb", + "bytes": 21941, + "lines": 156 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", From 383e1aa47a921df8edf3f8ef3f9d9aa93568eafc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:15:04 +0900 Subject: [PATCH 028/110] docs: track current validity handoff head --- docs/product-technical-gap-baseline.md | 4 ++-- manifest.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1c9c16fa3..7684c8853 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -95,7 +95,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | PR | Head branch / exact head | Scope | Current state | Next action | |---:|---|---|---|---| -| 57 | `feat/validation-analysis-handoff` / `a872938b508a42a7010c059789e012beffd5ef60` | Governed selection-validity analysis handoff | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | +| 57 | `feat/validation-analysis-handoff` / `f0c30c5e6cfd6cb90afcbef1efd0ba8825f3fd52` | Governed selection-validity analysis handoff | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflow runs queued/pending | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | | 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | | 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | | 54 | `feat/workforce-composition-change` / `8d4958e4215ba5c168b93b820bed9b384d87f0cf` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | @@ -109,7 +109,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 44 | `feat/governed-performance-review` / `b867f4cf1efdec198f34199e7e8c382338e94505` | Performance review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify criterion scope and human confirmation before protected merge. | | 43 | `feat/governed-offer-approval` / `8af2e3bf31bb9352033bded6530b9357405782af` | Offer approval packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify evidence-backed offer approval and audit/outbox behavior before protected merge. | | 42 | `feat/selection-outcome-monitoring-plan` / `a306d8199be475ba07682e31c1f9b4d9c701a50b` | Selection outcome monitoring | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify monitoring scope and temporal cohort semantics before protected merge. | -| 40 | `feat/structured-interview-plan` / `eddc92bf059b2ecb7f286754325b801e93a2042a` | Governed structured interview plan | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify human review, evidence versioning, and current-head checks before protected merge. | +| 40 | `feat/structured-interview-plan` / `6e6cc4f6b9c1356773cb3d89a52bcbf4bdfcadba` | Governed structured interview plan | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify human review, evidence versioning, and current-head checks before protected merge. | No open GitHub Issue was returned by the current `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. diff --git a/manifest.json b/manifest.json index 878841cee..62e23bee7 100644 --- a/manifest.json +++ b/manifest.json @@ -329,8 +329,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "fe3ec42c234fb051053d06d466f5a63859da78dd253e87e3db33b72bfca277bb", - "bytes": 21941, + "sha256": "3180412fcfd0b9e178c54c098cdb48c9cc1bd9d8bbebe07627ffb8afb5ef7032", + "bytes": 21979, "lines": 156 }, { From a9442e2375694894c084d08768046e9d6c8d7cb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:22:15 +0900 Subject: [PATCH 029/110] docs: refresh active PR evidence heads --- docs/product-technical-gap-baseline.md | 6 +++--- manifest.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7684c8853..7f29bc4d8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `eedcf1c34bd6dc6e94fc0c70802c6f7d43090034`; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `383e1aa47a921df8edf3f8ef3f9d9aa93568eafc`; 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. @@ -98,8 +98,8 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 57 | `feat/validation-analysis-handoff` / `f0c30c5e6cfd6cb90afcbef1efd0ba8825f3fd52` | Governed selection-validity analysis handoff | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflow runs queued/pending | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | | 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | | 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | -| 54 | `feat/workforce-composition-change` / `8d4958e4215ba5c168b93b820bed9b384d87f0cf` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | -| 53 | `codex/product-gap-baseline-workspace` / `eedcf1c34bd6dc6e94fc0c70802c6f7d43090034` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 54 | `feat/workforce-composition-change` / `db4ea77221e0318dc2ab1be26543e338683d7678` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | +| 53 | `codex/product-gap-baseline-workspace` / `383e1aa47a921df8edf3f8ef3f9d9aa93568eafc` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | | 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | | 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | | 48 | `feat/governed-compensation-change-review` / `f6cefeb64d214e020ed82840b64d7d0cca70ec6e` | Compensation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | diff --git a/manifest.json b/manifest.json index 62e23bee7..e924f7436 100644 --- a/manifest.json +++ b/manifest.json @@ -329,8 +329,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "3180412fcfd0b9e178c54c098cdb48c9cc1bd9d8bbebe07627ffb8afb5ef7032", - "bytes": 21979, + "sha256": "b29e07a744610ce30d195b7cfaf00776c42310eff43eeb81ace9f16ba03c52d9", + "bytes": 22005, "lines": 156 }, { From 9fccfd22717ee7f037cae34bce2f3086e7c61be0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:23:23 +0900 Subject: [PATCH 030/110] docs: record latest workspace head --- docs/product-technical-gap-baseline.md | 4 ++-- manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7f29bc4d8..032e3d173 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `383e1aa47a921df8edf3f8ef3f9d9aa93568eafc`; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `a9442e2375694894c084d08768046e9d6c8d7cb9`; 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. @@ -99,7 +99,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | | 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | | 54 | `feat/workforce-composition-change` / `db4ea77221e0318dc2ab1be26543e338683d7678` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | -| 53 | `codex/product-gap-baseline-workspace` / `383e1aa47a921df8edf3f8ef3f9d9aa93568eafc` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / `a9442e2375694894c084d08768046e9d6c8d7cb9` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | | 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | | 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | | 48 | `feat/governed-compensation-change-review` / `f6cefeb64d214e020ed82840b64d7d0cca70ec6e` | Compensation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | diff --git a/manifest.json b/manifest.json index e924f7436..c4a2798c7 100644 --- a/manifest.json +++ b/manifest.json @@ -329,7 +329,7 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "b29e07a744610ce30d195b7cfaf00776c42310eff43eeb81ace9f16ba03c52d9", + "sha256": "7e661dee73c2ff6a79c38ccef558612de8a4fd95b75474290123130c14747fe4", "bytes": 22005, "lines": 156 }, From 84cd959f7792e718c1c1fabbef7753a26bc3f7b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:24:16 +0900 Subject: [PATCH 031/110] docs: pin final workspace evidence head --- docs/product-technical-gap-baseline.md | 4 ++-- manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 032e3d173..8a4103a74 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `a9442e2375694894c084d08768046e9d6c8d7cb9`; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `9fccfd22717ee7f037cae34bce2f3086e7c61be0`; 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. @@ -99,7 +99,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | | 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | | 54 | `feat/workforce-composition-change` / `db4ea77221e0318dc2ab1be26543e338683d7678` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | -| 53 | `codex/product-gap-baseline-workspace` / `a9442e2375694894c084d08768046e9d6c8d7cb9` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / `9fccfd22717ee7f037cae34bce2f3086e7c61be0` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | | 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | | 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | | 48 | `feat/governed-compensation-change-review` / `f6cefeb64d214e020ed82840b64d7d0cca70ec6e` | Compensation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | diff --git a/manifest.json b/manifest.json index c4a2798c7..a838d0ee9 100644 --- a/manifest.json +++ b/manifest.json @@ -329,7 +329,7 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "7e661dee73c2ff6a79c38ccef558612de8a4fd95b75474290123130c14747fe4", + "sha256": "05057513e8f58661a16969a348cd5224cf6d5b6b1bb11f3515bb2e5eee9a5f57", "bytes": 22005, "lines": 156 }, From 1d66f40778cc9900fd21df51a28b9109c600b754 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:41:27 +0900 Subject: [PATCH 032/110] fix(workspace): preserve protected job analysis contracts --- .github/workflows/foundation-ci.yml | 1 + .../workflows/job-analysis-api-quality.yml | 105 ++++ .../migrations/0013_job_analysis_snapshot.sql | 260 +++++++++ manifest.json | 78 ++- schemas/openapi.yaml | 376 +++++++++++++ scripts/foundation-contract-core.mjs | 56 +- services/job-analysis-api/README.md | 39 ++ services/job-analysis-api/pyproject.toml | 42 ++ .../src/orgmetra_job_analysis_api/__init__.py | 43 ++ .../src/orgmetra_job_analysis_api/auth.py | 71 +++ .../authorization.py | 47 ++ .../src/orgmetra_job_analysis_api/http.py | 462 +++++++++++++++ .../src/orgmetra_job_analysis_api/postgres.py | 531 ++++++++++++++++++ .../src/orgmetra_job_analysis_api/py.typed | 0 .../src/orgmetra_job_analysis_api/snapshot.py | 494 ++++++++++++++++ services/job-analysis-api/tests/fixtures.py | 243 ++++++++ .../tests/test_audit_timestamp.py | 86 +++ services/job-analysis-api/tests/test_auth.py | 107 ++++ .../tests/test_fixture_provenance.py | 12 + .../tests/test_http_body_limit.py | 47 ++ .../tests/test_http_content_type.py | 123 ++++ .../tests/test_http_error_contract.py | 133 +++++ .../job-analysis-api/tests/test_http_route.py | 435 ++++++++++++++ .../tests/test_openapi_route_contract.py | 24 + .../tests/test_openapi_snapshot_schema.py | 39 ++ .../job-analysis-api/tests/test_postgres.py | 387 +++++++++++++ .../tests/test_postgres_concurrency.py | 191 +++++++ .../tests/test_postgres_contract_script.py | 13 + .../test_postgres_idempotency_authority.py | 97 ++++ .../tests/test_read_purpose_header.py | 108 ++++ .../job-analysis-api/tests/test_snapshot.py | 345 ++++++++++++ .../tests/test_snapshot_cardinality.py | 33 ++ .../tests/test_snapshot_field_contract.py | 43 ++ .../tests/test_workflow_contract.py | 26 + tests/foundation-contract.test.mjs | 20 +- tests/test_job_analysis_snapshot_postgres.sh | 296 ++++++++++ ..._job_analysis_snapshot_schema_hardening.sh | 65 +++ tests/validate_repository.py | 59 ++ 38 files changed, 5516 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/job-analysis-api-quality.yml create mode 100644 database/migrations/0013_job_analysis_snapshot.sql create mode 100644 services/job-analysis-api/README.md create mode 100644 services/job-analysis-api/pyproject.toml create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/__init__.py create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/auth.py create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/authorization.py create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/http.py create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/postgres.py create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/py.typed create mode 100644 services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py create mode 100644 services/job-analysis-api/tests/fixtures.py create mode 100644 services/job-analysis-api/tests/test_audit_timestamp.py create mode 100644 services/job-analysis-api/tests/test_auth.py create mode 100644 services/job-analysis-api/tests/test_fixture_provenance.py create mode 100644 services/job-analysis-api/tests/test_http_body_limit.py create mode 100644 services/job-analysis-api/tests/test_http_content_type.py create mode 100644 services/job-analysis-api/tests/test_http_error_contract.py create mode 100644 services/job-analysis-api/tests/test_http_route.py create mode 100644 services/job-analysis-api/tests/test_openapi_route_contract.py create mode 100644 services/job-analysis-api/tests/test_openapi_snapshot_schema.py create mode 100644 services/job-analysis-api/tests/test_postgres.py create mode 100644 services/job-analysis-api/tests/test_postgres_concurrency.py create mode 100644 services/job-analysis-api/tests/test_postgres_contract_script.py create mode 100644 services/job-analysis-api/tests/test_postgres_idempotency_authority.py create mode 100644 services/job-analysis-api/tests/test_read_purpose_header.py create mode 100644 services/job-analysis-api/tests/test_snapshot.py create mode 100644 services/job-analysis-api/tests/test_snapshot_cardinality.py create mode 100644 services/job-analysis-api/tests/test_snapshot_field_contract.py create mode 100644 services/job-analysis-api/tests/test_workflow_contract.py create mode 100755 tests/test_job_analysis_snapshot_postgres.sh create mode 100644 tests/test_job_analysis_snapshot_schema_hardening.sh diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 2afddc80d..64fa26e0d 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -92,6 +92,7 @@ jobs: - test_validity_study_case_postgres.sh - test_criterion_observation_scope_postgres.sh - test_people_mutation_idempotency_postgres.sh + - test_job_analysis_snapshot_postgres.sh services: postgres: image: postgres:16.14@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20 diff --git a/.github/workflows/job-analysis-api-quality.yml b/.github/workflows/job-analysis-api-quality.yml new file mode 100644 index 000000000..1e944644c --- /dev/null +++ b/.github/workflows/job-analysis-api-quality.yml @@ -0,0 +1,105 @@ +name: Job-Analysis API Quality + +on: + pull_request: + branches: + - develop + paths: + - "services/job-analysis-api/**" + - "packages/hris-kernel/**" + - "packages/keyverse-adapter/**" + - "database/migrations/0013_job_analysis_snapshot.sql" + - "tests/test_job_analysis_snapshot_postgres.sh" + - "tests/test_job_analysis_snapshot_schema_hardening.sh" + - "tests/validate_repository.py" + - "scripts/foundation-contract-core.mjs" + - "schemas/openapi.yaml" + - "manifest.json" + - "docs/adr/0007-governed-job-analysis-evidence.md" + - "docs/adr/0014-job-analysis-snapshot-persistence.md" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/foundation-ci.yml" + - ".github/workflows/job-analysis-api-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: job-analysis-api-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Job-analysis API contract and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Install reviewed test toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + python -m pip check + - name: Compile job-analysis API boundary + run: python -m compileall -q services/job-analysis-api/src packages/hris-kernel/src packages/keyverse-adapter/src services/job-analysis-api/tests + - name: Test governed job-analysis contracts with exact statement and branch coverage + env: + PYTHONPATH: services/job-analysis-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src + COVERAGE_FILE: /tmp/orgmetra-job-analysis-api.coverage + run: python -m pytest -c services/job-analysis-api/pyproject.toml services/job-analysis-api/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" + + postgres: + name: Job-analysis PostgreSQL integration + runs-on: ubuntu-latest + timeout-minutes: 12 + services: + postgres: + image: postgres:16.14@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20 + env: + POSTGRES_USER: orgmetra + POSTGRES_PASSWORD: orgmetra + POSTGRES_DB: orgmetra + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U orgmetra -d orgmetra" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql://orgmetra:orgmetra@localhost:5432/orgmetra + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Run persisted job-analysis PostgreSQL contract + run: | + bash tests/test_job_analysis_snapshot_postgres.sh + bash tests/test_job_analysis_snapshot_schema_hardening.sh + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/database/migrations/0013_job_analysis_snapshot.sql b/database/migrations/0013_job_analysis_snapshot.sql new file mode 100644 index 000000000..3cab8f255 --- /dev/null +++ b/database/migrations/0013_job_analysis_snapshot.sql @@ -0,0 +1,260 @@ +-- Persist one immutable job-analysis snapshot in 3NF. +-- The kernel JobAnalysisSnapshot remains the in-process evidence contract. +-- This migration stores that contract as tenant-scoped rows bound to the +-- existing job, and optionally position and criterion, identities. +-- A missing parent identity fails closed through tenant-qualified foreign keys. + +CREATE TABLE job_analysis_snapshot ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + analysis_record_id uuid PRIMARY KEY, + job_profile_id uuid NOT NULL, + position_record_id uuid, + criterion_blueprint_id uuid, + analysis_version_code text NOT NULL, + status_code text NOT NULL, + effective_from date NOT NULL, + recorded_at timestamptz NOT NULL, + reviewed_by_reference text, + reviewed_at timestamptz, + content_digest_sha256 text NOT NULL, + data_function_code integer NOT NULL, + people_function_code integer NOT NULL, + things_function_code integer NOT NULL, + fja_source_uri text NOT NULL, + fja_source_title text NOT NULL, + fja_source_version_code text NOT NULL, + fja_retrieved_at timestamptz NOT NULL, + fja_content_digest_sha256 text NOT NULL, + fja_origin_code text NOT NULL, + recorded_from timestamptz NOT NULL DEFAULT now(), + CONSTRAINT job_analysis_snapshot_job_tenant_fk + FOREIGN KEY (tenant_record_id, job_profile_id) + REFERENCES job_profile(tenant_record_id, job_profile_id), + CONSTRAINT job_analysis_snapshot_position_tenant_fk + FOREIGN KEY (tenant_record_id, position_record_id) + REFERENCES position_record(tenant_record_id, position_record_id), + CONSTRAINT job_analysis_snapshot_criterion_tenant_fk + FOREIGN KEY (tenant_record_id, criterion_blueprint_id) + REFERENCES criterion_blueprint(tenant_record_id, criterion_blueprint_id), + CONSTRAINT job_analysis_snapshot_tenant_identity_unique + UNIQUE (tenant_record_id, analysis_record_id), + CONSTRAINT job_analysis_snapshot_job_version_unique + UNIQUE (tenant_record_id, job_profile_id, analysis_version_code), + CONSTRAINT job_analysis_snapshot_status_code_check + CHECK (status_code IN ('analysis_draft', 'analysis_validated')), + CONSTRAINT job_analysis_snapshot_digest_format_check + CHECK (content_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_analysis_snapshot_fja_digest_format_check + CHECK (fja_content_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_analysis_snapshot_data_function_check + CHECK (data_function_code BETWEEN 0 AND 6), + CONSTRAINT job_analysis_snapshot_people_function_check + CHECK (people_function_code BETWEEN 0 AND 8), + CONSTRAINT job_analysis_snapshot_things_function_check + CHECK (things_function_code BETWEEN 0 AND 7), + CONSTRAINT job_analysis_snapshot_review_pair_check + CHECK ((reviewed_by_reference IS NULL) = (reviewed_at IS NULL)), + CONSTRAINT job_analysis_snapshot_tenant_operational_uuid_check + CHECK (is_operational_uuid(tenant_record_id)), + CONSTRAINT job_analysis_snapshot_analysis_operational_uuid_check + CHECK (is_operational_uuid(analysis_record_id)), + CONSTRAINT job_analysis_snapshot_job_operational_uuid_check + CHECK (is_operational_uuid(job_profile_id)), + CONSTRAINT job_analysis_snapshot_position_operational_uuid_check + CHECK (position_record_id IS NULL OR is_operational_uuid(position_record_id)), + CONSTRAINT job_analysis_snapshot_criterion_operational_uuid_check + CHECK (criterion_blueprint_id IS NULL OR is_operational_uuid(criterion_blueprint_id)) +); + +CREATE INDEX job_analysis_snapshot_position_idx + ON job_analysis_snapshot (tenant_record_id, position_record_id) + WHERE position_record_id IS NOT NULL; + +CREATE INDEX job_analysis_snapshot_criterion_idx + ON job_analysis_snapshot (tenant_record_id, criterion_blueprint_id) + WHERE criterion_blueprint_id IS NOT NULL; + +CREATE TABLE job_analysis_task_item ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + analysis_record_id uuid NOT NULL, + task_record_id uuid NOT NULL, + task_statement text NOT NULL, + importance_level integer NOT NULL, + difficulty_level integer NOT NULL, + source_uri text NOT NULL, + source_title text NOT NULL, + source_version_code text NOT NULL, + retrieved_at timestamptz NOT NULL, + content_digest_sha256 text NOT NULL, + origin_code text NOT NULL, + CONSTRAINT job_analysis_task_item_pk + PRIMARY KEY (tenant_record_id, analysis_record_id, task_record_id), + CONSTRAINT job_analysis_task_snapshot_tenant_fk + FOREIGN KEY (tenant_record_id, analysis_record_id) + REFERENCES job_analysis_snapshot(tenant_record_id, analysis_record_id), + CONSTRAINT job_analysis_task_importance_level_check + CHECK (importance_level BETWEEN 1 AND 5), + CONSTRAINT job_analysis_task_difficulty_level_check + CHECK (difficulty_level BETWEEN 1 AND 5), + CONSTRAINT job_analysis_task_digest_format_check + CHECK (content_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_analysis_task_tenant_operational_uuid_check + CHECK (is_operational_uuid(tenant_record_id)), + CONSTRAINT job_analysis_task_analysis_operational_uuid_check + CHECK (is_operational_uuid(analysis_record_id)), + CONSTRAINT job_analysis_task_record_operational_uuid_check + CHECK (is_operational_uuid(task_record_id)) +); + +CREATE TABLE job_analysis_ksao_item ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + analysis_record_id uuid NOT NULL, + ksao_record_id uuid NOT NULL, + category_code text NOT NULL, + requirement_statement text NOT NULL, + importance_level integer NOT NULL, + proficiency_level integer NOT NULL, + source_uri text NOT NULL, + source_title text NOT NULL, + source_version_code text NOT NULL, + retrieved_at timestamptz NOT NULL, + content_digest_sha256 text NOT NULL, + origin_code text NOT NULL, + CONSTRAINT job_analysis_ksao_item_pk + PRIMARY KEY (tenant_record_id, analysis_record_id, ksao_record_id), + CONSTRAINT job_analysis_ksao_snapshot_tenant_fk + FOREIGN KEY (tenant_record_id, analysis_record_id) + REFERENCES job_analysis_snapshot(tenant_record_id, analysis_record_id), + CONSTRAINT job_analysis_ksao_category_code_check + CHECK (category_code IN ( + 'knowledge_requirement', + 'skill_requirement', + 'ability_requirement', + 'other_characteristic' + )), + CONSTRAINT job_analysis_ksao_importance_level_check + CHECK (importance_level BETWEEN 1 AND 5), + CONSTRAINT job_analysis_ksao_proficiency_level_check + CHECK (proficiency_level BETWEEN 1 AND 5), + CONSTRAINT job_analysis_ksao_digest_format_check + CHECK (content_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_analysis_ksao_tenant_operational_uuid_check + CHECK (is_operational_uuid(tenant_record_id)), + CONSTRAINT job_analysis_ksao_analysis_operational_uuid_check + CHECK (is_operational_uuid(analysis_record_id)), + CONSTRAINT job_analysis_ksao_record_operational_uuid_check + CHECK (is_operational_uuid(ksao_record_id)) +); + +CREATE TABLE job_analysis_task_ksao_link ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + analysis_record_id uuid NOT NULL, + task_record_id uuid NOT NULL, + ksao_record_id uuid NOT NULL, + relationship_strength integer NOT NULL, + essential_for_task boolean NOT NULL, + CONSTRAINT job_analysis_task_ksao_link_pk + PRIMARY KEY (tenant_record_id, analysis_record_id, task_record_id, ksao_record_id), + CONSTRAINT job_analysis_link_task_tenant_fk + FOREIGN KEY (tenant_record_id, analysis_record_id, task_record_id) + REFERENCES job_analysis_task_item(tenant_record_id, analysis_record_id, task_record_id), + CONSTRAINT job_analysis_link_ksao_tenant_fk + FOREIGN KEY (tenant_record_id, analysis_record_id, ksao_record_id) + REFERENCES job_analysis_ksao_item(tenant_record_id, analysis_record_id, ksao_record_id), + CONSTRAINT job_analysis_link_strength_check + CHECK (relationship_strength BETWEEN 1 AND 5), + CONSTRAINT job_analysis_link_tenant_operational_uuid_check + CHECK (is_operational_uuid(tenant_record_id)), + CONSTRAINT job_analysis_link_analysis_operational_uuid_check + CHECK (is_operational_uuid(analysis_record_id)), + CONSTRAINT job_analysis_link_task_operational_uuid_check + CHECK (is_operational_uuid(task_record_id)), + CONSTRAINT job_analysis_link_ksao_operational_uuid_check + CHECK (is_operational_uuid(ksao_record_id)) +); + +CREATE TABLE job_analysis_write_command ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + write_command_id uuid PRIMARY KEY, + analysis_record_id uuid NOT NULL, + idempotency_key text NOT NULL, + request_digest_sha256 text NOT NULL, + actor_reference text NOT NULL, + purpose_code text NOT NULL, + recorded_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT job_analysis_write_command_snapshot_tenant_fk + FOREIGN KEY (tenant_record_id, analysis_record_id) + REFERENCES job_analysis_snapshot(tenant_record_id, analysis_record_id), + CONSTRAINT job_analysis_write_command_tenant_identity_unique + UNIQUE (tenant_record_id, write_command_id), + CONSTRAINT job_analysis_write_command_idempotency_unique + UNIQUE (tenant_record_id, idempotency_key), + CONSTRAINT job_analysis_write_command_digest_format_check + CHECK (request_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_analysis_write_command_key_length_check + CHECK (char_length(idempotency_key) BETWEEN 16 AND 200), + CONSTRAINT job_analysis_write_command_tenant_operational_uuid_check + CHECK (is_operational_uuid(tenant_record_id)), + CONSTRAINT job_analysis_write_command_id_operational_uuid_check + CHECK (is_operational_uuid(write_command_id)), + CONSTRAINT job_analysis_write_command_analysis_operational_uuid_check + CHECK (is_operational_uuid(analysis_record_id)) +); + +CREATE INDEX job_analysis_write_command_analysis_idx + ON job_analysis_write_command (tenant_record_id, analysis_record_id); + +CREATE TRIGGER job_analysis_snapshot_append_only_guard +BEFORE UPDATE OR DELETE ON job_analysis_snapshot +FOR EACH ROW +EXECUTE FUNCTION reject_append_only_mutation(); + +CREATE TRIGGER job_analysis_task_item_append_only_guard +BEFORE UPDATE OR DELETE ON job_analysis_task_item +FOR EACH ROW +EXECUTE FUNCTION reject_append_only_mutation(); + +CREATE TRIGGER job_analysis_ksao_item_append_only_guard +BEFORE UPDATE OR DELETE ON job_analysis_ksao_item +FOR EACH ROW +EXECUTE FUNCTION reject_append_only_mutation(); + +CREATE TRIGGER job_analysis_task_ksao_link_append_only_guard +BEFORE UPDATE OR DELETE ON job_analysis_task_ksao_link +FOR EACH ROW +EXECUTE FUNCTION reject_append_only_mutation(); + +CREATE TRIGGER job_analysis_write_command_append_only_guard +BEFORE UPDATE OR DELETE ON job_analysis_write_command +FOR EACH ROW +EXECUTE FUNCTION reject_append_only_mutation(); + +ALTER TABLE job_analysis_snapshot ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_analysis_snapshot FORCE ROW LEVEL SECURITY; +CREATE POLICY job_analysis_snapshot_scope_policy ON job_analysis_snapshot +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +ALTER TABLE job_analysis_task_item ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_analysis_task_item FORCE ROW LEVEL SECURITY; +CREATE POLICY job_analysis_task_item_scope_policy ON job_analysis_task_item +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +ALTER TABLE job_analysis_ksao_item ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_analysis_ksao_item FORCE ROW LEVEL SECURITY; +CREATE POLICY job_analysis_ksao_item_scope_policy ON job_analysis_ksao_item +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +ALTER TABLE job_analysis_task_ksao_link ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_analysis_task_ksao_link FORCE ROW LEVEL SECURITY; +CREATE POLICY job_analysis_task_ksao_link_scope_policy ON job_analysis_task_ksao_link +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +ALTER TABLE job_analysis_write_command ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_analysis_write_command FORCE ROW LEVEL SECURITY; +CREATE POLICY job_analysis_write_command_scope_policy ON job_analysis_write_command +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); \ No newline at end of file diff --git a/manifest.json b/manifest.json index a838d0ee9..dd3290511 100644 --- a/manifest.json +++ b/manifest.json @@ -5,9 +5,15 @@ "files": [ { "path": ".github/workflows/foundation-ci.yml", - "sha256": "76e9707b902946d9f3953e76aa647b87f572273be3c222df9b82c47ca022688b", - "bytes": 4455, - "lines": 126 + "sha256": "7f664425d1307c21f7d26bea40553426b434d141fa2fedaf5ea25e798247878d", + "bytes": 4506, + "lines": 127 + }, + { + "path": ".github/workflows/job-analysis-api-quality.yml", + "sha256": "352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a", + "bytes": 4159, + "lines": 105 }, { "path": ".gitignore", @@ -165,6 +171,12 @@ "bytes": 3162, "lines": 76 }, + { + "path": "database/migrations/0013_job_analysis_snapshot.sql", + "sha256": "b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee", + "bytes": 12713, + "lines": 260 + }, { "path": "docs/API_CONTRACT.md", "sha256": "63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589", @@ -291,6 +303,12 @@ "bytes": 14100, "lines": 66 }, + { + "path": "docs/adr/0007-governed-job-analysis-evidence.md", + "sha256": "abdff206a700018b5997acce49d5c66eddcf2f9d76cb3446e964749011c8f2da", + "bytes": 5638, + "lines": 57 + }, { "path": "docs/adr/0008-purpose-bound-pii-authorization.md", "sha256": "c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7", @@ -303,6 +321,30 @@ "bytes": 7057, "lines": 57 }, + { + "path": "docs/adr/0010-naruon-calendar-intent-boundary.md", + "sha256": "3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9", + "bytes": 3917, + "lines": 35 + }, + { + "path": "docs/adr/0011-bitemporal-workforce-composition.md", + "sha256": "1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b", + "bytes": 5568, + "lines": 53 + }, + { + "path": "docs/adr/0012-governed-migration-handoff.md", + "sha256": "713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80", + "bytes": 5965, + "lines": 59 + }, + { + "path": "docs/adr/0013-governed-requisition-review-packet.md", + "sha256": "70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802", + "bytes": 4693, + "lines": 46 + }, { "path": "docs/adr/0014-job-analysis-snapshot-persistence.md", "sha256": "a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105", @@ -371,15 +413,15 @@ }, { "path": "schemas/openapi.yaml", - "sha256": "3e4e5d3f896a874eb152fe7252672cd576e993c0755fb33ec303d5dc4a317f01", - "bytes": 18988, - "lines": 644 + "sha256": "09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f", + "bytes": 29503, + "lines": 1020 }, { "path": "scripts/foundation-contract-core.mjs", - "sha256": "2bfbe6f00aa5246c9ccbc973b0858e7b950bc835d64b039b516f442592802f18", - "bytes": 25833, - "lines": 654 + "sha256": "7f15517490390e4460e23e4f8ff689cf9bc33efd681b79b1d350fb16990dfa41", + "bytes": 28557, + "lines": 700 }, { "path": "scripts/foundation-contract.mjs", @@ -395,9 +437,9 @@ }, { "path": "tests/foundation-contract.test.mjs", - "sha256": "e9633d28ff7d66db2930b6c6e199dd9aaaacbae48c4b9aa939029f2a19fbacac", - "bytes": 14101, - "lines": 368 + "sha256": "960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615", + "bytes": 14860, + "lines": 386 }, { "path": "tests/hr-workspace.test.mjs", @@ -447,6 +489,12 @@ "bytes": 11349, "lines": 370 }, + { + "path": "tests/test_job_analysis_snapshot_postgres.sh", + "sha256": "ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f", + "bytes": 13542, + "lines": 296 + }, { "path": "tests/test_operational_uuid_postgres.sh", "sha256": "7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7", @@ -485,9 +533,9 @@ }, { "path": "tests/validate_repository.py", - "sha256": "546485bb47e18f7dc0b0090f401b0c4491183529ea08489bf993cb13fcbd6796", - "bytes": 24891, - "lines": 591 + "sha256": "e3934e3ce7fb3b2e053d21556c495e84112253b985d5082f8f44a6513c87c930", + "bytes": 27695, + "lines": 650 } ] } diff --git a/schemas/openapi.yaml b/schemas/openapi.yaml index 5826bb3ed..0fd397e92 100644 --- a/schemas/openapi.yaml +++ b/schemas/openapi.yaml @@ -261,6 +261,89 @@ paths: $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/IdempotencyConflict' + /tenants/{tenant_record_id}/job-analysis-snapshots: + post: + operationId: persistJobAnalysisSnapshot + summary: Persist a governed job-analysis snapshot + tags: + - job-architecture + security: + - keyverse_oidc: + - orgmetra.job_architecture.write + parameters: + - name: tenant_record_id + in: path + required: true + schema: + type: string + format: uuid + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/PurposeCode' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PersistJobAnalysisSnapshotCommand' + responses: + '201': + description: Persisted + headers: + Location: + description: Canonical URI for the persisted job-analysis snapshot. + schema: + type: string + format: uri + content: + application/json: + schema: + $ref: '#/components/schemas/JobAnalysisSnapshotDocument' + '400': + $ref: '#/components/responses/InvalidCommand' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '409': + $ref: '#/components/responses/IdempotencyConflict' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + /tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}: + get: + operationId: readJobAnalysisSnapshot + summary: Read the persisted job-analysis snapshot + tags: + - job-architecture + security: + - keyverse_oidc: + - orgmetra.job_architecture.read + parameters: + - name: tenant_record_id + in: path + required: true + schema: + type: string + format: uuid + - name: analysis_record_id + in: path + required: true + schema: + type: string + format: uuid + - $ref: '#/components/parameters/PurposeCode' + responses: + '200': + description: The exact persisted snapshot document. + content: + application/json: + schema: + $ref: '#/components/schemas/JobAnalysisSnapshotDocument' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/SnapshotNotFound' components: securitySchemes: keyverse_oidc: @@ -586,6 +669,282 @@ components: assignment_record_id: type: string format: uuid + JobAnalysisEvidenceSource: + type: object + additionalProperties: false + required: + - source_uri + - source_title + - source_version_code + - retrieved_at + - content_digest_sha256 + - origin_code + properties: + source_uri: + type: string + format: uri + pattern: '^https://' + source_title: + type: string + minLength: 3 + source_version_code: + type: string + pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]*$' + retrieved_at: + type: string + format: date-time + content_digest_sha256: + type: string + pattern: '^[0-9a-f]{64}$' + origin_code: + type: string + enum: + - authoritative_occupation_source + - job_incumbent + - supervisor_sme + - occupational_expert + - web_research + - llm_draft + JobAnalysisTaskEvidence: + type: object + additionalProperties: false + required: + - task_record_id + - task_statement + - importance_level + - difficulty_level + - source + properties: + task_record_id: + type: string + format: uuid + task_statement: + type: string + minLength: 10 + importance_level: + type: integer + minimum: 1 + maximum: 5 + difficulty_level: + type: integer + minimum: 1 + maximum: 5 + source: + $ref: '#/components/schemas/JobAnalysisEvidenceSource' + JobAnalysisKSAORequirement: + type: object + additionalProperties: false + required: + - ksao_record_id + - category_code + - requirement_statement + - importance_level + - proficiency_level + - source + properties: + ksao_record_id: + type: string + format: uuid + category_code: + type: string + enum: + - knowledge_requirement + - skill_requirement + - ability_requirement + - other_characteristic + requirement_statement: + type: string + minLength: 10 + importance_level: + type: integer + minimum: 1 + maximum: 5 + proficiency_level: + type: integer + minimum: 1 + maximum: 5 + source: + $ref: '#/components/schemas/JobAnalysisEvidenceSource' + JobAnalysisTaskKSAOLink: + type: object + additionalProperties: false + required: + - task_record_id + - ksao_record_id + - relationship_strength + - essential_for_task + properties: + task_record_id: + type: string + format: uuid + ksao_record_id: + type: string + format: uuid + relationship_strength: + type: integer + minimum: 1 + maximum: 5 + essential_for_task: + type: boolean + FunctionalJobAnalysisProfile: + type: object + additionalProperties: false + required: + - data_function_code + - people_function_code + - things_function_code + - source + properties: + data_function_code: + type: integer + minimum: 0 + maximum: 6 + people_function_code: + type: integer + minimum: 0 + maximum: 8 + things_function_code: + type: integer + minimum: 0 + maximum: 7 + source: + $ref: '#/components/schemas/JobAnalysisEvidenceSource' + PersistJobAnalysisSnapshotCommand: + type: object + additionalProperties: false + required: + - analysis_record_id + - tenant_record_id + - job_record_id + - analysis_version_code + - status_code + - effective_from + - recorded_at + - tasks + - ksao_requirements + - task_ksao_links + - fja_profile + properties: + analysis_record_id: + type: string + format: uuid + tenant_record_id: + type: string + format: uuid + job_record_id: + type: string + format: uuid + position_record_id: + type: string + format: uuid + criterion_blueprint_id: + type: string + format: uuid + analysis_version_code: + type: string + minLength: 1 + maxLength: 100 + status_code: + type: string + pattern: '^(analysis_draft|analysis_validated)$' + effective_from: + type: string + format: date + recorded_at: + type: string + format: date-time + reviewed_by_reference: + type: string + minLength: 1 + maxLength: 300 + reviewed_at: + type: string + format: date-time + tasks: + type: array + minItems: 1 + maxItems: 500 + items: + $ref: '#/components/schemas/JobAnalysisTaskEvidence' + ksao_requirements: + type: array + minItems: 1 + maxItems: 500 + items: + $ref: '#/components/schemas/JobAnalysisKSAORequirement' + task_ksao_links: + type: array + minItems: 1 + maxItems: 5000 + items: + $ref: '#/components/schemas/JobAnalysisTaskKSAOLink' + fja_profile: + $ref: '#/components/schemas/FunctionalJobAnalysisProfile' + JobAnalysisSnapshotDocument: + type: object + additionalProperties: false + required: + - analysis_record_id + - tenant_record_id + - job_record_id + - analysis_version_code + - status_code + - effective_from + - recorded_at + - tasks + - ksao_requirements + - task_ksao_links + - fja_profile + properties: + analysis_record_id: + type: string + format: uuid + tenant_record_id: + type: string + format: uuid + job_record_id: + type: string + format: uuid + analysis_version_code: + type: string + minLength: 1 + maxLength: 100 + status_code: + type: string + pattern: '^(analysis_draft|analysis_validated)$' + effective_from: + type: string + format: date + recorded_at: + type: string + format: date-time + reviewed_by_reference: + type: string + minLength: 1 + maxLength: 300 + reviewed_at: + type: string + format: date-time + tasks: + type: array + minItems: 1 + maxItems: 500 + items: + $ref: '#/components/schemas/JobAnalysisTaskEvidence' + ksao_requirements: + type: array + minItems: 1 + maxItems: 500 + items: + $ref: '#/components/schemas/JobAnalysisKSAORequirement' + task_ksao_links: + type: array + minItems: 1 + maxItems: 5000 + items: + $ref: '#/components/schemas/JobAnalysisTaskKSAOLink' + fja_profile: + $ref: '#/components/schemas/FunctionalJobAnalysisProfile' ErrorResponse: type: object additionalProperties: false @@ -595,6 +954,11 @@ components: - next_action - support_reference properties: + error: + type: string + pattern: '^[a-z][a-z0-9_]{2,63}$' + deprecated: true + description: Backward-compatible alias for error_code; new clients must use error_code. error_code: type: string pattern: '^[a-z][a-z0-9_]{2,63}$' @@ -642,3 +1006,15 @@ components: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + SnapshotNotFound: + description: The authorized job-analysis snapshot target does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + UnsupportedMediaType: + description: The job-analysis POST body is missing application/json media type. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 2c5dbc1db..3831df819 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -28,6 +28,7 @@ export const REQUIRED_FILES = Object.freeze([ 'apps/hr-workspace/app.js', 'apps/hr-workspace/workspace.stories.js', '.github/workflows/foundation-ci.yml', + '.github/workflows/job-analysis-api-quality.yml', 'docs/PRD.md', 'docs/TRD.md', 'docs/USER_STORIES.md', @@ -51,8 +52,13 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/adr/0004-employment-position-version-and-assignment-binding.md', 'docs/adr/0005-exclusive-employment-and-staffable-seats.md', 'docs/adr/0006-governed-audit-outbox-envelope.md', + 'docs/adr/0007-governed-job-analysis-evidence.md', 'docs/adr/0008-purpose-bound-pii-authorization.md', 'docs/adr/0009-performance-criterion-observation-scope.md', + 'docs/adr/0010-naruon-calendar-intent-boundary.md', + 'docs/adr/0011-bitemporal-workforce-composition.md', + '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', @@ -70,6 +76,7 @@ export const REQUIRED_FILES = Object.freeze([ 'database/migrations/0010_validity_study_case_integrity.sql', 'database/migrations/0011_criterion_observation_scope.sql', 'database/migrations/0012_people_mutation_idempotency.sql', + 'database/migrations/0013_job_analysis_snapshot.sql', 'packages/hris-kernel/src/orgmetra_hris_kernel/audit.py', 'packages/hris-kernel/tests/test_audit_outbox.py', 'schemas/openapi.yaml', @@ -91,6 +98,7 @@ export const REQUIRED_FILES = Object.freeze([ 'tests/test_validity_study_case_postgres.sh', 'tests/test_criterion_observation_scope_postgres.sh', 'tests/test_people_mutation_idempotency_postgres.sh', + 'tests/test_job_analysis_snapshot_postgres.sh', 'tests/validate_repository.py' ]); @@ -116,9 +124,9 @@ export const DATABASE_OBJECT_NAMES = Object.freeze([ 'cost_center_record', 'job_family', 'job_profile', 'job_profile_version', 'position_record', 'position_record_version', 'position_relation', 'assignment_record', - 'job_analysis_case', 'source_record', 'source_version', 'task_statement', - 'task_rating', 'fja_function', 'task_fja_link', 'ksao_requirement', - 'task_ksao_link', 'qualification_rule', 'candidate_profile', + 'job_analysis_snapshot', 'job_analysis_task_item', 'job_analysis_ksao_item', + 'job_analysis_task_ksao_link', 'job_analysis_write_command', + 'qualification_rule', 'candidate_profile', 'requisition_record', 'application_record', 'application_stage_history', 'candidate_evidence_link', 'assessment_assignment', 'interview_session', 'interview_rating', 'selection_decision', 'decision_evidence_link', @@ -137,7 +145,12 @@ export const DATABASE_OBJECT_NAMES = Object.freeze([ /** Migration-backed logical objects whose persisted table identity must not drift. */ export const MIGRATION_BACKED_DATABASE_OBJECT_NAMES = Object.freeze([ - 'people_mutation_idempotency_record' + 'people_mutation_idempotency_record', + 'job_analysis_snapshot', + 'job_analysis_task_item', + 'job_analysis_ksao_item', + 'job_analysis_task_ksao_link', + 'job_analysis_write_command' ]); const UNFINISHED_MARKER_LINE_PATTERN = /^\s*(?:#{1,6}\s+|[-*+]\s+)?(?:\[(?:TODO|TBD|FIXME)\]|\{\{(?:TODO|TBD|FIXME)\}\}|<(?:TODO|TBD|FIXME)>|(?:TODO|TBD|FIXME)(?:\s*:\s*.*)?\s*)$/i; @@ -313,15 +326,23 @@ export function validateMigrationBackedDatabaseObjectNames( const migrationDirectory = join(rootPath, 'database/migrations'); const inventoryNames = new Set(DATABASE_OBJECT_NAMES); const createdTableNames = new Set(); + const migrationPrefixes = new Map(); + const errors = []; if (existsSync(migrationDirectory)) { for (const entryName of readdirSync(migrationDirectory).sort()) { if (!/^\d{4}_[a-z0-9_]+\.sql$/.test(entryName)) continue; + const prefix = entryName.slice(0, 4); + const prior = migrationPrefixes.get(prefix); + if (prior) { + errors.push(`Duplicate migration number prefix ${prefix}: ${prior}, ${entryName}`); + } else { + migrationPrefixes.set(prefix, entryName); + } const migrationText = readFileSync(join(migrationDirectory, entryName), 'utf8'); for (const tableName of extractCreatedTableNames(migrationText)) createdTableNames.add(tableName); } } - const errors = []; for (const objectName of objectNames) { if (!inventoryNames.has(objectName)) { errors.push(`Database object inventory omitted migration-backed object: ${objectName}`); @@ -504,6 +525,31 @@ export function validateOpenApiContract(openapiText) { requireWithin(errors, contract.operationId, pathBlock, ' Location:', '201 Location header'); } + const jobAnalysisWrite = extractYamlBlock(openapiText, ' /tenants/{tenant_record_id}/job-analysis-snapshots:'); + if (!jobAnalysisWrite) { + errors.push('persistJobAnalysisSnapshot: path block is missing'); + } else { + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, 'operationId: persistJobAnalysisSnapshot', 'operationId'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, ' - orgmetra.job_architecture.write', 'least-privilege write scope'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, `$ref: '#/components/parameters/IdempotencyKey'`, 'Idempotency-Key'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, `$ref: '#/components/parameters/PurposeCode'`, 'purpose parameter'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, `$ref: '#/components/schemas/PersistJobAnalysisSnapshotCommand'`, 'request body binding'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, " '201':", '201 response'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, ' Location:', '201 Location header'); + requireWithin(errors, 'persistJobAnalysisSnapshot', jobAnalysisWrite, " '415':", 'unsupported-media response'); + } + + const jobAnalysisRead = extractYamlBlock( + openapiText, + ' /tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}:' + ); + if (!jobAnalysisRead) { + errors.push('readJobAnalysisSnapshot: path block is missing'); + } else { + requireWithin(errors, 'readJobAnalysisSnapshot', jobAnalysisRead, 'operationId: readJobAnalysisSnapshot', 'operationId'); + requireWithin(errors, 'readJobAnalysisSnapshot', jobAnalysisRead, ' - orgmetra.job_architecture.read', 'least-privilege read scope'); + } + const jobCommand = extractYamlBlock(openapiText, ' CreateJobProfileCommand:'); if (!jobCommand) { errors.push('CreateJobProfileCommand: schema block is missing'); diff --git a/services/job-analysis-api/README.md b/services/job-analysis-api/README.md new file mode 100644 index 000000000..5e6322f57 --- /dev/null +++ b/services/job-analysis-api/README.md @@ -0,0 +1,39 @@ +# Orgmetra Job-Analysis API + +This service persists one immutable `JobAnalysisSnapshot` as tenant-scoped 3NF +rows and returns the same snapshot document on read. The kernel remains the +evidence contract. This boundary owns authorization, Idempotency-Key handling, +parent-scope fail-closed checks, and the transactional +`record_audit_outbox_event(...)` write. + +The snapshot is occupational evidence about a Job, not a hiring, promotion, +termination, compensation, or other high-impact employment decision. Purpose-bound +authorization is required; job-analysis fields are not blanket-masked as PII. + +Supported routes: + +- `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` +- `GET /v1/tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}` + +The tenant authority comes from the path and must match the authenticated +principal's tenant. The actor authority comes from the authenticated principal; +these routes do not accept duplicate caller-controlled tenant or actor headers. +POST requires `Authorization`, `Content-Type: application/json`, +`Idempotency-Key`, and `X-Purpose-Code`. Missing, non-ASCII, or non-JSON media +types receive `415 unsupported_media_type` before the body is read. Optional +`position_record_id` and `criterion_blueprint_id` may be included and are bound +through foreign keys that fail closed when the parent is missing. GET requires +`purpose` and returns the persisted snapshot document. + +Posted evidence is bounded and unambiguous. The transport stops reading once the +cumulative chunked body exceeds 1 MiB and rejects duplicate JSON member names at +any object depth. The snapshot parser enforces exact top-level, Task, KSAO, +Task–KSAO link, FJA, and provenance-source field allowlists. Unsupported fields +are rejected rather than silently discarded or collapsed into the digest of a +different accepted command. + +Every customer-facing error includes the stable `error_code`, explanatory +`message`, actionable `next_action`, and a random `support_reference` that does +not encode tenant, trace, topology, timestamp, credential, or PII. The legacy +`error` field is retained only as a deprecated alias for `error_code` while +clients migrate; new integrations must use `error_code`. diff --git a/services/job-analysis-api/pyproject.toml b/services/job-analysis-api/pyproject.toml new file mode 100644 index 000000000..5e27bf477 --- /dev/null +++ b/services/job-analysis-api/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools==82.0.1"] +build-backend = "setuptools.build_meta" + +[project] +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" +license = { text = "Apache-2.0" } +authors = [{ name = "ContextualWisdomLab" }] +dependencies = [ + "orgmetra-hris-kernel==0.4.0", + "orgmetra-keyverse-adapter==0.1.0", +] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +orgmetra_job_analysis_api = ["py.typed"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_job_analysis_api", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] + +[tool.coverage.run] +branch = true +source = ["orgmetra_job_analysis_api"] + +[tool.coverage.report] +fail_under = 100 +show_missing = true diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/__init__.py b/services/job-analysis-api/src/orgmetra_job_analysis_api/__init__.py new file mode 100644 index 000000000..df6c49ec5 --- /dev/null +++ b/services/job-analysis-api/src/orgmetra_job_analysis_api/__init__.py @@ -0,0 +1,43 @@ +"""Governed write and read contracts for persisted job-analysis snapshots.""" + +from orgmetra_job_analysis_api.auth import ( + AuthenticatedPrincipal, + AuthenticationFailed, + TokenAuthenticator, + extract_bearer_token, +) +from orgmetra_job_analysis_api.authorization import authorize_resource_fields +from orgmetra_job_analysis_api.http import JobAnalysisAsgiApp +from orgmetra_job_analysis_api.postgres import PostgresJobAnalysisPort +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIdempotencyConflict, + JobAnalysisIntegrityError, + JobAnalysisReadPort, + JobAnalysisScopeMissing, + JobAnalysisSnapshotNotFound, + JobAnalysisWritePort, + PersistedJobAnalysisView, + persist_job_analysis_snapshot, + read_job_analysis_snapshot, + snapshot_from_document, +) + +__all__ = [ + "AuthenticatedPrincipal", + "AuthenticationFailed", + "JobAnalysisAsgiApp", + "JobAnalysisIdempotencyConflict", + "JobAnalysisIntegrityError", + "JobAnalysisReadPort", + "JobAnalysisScopeMissing", + "JobAnalysisSnapshotNotFound", + "JobAnalysisWritePort", + "PersistedJobAnalysisView", + "PostgresJobAnalysisPort", + "TokenAuthenticator", + "authorize_resource_fields", + "extract_bearer_token", + "persist_job_analysis_snapshot", + "read_job_analysis_snapshot", + "snapshot_from_document", +] diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/auth.py b/services/job-analysis-api/src/orgmetra_job_analysis_api/auth.py new file mode 100644 index 000000000..dc3ef9274 --- /dev/null +++ b/services/job-analysis-api/src/orgmetra_job_analysis_api/auth.py @@ -0,0 +1,71 @@ +"""Fail-closed request-edge authentication for job-analysis persistence. + +Authentication proves who the caller is and which operation scopes Keyverse +issued. Purpose-bound authorization stays outside this principal so a token +cannot smuggle an HR purpose grant. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import re +from typing import Protocol, runtime_checkable +from uuid import UUID + +_MAX_UUID_INT = (1 << 128) - 1 +_REFERENCE_PATTERN = re.compile(r"^[a-z][a-z0-9_]*:[A-Za-z0-9][A-Za-z0-9._~-]*$") +_SCOPE_PATTERN = re.compile(r"^orgmetra(?:\.[a-z][a-z0-9_]*){2,}$") + + +class AuthenticationFailed(RuntimeError): + """Indicate that bearer authentication evidence is absent or malformed.""" + + +@dataclass(frozen=True, slots=True) +class AuthenticatedPrincipal: + """Identity attributes that may be trusted only after token authentication. + + ``tenant_record_id`` binds the authenticated actor to one Orgmetra tenant. + ``actor_reference`` is opaque audit correlation. ``granted_scope_codes`` + carries explicit operation capabilities and never an HR purpose decision. + """ + + tenant_record_id: UUID + actor_reference: str + granted_scope_codes: frozenset[str] + + def __post_init__(self) -> None: + """Reject sentinel identities, mutable grants, wildcards, and bad references.""" + if not isinstance(self.tenant_record_id, UUID): + raise ValueError("tenant_record_id must be a UUID.") + if self.tenant_record_id.int in (0, _MAX_UUID_INT): + raise ValueError("tenant_record_id must not use a reserved UUID sentinel.") + if not isinstance(self.actor_reference, str) or _REFERENCE_PATTERN.fullmatch(self.actor_reference) is None: + raise ValueError("actor_reference must be a namespaced opaque reference.") + if not isinstance(self.granted_scope_codes, frozenset) or not self.granted_scope_codes: + raise ValueError("granted_scope_codes must be a non-empty frozenset.") + if any(not isinstance(scope, str) or _SCOPE_PATTERN.fullmatch(scope) is None for scope in self.granted_scope_codes): + raise ValueError("granted_scope_codes must contain explicit Orgmetra scopes.") + + +@runtime_checkable +class TokenAuthenticator(Protocol): + """Authenticate one bearer token without making an HR authorization decision.""" + + async def authenticate(self, bearer_token: str) -> AuthenticatedPrincipal: + """Return authenticated identity/scope attributes or raise a stable error.""" + + +def extract_bearer_token(authorization_header: str | None) -> str: + """Return one bounded printable bearer token without logging its value.""" + if authorization_header is None: + raise AuthenticationFailed("bearer authentication is required") + parts = authorization_header.split(" ", 1) + if len(parts) != 2 or parts[0].casefold() != "bearer": + raise AuthenticationFailed("authorization must use the Bearer scheme") + token = parts[1] + if not token or len(token) > 8192: + raise AuthenticationFailed("bearer token length is invalid") + if any(ord(character) < 0x21 or ord(character) > 0x7E for character in token): + raise AuthenticationFailed("bearer token contains invalid characters") + return token diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/authorization.py b/services/job-analysis-api/src/orgmetra_job_analysis_api/authorization.py new file mode 100644 index 000000000..4d936b29b --- /dev/null +++ b/services/job-analysis-api/src/orgmetra_job_analysis_api/authorization.py @@ -0,0 +1,47 @@ +"""Delegate job-analysis field access to Orgmetra's integrated policy evaluator.""" + +from __future__ import annotations + +from uuid import UUID + +from orgmetra_keyverse_adapter import ( + AuthorizationDecision, + PurposeBoundAccessPolicy, + PurposeBoundAccessRequest, + require_purpose_bound_access, +) + +from orgmetra_job_analysis_api.auth import AuthenticatedPrincipal + + +def authorize_resource_fields( + *, + principal: AuthenticatedPrincipal, + tenant_record_id: UUID, + resource_tenant_record_id: UUID, + resource_reference: str, + purpose_code: str, + operation_code: str, + resource_kind: str, + requested_fields: frozenset[str], + policy: PurposeBoundAccessPolicy, +) -> AuthorizationDecision: + """Authorize one exact job-analysis resource without duplicating policy logic. + + The API contributes only request-edge identity/scope attributes and the + resolved target. The Keyverse adapter remains the single owner of + tenant/resource/purpose/operation/scope/field evaluation. + """ + request = PurposeBoundAccessRequest( + tenant_record_id=tenant_record_id, + actor_tenant_record_id=principal.tenant_record_id, + resource_tenant_record_id=resource_tenant_record_id, + actor_reference=principal.actor_reference, + resource_reference=resource_reference, + purpose_code=purpose_code, + operation_code=operation_code, + resource_kind=resource_kind, + requested_fields=requested_fields, + granted_scope_codes=principal.granted_scope_codes, + ) + return require_purpose_bound_access(request=request, policy=policy) diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/http.py b/services/job-analysis-api/src/orgmetra_job_analysis_api/http.py new file mode 100644 index 000000000..9e32ba8bb --- /dev/null +++ b/services/job-analysis-api/src/orgmetra_job_analysis_api/http.py @@ -0,0 +1,462 @@ +"""Dependency-light ASGI routes for governed job-analysis snapshot persistence. + +The transport adapter owns HTTP parsing and stable, non-disclosing responses. +Authentication, purpose-bound authorization, kernel validation, and tenant +isolation remain delegated to the snapshot use cases and persistence port. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import json +import re +from secrets import token_urlsafe +from typing import Awaitable, Callable, Mapping, Sequence +from uuid import UUID + +from orgmetra_keyverse_adapter import AuthorizationDeniedError, PurposeBoundAccessPolicy + +from orgmetra_job_analysis_api.auth import AuthenticationFailed, TokenAuthenticator, extract_bearer_token +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIdempotencyConflict, + JobAnalysisIntegrityError, + JobAnalysisReadPort, + JobAnalysisScopeMissing, + JobAnalysisSnapshotNotFound, + JobAnalysisWritePort, + persist_job_analysis_snapshot, + read_job_analysis_snapshot, +) + +AsgiReceive = Callable[[], Awaitable[dict[str, object]]] +AsgiSend = Callable[[dict[str, object]], Awaitable[None]] + +_ROUTE_PREFIX = ("v1", "tenants") +_PURPOSE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$") +_MAX_UUID_INT = (1 << 128) - 1 +_MAX_REQUEST_BODY_BYTES = 1 << 20 +_ERROR_NEXT_ACTION = { + "route_not_found": "Use the documented tenant-scoped job-analysis route and retry.", + "method_not_allowed": "Use POST to persist or GET to read one job-analysis snapshot.", + "invalid_request": "Correct the referenced identifiers, headers, query, or snapshot document and retry.", + "authentication_required": "Obtain one valid Keyverse Bearer credential and retry.", + "unsupported_media_type": "Set Content-Type to application/json and retry without changing the evidence bytes.", + "access_denied": "Request the exact tenant, purpose, resource, and least-privilege scope required for this operation.", + "snapshot_not_found": "Verify the tenant and snapshot references, then retry the exact target.", + "scope_missing": "Resolve the required Job, Position, or criterion parent in the same tenant before retrying.", + "idempotency_conflict": "Reuse the original snapshot bytes or submit the changed command under a new Idempotency-Key.", + "internal_error": "Retry later or contact an Orgmetra operator with the support_reference; never include the bearer token.", +} + + +class _InvalidHttpRequest(ValueError): + """Indicate malformed request input that must fail before persistence.""" + + +class _UnsupportedMediaType(ValueError): + """Indicate a POST body whose media type is absent or not JSON.""" + + +@dataclass(frozen=True, slots=True) +class JobAnalysisAsgiApp: + """Expose governed snapshot write and read through versioned ASGI routes. + + Supported routes:: + + POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots + GET /v1/tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id} + + POST requires ``Authorization``, ``Content-Type: application/json``, + ``Idempotency-Key``, and ``X-Purpose-Code``. The request body is the + snapshot document. GET requires ``Authorization`` and ``X-Purpose-Code``; + purpose never travels in the URL or query string. Successful responses are + never cached as shared evidence. + """ + + authenticator: TokenAuthenticator + write_policy: PurposeBoundAccessPolicy + read_policy: PurposeBoundAccessPolicy + write_port: JobAnalysisWritePort + read_port: JobAnalysisReadPort + + def __post_init__(self) -> None: + """Reject incomplete dependency injection before serving protected data.""" + if not isinstance(self.authenticator, TokenAuthenticator): + raise TypeError("authenticator must implement TokenAuthenticator") + if not isinstance(self.write_policy, PurposeBoundAccessPolicy): + raise TypeError("write_policy must be a PurposeBoundAccessPolicy") + if not isinstance(self.read_policy, PurposeBoundAccessPolicy): + raise TypeError("read_policy must be a PurposeBoundAccessPolicy") + if not isinstance(self.write_port, JobAnalysisWritePort): + raise TypeError("write_port must implement JobAnalysisWritePort") + if not isinstance(self.read_port, JobAnalysisReadPort): + raise TypeError("read_port must implement JobAnalysisReadPort") + + async def __call__(self, scope: Mapping[str, object], receive: AsgiReceive, send: AsgiSend) -> None: + """Serve one HTTP request without exposing bearer tokens or internal errors.""" + if scope.get("type") != "http": + raise ValueError("JobAnalysisAsgiApp accepts only HTTP ASGI scopes") + + method = scope.get("method") + path = scope.get("path") + if not isinstance(path, str) or not _looks_like_snapshot_route(path): + await _send_json( + send, + status=404, + payload={ + "error": "route_not_found", + "message": "Use /v1/tenants/{tenant_record_id}/job-analysis-snapshots.", + }, + ) + return + if method not in {"GET", "POST"}: + await _send_json( + send, + status=405, + payload={ + "error": "method_not_allowed", + "message": "Use POST to persist or GET to read one job-analysis snapshot.", + }, + extra_headers=((b"allow", b"GET, POST"),), + ) + return + + parts = path.strip("/").split("/") + try: + tenant_record_id = UUID(parts[2]) + analysis_record_id = UUID(parts[4]) if len(parts) == 5 else None + except (ValueError, IndexError): + await _send_invalid_request(send) + return + if tenant_record_id.int in (0, _MAX_UUID_INT): + await _send_invalid_request(send) + return + if analysis_record_id is not None and analysis_record_id.int in (0, _MAX_UUID_INT): + await _send_invalid_request(send) + return + if method == "POST" and analysis_record_id is not None: + await _send_invalid_request(send) + return + if method == "GET" and analysis_record_id is None: + await _send_invalid_request(send) + return + + try: + bearer_token = extract_bearer_token(_authorization_header(scope)) + principal = await self.authenticator.authenticate(bearer_token) + except AuthenticationFailed: + await _send_json( + send, + status=401, + payload={ + "error": "authentication_required", + "message": "Provide one valid Bearer credential and retry.", + }, + 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: + headers = _typed_headers(scope) + purpose_code = _required_header(headers, b"x-purpose-code") + if _PURPOSE_PATTERN.fullmatch(purpose_code) is None: + raise _InvalidHttpRequest("purpose must be a lower snake_case code") + + if method == "POST": + idempotency_key = _required_header(headers, b"idempotency-key") + _require_json_content_type(headers) + document = await _read_json_object(receive) + position_record_id = _optional_uuid(document.pop("position_record_id", None)) + criterion_blueprint_id = _optional_uuid(document.pop("criterion_blueprint_id", None)) + view = persist_job_analysis_snapshot( + principal=principal, + tenant_record_id=tenant_record_id, + document=document, + idempotency_key=idempotency_key, + purpose_code=purpose_code, + position_record_id=position_record_id, + criterion_blueprint_id=criterion_blueprint_id, + policy=self.write_policy, + write_port=self.write_port, + ) + await _send_json( + send, + status=201, + payload=view.snapshot, + extra_headers=( + ( + b"location", + f"/v1/tenants/{tenant_record_id}/job-analysis-snapshots/{view.snapshot['analysis_record_id']}".encode("ascii"), + ), + ), + ) + return + + raw_query = scope.get("query_string", b"") + if not isinstance(raw_query, bytes) or raw_query: + raise _InvalidHttpRequest("job-analysis reads do not accept query parameters") + view = read_job_analysis_snapshot( + principal=principal, + tenant_record_id=tenant_record_id, + analysis_record_id=analysis_record_id, + purpose_code=purpose_code, + policy=self.read_policy, + read_port=self.read_port, + ) + except _UnsupportedMediaType: + await _send_json( + send, + status=415, + payload={ + "error": "unsupported_media_type", + "message": "Send the snapshot document as application/json and retry.", + }, + ) + return + except _InvalidHttpRequest: + await _send_invalid_request(send) + return + except AuthorizationDeniedError: + await _send_json( + send, + status=403, + payload={ + "error": "access_denied", + "message": "Request a purpose and scope authorized for this exact job-analysis snapshot.", + }, + ) + return + except JobAnalysisSnapshotNotFound: + await _send_json( + send, + status=404, + payload={ + "error": "snapshot_not_found", + "message": "Verify the snapshot reference and tenant, then retry.", + }, + ) + return + except JobAnalysisScopeMissing: + await _send_json( + send, + status=409, + payload={ + "error": "scope_missing", + "message": "Bind the snapshot to an existing job, and to a present position or criterion when those identifiers are supplied.", + }, + ) + return + except JobAnalysisIdempotencyConflict: + await _send_json( + send, + status=409, + payload={ + "error": "idempotency_conflict", + "message": "Reuse the same snapshot payload or retry with a new Idempotency-Key.", + }, + ) + return + except (JobAnalysisIntegrityError, ValueError, TypeError): + await _send_json( + send, + status=400, + payload={ + "error": "invalid_request", + "message": "Correct the snapshot document, tenant, and required write headers, then retry.", + }, + ) + return + except Exception: # noqa: BLE001 - HTTP boundary must fail closed without leaking backend details. + 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 + + await _send_json(send, status=200, payload=view.snapshot) + + +def _looks_like_snapshot_route(path: str) -> bool: + """Recognize collection or item snapshot routes before parsing identifiers.""" + parts = path.strip("/").split("/") + if len(parts) not in {4, 5}: + return False + return tuple(parts[:2]) == _ROUTE_PREFIX and parts[3] == "job-analysis-snapshots" + + +def _optional_uuid(value: object) -> UUID | None: + """Parse an optional posted UUID or reject a malformed identifier.""" + if value is None: + return None + if not isinstance(value, str): + raise _InvalidHttpRequest("optional scope IDs must be UUID strings") + try: + parsed = UUID(value) + except ValueError as error: + raise _InvalidHttpRequest("optional scope IDs must be UUID strings") from error + if parsed.int in (0, _MAX_UUID_INT): + raise _InvalidHttpRequest("optional scope IDs must be operational UUIDs") + return parsed + + +def _typed_headers(scope: Mapping[str, object]) -> dict[bytes, bytes]: + """Return lower-cased singleton headers or reject malformed header frames.""" + raw_headers = scope.get("headers", ()) + if not isinstance(raw_headers, Sequence): + raise AuthenticationFailed("request headers are invalid") + headers: dict[bytes, bytes] = {} + for header in raw_headers: + if not isinstance(header, Sequence) or len(header) != 2: + raise AuthenticationFailed("request headers are invalid") + name, value = header + if not isinstance(name, bytes) or not isinstance(value, bytes): + raise AuthenticationFailed("request headers are invalid") + key = name.lower() + if key in headers: + raise AuthenticationFailed("duplicate request header") + headers[key] = value + return headers + + +def _authorization_header(scope: Mapping[str, object]) -> str | None: + """Return one ASCII Authorization header, rejecting duplicates and bad bytes.""" + headers = _typed_headers(scope) + value = headers.get(b"authorization") + if value is None: + raise AuthenticationFailed("exactly one authorization header is required") + try: + return value.decode("ascii") + except UnicodeDecodeError as error: + raise AuthenticationFailed("authorization header must be ASCII") from error + + +def _required_header(headers: Mapping[bytes, bytes], name: bytes) -> str: + """Return one required printable ASCII header used by governed requests.""" + value = headers.get(name) + if value is None: + raise _InvalidHttpRequest(f"{name.decode('ascii')} header is required") + try: + text = value.decode("ascii") + except UnicodeDecodeError as error: + raise _InvalidHttpRequest(f"{name.decode('ascii')} header must be ASCII") from error + if not text: + raise _InvalidHttpRequest(f"{name.decode('ascii')} header must not be blank") + return text + + +def _require_json_content_type(headers: Mapping[bytes, bytes]) -> None: + """Require an ASCII application/json media type before reading POST bytes.""" + value = headers.get(b"content-type") + if value is None: + raise _UnsupportedMediaType("content-type header is required") + try: + media_type = value.decode("ascii").split(";", 1)[0].strip().lower() + except UnicodeDecodeError as error: + raise _UnsupportedMediaType("content-type header must be ASCII") from error + if media_type != "application/json": + raise _UnsupportedMediaType("content-type must be application/json") + + +def _object_without_duplicate_members(pairs: list[tuple[str, object]]) -> dict[str, object]: + """Build one JSON object while rejecting ambiguous repeated member names.""" + value: dict[str, object] = {} + for member_name, member_value in pairs: + if member_name in value: + raise _InvalidHttpRequest("request body contains a duplicate JSON member") + value[member_name] = member_value + return value + + +async def _read_json_object(receive: AsgiReceive) -> dict[str, object]: + """Read one bounded, unambiguous JSON object body from chunked ASGI frames.""" + chunks: list[bytes] = [] + total_bytes = 0 + more_body = True + while more_body: + message = await receive() + if message.get("type") != "http.request": + raise _InvalidHttpRequest("request body frame is invalid") + body = message.get("body", b"") + if not isinstance(body, bytes): + raise _InvalidHttpRequest("request body must be bytes") + total_bytes += len(body) + if total_bytes > _MAX_REQUEST_BODY_BYTES: + raise _InvalidHttpRequest("request body exceeds the accepted size") + chunks.append(body) + more_body = bool(message.get("more_body")) + raw = b"".join(chunks) + try: + payload = json.loads( + raw.decode("utf-8"), + object_pairs_hook=_object_without_duplicate_members, + ) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + raise _InvalidHttpRequest("request body must be JSON") from error + if not isinstance(payload, dict): + raise _InvalidHttpRequest("request body must be a JSON object") + return payload + + +async def _send_invalid_request(send: AsgiSend) -> None: + """Emit the stable invalid-request response used by transport validation.""" + await _send_json( + send, + status=400, + payload={ + "error": "invalid_request", + "message": "Correct the tenant/snapshot IDs, purpose, Idempotency-Key, and snapshot document, then retry.", + }, + ) + + +def _governed_payload(payload: Mapping[str, object]) -> dict[str, object]: + """Add the customer-safe error envelope while preserving the legacy alias.""" + response_payload = dict(payload) + error_code = response_payload.get("error") + if isinstance(error_code, str): + response_payload.update( + { + "error_code": error_code, + "next_action": _ERROR_NEXT_ACTION[error_code], + "support_reference": f"err_{token_urlsafe(18)}", + } + ) + return response_payload + + +async def _send_json( + send: AsgiSend, + *, + status: int, + payload: Mapping[str, object], + extra_headers: tuple[tuple[bytes, bytes], ...] = (), +) -> None: + """Emit deterministic JSON with governed, non-cacheable error metadata.""" + body = json.dumps( + _governed_payload(payload), + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + headers = ( + (b"content-type", b"application/json"), + (b"cache-control", b"no-store"), + (b"vary", b"Authorization"), + *extra_headers, + ) + await send({"type": "http.response.start", "status": status, "headers": list(headers)}) + await send({"type": "http.response.body", "body": body, "more_body": False}) diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/postgres.py b/services/job-analysis-api/src/orgmetra_job_analysis_api/postgres.py new file mode 100644 index 000000000..48fa57ce6 --- /dev/null +++ b/services/job-analysis-api/src/orgmetra_job_analysis_api/postgres.py @@ -0,0 +1,531 @@ +"""Tenant-bound PostgreSQL adapter for job-analysis snapshot persistence. + +The adapter owns write-side and read-side SQL for the 3NF snapshot relations. +Writes bind tenant RLS, require the existing job (and optional position or +criterion) identity, persist the Idempotency-Key on the write-command row, and +call ``record_audit_outbox_event(...)`` in the same transaction as the +authoritative insert. A missing parent identity fails closed. +""" + +from __future__ import annotations + +from contextlib import AbstractContextManager +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Any, Callable +from uuid import UUID + +from orgmetra_hris_kernel import ( + AuditOutboxEvent, + EvidenceSource, + FunctionalJobAnalysisProfile, + JobAnalysisSnapshot, + KSAORequirement, + TaskEvidence, + TaskKSAOLink, +) + +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIdempotencyConflict, + JobAnalysisIntegrityError, + JobAnalysisScopeMissing, + validate_operational_uuid, +) + +PostgresConnectionFactory = Callable[[], AbstractContextManager[Any]] + +_TENANT_CONTEXT_SQL = "SELECT pg_catalog.set_config('orgmetra.tenant_record_id', %s, true)" +_READ_ONLY_SQL = "SET TRANSACTION READ ONLY" +_IDEMPOTENCY_LOOKUP_SQL = """ +WITH idempotency_lock AS MATERIALIZED ( + SELECT pg_catalog.pg_advisory_xact_lock( + pg_catalog.hashtextextended(pg_catalog.concat(%s, ':', %s), 0) + ) +) +SELECT + command_record.request_digest_sha256, + command_record.analysis_record_id, + command_record.actor_reference, + command_record.purpose_code +FROM idempotency_lock +LEFT JOIN LATERAL ( + SELECT request_digest_sha256, analysis_record_id, actor_reference, purpose_code + FROM public.job_analysis_write_command + WHERE tenant_record_id = %s + AND idempotency_key = %s + LIMIT 1 +) AS command_record ON TRUE +""".strip() +_JOB_SCOPE_SQL = """ +SELECT job_profile_id +FROM public.job_profile +WHERE tenant_record_id = %s + AND job_profile_id = %s + AND recorded_to IS NULL +LIMIT 1 +""".strip() +_POSITION_SCOPE_SQL = """ +SELECT position_record_id, job_profile_id +FROM public.position_record +WHERE tenant_record_id = %s + AND position_record_id = %s + AND recorded_to IS NULL +LIMIT 1 +""".strip() +_CRITERION_SCOPE_SQL = """ +SELECT criterion_blueprint_id, job_profile_id +FROM public.criterion_blueprint +WHERE tenant_record_id = %s + AND criterion_blueprint_id = %s + AND recorded_to IS NULL +LIMIT 1 +""".strip() +_INSERT_SNAPSHOT_SQL = """ +INSERT INTO public.job_analysis_snapshot ( + tenant_record_id, analysis_record_id, job_profile_id, position_record_id, + criterion_blueprint_id, analysis_version_code, status_code, effective_from, + recorded_at, reviewed_by_reference, reviewed_at, content_digest_sha256, + data_function_code, people_function_code, things_function_code, + fja_source_uri, fja_source_title, fja_source_version_code, fja_retrieved_at, + fja_content_digest_sha256, fja_origin_code +) VALUES ( + %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s +) +""".strip() +_INSERT_TASK_SQL = """ +INSERT INTO public.job_analysis_task_item ( + tenant_record_id, analysis_record_id, task_record_id, task_statement, + importance_level, difficulty_level, source_uri, source_title, + source_version_code, retrieved_at, content_digest_sha256, origin_code +) VALUES ( + %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s +) +""".strip() +_INSERT_KSAO_SQL = """ +INSERT INTO public.job_analysis_ksao_item ( + tenant_record_id, analysis_record_id, ksao_record_id, category_code, + requirement_statement, importance_level, proficiency_level, source_uri, + source_title, source_version_code, retrieved_at, content_digest_sha256, + origin_code +) VALUES ( + %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s +) +""".strip() +_INSERT_LINK_SQL = """ +INSERT INTO public.job_analysis_task_ksao_link ( + tenant_record_id, analysis_record_id, task_record_id, ksao_record_id, + relationship_strength, essential_for_task +) VALUES ( + %s, %s, %s, %s, %s, %s +) +""".strip() +_INSERT_COMMAND_SQL = """ +INSERT INTO public.job_analysis_write_command ( + tenant_record_id, write_command_id, analysis_record_id, idempotency_key, + request_digest_sha256, actor_reference, purpose_code +) VALUES ( + %s, %s, %s, %s, %s, %s, %s +) +""".strip() +_AUDIT_OUTBOX_SQL = """ +SELECT public.record_audit_outbox_event(%s, %s, %s, %s, %s, %s) +""".strip() +_READ_SNAPSHOT_SQL = """ +SELECT + tenant_record_id, analysis_record_id, job_profile_id, analysis_version_code, + status_code, effective_from, recorded_at, reviewed_by_reference, reviewed_at, + content_digest_sha256, data_function_code, people_function_code, + things_function_code, fja_source_uri, fja_source_title, + fja_source_version_code, fja_retrieved_at, fja_content_digest_sha256, + fja_origin_code +FROM public.job_analysis_snapshot +WHERE tenant_record_id = %s + AND analysis_record_id = %s +LIMIT 2 +""".strip() +_READ_TASKS_SQL = """ +SELECT + task_record_id, task_statement, importance_level, difficulty_level, + source_uri, source_title, source_version_code, retrieved_at, + content_digest_sha256, origin_code +FROM public.job_analysis_task_item +WHERE tenant_record_id = %s + AND analysis_record_id = %s +ORDER BY task_record_id +""".strip() +_READ_KSAOS_SQL = """ +SELECT + ksao_record_id, category_code, requirement_statement, importance_level, + proficiency_level, source_uri, source_title, source_version_code, + retrieved_at, content_digest_sha256, origin_code +FROM public.job_analysis_ksao_item +WHERE tenant_record_id = %s + AND analysis_record_id = %s +ORDER BY ksao_record_id +""".strip() +_READ_LINKS_SQL = """ +SELECT task_record_id, ksao_record_id, relationship_strength, essential_for_task +FROM public.job_analysis_task_ksao_link +WHERE tenant_record_id = %s + AND analysis_record_id = %s +ORDER BY task_record_id, ksao_record_id +""".strip() + + +def _utc(value: datetime) -> datetime: + """Normalize an already-validated instant to UTC for persistence binding.""" + return value.astimezone(timezone.utc) + + +def _source_params(source: EvidenceSource) -> tuple[object, ...]: + """Return bound evidence-source columns in insert order.""" + return ( + source.source_uri, + source.source_title, + source.source_version_code, + _utc(source.retrieved_at), + source.content_digest_sha256, + source.origin_code, + ) + + +def _is_unique_violation(error: Exception) -> bool: + """Return whether a PostgreSQL DB-API error reports SQLSTATE 23505.""" + return getattr(error, "sqlstate", getattr(error, "pgcode", None)) == "23505" + + +def _constraint_name(error: Exception) -> str | None: + """Return a driver-provided PostgreSQL constraint name when available.""" + diagnostic = getattr(error, "diag", None) + constraint_name = getattr(diagnostic, "constraint_name", None) + return constraint_name if isinstance(constraint_name, str) else None + + +@dataclass(frozen=True, slots=True) +class PostgresJobAnalysisPort: + """Persist and reconstruct snapshots through parameterized PostgreSQL SQL. + + ``connection_factory`` must return a DB-API-compatible connection context + manager. Deployment code owns pooling, credentials, TLS, and role selection. + """ + + connection_factory: PostgresConnectionFactory + + def __post_init__(self) -> None: + """Reject unusable factories before any protected write or read.""" + if not callable(self.connection_factory): + raise TypeError("connection_factory must be callable") + + def persist_snapshot( + self, + *, + snapshot: JobAnalysisSnapshot, + idempotency_key: str, + request_digest: str, + actor_reference: str, + purpose_code: str, + position_record_id: UUID | None, + criterion_blueprint_id: UUID | None, + audit_event: AuditOutboxEvent, + outbox_delivery_record_id: UUID, + write_command_id: UUID, + ) -> JobAnalysisSnapshot: + """Insert one snapshot or replay an identical Idempotency-Key command. + + The Idempotency-Key is written to ``job_analysis_write_command``. A + reused key with a different digest, actor, or purpose is rejected. + ``record_audit_outbox_event`` runs only for a new write, inside the same + transaction. + """ + if not isinstance(snapshot, JobAnalysisSnapshot): + raise TypeError("snapshot must be a JobAnalysisSnapshot") + if not isinstance(audit_event, AuditOutboxEvent): + raise TypeError("audit_event must be an AuditOutboxEvent") + if not isinstance(idempotency_key, str): + raise ValueError("idempotency_key must reach the write port as a string.") + validate_operational_uuid("write_command_id", write_command_id) + validate_operational_uuid("outbox_delivery_record_id", outbox_delivery_record_id) + if position_record_id is not None: + validate_operational_uuid("position_record_id", position_record_id) + if criterion_blueprint_id is not None: + validate_operational_uuid("criterion_blueprint_id", criterion_blueprint_id) + + with self.connection_factory() as connection: + with connection.cursor() as cursor: + cursor.execute(_TENANT_CONTEXT_SQL, (str(snapshot.tenant_record_id),)) + cursor.execute( + _IDEMPOTENCY_LOOKUP_SQL, + ( + snapshot.tenant_record_id, + idempotency_key, + snapshot.tenant_record_id, + idempotency_key, + ), + ) + existing = cursor.fetchone() + if existing is not None and existing[0] is not None: + stored_digest, stored_analysis_id, *stored_authority = existing + if stored_digest != request_digest: + raise JobAnalysisIdempotencyConflict( + "idempotency key is bound to a different snapshot digest" + ) + if stored_authority: + stored_actor_reference, stored_purpose_code = stored_authority + if stored_actor_reference != actor_reference: + raise JobAnalysisIdempotencyConflict( + "idempotency key is bound to a different actor" + ) + if stored_purpose_code != purpose_code: + raise JobAnalysisIdempotencyConflict( + "idempotency key is bound to a different purpose" + ) + replayed = self._load_snapshot( + cursor, + tenant_record_id=snapshot.tenant_record_id, + analysis_record_id=stored_analysis_id, + ) + if replayed is None: + raise JobAnalysisIntegrityError("idempotent command lost its snapshot") + return replayed + + try: + cursor.execute( + _JOB_SCOPE_SQL, + (snapshot.tenant_record_id, snapshot.job_record_id), + ) + if cursor.fetchone() is None: + raise JobAnalysisScopeMissing("job_profile does not exist in the tenant") + if position_record_id is not None: + cursor.execute( + _POSITION_SCOPE_SQL, + (snapshot.tenant_record_id, position_record_id), + ) + position_row = cursor.fetchone() + if position_row is None or position_row[1] != snapshot.job_record_id: + raise JobAnalysisScopeMissing("position_record is missing or not bound to the job") + if criterion_blueprint_id is not None: + cursor.execute( + _CRITERION_SCOPE_SQL, + (snapshot.tenant_record_id, criterion_blueprint_id), + ) + criterion_row = cursor.fetchone() + if criterion_row is None or criterion_row[1] != snapshot.job_record_id: + raise JobAnalysisScopeMissing( + "criterion_blueprint is missing or not bound to the job" + ) + + cursor.execute( + _INSERT_SNAPSHOT_SQL, + ( + snapshot.tenant_record_id, + snapshot.analysis_record_id, + snapshot.job_record_id, + position_record_id, + criterion_blueprint_id, + snapshot.analysis_version_code, + snapshot.status_code, + snapshot.effective_from, + _utc(snapshot.recorded_at), + snapshot.reviewed_by_reference, + None if snapshot.reviewed_at is None else _utc(snapshot.reviewed_at), + snapshot.content_digest(), + snapshot.fja_profile.data_function_code, + snapshot.fja_profile.people_function_code, + snapshot.fja_profile.things_function_code, + *_source_params(snapshot.fja_profile.source), + ), + ) + except Exception as error: # noqa: BLE001 - DB-API errors are normalized below. + if not _is_unique_violation(error): + raise + constraint_name = _constraint_name(error) + raise JobAnalysisIntegrityError( + f"job-analysis snapshot identity or version already exists ({constraint_name!r})" + ) from error + + for task in snapshot.tasks: + cursor.execute( + _INSERT_TASK_SQL, + ( + snapshot.tenant_record_id, + snapshot.analysis_record_id, + task.task_record_id, + task.task_statement, + task.importance_level, + task.difficulty_level, + *_source_params(task.source), + ), + ) + for item in snapshot.ksao_requirements: + cursor.execute( + _INSERT_KSAO_SQL, + ( + snapshot.tenant_record_id, + snapshot.analysis_record_id, + item.ksao_record_id, + item.category_code, + item.requirement_statement, + item.importance_level, + item.proficiency_level, + *_source_params(item.source), + ), + ) + for link in snapshot.task_ksao_links: + cursor.execute( + _INSERT_LINK_SQL, + ( + snapshot.tenant_record_id, + snapshot.analysis_record_id, + link.task_record_id, + link.ksao_record_id, + link.relationship_strength, + link.essential_for_task, + ), + ) + try: + cursor.execute( + _INSERT_COMMAND_SQL, + ( + snapshot.tenant_record_id, + write_command_id, + snapshot.analysis_record_id, + idempotency_key, + request_digest, + actor_reference, + purpose_code, + ), + ) + except Exception as error: # noqa: BLE001 - DB-API errors are normalized below. + if not _is_unique_violation(error): + raise + constraint_name = _constraint_name(error) + raise JobAnalysisIdempotencyConflict( + f"idempotency or command identity was recorded concurrently ({constraint_name!r})" + ) from error + cursor.execute( + _AUDIT_OUTBOX_SQL, + ( + snapshot.tenant_record_id, + audit_event.event_id, + outbox_delivery_record_id, + audit_event.canonical_json(), + audit_event.content_digest(), + "integration_hub", + ), + ) + return snapshot + + def read_snapshot( + self, + *, + tenant_record_id: UUID, + analysis_record_id: UUID, + ) -> JobAnalysisSnapshot | None: + """Read one snapshot under forced tenant RLS and reconstruct the kernel document.""" + validate_operational_uuid("tenant_record_id", tenant_record_id) + validate_operational_uuid("analysis_record_id", analysis_record_id) + with self.connection_factory() as connection: + with connection.cursor() as cursor: + cursor.execute(_READ_ONLY_SQL) + cursor.execute(_TENANT_CONTEXT_SQL, (str(tenant_record_id),)) + return self._load_snapshot( + cursor, + tenant_record_id=tenant_record_id, + analysis_record_id=analysis_record_id, + ) + + def _load_snapshot( + self, + cursor: Any, + *, + tenant_record_id: UUID, + analysis_record_id: UUID, + ) -> JobAnalysisSnapshot | None: + """Assemble one kernel snapshot from normalized rows or return None.""" + cursor.execute(_READ_SNAPSHOT_SQL, (tenant_record_id, analysis_record_id)) + headers = cursor.fetchmany(2) + if not headers: + return None + if len(headers) != 1: + raise JobAnalysisIntegrityError("multiple snapshot headers match the requested target") + header = headers[0] + if header[0] != tenant_record_id or header[1] != analysis_record_id: + raise JobAnalysisIntegrityError("database row escaped requested target") + cursor.execute(_READ_TASKS_SQL, (tenant_record_id, analysis_record_id)) + task_rows = cursor.fetchall() + cursor.execute(_READ_KSAOS_SQL, (tenant_record_id, analysis_record_id)) + ksao_rows = cursor.fetchall() + cursor.execute(_READ_LINKS_SQL, (tenant_record_id, analysis_record_id)) + link_rows = cursor.fetchall() + snapshot = JobAnalysisSnapshot( + analysis_record_id=header[1], + tenant_record_id=header[0], + job_record_id=header[2], + analysis_version_code=header[3], + status_code=header[4], + effective_from=header[5], + recorded_at=header[6], + tasks=tuple(_task_from_row(tenant_record_id, header[2], row) for row in task_rows), + ksao_requirements=tuple(_ksao_from_row(tenant_record_id, header[2], row) for row in ksao_rows), + task_ksao_links=tuple( + TaskKSAOLink( + task_record_id=row[0], + ksao_record_id=row[1], + relationship_strength=row[2], + essential_for_task=row[3], + ) + for row in link_rows + ), + fja_profile=FunctionalJobAnalysisProfile( + tenant_record_id=tenant_record_id, + job_record_id=header[2], + data_function_code=header[10], + people_function_code=header[11], + things_function_code=header[12], + source=_source_from_row(header[13:19]), + ), + reviewed_by_reference=header[7], + reviewed_at=header[8], + ) + if snapshot.content_digest() != header[9]: + raise JobAnalysisIntegrityError("stored snapshot digest does not match reconstructed evidence") + return snapshot + + +def _source_from_row(values: tuple[object, ...]) -> EvidenceSource: + """Rebuild one evidence source from six persisted provenance columns.""" + return EvidenceSource( + source_uri=values[0], + source_title=values[1], + source_version_code=values[2], + retrieved_at=values[3], + content_digest_sha256=values[4], + origin_code=values[5], + ) + + +def _task_from_row(tenant_record_id: UUID, job_record_id: UUID, row: tuple[object, ...]) -> TaskEvidence: + """Rebuild one task item from its persisted 3NF row.""" + return TaskEvidence( + tenant_record_id=tenant_record_id, + job_record_id=job_record_id, + task_record_id=row[0], + task_statement=row[1], + importance_level=row[2], + difficulty_level=row[3], + source=_source_from_row(row[4:10]), + ) + + +def _ksao_from_row(tenant_record_id: UUID, job_record_id: UUID, row: tuple[object, ...]) -> KSAORequirement: + """Rebuild one KSAO item from its persisted 3NF row.""" + return KSAORequirement( + tenant_record_id=tenant_record_id, + job_record_id=job_record_id, + ksao_record_id=row[0], + category_code=row[1], + requirement_statement=row[2], + importance_level=row[3], + proficiency_level=row[4], + source=_source_from_row(row[5:11]), + ) diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/py.typed b/services/job-analysis-api/src/orgmetra_job_analysis_api/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py b/services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py new file mode 100644 index 000000000..28fcbe62a --- /dev/null +++ b/services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py @@ -0,0 +1,494 @@ +"""Governed persist and read path for one job-analysis snapshot. + +Authorization happens before the write or read port is invoked. The posted +document is rebuilt through ``JobAnalysisSnapshot`` so kernel evidence rules +cannot be bypassed by persistence. The write port must receive the caller +Idempotency-Key; a replay with a different digest fails closed. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, datetime, timezone +from hashlib import sha256 +import json +from typing import Protocol, runtime_checkable +from uuid import UUID, uuid4 + +from orgmetra_hris_kernel import ( + AuditOutboxEvent, + EvidenceSource, + FunctionalJobAnalysisProfile, + JobAnalysisSnapshot, + KSAORequirement, + TaskEvidence, + TaskKSAOLink, +) +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy + +from orgmetra_job_analysis_api.auth import AuthenticatedPrincipal +from orgmetra_job_analysis_api.authorization import authorize_resource_fields + +_MAX_UUID_INT = (1 << 128) - 1 +_IDEMPOTENCY_MIN = 16 +_IDEMPOTENCY_MAX = 200 +_MAX_TASKS = 500 +_MAX_KSAOS = 500 +_MAX_TASK_KSAO_LINKS = 5000 +_SNAPSHOT_FIELDS = frozenset( + { + "analysis_record_id", + "tenant_record_id", + "job_record_id", + "analysis_version_code", + "status_code", + "effective_from", + "recorded_at", + "tasks", + "ksao_requirements", + "task_ksao_links", + "fja_profile", + "reviewed_by_reference", + "reviewed_at", + } +) +_SOURCE_FIELDS = frozenset( + { + "source_uri", + "source_title", + "source_version_code", + "retrieved_at", + "content_digest_sha256", + "origin_code", + } +) +_TASK_FIELDS = frozenset( + { + "task_record_id", + "task_statement", + "importance_level", + "difficulty_level", + "source", + } +) +_KSAO_FIELDS = frozenset( + { + "ksao_record_id", + "category_code", + "requirement_statement", + "importance_level", + "proficiency_level", + "source", + } +) +_TASK_KSAO_LINK_FIELDS = frozenset( + { + "task_record_id", + "ksao_record_id", + "relationship_strength", + "essential_for_task", + } +) +_FJA_FIELDS = frozenset( + { + "data_function_code", + "people_function_code", + "things_function_code", + "source", + } +) +_WRITE_FIELDS = _SNAPSHOT_FIELDS | frozenset({"idempotency_key"}) + + +class JobAnalysisSnapshotNotFound(LookupError): + """Indicate that the authorized snapshot target has no persisted record.""" + + +class JobAnalysisIntegrityError(RuntimeError): + """Indicate that persistence returned data outside the authorized snapshot.""" + + +class JobAnalysisScopeMissing(LookupError): + """Indicate that a required job, position, or criterion parent is absent.""" + + +class JobAnalysisIdempotencyConflict(ValueError): + """Indicate that an Idempotency-Key is already bound to different content.""" + + +def _reject_unknown_fields( + boundary_name: str, + value: dict[object, object], + allowed_fields: frozenset[str], +) -> None: + """Reject object members that the published evidence contract does not own.""" + unknown_fields = sorted(str(key) for key in value if key not in allowed_fields) + if unknown_fields: + raise ValueError( + f"{boundary_name} contains unsupported fields: {', '.join(unknown_fields)}." + ) + + +def validate_operational_uuid(field_name: str, value: object) -> UUID: + """Require a UUID that is not one of Orgmetra's reserved protocol sentinels.""" + if not isinstance(value, UUID) or value.int in (0, _MAX_UUID_INT): + raise ValueError(f"{field_name} must be an operational UUID.") + return value + + +def _validate_idempotency_key(value: object) -> str: + """Require the exact caller Idempotency-Key that must reach the write port.""" + if not isinstance(value, str) or not (_IDEMPOTENCY_MIN <= len(value) <= _IDEMPOTENCY_MAX): + raise ValueError("idempotency_key must be 16 to 200 characters.") + if any(ord(character) < 0x21 or ord(character) > 0x7E for character in value): + raise ValueError("idempotency_key must be printable ASCII.") + return value + + +def _parse_uuid(field_name: str, value: object) -> UUID: + """Parse one posted UUID string or reject a non-operational identity.""" + if isinstance(value, UUID): + return validate_operational_uuid(field_name, value) + if not isinstance(value, str): + raise ValueError(f"{field_name} must be a UUID string.") + try: + parsed = UUID(value) + except ValueError as error: + raise ValueError(f"{field_name} must be a UUID string.") from error + return validate_operational_uuid(field_name, parsed) + + +def _parse_aware_datetime(field_name: str, value: object) -> datetime: + """Parse one posted UTC instant used as evidence time.""" + if isinstance(value, datetime): + if value.tzinfo is None or value.utcoffset() is None: + raise ValueError(f"{field_name} must be timezone-aware.") + return value + if not isinstance(value, str): + raise ValueError(f"{field_name} must be an ISO-8601 datetime.") + normalized = value.replace("Z", "+00:00") + try: + parsed = datetime.fromisoformat(normalized) + except ValueError as error: + raise ValueError(f"{field_name} must be an ISO-8601 datetime.") from error + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise ValueError(f"{field_name} must be timezone-aware.") + return parsed + + +def _parse_business_date(field_name: str, value: object) -> date: + """Parse one posted business date without accepting a datetime.""" + if isinstance(value, datetime): + raise ValueError(f"{field_name} must be a date.") + if isinstance(value, date): + return value + if not isinstance(value, str): + raise ValueError(f"{field_name} must be an ISO business date.") + try: + return date.fromisoformat(value) + except ValueError as error: + raise ValueError(f"{field_name} must be an ISO business date.") from error + + +def _parse_source(value: object) -> EvidenceSource: + """Rebuild one evidence source from posted provenance fields.""" + if not isinstance(value, dict): + raise ValueError("source must be an object.") + _reject_unknown_fields("source", value, _SOURCE_FIELDS) + return EvidenceSource( + source_uri=value.get("source_uri"), + source_title=value.get("source_title"), + source_version_code=value.get("source_version_code"), + retrieved_at=_parse_aware_datetime("retrieved_at", value.get("retrieved_at")), + content_digest_sha256=value.get("content_digest_sha256"), + origin_code=value.get("origin_code"), + ) + + +def snapshot_from_document( + document: object, + *, + tenant_record_id: UUID, +) -> JobAnalysisSnapshot: + """Rebuild one kernel snapshot from a posted JSON document. + + The posted tenant must match the authorized route tenant. Kernel constructors + then enforce linkage completeness, provenance, and review governance. + """ + if not isinstance(document, dict): + raise ValueError("snapshot document must be an object.") + _reject_unknown_fields("snapshot document", document, _SNAPSHOT_FIELDS) + posted_tenant = _parse_uuid("tenant_record_id", document.get("tenant_record_id")) + if posted_tenant != tenant_record_id: + raise ValueError("snapshot tenant_record_id must match the authorized tenant.") + job_record_id = _parse_uuid("job_record_id", document.get("job_record_id")) + raw_tasks = document.get("tasks") + raw_ksaos = document.get("ksao_requirements") + raw_links = document.get("task_ksao_links") + raw_fja = document.get("fja_profile") + if not isinstance(raw_tasks, list) or not raw_tasks: + raise ValueError("tasks must be a non-empty list.") + if len(raw_tasks) > _MAX_TASKS: + raise ValueError(f"tasks must contain at most {_MAX_TASKS} items.") + if not isinstance(raw_ksaos, list) or not raw_ksaos: + raise ValueError("ksao_requirements must be a non-empty list.") + if len(raw_ksaos) > _MAX_KSAOS: + raise ValueError(f"ksao_requirements must contain at most {_MAX_KSAOS} items.") + if not isinstance(raw_links, list) or not raw_links: + raise ValueError("task_ksao_links must be a non-empty list.") + if len(raw_links) > _MAX_TASK_KSAO_LINKS: + raise ValueError( + f"task_ksao_links must contain at most {_MAX_TASK_KSAO_LINKS} items." + ) + if not isinstance(raw_fja, dict): + raise ValueError("fja_profile must be an object.") + _reject_unknown_fields("fja_profile", raw_fja, _FJA_FIELDS) + tasks = [] + for item in raw_tasks: + if not isinstance(item, dict): + raise ValueError("tasks must contain objects.") + _reject_unknown_fields("task", item, _TASK_FIELDS) + tasks.append( + TaskEvidence( + tenant_record_id=tenant_record_id, + job_record_id=job_record_id, + task_record_id=_parse_uuid("task_record_id", item.get("task_record_id")), + task_statement=item.get("task_statement"), + importance_level=item.get("importance_level"), + difficulty_level=item.get("difficulty_level"), + source=_parse_source(item.get("source")), + ) + ) + ksaos = [] + for item in raw_ksaos: + if not isinstance(item, dict): + raise ValueError("ksao_requirements must contain objects.") + _reject_unknown_fields("ksao_requirement", item, _KSAO_FIELDS) + ksaos.append( + KSAORequirement( + tenant_record_id=tenant_record_id, + job_record_id=job_record_id, + ksao_record_id=_parse_uuid("ksao_record_id", item.get("ksao_record_id")), + category_code=item.get("category_code"), + requirement_statement=item.get("requirement_statement"), + importance_level=item.get("importance_level"), + proficiency_level=item.get("proficiency_level"), + source=_parse_source(item.get("source")), + ) + ) + links = [] + for item in raw_links: + if not isinstance(item, dict): + raise ValueError("task_ksao_links must contain objects.") + _reject_unknown_fields("task_ksao_link", item, _TASK_KSAO_LINK_FIELDS) + links.append( + TaskKSAOLink( + task_record_id=_parse_uuid("task_record_id", item.get("task_record_id")), + ksao_record_id=_parse_uuid("ksao_record_id", item.get("ksao_record_id")), + relationship_strength=item.get("relationship_strength"), + essential_for_task=item.get("essential_for_task"), + ) + ) + reviewed_by_reference = document.get("reviewed_by_reference") + reviewed_at = document.get("reviewed_at") + return JobAnalysisSnapshot( + analysis_record_id=_parse_uuid("analysis_record_id", document.get("analysis_record_id")), + tenant_record_id=tenant_record_id, + job_record_id=job_record_id, + analysis_version_code=document.get("analysis_version_code"), + status_code=document.get("status_code"), + effective_from=_parse_business_date("effective_from", document.get("effective_from")), + recorded_at=_parse_aware_datetime("recorded_at", document.get("recorded_at")), + tasks=tuple(tasks), + ksao_requirements=tuple(ksaos), + task_ksao_links=tuple(links), + fja_profile=FunctionalJobAnalysisProfile( + tenant_record_id=tenant_record_id, + job_record_id=job_record_id, + data_function_code=raw_fja.get("data_function_code"), + people_function_code=raw_fja.get("people_function_code"), + things_function_code=raw_fja.get("things_function_code"), + source=_parse_source(raw_fja.get("source")), + ), + reviewed_by_reference=reviewed_by_reference, + reviewed_at=None if reviewed_at is None else _parse_aware_datetime("reviewed_at", reviewed_at), + ) + + +def command_digest( + *, + snapshot: JobAnalysisSnapshot, + position_record_id: UUID | None, + criterion_blueprint_id: UUID | None, +) -> str: + """Return SHA-256 over the exact snapshot bytes plus optional scope identities.""" + payload = { + "criterion_blueprint_id": None if criterion_blueprint_id is None else str(criterion_blueprint_id), + "position_record_id": None if position_record_id is None else str(position_record_id), + "snapshot": json.loads(snapshot.canonical_json()), + } + canonical = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True) + return sha256(canonical.encode("utf-8")).hexdigest() + + +def _optional_scope_id(field_name: str, value: object) -> UUID | None: + """Accept an omitted optional parent identity or require an operational UUID.""" + if value is None: + return None + return _parse_uuid(field_name, value) + + +@runtime_checkable +class JobAnalysisWritePort(Protocol): + """Persist one authorized snapshot and its Idempotency-Key in one transaction.""" + + def persist_snapshot( + self, + *, + snapshot: JobAnalysisSnapshot, + idempotency_key: str, + request_digest: str, + actor_reference: str, + purpose_code: str, + position_record_id: UUID | None, + criterion_blueprint_id: UUID | None, + audit_event: AuditOutboxEvent, + outbox_delivery_record_id: UUID, + write_command_id: UUID, + ) -> JobAnalysisSnapshot: + """Write the snapshot or return the prior snapshot for the same command digest.""" + + +@runtime_checkable +class JobAnalysisReadPort(Protocol): + """Read one persisted snapshot under the caller's tenant transaction.""" + + def read_snapshot( + self, + *, + tenant_record_id: UUID, + analysis_record_id: UUID, + ) -> JobAnalysisSnapshot | None: + """Resolve one snapshot by durable analysis identity.""" + + +@dataclass(frozen=True, slots=True) +class PersistedJobAnalysisView: + """Customer response containing the exact persisted snapshot document.""" + + resource_reference: str + snapshot: dict[str, object] + + +def persist_job_analysis_snapshot( + *, + principal: AuthenticatedPrincipal, + tenant_record_id: UUID, + document: object, + idempotency_key: str, + purpose_code: str, + position_record_id: object = None, + criterion_blueprint_id: object = None, + policy: PurposeBoundAccessPolicy, + write_port: JobAnalysisWritePort, +) -> PersistedJobAnalysisView: + """Authorize, validate, and persist one snapshot without masking job evidence. + + The Idempotency-Key is validated here and passed unchanged to the write port. + Job-analysis rows are occupational evidence, not person PII, so the authorized + field set is the snapshot document itself rather than a masked subset. + """ + tenant_record_id = validate_operational_uuid("tenant_record_id", tenant_record_id) + key = _validate_idempotency_key(idempotency_key) + snapshot = snapshot_from_document(document, tenant_record_id=tenant_record_id) + position_id = _optional_scope_id("position_record_id", position_record_id) + criterion_id = _optional_scope_id("criterion_blueprint_id", criterion_blueprint_id) + resource_reference = f"job_analysis_snapshot:{snapshot.analysis_record_id.hex}" + decision = authorize_resource_fields( + principal=principal, + tenant_record_id=tenant_record_id, + resource_tenant_record_id=snapshot.tenant_record_id, + resource_reference=resource_reference, + purpose_code=purpose_code, + operation_code="write_record", + resource_kind="job_analysis_snapshot", + requested_fields=_WRITE_FIELDS, + policy=policy, + ) + audit_event = AuditOutboxEvent( + event_id=uuid4(), + tenant_record_id=tenant_record_id, + source_service="job_analysis_api", + event_type="orgmetra.job_architecture.snapshot_recorded", + resource_reference=resource_reference, + actor_reference=principal.actor_reference, + purpose_code=purpose_code, + reason_code="snapshot_persisted", + evidence_version_code=snapshot.analysis_version_code, + result_code="recorded", + occurred_at=datetime.now(timezone.utc), + high_impact=False, + ) + persisted = write_port.persist_snapshot( + snapshot=snapshot, + idempotency_key=key, + request_digest=command_digest( + snapshot=snapshot, + position_record_id=position_id, + criterion_blueprint_id=criterion_id, + ), + actor_reference=principal.actor_reference, + purpose_code=purpose_code, + position_record_id=position_id, + criterion_blueprint_id=criterion_id, + audit_event=audit_event, + outbox_delivery_record_id=uuid4(), + write_command_id=uuid4(), + ) + if persisted.to_snapshot() != snapshot.to_snapshot(): + raise JobAnalysisIntegrityError("persisted snapshot escaped posted payload") + return PersistedJobAnalysisView( + resource_reference=decision.resource_reference, + snapshot=persisted.to_snapshot(), + ) + + +def read_job_analysis_snapshot( + *, + principal: AuthenticatedPrincipal, + tenant_record_id: UUID, + analysis_record_id: UUID, + purpose_code: str, + policy: PurposeBoundAccessPolicy, + read_port: JobAnalysisReadPort, +) -> PersistedJobAnalysisView: + """Authorize an exact snapshot target before reconstructing persisted evidence.""" + tenant_record_id = validate_operational_uuid("tenant_record_id", tenant_record_id) + analysis_record_id = validate_operational_uuid("analysis_record_id", analysis_record_id) + resource_reference = f"job_analysis_snapshot:{analysis_record_id.hex}" + decision = authorize_resource_fields( + principal=principal, + tenant_record_id=tenant_record_id, + resource_tenant_record_id=tenant_record_id, + resource_reference=resource_reference, + purpose_code=purpose_code, + operation_code="read_record", + resource_kind="job_analysis_snapshot", + requested_fields=_SNAPSHOT_FIELDS, + policy=policy, + ) + snapshot = read_port.read_snapshot( + tenant_record_id=tenant_record_id, + analysis_record_id=analysis_record_id, + ) + if snapshot is None: + raise JobAnalysisSnapshotNotFound("job-analysis snapshot is unavailable") + if ( + snapshot.tenant_record_id != tenant_record_id + or snapshot.analysis_record_id != analysis_record_id + ): + raise JobAnalysisIntegrityError("resolved snapshot does not match authorized target") + return PersistedJobAnalysisView( + resource_reference=decision.resource_reference, + snapshot=snapshot.to_snapshot(), + ) diff --git a/services/job-analysis-api/tests/fixtures.py b/services/job-analysis-api/tests/fixtures.py new file mode 100644 index 000000000..99581c0e3 --- /dev/null +++ b/services/job-analysis-api/tests/fixtures.py @@ -0,0 +1,243 @@ +"""Shared 임상심리사 job-analysis fixtures for persistence contracts.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from uuid import UUID + +from orgmetra_hris_kernel import ( + EvidenceSource, + FunctionalJobAnalysisProfile, + JobAnalysisSnapshot, + KSAORequirement, + TaskEvidence, + TaskKSAOLink, +) +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy + +from orgmetra_job_analysis_api import AuthenticatedPrincipal + +TENANT = UUID("0198a412-6000-7000-8000-000000000001") +OTHER_TENANT = UUID("0198a412-6000-7000-8000-000000000002") +JOB = UUID("0198a412-6000-7000-8000-000000000101") +POSITION = UUID("0198a412-6000-7000-8000-000000000102") +CRITERION = UUID("0198a412-6000-7000-8000-000000000103") +ANALYSIS = UUID("0198a412-6000-7000-8000-000000000201") +TASK_ASSESS = UUID("0198a412-6000-7000-8000-000000000211") +TASK_THERAPY = UUID("0198a412-6000-7000-8000-000000000212") +TASK_HUDDLE = UUID("0198a412-6000-7000-8000-000000000213") +KSAO_KNOWLEDGE = UUID("0198a412-6000-7000-8000-000000000221") +KSAO_SKILL = UUID("0198a412-6000-7000-8000-000000000222") +KSAO_ABILITY = UUID("0198a412-6000-7000-8000-000000000223") +RECORDED_AT = datetime(2026, 8, 18, 5, 0, tzinfo=timezone.utc) +REVIEWED_AT = datetime(2026, 8, 18, 4, 50, tzinfo=timezone.utc) +RETRIEVED_AT = datetime(2026, 8, 18, 3, 0, tzinfo=timezone.utc) +ONET_DIGEST = "b" * 64 +DOT_DIGEST = "c" * 64 +SME_DIGEST = "d" * 64 +IDEMPOTENCY_KEY = "idempotency-clinical-psych-01" + + +def onet_source() -> EvidenceSource: + """Return current O*NET 30.3 provenance for the clinical psychologist job.""" + return EvidenceSource( + source_uri="https://www.onetcenter.org/database.html", + source_title="O*NET 30.3 Clinical and Counseling Psychologists", + source_version_code="onet:30.3", + retrieved_at=RETRIEVED_AT, + content_digest_sha256=ONET_DIGEST, + origin_code="authoritative_occupation_source", + ) + + +def sme_source() -> EvidenceSource: + """Return hospital SME provenance for local 임상심리사 duties.""" + return EvidenceSource( + source_uri="https://evidence.orgmetra.example/job-analysis/clinical-psychologist/sme-review/2026-08", + source_title="Hospital clinical-psychologist SME review", + source_version_code="sme:2026-08", + retrieved_at=RETRIEVED_AT, + content_digest_sha256=SME_DIGEST, + origin_code="supervisor_sme", + ) + + +def clinical_psychologist_snapshot() -> JobAnalysisSnapshot: + """Return a realistic validated 임상심리사 snapshot used for round-trip tests.""" + return JobAnalysisSnapshot( + analysis_record_id=ANALYSIS, + tenant_record_id=TENANT, + job_record_id=JOB, + analysis_version_code="clinical-psychologist:v1", + status_code="analysis_validated", + effective_from=date(2026, 8, 1), + recorded_at=RECORDED_AT, + tasks=( + TaskEvidence( + tenant_record_id=TENANT, + job_record_id=JOB, + task_record_id=TASK_ASSESS, + task_statement="표준화된 심리검사를 실시하고 결과를 해석하여 진단 가설을 정리한다.", + importance_level=5, + difficulty_level=4, + source=onet_source(), + ), + TaskEvidence( + tenant_record_id=TENANT, + job_record_id=JOB, + task_record_id=TASK_THERAPY, + task_statement="근거기반 심리치료를 계획하고 회기별 개입을 수행한다.", + importance_level=5, + difficulty_level=5, + source=sme_source(), + ), + TaskEvidence( + tenant_record_id=TENANT, + job_record_id=JOB, + task_record_id=TASK_HUDDLE, + task_statement="다학제 사례회의에서 평가 근거와 위험 요인을 보고한다.", + importance_level=4, + difficulty_level=3, + source=sme_source(), + ), + ), + ksao_requirements=( + KSAORequirement( + tenant_record_id=TENANT, + job_record_id=JOB, + ksao_record_id=KSAO_KNOWLEDGE, + category_code="knowledge_requirement", + requirement_statement="DSM-5-TR 진단 기준과 심리측정 이론에 대한 지식.", + importance_level=5, + proficiency_level=4, + source=onet_source(), + ), + KSAORequirement( + tenant_record_id=TENANT, + job_record_id=JOB, + ksao_record_id=KSAO_SKILL, + category_code="skill_requirement", + requirement_statement="임상면담과 표준화 검사 배터리를 구성하는 기술.", + importance_level=5, + proficiency_level=5, + source=sme_source(), + ), + KSAORequirement( + tenant_record_id=TENANT, + job_record_id=JOB, + ksao_record_id=KSAO_ABILITY, + category_code="ability_requirement", + requirement_statement="복합 사례를 개념화하고 평가 근거를 연결하는 능력.", + importance_level=5, + proficiency_level=4, + source=sme_source(), + ), + ), + task_ksao_links=( + TaskKSAOLink(TASK_ASSESS, KSAO_KNOWLEDGE, 5, True), + TaskKSAOLink(TASK_ASSESS, KSAO_SKILL, 5, True), + TaskKSAOLink(TASK_THERAPY, KSAO_SKILL, 5, True), + TaskKSAOLink(TASK_THERAPY, KSAO_ABILITY, 4, True), + TaskKSAOLink(TASK_HUDDLE, KSAO_KNOWLEDGE, 4, True), + TaskKSAOLink(TASK_HUDDLE, KSAO_ABILITY, 5, True), + ), + fja_profile=FunctionalJobAnalysisProfile( + tenant_record_id=TENANT, + job_record_id=JOB, + data_function_code=1, + people_function_code=0, + things_function_code=7, + source=EvidenceSource( + source_uri="https://www.dol.gov/agencies/oalj/PUBLIC/DOT/REFERENCES/DOTAPPB", + source_title="Dictionary of Occupational Titles Appendix B", + source_version_code="dot:1991", + retrieved_at=RETRIEVED_AT, + content_digest_sha256=DOT_DIGEST, + origin_code="authoritative_occupation_source", + ), + ), + reviewed_by_reference="keyverse_subject:01JCLINICALSME", + reviewed_at=REVIEWED_AT, + ) + + +def clinical_psychologist_document() -> dict[str, object]: + """Return the posted payload buyers persist for 임상심리사 직무분석.""" + return clinical_psychologist_snapshot().to_snapshot() + + +def write_principal() -> AuthenticatedPrincipal: + """Return one actor authorized to persist job-analysis evidence.""" + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-ja-1", + granted_scope_codes=frozenset({"orgmetra.job_architecture.write"}), + ) + + +def read_principal() -> AuthenticatedPrincipal: + """Return one actor authorized to read job-analysis evidence.""" + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-ja-1", + granted_scope_codes=frozenset({"orgmetra.job_architecture.read"}), + ) + + +def write_policy() -> PurposeBoundAccessPolicy: + """Return the purpose-bound write policy for job-analysis snapshots.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="ja-write-v1", + resource_kind="job_analysis_snapshot", + purpose_code="job_analysis_write", + operation_code="write_record", + required_scope_code="orgmetra.job_architecture.write", + permitted_fields=frozenset( + { + "analysis_record_id", + "tenant_record_id", + "job_record_id", + "analysis_version_code", + "status_code", + "effective_from", + "recorded_at", + "tasks", + "ksao_requirements", + "task_ksao_links", + "fja_profile", + "reviewed_by_reference", + "reviewed_at", + "idempotency_key", + } + ), + ) + + +def read_policy() -> PurposeBoundAccessPolicy: + """Return the purpose-bound read policy for job-analysis snapshots.""" + return PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="ja-read-v1", + resource_kind="job_analysis_snapshot", + purpose_code="job_analysis_read", + operation_code="read_record", + required_scope_code="orgmetra.job_architecture.read", + permitted_fields=frozenset( + { + "analysis_record_id", + "tenant_record_id", + "job_record_id", + "analysis_version_code", + "status_code", + "effective_from", + "recorded_at", + "tasks", + "ksao_requirements", + "task_ksao_links", + "fja_profile", + "reviewed_by_reference", + "reviewed_at", + } + ), + ) diff --git a/services/job-analysis-api/tests/test_audit_timestamp.py b/services/job-analysis-api/tests/test_audit_timestamp.py new file mode 100644 index 000000000..e35892a9e --- /dev/null +++ b/services/job-analysis-api/tests/test_audit_timestamp.py @@ -0,0 +1,86 @@ +"""Regression contract for trustworthy job-analysis mutation audit chronology.""" + +from __future__ import annotations + +from datetime import datetime, timezone +import unittest + +from orgmetra_hris_kernel import AuditOutboxEvent, JobAnalysisSnapshot + +from orgmetra_job_analysis_api.snapshot import persist_job_analysis_snapshot +from fixtures import ( + IDEMPOTENCY_KEY, + TENANT, + clinical_psychologist_document, + write_policy, + write_principal, +) + + +class _RecordingWritePort: + """Capture the audit event crossing the persistence boundary.""" + + def __init__(self) -> None: + self.audit_event: AuditOutboxEvent | None = None + + def persist_snapshot( + self, + *, + snapshot: JobAnalysisSnapshot, + idempotency_key: str, + request_digest: str, + actor_reference: str, + purpose_code: str, + position_record_id: object, + criterion_blueprint_id: object, + audit_event: AuditOutboxEvent, + outbox_delivery_record_id: object, + write_command_id: object, + ) -> JobAnalysisSnapshot: + """Record immutable audit evidence and return the accepted snapshot.""" + del ( + idempotency_key, + request_digest, + actor_reference, + purpose_code, + position_record_id, + criterion_blueprint_id, + outbox_delivery_record_id, + write_command_id, + ) + self.audit_event = audit_event + return snapshot + + +class JobAnalysisAuditTimestampTests(unittest.TestCase): + """Separate caller evidence time from system-recorded mutation audit time.""" + + def test_audit_occurrence_uses_command_time_not_caller_snapshot_time(self) -> None: + """A caller must not be able to backdate or future-date the immutable audit event.""" + posted = clinical_psychologist_document() + posted_recorded_at = datetime.fromisoformat( + str(posted["recorded_at"]).replace("Z", "+00:00") + ).astimezone(timezone.utc) + port = _RecordingWritePort() + + before_command = datetime.now(timezone.utc) + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=posted, + idempotency_key=IDEMPOTENCY_KEY, + purpose_code="job_analysis_write", + policy=write_policy(), + write_port=port, + ) + after_command = datetime.now(timezone.utc) + + self.assertIsNotNone(port.audit_event) + assert port.audit_event is not None + self.assertGreaterEqual(port.audit_event.occurred_at, before_command) + self.assertLessEqual(port.audit_event.occurred_at, after_command) + self.assertNotEqual(port.audit_event.occurred_at, posted_recorded_at) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_auth.py b/services/job-analysis-api/tests/test_auth.py new file mode 100644 index 000000000..fa5923fec --- /dev/null +++ b/services/job-analysis-api/tests/test_auth.py @@ -0,0 +1,107 @@ +"""Executable contracts for job-analysis authentication and delegated authorization.""" + +from __future__ import annotations + +import unittest +from uuid import UUID + +from orgmetra_keyverse_adapter import AuthorizationDeniedError + +from orgmetra_job_analysis_api import ( + AuthenticatedPrincipal, + AuthenticationFailed, + authorize_resource_fields, + extract_bearer_token, +) +from fixtures import TENANT, OTHER_TENANT, ANALYSIS, write_policy + + +class BearerBoundaryTests(unittest.TestCase): + """Prove that malformed token syntax never reaches an injected authenticator.""" + + def test_accepts_case_insensitive_bearer_scheme(self) -> None: + self.assertEqual(extract_bearer_token("bEaReR safe-token_123"), "safe-token_123") + + def test_rejects_absent_wrong_or_ambiguous_scheme(self) -> None: + for header in (None, "", "Basic token", "Bearer", "Bearer one two"): + with self.subTest(header=header), self.assertRaises(AuthenticationFailed): + extract_bearer_token(header) + + def test_rejects_hidden_control_non_ascii_and_unbounded_tokens(self) -> None: + for token in ("bad\x1ftoken", "tökén", "x" * 8193): + with self.subTest(token_length=len(token)), self.assertRaises(AuthenticationFailed): + extract_bearer_token(f"Bearer {token}") + + +class PrincipalBoundaryTests(unittest.TestCase): + """Keep authenticated identity/scope facts narrow and immutable.""" + + def test_rejects_malformed_identity_and_scope_shapes(self) -> None: + cases = ( + {"tenant_record_id": "tenant-1", "actor_reference": "keyverse:actor-1", "granted_scope_codes": frozenset({"orgmetra.job_architecture.write"})}, + {"tenant_record_id": UUID(int=0), "actor_reference": "keyverse:actor-1", "granted_scope_codes": frozenset({"orgmetra.job_architecture.write"})}, + {"tenant_record_id": UUID(int=(1 << 128) - 1), "actor_reference": "keyverse:actor-1", "granted_scope_codes": frozenset({"orgmetra.job_architecture.write"})}, + {"tenant_record_id": TENANT, "actor_reference": "actor with pii", "granted_scope_codes": frozenset({"orgmetra.job_architecture.write"})}, + {"tenant_record_id": TENANT, "actor_reference": "keyverse:actor-1", "granted_scope_codes": set({"orgmetra.job_architecture.write"})}, + {"tenant_record_id": TENANT, "actor_reference": "keyverse:actor-1", "granted_scope_codes": frozenset()}, + {"tenant_record_id": TENANT, "actor_reference": "keyverse:actor-1", "granted_scope_codes": frozenset({"orgmetra.*"})}, + {"tenant_record_id": TENANT, "actor_reference": "keyverse:actor-1", "granted_scope_codes": frozenset({1})}, + ) + for values in cases: + with self.subTest(values=values), self.assertRaises(ValueError): + AuthenticatedPrincipal(**values) + + def test_principal_contains_no_purpose_grant(self) -> None: + principal = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-1", + granted_scope_codes=frozenset({"orgmetra.job_architecture.write"}), + ) + self.assertFalse(hasattr(principal, "allowed_purpose_codes")) + + +class DelegatedAuthorizationTests(unittest.TestCase): + """Prove the service delegates exact-target job-analysis policy decisions.""" + + def test_allows_only_exact_authorized_target(self) -> None: + principal = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-1", + granted_scope_codes=frozenset({"orgmetra.job_architecture.write"}), + ) + decision = authorize_resource_fields( + principal=principal, + tenant_record_id=TENANT, + resource_tenant_record_id=TENANT, + resource_reference=f"job_analysis_snapshot:{ANALYSIS.hex}", + purpose_code="job_analysis_write", + operation_code="write_record", + resource_kind="job_analysis_snapshot", + requested_fields=frozenset({"tasks", "idempotency_key"}), + policy=write_policy(), + ) + self.assertTrue(decision.allowed) + + def test_denies_cross_tenant_resource(self) -> None: + principal = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-1", + granted_scope_codes=frozenset({"orgmetra.job_architecture.write"}), + ) + with self.assertRaises(AuthorizationDeniedError) as caught: + authorize_resource_fields( + principal=principal, + tenant_record_id=TENANT, + resource_tenant_record_id=OTHER_TENANT, + resource_reference=f"job_analysis_snapshot:{ANALYSIS.hex}", + purpose_code="job_analysis_write", + operation_code="write_record", + resource_kind="job_analysis_snapshot", + requested_fields=frozenset({"tasks"}), + policy=write_policy(), + ) + self.assertEqual(caught.exception.reason_code, "tenant_scope_mismatch") + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_fixture_provenance.py b/services/job-analysis-api/tests/test_fixture_provenance.py new file mode 100644 index 000000000..86fd5b6be --- /dev/null +++ b/services/job-analysis-api/tests/test_fixture_provenance.py @@ -0,0 +1,12 @@ +"""Regression contracts for realistic job-analysis evidence fixtures.""" + +from fixtures import onet_source, sme_source + + +def test_supervisor_sme_fixture_has_independent_source_provenance() -> None: + """Do not let local SME evidence masquerade as the O*NET source artifact.""" + onet = onet_source() + sme = sme_source() + + assert sme.source_uri != onet.source_uri + assert sme.content_digest_sha256 != onet.content_digest_sha256 diff --git a/services/job-analysis-api/tests/test_http_body_limit.py b/services/job-analysis-api/tests/test_http_body_limit.py new file mode 100644 index 000000000..421e785ad --- /dev/null +++ b/services/job-analysis-api/tests/test_http_body_limit.py @@ -0,0 +1,47 @@ +"""Regression coverage for bounded and unambiguous job-analysis JSON bodies.""" + +from __future__ import annotations + +import unittest + +from orgmetra_job_analysis_api.http import _InvalidHttpRequest, _read_json_object + +_MAX_ACCEPTED_BODY_BYTES = 1 << 20 + + +class JobAnalysisHttpBodyLimitTests(unittest.IsolatedAsyncioTestCase): + """Prove chunked clients cannot bypass memory or JSON-integrity bounds.""" + + async def test_valid_json_over_limit_is_rejected_while_reading_chunks(self) -> None: + """Reject a valid oversized object instead of buffering and parsing it.""" + raw = b'{"payload":"' + (b"x" * _MAX_ACCEPTED_BODY_BYTES) + b'"}' + frames = [ + {"type": "http.request", "body": raw[:700_000], "more_body": True}, + {"type": "http.request", "body": raw[700_000:], "more_body": False}, + ] + + async def receive() -> dict[str, object]: + return frames.pop(0) + + with self.assertRaisesRegex(_InvalidHttpRequest, "exceeds the accepted size"): + await _read_json_object(receive) + + async def test_duplicate_json_member_is_rejected_instead_of_last_value_wins(self) -> None: + """Prevent ambiguous evidence from collapsing to one parsed command digest.""" + frames = [ + { + "type": "http.request", + "body": b'{"tenant_record_id":"first","tenant_record_id":"second"}', + "more_body": False, + } + ] + + async def receive() -> dict[str, object]: + return frames.pop(0) + + with self.assertRaisesRegex(_InvalidHttpRequest, "duplicate JSON member"): + await _read_json_object(receive) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_http_content_type.py b/services/job-analysis-api/tests/test_http_content_type.py new file mode 100644 index 000000000..3082fa74a --- /dev/null +++ b/services/job-analysis-api/tests/test_http_content_type.py @@ -0,0 +1,123 @@ +"""Media-type regressions for governed job-analysis request bodies.""" + +from __future__ import annotations + +import json +from pathlib import Path +import unittest + +from orgmetra_job_analysis_api.http import JobAnalysisAsgiApp +from fixtures import IDEMPOTENCY_KEY, TENANT, clinical_psychologist_document, read_policy, write_policy +from test_http_route import FakeAuthenticator, FakeReadPort, FakeWritePort, _api_principal + + +class JobAnalysisHttpContentTypeTests(unittest.IsolatedAsyncioTestCase): + """Require JSON media type before reading or persisting a posted document.""" + + def _app(self, write_port: FakeWritePort) -> JobAnalysisAsgiApp: + """Build the public transport boundary with deterministic test ports.""" + return JobAnalysisAsgiApp( + authenticator=FakeAuthenticator(_api_principal()), + write_policy=write_policy(), + read_policy=read_policy(), + write_port=write_port, + read_port=FakeReadPort(None), + ) + + async def _post_without_reading_body( + self, + *, + content_type: bytes | None, + ) -> tuple[int, dict[str, object], FakeWritePort, int]: + """Post headers while proving an unsupported type is rejected pre-body.""" + write_port = FakeWritePort() + app = self._app(write_port) + headers = [ + (b"authorization", b"Bearer opaque-token"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + (b"x-purpose-code", b"job_analysis_write"), + ] + if content_type is not None: + headers.append((b"content-type", content_type)) + scope = { + "type": "http", + "method": "POST", + "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots", + "query_string": b"", + "headers": headers, + } + messages: list[dict[str, object]] = [] + receive_count = 0 + + async def receive() -> dict[str, object]: + nonlocal receive_count + receive_count += 1 + raise AssertionError("unsupported media type must fail before body read") + + async def send(message: dict[str, object]) -> None: + messages.append(message) + + await app(scope, receive, send) + start, body = messages + return ( + int(start["status"]), + json.loads(bytes(body["body"])), + write_port, + receive_count, + ) + + async def test_missing_or_non_json_media_type_fails_before_body_read(self) -> None: + """Do not accept JSON bytes under an absent or misleading media type.""" + for content_type in (None, b"text/plain", b"application/xml", b"\xff"): + with self.subTest(content_type=content_type): + status, payload, write_port, receive_count = await self._post_without_reading_body( + content_type=content_type + ) + self.assertEqual((status, payload["error"]), (415, "unsupported_media_type")) + self.assertEqual(receive_count, 0) + self.assertEqual(write_port.calls, []) + + async def test_json_media_type_with_charset_is_accepted(self) -> None: + """Accept a standard JSON media type parameter without weakening the type check.""" + write_port = FakeWritePort() + app = self._app(write_port) + raw = json.dumps(clinical_psychologist_document()).encode("utf-8") + scope = { + "type": "http", + "method": "POST", + "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots", + "query_string": b"", + "headers": [ + (b"authorization", b"Bearer opaque-token"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + (b"x-purpose-code", b"job_analysis_write"), + (b"content-type", b"application/json; charset=utf-8"), + ], + } + messages: list[dict[str, object]] = [] + + async def receive() -> dict[str, object]: + return {"type": "http.request", "body": raw, "more_body": False} + + async def send(message: dict[str, object]) -> None: + messages.append(message) + + await app(scope, receive, send) + self.assertEqual(messages[0]["status"], 201) + self.assertEqual(len(write_port.calls), 1) + + def test_openapi_publishes_the_unsupported_media_type_response(self) -> None: + """Keep generated clients aligned with the runtime 415 contract.""" + schema = Path("schemas/openapi.yaml").read_text(encoding="utf-8") + collection = schema.split( + " /tenants/{tenant_record_id}/job-analysis-snapshots:", 1 + )[1].split( + " /tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}:", + 1, + )[0] + self.assertIn(" '415':\n", collection) + self.assertIn("$ref: '#/components/responses/UnsupportedMediaType'", collection) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_http_error_contract.py b/services/job-analysis-api/tests/test_http_error_contract.py new file mode 100644 index 000000000..b8ce63d6f --- /dev/null +++ b/services/job-analysis-api/tests/test_http_error_contract.py @@ -0,0 +1,133 @@ +"""Customer-safe error-envelope regressions for the job-analysis API.""" + +from __future__ import annotations + +import json +from pathlib import Path +import re +import unittest + +from orgmetra_job_analysis_api.http import JobAnalysisAsgiApp +from fixtures import ANALYSIS, TENANT, read_policy, write_policy +from test_http_route import FakeAuthenticator, FakeReadPort, FakeWritePort, _api_principal + +_SUPPORT_REFERENCE = re.compile(r"^err_[A-Za-z0-9_-]{20,80}$") + + +class ExplodingAuthenticator: + """Model an unavailable identity backend with a secret-bearing exception.""" + + async def authenticate(self, bearer_token: str) -> object: + """Fail after receiving a syntactically valid bearer token.""" + del bearer_token + raise RuntimeError("oidc client_secret=do-not-leak") + + +class JobAnalysisHttpErrorContractTests(unittest.IsolatedAsyncioTestCase): + """Keep runtime errors aligned with the published client-safe envelope.""" + + def _app(self) -> JobAnalysisAsgiApp: + """Build a deterministic app for transport-error assertions.""" + return JobAnalysisAsgiApp( + authenticator=FakeAuthenticator(_api_principal()), + write_policy=write_policy(), + read_policy=read_policy(), + write_port=FakeWritePort(), + read_port=FakeReadPort(None), + ) + + async def test_route_error_has_actionable_non_disclosing_support_envelope(self) -> None: + """Return a safe next action and random support handle, not a trace identifier.""" + messages: list[dict[str, object]] = [] + + async def receive() -> dict[str, object]: + raise AssertionError("route rejection must not read a body") + + async def send(message: dict[str, object]) -> None: + messages.append(message) + + await self._app()( + { + "type": "http", + "method": "POST", + "path": f"/v1/tenants/{TENANT}/wrong-route", + "query_string": b"", + "headers": [], + }, + receive, + send, + ) + start, body = messages + payload = json.loads(bytes(body["body"])) + self.assertEqual(start["status"], 404) + self.assertEqual(payload["error_code"], "route_not_found") + self.assertEqual(payload["error"], payload["error_code"]) + self.assertTrue(payload["message"]) + self.assertTrue(payload["next_action"]) + self.assertRegex(payload["support_reference"], _SUPPORT_REFERENCE) + self.assertNotIn(str(TENANT), json.dumps(payload)) + self.assertNotIn("trace", payload["support_reference"].lower()) + + async def test_authentication_backend_failure_is_normalized_before_protected_ports(self) -> None: + """Keep identity-provider failures client-safe and deny downstream data access.""" + write_port = FakeWritePort() + read_port = FakeReadPort(None) + app = JobAnalysisAsgiApp( + authenticator=ExplodingAuthenticator(), + write_policy=write_policy(), + read_policy=read_policy(), + write_port=write_port, + read_port=read_port, + ) + messages: list[dict[str, object]] = [] + + async def receive() -> dict[str, object]: + raise AssertionError("GET authentication failure must not read a request body") + + async def send(message: dict[str, object]) -> None: + messages.append(message) + + await app( + { + "type": "http", + "method": "GET", + "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + "query_string": b"", + "headers": [(b"authorization", b"Bearer opaque-token")], + }, + receive, + send, + ) + + start, body = messages + payload = json.loads(bytes(body["body"])) + serialized = json.dumps(payload) + self.assertEqual((start["status"], payload["error_code"]), (500, "internal_error")) + self.assertEqual(payload["error"], payload["error_code"]) + self.assertRegex(payload["support_reference"], _SUPPORT_REFERENCE) + self.assertNotIn("client_secret", serialized) + self.assertNotIn("do-not-leak", serialized) + self.assertEqual(write_port.calls, []) + self.assertEqual(read_port.calls, []) + + def test_openapi_allows_the_deprecated_error_alias_without_weakening_required_fields(self) -> None: + """Preserve current clients while requiring the governed four-field envelope.""" + schema = Path("schemas/openapi.yaml").read_text(encoding="utf-8") + error_schema = schema.split(" ErrorResponse:\n", 1)[1].split( + " responses:\n", 1 + )[0] + for required_field in ( + "error_code", + "message", + "next_action", + "support_reference", + ): + self.assertIn(f" - {required_field}\n", error_schema) + self.assertIn(" error:\n", error_schema) + self.assertIn(" deprecated: true\n", error_schema) + unsupported_response = schema.split(" UnsupportedMediaType:\n", 1)[1] + self.assertIn("$ref: '#/components/schemas/ErrorResponse'", unsupported_response) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_http_route.py b/services/job-analysis-api/tests/test_http_route.py new file mode 100644 index 000000000..fd5e68420 --- /dev/null +++ b/services/job-analysis-api/tests/test_http_route.py @@ -0,0 +1,435 @@ +"""Executable HTTP transport contracts for job-analysis snapshot persistence.""" + +from __future__ import annotations + +import json +import unittest +from uuid import UUID + +from orgmetra_hris_kernel import AuditOutboxEvent, JobAnalysisSnapshot + +from orgmetra_job_analysis_api import AuthenticatedPrincipal, AuthenticationFailed +from orgmetra_job_analysis_api.http import JobAnalysisAsgiApp +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIdempotencyConflict, + JobAnalysisIntegrityError, + JobAnalysisScopeMissing, +) +from fixtures import ( + ANALYSIS, + CRITERION, + IDEMPOTENCY_KEY, + POSITION, + TENANT, + clinical_psychologist_document, + clinical_psychologist_snapshot, + read_policy, + write_policy, +) + + +def _api_principal() -> AuthenticatedPrincipal: + """Return one actor that can both persist and read snapshots in HTTP tests.""" + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-ja-1", + granted_scope_codes=frozenset( + {"orgmetra.job_architecture.write", "orgmetra.job_architecture.read"} + ), + ) + + +class FakeAuthenticator: + """Return one principal while recording whether authentication ran.""" + + def __init__(self, principal: AuthenticatedPrincipal, *, error: Exception | None = None) -> None: + self.principal = principal + self.error = error + self.tokens: list[str] = [] + + async def authenticate(self, bearer_token: str) -> AuthenticatedPrincipal: + """Authenticate one token without logging it.""" + self.tokens.append(bearer_token) + if self.error is not None: + raise self.error + return self.principal + + +class FakeWritePort: + """Return a posted snapshot and capture Idempotency-Key plus scope IDs.""" + + def __init__(self, *, error: Exception | None = None) -> None: + self.error = error + self.calls: list[dict[str, object]] = [] + + def persist_snapshot( + self, + *, + snapshot: JobAnalysisSnapshot, + idempotency_key: str, + request_digest: str, + actor_reference: str, + purpose_code: str, + position_record_id: UUID | None, + criterion_blueprint_id: UUID | None, + audit_event: AuditOutboxEvent, + outbox_delivery_record_id: UUID, + write_command_id: UUID, + ) -> JobAnalysisSnapshot: + """Record the write and echo the authorized snapshot.""" + self.calls.append( + { + "idempotency_key": idempotency_key, + "position_record_id": position_record_id, + "criterion_blueprint_id": criterion_blueprint_id, + "request_digest": request_digest, + "audit_event": audit_event, + } + ) + if self.error is not None: + raise self.error + return snapshot + + +class FakeReadPort: + """Return a configured snapshot for HTTP GET tests.""" + + def __init__(self, result: JobAnalysisSnapshot | None, *, error: Exception | None = None) -> None: + self.result = result + self.error = error + self.calls: list[tuple[UUID, UUID]] = [] + + def read_snapshot(self, *, tenant_record_id: UUID, analysis_record_id: UUID) -> JobAnalysisSnapshot | None: + """Return deterministic snapshot truth or raise a configured failure.""" + self.calls.append((tenant_record_id, analysis_record_id)) + if self.error is not None: + raise self.error + return self.result + + +class JobAnalysisHttpRouteTests(unittest.IsolatedAsyncioTestCase): + """Prove write/read HTTP contracts, including payload equality.""" + + def _app( + self, + *, + authenticator: object | None = None, + write_port: object | None = None, + read_port: object | None = None, + write_policy_value: object | None = None, + read_policy_value: object | None = None, + ) -> JobAnalysisAsgiApp: + """Build the ASGI app with explicit injected boundaries.""" + return JobAnalysisAsgiApp( + authenticator=authenticator if authenticator is not None else FakeAuthenticator(_api_principal()), + write_policy=write_policy() if write_policy_value is None else write_policy_value, + read_policy=read_policy() if read_policy_value is None else read_policy_value, + write_port=write_port if write_port is not None else FakeWritePort(), + read_port=read_port if read_port is not None else FakeReadPort(clinical_psychologist_snapshot()), + ) + + async def _request( + self, + app: JobAnalysisAsgiApp, + *, + method: str = "POST", + path: object | None = None, + query: object = b"", + headers: object | None = None, + body: object | None = None, + chunked: bool = False, + ) -> tuple[int, dict[bytes, bytes], dict[str, object]]: + posted = clinical_psychologist_document() if body is None else body + raw = posted if isinstance(posted, bytes) else json.dumps(posted).encode("utf-8") + scope = { + "type": "http", + "method": method, + "path": path if path is not None else f"/v1/tenants/{TENANT}/job-analysis-snapshots", + "query_string": query, + "headers": headers + if headers is not None + else [ + (b"authorization", b"Bearer opaque-token"), + (b"content-type", b"application/json"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + (b"x-purpose-code", b"job_analysis_write"), + ], + } + messages: list[dict[str, object]] = [] + frames = ( + [ + {"type": "http.request", "body": raw[:20], "more_body": True}, + {"type": "http.request", "body": raw[20:], "more_body": False}, + ] + if chunked + else [{"type": "http.request", "body": raw, "more_body": False}] + ) + + async def receive() -> dict[str, object]: + return frames.pop(0) if frames else {"type": "http.request", "body": b"", "more_body": False} + + async def send(message: dict[str, object]) -> None: + messages.append(message) + + await app(scope, receive, send) + start, body_message = messages + return int(start["status"]), dict(start["headers"]), json.loads(bytes(body_message["body"])) + + def test_constructor_rejects_missing_transport_dependencies(self) -> None: + with self.assertRaisesRegex(TypeError, "authenticator"): + self._app(authenticator=object()) + with self.assertRaisesRegex(TypeError, "write_policy"): + self._app(write_policy_value=object()) + with self.assertRaisesRegex(TypeError, "read_policy"): + self._app(read_policy_value=object()) + with self.assertRaisesRegex(TypeError, "write_port"): + self._app(write_port=object()) + with self.assertRaisesRegex(TypeError, "read_port"): + self._app(read_port=object()) + + async def test_post_then_get_returns_exact_clinical_psychologist_payload(self) -> None: + write_port = FakeWritePort() + read_port = FakeReadPort(clinical_psychologist_snapshot()) + app = self._app(write_port=write_port, read_port=read_port) + posted = clinical_psychologist_document() + posted["position_record_id"] = str(POSITION) + posted["criterion_blueprint_id"] = str(CRITERION) + + status, headers, created = await self._request(app, body=posted, chunked=True) + self.assertEqual(status, 201) + self.assertEqual(headers[b"cache-control"], b"no-store") + self.assertEqual(created, clinical_psychologist_document()) + self.assertEqual(write_port.calls[0]["idempotency_key"], IDEMPOTENCY_KEY) + self.assertEqual(write_port.calls[0]["position_record_id"], POSITION) + self.assertEqual(write_port.calls[0]["criterion_blueprint_id"], CRITERION) + + status, _, fetched = await self._request( + app, + method="GET", + path=f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"job_analysis_read"), + ], + ) + self.assertEqual(status, 200) + self.assertEqual(fetched, clinical_psychologist_document()) + self.assertEqual(fetched, created) + + async def test_wrong_path_and_method_return_transport_errors(self) -> None: + app = self._app() + for path in ("/v1/unknown", f"/v2/tenants/{TENANT}/job-analysis-snapshots", 42): + status, _, payload = await self._request(app, path=path) + self.assertEqual((status, payload["error"]), (404, "route_not_found")) + status, headers, payload = await self._request(app, method="DELETE") + self.assertEqual((status, payload["error"]), (405, "method_not_allowed")) + self.assertEqual(headers[b"allow"], b"GET, POST") + + async def test_malformed_route_ids_and_post_item_paths_are_invalid(self) -> None: + app = self._app() + cases = ( + {"path": "/v1/tenants/not-a-uuid/job-analysis-snapshots"}, + {"path": f"/v1/tenants/{UUID(int=0)}/job-analysis-snapshots"}, + {"method": "GET", "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots/{UUID(int=(1 << 128) - 1)}"}, + {"path": f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}"}, + {"method": "GET", "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots"}, + ) + for case in cases: + with self.subTest(case=case): + status, _, payload = await self._request(app, **case) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + + async def test_authentication_failures_do_not_write(self) -> None: + write_port = FakeWritePort() + app = self._app(write_port=write_port) + header_cases: tuple[object, ...] = ( + [], + object(), + [(b"authorization", b"Bearer one"), (b"authorization", b"Bearer two")], + [(b"x-request-id", b"request-1")], + [(b"authorization",)], + [("authorization", "Bearer opaque-token")], + [(b"authorization", b"Bearer \xff")], + ) + for headers in header_cases: + with self.subTest(headers=headers): + status, _, payload = await self._request(app, headers=headers) + self.assertEqual((status, payload["error"]), (401, "authentication_required")) + self.assertEqual(write_port.calls, []) + + app = self._app(authenticator=FakeAuthenticator(_api_principal(), error=AuthenticationFailed("expired")), write_port=write_port) + status, _, payload = await self._request(app) + self.assertEqual((status, payload["error"]), (401, "authentication_required")) + + async def test_missing_write_headers_and_bad_body_are_invalid(self) -> None: + app = self._app() + status, _, payload = await self._request( + app, + headers=[(b"authorization", b"Bearer opaque-token"), (b"x-purpose-code", b"job_analysis_write")], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request( + app, + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + ], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request( + app, + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"idempotency-key", b""), + (b"x-purpose-code", b"job_analysis_write"), + ], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request( + app, + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + (b"x-purpose-code", b"JobAnalysis"), + ], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request(app, body=b"{") + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request(app, body=b"[]") + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request(app, body={**clinical_psychologist_document(), "position_record_id": "nope"}) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request(app, body={**clinical_psychologist_document(), "position_record_id": 12}) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request( + app, + body={**clinical_psychologist_document(), "criterion_blueprint_id": str(UUID(int=0))}, + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + + async def test_domain_failures_map_to_stable_status_codes(self) -> None: + cases = ( + (JobAnalysisScopeMissing("missing job"), 409, "scope_missing"), + (JobAnalysisIdempotencyConflict("digest"), 409, "idempotency_conflict"), + (JobAnalysisIntegrityError("drift"), 400, "invalid_request"), + (ValueError("bad snapshot"), 400, "invalid_request"), + (RuntimeError("postgres password=do-not-leak"), 500, "internal_error"), + ) + for error, status_code, error_code in cases: + with self.subTest(error_code=error_code): + status, _, payload = await self._request(self._app(write_port=FakeWritePort(error=error))) + self.assertEqual((status, payload["error"]), (status_code, error_code)) + self.assertNotIn("password", json.dumps(payload)) + + status, _, payload = await self._request( + self._app(read_port=FakeReadPort(None)), + method="GET", + path=f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"job_analysis_read"), + ], + ) + self.assertEqual((status, payload["error"]), (404, "snapshot_not_found")) + + status, _, payload = await self._request( + self._app(), + method="GET", + path=f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"people_read"), + ], + ) + self.assertEqual((status, payload["error"]), (403, "access_denied")) + + for query in (b"\xff", b"bogus", b"purpose=job_analysis_read&purpose=other", "purpose=job_analysis_read", b"fields=tasks"): + status, _, payload = await self._request( + self._app(), + method="GET", + path=f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + query=query, + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"job_analysis_read"), + ], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + + status, _, payload = await self._request( + self._app(), + method="GET", + path=f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"JobAnalysis"), + ], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + + async def test_non_http_scope_is_rejected_as_programming_error(self) -> None: + app = self._app() + + async def receive() -> dict[str, object]: + return {"type": "lifespan.startup"} + + async def send(message: dict[str, object]) -> None: + del message + + with self.assertRaisesRegex(ValueError, "HTTP ASGI scopes"): + await app({"type": "lifespan"}, receive, send) + + async def test_invalid_body_frames_and_header_bytes_fail_closed(self) -> None: + app = self._app() + + async def bad_type_receive() -> dict[str, object]: + return {"type": "http.disconnect"} + + async def bad_body_receive() -> dict[str, object]: + return {"type": "http.request", "body": "not-bytes", "more_body": False} + + messages: list[dict[str, object]] = [] + + async def send(message: dict[str, object]) -> None: + messages.append(message) + + scope = { + "type": "http", + "method": "POST", + "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots", + "query_string": b"", + "headers": [ + (b"authorization", b"Bearer opaque-token"), + (b"content-type", b"application/json"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + (b"x-purpose-code", b"job_analysis_write"), + ], + } + await app(scope, bad_type_receive, send) + self.assertEqual(messages[0]["status"], 400) + messages.clear() + await app(scope, bad_body_receive, send) + self.assertEqual(messages[0]["status"], 400) + status, _, payload = await self._request( + app, + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"idempotency-key", b"\xff"), + (b"x-purpose-code", b"job_analysis_write"), + ], + ) + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + status, _, payload = await self._request( + app, + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"authorization", b"Bearer other"), + (b"idempotency-key", IDEMPOTENCY_KEY.encode("ascii")), + ], + ) + self.assertEqual((status, payload["error"]), (401, "authentication_required")) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_openapi_route_contract.py b/services/job-analysis-api/tests/test_openapi_route_contract.py new file mode 100644 index 000000000..c1eb49138 --- /dev/null +++ b/services/job-analysis-api/tests/test_openapi_route_contract.py @@ -0,0 +1,24 @@ +"""OpenAPI regressions for the executable job-analysis tenant route.""" + +from pathlib import Path + + +def test_openapi_matches_the_path_tenant_and_authenticated_actor_authority() -> None: + """Do not publish a global route or duplicate tenant/actor header authority.""" + schema = Path("schemas/openapi.yaml").read_text(encoding="utf-8") + collection_path = " /tenants/{tenant_record_id}/job-analysis-snapshots:" + item_path = " /tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}:" + assert collection_path in schema + assert item_path in schema + assert " /job-analysis-snapshots:" not in schema + assert " /job-analysis-snapshots/{analysis_record_id}:" not in schema + + collection_block = schema.split(collection_path, 1)[1].split(item_path, 1)[0] + assert " - name: tenant_record_id\n" in collection_block + assert " in: path\n" in collection_block + assert "#/components/parameters/TenantReference" not in collection_block + assert "#/components/parameters/ActorReference" not in collection_block + + item_block = schema.split(item_path, 1)[1].split("components:", 1)[0] + assert " - name: tenant_record_id\n" in item_block + assert " - name: analysis_record_id\n" in item_block diff --git a/services/job-analysis-api/tests/test_openapi_snapshot_schema.py b/services/job-analysis-api/tests/test_openapi_snapshot_schema.py new file mode 100644 index 000000000..a32527229 --- /dev/null +++ b/services/job-analysis-api/tests/test_openapi_snapshot_schema.py @@ -0,0 +1,39 @@ +"""OpenAPI regressions for bounded, typed job-analysis snapshot evidence.""" + +from pathlib import Path + + +def test_snapshot_arrays_publish_runtime_cardinality_and_item_types() -> None: + """Keep client schemas aligned with bounded runtime parsing and evidence shapes.""" + schema = Path("schemas/openapi.yaml").read_text(encoding="utf-8") + command = schema.split(" PersistJobAnalysisSnapshotCommand:", 1)[1].split( + " JobAnalysisSnapshotDocument:", 1 + )[0] + document = schema.split(" JobAnalysisSnapshotDocument:", 1)[1].split( + " ErrorResponse:", 1 + )[0] + + for block in (command, document): + assert " tasks:\n type: array\n minItems: 1\n maxItems: 500\n items:\n $ref: '#/components/schemas/JobAnalysisTaskEvidence'" in block + assert " ksao_requirements:\n type: array\n minItems: 1\n maxItems: 500\n items:\n $ref: '#/components/schemas/JobAnalysisKSAORequirement'" in block + assert " task_ksao_links:\n type: array\n minItems: 1\n maxItems: 5000\n items:\n $ref: '#/components/schemas/JobAnalysisTaskKSAOLink'" in block + assert " fja_profile:\n $ref: '#/components/schemas/FunctionalJobAnalysisProfile'" in block + + for component in ( + " JobAnalysisEvidenceSource:", + " JobAnalysisTaskEvidence:", + " JobAnalysisKSAORequirement:", + " JobAnalysisTaskKSAOLink:", + " FunctionalJobAnalysisProfile:", + ): + assert component in schema + + +def test_snapshot_get_publishes_dedicated_not_found_response() -> None: + """Do not document a missing snapshot as generic invalid-command validation.""" + schema = Path("schemas/openapi.yaml").read_text(encoding="utf-8") + item_path = " /tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}:" + item_block = schema.split(item_path, 1)[1].split("components:", 1)[0] + + assert " '404':\n $ref: '#/components/responses/SnapshotNotFound'" in item_block + assert " SnapshotNotFound:" in schema diff --git a/services/job-analysis-api/tests/test_postgres.py b/services/job-analysis-api/tests/test_postgres.py new file mode 100644 index 000000000..61c6c8627 --- /dev/null +++ b/services/job-analysis-api/tests/test_postgres.py @@ -0,0 +1,387 @@ +"""Executable contracts for the PostgreSQL job-analysis persistence adapter.""" + +from __future__ import annotations + +from datetime import date +import unittest +from uuid import UUID + +from orgmetra_hris_kernel import AuditOutboxEvent + +from orgmetra_job_analysis_api.postgres import PostgresJobAnalysisPort +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIdempotencyConflict, + JobAnalysisIntegrityError, + JobAnalysisScopeMissing, + command_digest, +) +from fixtures import ( + ANALYSIS, + CRITERION, + IDEMPOTENCY_KEY, + JOB, + OTHER_TENANT, + POSITION, + RECORDED_AT, + REVIEWED_AT, + TENANT, + clinical_psychologist_snapshot, +) + + +class FakeCursor: + """Serve deterministic rows while recording parameterized SQL.""" + + def __init__(self, script: list[object]) -> None: + self.script = list(script) + self.executions: list[tuple[str, tuple[object, ...] | None]] = [] + self._last: object = None + + def __enter__(self) -> FakeCursor: + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + return None + + def execute(self, sql: str, parameters: tuple[object, ...] | None = None) -> None: + """Record each SQL statement and advance the scripted response.""" + self.executions.append((sql, parameters)) + self._last = self.script.pop(0) if self.script else None + + def fetchone(self) -> object: + """Return the row prepared by the previous execute.""" + return self._last + + def fetchmany(self, size: int) -> list[object]: + """Return at most ``size`` scripted rows, like a DB-API cursor.""" + return list(self._last or [])[:size] + + def fetchall(self) -> list[object]: + """Return scripted child rows for tasks, KSAOs, or links.""" + return list(self._last or []) + + +class FakeConnection: + """Provide one transaction-scoped cursor context for the adapter.""" + + def __init__(self, cursor: FakeCursor) -> None: + self.cursor_instance = cursor + + def __enter__(self) -> FakeConnection: + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + return None + + def cursor(self) -> FakeCursor: + """Return the deterministic cursor used by this transaction.""" + return self.cursor_instance + + +def _audit_event() -> AuditOutboxEvent: + """Build one non-high-impact audit envelope for write-port tests.""" + snapshot = clinical_psychologist_snapshot() + return AuditOutboxEvent( + event_id=UUID("0198a412-6000-7000-8000-000000000301"), + tenant_record_id=TENANT, + source_service="job_analysis_api", + event_type="orgmetra.job_architecture.snapshot_recorded", + resource_reference=f"job_analysis_snapshot:{ANALYSIS.hex}", + actor_reference="keyverse:actor-ja-1", + purpose_code="job_analysis_write", + reason_code="snapshot_persisted", + evidence_version_code=snapshot.analysis_version_code, + result_code="recorded", + occurred_at=RECORDED_AT, + high_impact=False, + ) + + +def _header_row(*, tenant_record_id: UUID = TENANT, analysis_record_id: UUID = ANALYSIS, digest: str | None = None) -> tuple[object, ...]: + """Return one persisted snapshot header in SELECT column order.""" + snapshot = clinical_psychologist_snapshot() + source = snapshot.fja_profile.source + return ( + tenant_record_id, + analysis_record_id, + JOB, + snapshot.analysis_version_code, + snapshot.status_code, + date(2026, 8, 1), + RECORDED_AT, + snapshot.reviewed_by_reference, + REVIEWED_AT, + digest if digest is not None else snapshot.content_digest(), + snapshot.fja_profile.data_function_code, + snapshot.fja_profile.people_function_code, + snapshot.fja_profile.things_function_code, + source.source_uri, + source.source_title, + source.source_version_code, + source.retrieved_at, + source.content_digest_sha256, + source.origin_code, + ) + + +def _task_rows() -> list[tuple[object, ...]]: + """Return persisted task rows in SELECT column order.""" + rows = [] + for task in clinical_psychologist_snapshot().tasks: + source = task.source + rows.append( + ( + task.task_record_id, + task.task_statement, + task.importance_level, + task.difficulty_level, + source.source_uri, + source.source_title, + source.source_version_code, + source.retrieved_at, + source.content_digest_sha256, + source.origin_code, + ) + ) + return rows + + +def _ksao_rows() -> list[tuple[object, ...]]: + """Return persisted KSAO rows in SELECT column order.""" + rows = [] + for item in clinical_psychologist_snapshot().ksao_requirements: + source = item.source + rows.append( + ( + item.ksao_record_id, + item.category_code, + item.requirement_statement, + item.importance_level, + item.proficiency_level, + source.source_uri, + source.source_title, + source.source_version_code, + source.retrieved_at, + source.content_digest_sha256, + source.origin_code, + ) + ) + return rows + + +def _link_rows() -> list[tuple[object, ...]]: + """Return persisted task-KSAO link rows in SELECT column order.""" + return [ + (link.task_record_id, link.ksao_record_id, link.relationship_strength, link.essential_for_task) + for link in clinical_psychologist_snapshot().task_ksao_links + ] + + +class PostgresJobAnalysisPortTests(unittest.TestCase): + """Prove tenant RLS, fail-closed parents, idempotency, and outbox writes.""" + + def _port(self, script: list[object]) -> tuple[PostgresJobAnalysisPort, FakeCursor]: + cursor = FakeCursor(script) + return PostgresJobAnalysisPort(lambda: FakeConnection(cursor)), cursor + + def _persist(self, port: PostgresJobAnalysisPort, **overrides: object) -> object: + snapshot = clinical_psychologist_snapshot() + values = { + "snapshot": snapshot, + "idempotency_key": IDEMPOTENCY_KEY, + "request_digest": command_digest( + snapshot=snapshot, + position_record_id=None, + criterion_blueprint_id=None, + ), + "actor_reference": "keyverse:actor-ja-1", + "purpose_code": "job_analysis_write", + "position_record_id": None, + "criterion_blueprint_id": None, + "audit_event": _audit_event(), + "outbox_delivery_record_id": UUID("0198a412-6000-7000-8000-000000000302"), + "write_command_id": UUID("0198a412-6000-7000-8000-000000000303"), + } + values.update(overrides) + return port.persist_snapshot(**values) + + def test_persists_snapshot_with_idempotency_key_and_audit_outbox(self) -> None: + snapshot = clinical_psychologist_snapshot() + write_statement_count = ( + 1 + + len(snapshot.tasks) + + len(snapshot.ksao_requirements) + + len(snapshot.task_ksao_links) + + 2 + ) + port, cursor = self._port([None, None, (JOB,)] + [None] * write_statement_count) + + persisted = self._persist(port) + + self.assertEqual(persisted.to_snapshot(), snapshot.to_snapshot()) + sql = [statement for statement, _ in cursor.executions] + self.assertTrue(any("job_analysis_write_command" in item and "INSERT" in item for item in sql)) + self.assertTrue(any("record_audit_outbox_event" in item for item in sql)) + self.assertTrue(any("job_analysis_task_item" in item for item in sql)) + self.assertTrue(any("job_analysis_ksao_item" in item for item in sql)) + idempotency_insert = next( + parameters + for statement, parameters in cursor.executions + if statement.startswith("INSERT INTO public.job_analysis_write_command") + ) + self.assertIn(IDEMPOTENCY_KEY, idempotency_insert) + job_lookup = next(parameters for statement, parameters in cursor.executions if "FROM public.job_profile" in statement) + self.assertEqual(job_lookup, (TENANT, JOB)) + + def test_persists_when_optional_parents_exist(self) -> None: + snapshot = clinical_psychologist_snapshot() + script = [ + None, + None, + (JOB,), + (POSITION, JOB), + (CRITERION, JOB), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + port, cursor = self._port(script) + persisted = self._persist( + port, + position_record_id=POSITION, + criterion_blueprint_id=CRITERION, + request_digest=command_digest( + snapshot=snapshot, + position_record_id=POSITION, + criterion_blueprint_id=CRITERION, + ), + ) + self.assertEqual(persisted.to_snapshot(), snapshot.to_snapshot()) + self.assertTrue(any("record_audit_outbox_event" in sql for sql, _ in cursor.executions)) + + def test_missing_or_mismatched_scope_fails_closed(self) -> None: + cases = ( + ([None, None, None], None, None, "job_profile"), + ([None, None, (JOB,), None], POSITION, None, "position_record"), + ([None, None, (JOB,), (POSITION, OTHER_TENANT)], POSITION, None, "position_record"), + ([None, None, (JOB,), (CRITERION, OTHER_TENANT)], None, CRITERION, "criterion_blueprint"), + ([None, None, (JOB,), None], None, CRITERION, "criterion_blueprint"), + ) + for script, position_id, criterion_id, expected in cases: + with self.subTest(expected=expected, position_id=position_id): + port, _ = self._port(script) + with self.assertRaisesRegex(JobAnalysisScopeMissing, expected): + self._persist( + port, + position_record_id=position_id, + criterion_blueprint_id=criterion_id, + request_digest=command_digest( + snapshot=clinical_psychologist_snapshot(), + position_record_id=position_id, + criterion_blueprint_id=criterion_id, + ), + ) + + def test_idempotent_replay_returns_stored_snapshot_without_new_write(self) -> None: + snapshot = clinical_psychologist_snapshot() + digest = command_digest(snapshot=snapshot, position_record_id=None, criterion_blueprint_id=None) + script = [ + None, + (digest, ANALYSIS), + [_header_row()], + _task_rows(), + _ksao_rows(), + _link_rows(), + ] + port, cursor = self._port(script) + persisted = self._persist(port, request_digest=digest) + self.assertEqual(persisted.to_snapshot(), snapshot.to_snapshot()) + self.assertFalse(any("record_audit_outbox_event" in sql for sql, _ in cursor.executions)) + + def test_idempotency_conflict_and_lost_snapshot_fail_closed(self) -> None: + snapshot = clinical_psychologist_snapshot() + digest = command_digest(snapshot=snapshot, position_record_id=None, criterion_blueprint_id=None) + port, _ = self._port([None, ("other" * 16, ANALYSIS)]) + with self.assertRaises(JobAnalysisIdempotencyConflict): + self._persist(port, request_digest=digest) + port, _ = self._port([None, (digest, ANALYSIS), []]) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "lost its snapshot"): + self._persist(port, request_digest=digest) + + def test_read_reconstructs_exact_clinical_psychologist_snapshot(self) -> None: + port, cursor = self._port( + [ + None, + None, + [_header_row()], + _task_rows(), + _ksao_rows(), + _link_rows(), + ] + ) + snapshot = port.read_snapshot(tenant_record_id=TENANT, analysis_record_id=ANALYSIS) + self.assertEqual(snapshot.to_snapshot(), clinical_psychologist_snapshot().to_snapshot()) + self.assertEqual(cursor.executions[0], ("SET TRANSACTION READ ONLY", None)) + + def test_read_fail_closed_paths(self) -> None: + port, _ = self._port([None, None, []]) + self.assertIsNone(port.read_snapshot(tenant_record_id=TENANT, analysis_record_id=ANALYSIS)) + port, _ = self._port([None, None, [_header_row(), _header_row()]]) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "multiple snapshot headers"): + port.read_snapshot(tenant_record_id=TENANT, analysis_record_id=ANALYSIS) + port, _ = self._port([None, None, [_header_row(tenant_record_id=OTHER_TENANT)]]) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "escaped requested target"): + port.read_snapshot(tenant_record_id=TENANT, analysis_record_id=ANALYSIS) + port, _ = self._port( + [ + None, + None, + [_header_row(digest="a" * 64)], + _task_rows(), + _ksao_rows(), + _link_rows(), + ] + ) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "digest"): + port.read_snapshot(tenant_record_id=TENANT, analysis_record_id=ANALYSIS) + + def test_constructor_and_direct_argument_guards(self) -> None: + with self.assertRaisesRegex(TypeError, "connection_factory"): + PostgresJobAnalysisPort(None) # type: ignore[arg-type] + port, _ = self._port([]) + with self.assertRaises(TypeError): + port.persist_snapshot( + snapshot=object(), # type: ignore[arg-type] + idempotency_key=IDEMPOTENCY_KEY, + request_digest="a" * 64, + actor_reference="keyverse:actor-ja-1", + purpose_code="job_analysis_write", + position_record_id=None, + criterion_blueprint_id=None, + audit_event=_audit_event(), + outbox_delivery_record_id=UUID("0198a412-6000-7000-8000-000000000302"), + write_command_id=UUID("0198a412-6000-7000-8000-000000000303"), + ) + with self.assertRaises(TypeError): + self._persist(port, audit_event=object()) + with self.assertRaises(ValueError): + self._persist(port, idempotency_key=12) + with self.assertRaises(ValueError): + port.read_snapshot(tenant_record_id=UUID(int=0), analysis_record_id=ANALYSIS) + with self.assertRaises(ValueError): + self._persist(port, position_record_id=UUID(int=0)) + with self.assertRaises(ValueError): + self._persist(port, criterion_blueprint_id=UUID(int=(1 << 128) - 1)) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_postgres_concurrency.py b/services/job-analysis-api/tests/test_postgres_concurrency.py new file mode 100644 index 000000000..67f2abdcf --- /dev/null +++ b/services/job-analysis-api/tests/test_postgres_concurrency.py @@ -0,0 +1,191 @@ +"""Concurrency regressions for PostgreSQL job-analysis persistence.""" + +from __future__ import annotations + +from contextlib import AbstractContextManager +import unittest +from typing import Any +from uuid import UUID + +from orgmetra_job_analysis_api.postgres import ( + PostgresJobAnalysisPort, + _IDEMPOTENCY_LOOKUP_SQL, + _constraint_name, + _is_unique_violation, +) +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIdempotencyConflict, + JobAnalysisIntegrityError, + command_digest, +) +from test_postgres import _audit_event +from fixtures import IDEMPOTENCY_KEY, JOB, clinical_psychologist_snapshot + + +class _Diagnostic: + """Expose the PostgreSQL constraint name carried by a driver error.""" + + def __init__(self, constraint_name: str) -> None: + self.constraint_name = constraint_name + + +class _UniqueViolation(Exception): + """Mimic a DB-API unique violation from psycopg-style drivers.""" + + sqlstate = "23505" + + def __init__(self, constraint_name: str) -> None: + super().__init__(constraint_name) + self.diag = _Diagnostic(constraint_name) + + +class _ConstraintCursor: + """Return valid parent lookups and fail at one selected insert constraint.""" + + def __init__(self, failing_constraint: str) -> None: + self.failing_constraint = failing_constraint + self.executions: list[tuple[str, tuple[object, ...] | None]] = [] + self._last: object = None + + def __enter__(self) -> _ConstraintCursor: + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + return None + + def execute(self, sql: str, parameters: tuple[object, ...] | None = None) -> None: + """Record SQL, return required parents, and inject one unique violation.""" + self.executions.append((sql, parameters)) + if sql.startswith("INSERT INTO public.job_analysis_snapshot"): + if self.failing_constraint.startswith("job_analysis_snapshot_"): + raise _UniqueViolation(self.failing_constraint) + self._last = None + return + if sql.startswith("INSERT INTO public.job_analysis_write_command"): + if self.failing_constraint.startswith("job_analysis_write_command_"): + raise _UniqueViolation(self.failing_constraint) + self._last = None + return + if "FROM public.job_analysis_write_command" in sql: + self._last = (None, None) + elif "FROM public.job_profile" in sql: + self._last = (JOB,) + else: + self._last = None + + def fetchone(self) -> object: + """Return the row prepared by the previous statement.""" + return self._last + + +class _DriverFailureCursor(_ConstraintCursor): + """Raise a non-unique driver failure at one selected insert.""" + + def __init__(self, target_prefix: str) -> None: + super().__init__("no_unique_constraint") + self.target_prefix = target_prefix + + def execute(self, sql: str, parameters: tuple[object, ...] | None = None) -> None: + """Preserve unrelated driver failures instead of reclassifying them.""" + if sql.startswith(self.target_prefix): + self.executions.append((sql, parameters)) + raise RuntimeError("unrelated driver failure") + super().execute(sql, parameters) + + +class _Connection(AbstractContextManager[Any]): + """Provide one deterministic transaction cursor.""" + + def __init__(self, cursor: _ConstraintCursor) -> None: + self.cursor_instance = cursor + + def __enter__(self) -> _Connection: + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + return None + + def cursor(self) -> _ConstraintCursor: + """Return the configured cursor.""" + return self.cursor_instance + + +def _persist_with_cursor(cursor: _ConstraintCursor) -> None: + """Run one realistic write until the configured cursor succeeds or fails.""" + snapshot = clinical_psychologist_snapshot() + port = PostgresJobAnalysisPort(lambda: _Connection(cursor)) + port.persist_snapshot( + snapshot=snapshot, + idempotency_key=IDEMPOTENCY_KEY, + request_digest=command_digest( + snapshot=snapshot, + position_record_id=None, + criterion_blueprint_id=None, + ), + actor_reference="keyverse:actor-ja-1", + purpose_code="job_analysis_write", + position_record_id=None, + criterion_blueprint_id=None, + audit_event=_audit_event(), + outbox_delivery_record_id=UUID("0198a412-6000-7000-8000-000000000302"), + write_command_id=UUID("0198a412-6000-7000-8000-000000000303"), + ) + + +def _persist_with_constraint(constraint_name: str) -> None: + """Run one realistic write until the selected unique constraint fires.""" + _persist_with_cursor(_ConstraintCursor(constraint_name)) + + +class PostgresJobAnalysisConcurrencyTests(unittest.TestCase): + """Prove exact-key serialization and stable constraint-domain mapping.""" + + def test_idempotency_lookup_serializes_the_tenant_and_key_before_reading(self) -> None: + """Prevent two transactions from both observing an absent command key.""" + normalized = " ".join(_IDEMPOTENCY_LOOKUP_SQL.lower().split()) + self.assertIn("pg_advisory_xact_lock", normalized) + self.assertIn("hashtextextended", normalized) + self.assertIn("tenant_record_id", normalized) + self.assertIn("idempotency_key", normalized) + + def test_unique_violation_metadata_is_read_without_driver_lock_in(self) -> None: + """Support modern and legacy PostgreSQL DB-API error attributes.""" + error = _UniqueViolation("job_analysis_snapshot_job_version_unique") + self.assertTrue(_is_unique_violation(error)) + self.assertEqual( + _constraint_name(error), + "job_analysis_snapshot_job_version_unique", + ) + legacy = Exception("legacy") + legacy.pgcode = "23505" # type: ignore[attr-defined] + self.assertTrue(_is_unique_violation(legacy)) + self.assertFalse(_is_unique_violation(RuntimeError("other"))) + self.assertIsNone(_constraint_name(RuntimeError("other"))) + + def test_snapshot_version_race_maps_to_integrity_error(self) -> None: + """Do not leak a raw driver exception for a concurrent job/version write.""" + with self.assertRaisesRegex(JobAnalysisIntegrityError, "already exists"): + _persist_with_constraint("job_analysis_snapshot_job_version_unique") + + def test_command_key_race_maps_to_idempotency_conflict(self) -> None: + """Do not expose a raw driver error if an uncoordinated writer wins the key.""" + with self.assertRaisesRegex(JobAnalysisIdempotencyConflict, "concurrently"): + _persist_with_constraint("job_analysis_write_command_idempotency_unique") + + def test_non_unique_snapshot_failure_is_not_reclassified(self) -> None: + """Preserve an unrelated database failure for the outer fail-closed boundary.""" + with self.assertRaisesRegex(RuntimeError, "unrelated driver failure"): + _persist_with_cursor( + _DriverFailureCursor("INSERT INTO public.job_analysis_snapshot") + ) + + def test_non_unique_command_failure_is_not_reclassified(self) -> None: + """Preserve an unrelated command-insert failure for operator diagnosis.""" + with self.assertRaisesRegex(RuntimeError, "unrelated driver failure"): + _persist_with_cursor( + _DriverFailureCursor("INSERT INTO public.job_analysis_write_command") + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_postgres_contract_script.py b/services/job-analysis-api/tests/test_postgres_contract_script.py new file mode 100644 index 000000000..34da009e5 --- /dev/null +++ b/services/job-analysis-api/tests/test_postgres_contract_script.py @@ -0,0 +1,13 @@ +"""Static regressions for executable job-analysis PostgreSQL evidence.""" + +from pathlib import Path + + +def test_missing_position_failure_requires_the_expected_foreign_key_reason() -> None: + """Do not accept an unrelated SQL failure as position-scope evidence.""" + contract = Path("tests/test_job_analysis_snapshot_postgres.sh").read_text( + encoding="utf-8" + ) + assert '"${missing_position_output}" != *"foreign key"*' in contract + assert '"${missing_position_output}" != *"job_analysis_snapshot_position_tenant_fk"*' in contract + assert "missing position failed for an unexpected reason" in contract diff --git a/services/job-analysis-api/tests/test_postgres_idempotency_authority.py b/services/job-analysis-api/tests/test_postgres_idempotency_authority.py new file mode 100644 index 000000000..ccf9bbf2b --- /dev/null +++ b/services/job-analysis-api/tests/test_postgres_idempotency_authority.py @@ -0,0 +1,97 @@ +"""Authority-binding regressions for job-analysis idempotency replays.""" + +from __future__ import annotations + +import unittest + +from orgmetra_job_analysis_api.postgres import PostgresJobAnalysisPort, _IDEMPOTENCY_LOOKUP_SQL +from orgmetra_job_analysis_api.snapshot import JobAnalysisIdempotencyConflict, command_digest + +from fixtures import ANALYSIS, IDEMPOTENCY_KEY, clinical_psychologist_snapshot +from test_postgres import ( + FakeConnection, + FakeCursor, + _audit_event, + _header_row, + _ksao_rows, + _link_rows, + _task_rows, +) + + +class PostgresIdempotencyAuthorityTests(unittest.TestCase): + """Prove a durable idempotency key cannot cross actor or purpose authority.""" + + def _persist_replay( + self, + *, + stored_actor_reference: str, + stored_purpose_code: str, + actor_reference: str = "keyverse:actor-ja-1", + purpose_code: str = "job_analysis_write", + include_snapshot: bool = False, + ) -> object: + snapshot = clinical_psychologist_snapshot() + digest = command_digest( + snapshot=snapshot, + position_record_id=None, + criterion_blueprint_id=None, + ) + script: list[object] = [ + None, + ( + digest, + ANALYSIS, + stored_actor_reference, + stored_purpose_code, + ), + ] + if include_snapshot: + script.extend([[_header_row()], _task_rows(), _ksao_rows(), _link_rows()]) + cursor = FakeCursor(script) + port = PostgresJobAnalysisPort(lambda: FakeConnection(cursor)) + return port.persist_snapshot( + snapshot=snapshot, + idempotency_key=IDEMPOTENCY_KEY, + request_digest=digest, + actor_reference=actor_reference, + purpose_code=purpose_code, + position_record_id=None, + criterion_blueprint_id=None, + audit_event=_audit_event(), + outbox_delivery_record_id=__import__("uuid").UUID( + "0198a412-6000-7000-8000-000000000302" + ), + write_command_id=__import__("uuid").UUID( + "0198a412-6000-7000-8000-000000000303" + ), + ) + + def test_lookup_reads_the_immutable_actor_and_purpose_binding(self) -> None: + """Keep replay authority in the same serialized command lookup.""" + normalized = " ".join(_IDEMPOTENCY_LOOKUP_SQL.lower().split()) + self.assertIn("actor_reference", normalized) + self.assertIn("purpose_code", normalized) + + def test_same_key_and_digest_cannot_replay_under_a_different_actor(self) -> None: + """Prevent one authorized principal from inheriting another actor's command.""" + with self.assertRaisesRegex(JobAnalysisIdempotencyConflict, "actor"): + self._persist_replay(stored_actor_reference="keyverse:actor-ja-other", stored_purpose_code="job_analysis_write") + + def test_same_key_and_digest_cannot_replay_under_a_different_purpose(self) -> None: + """Prevent an idempotent command from escaping its original purpose boundary.""" + with self.assertRaisesRegex(JobAnalysisIdempotencyConflict, "purpose"): + self._persist_replay(stored_actor_reference="keyverse:actor-ja-1", stored_purpose_code="job_analysis_read") + + def test_exact_actor_and_purpose_replay_returns_the_stored_snapshot(self) -> None: + """Preserve the successful retry contract for the exact original authority.""" + replayed = self._persist_replay( + stored_actor_reference="keyverse:actor-ja-1", + stored_purpose_code="job_analysis_write", + include_snapshot=True, + ) + self.assertEqual(replayed.to_snapshot(), clinical_psychologist_snapshot().to_snapshot()) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_read_purpose_header.py b/services/job-analysis-api/tests/test_read_purpose_header.py new file mode 100644 index 000000000..7f23488e6 --- /dev/null +++ b/services/job-analysis-api/tests/test_read_purpose_header.py @@ -0,0 +1,108 @@ +"""Regression contracts for purpose-bound job-analysis reads.""" + +from __future__ import annotations + +import json +import unittest +from uuid import UUID + +from orgmetra_hris_kernel import JobAnalysisSnapshot +from orgmetra_job_analysis_api import AuthenticatedPrincipal +from orgmetra_job_analysis_api.http import JobAnalysisAsgiApp + +from fixtures import ANALYSIS, TENANT, clinical_psychologist_snapshot, read_policy, write_policy + + +class _Authenticator: + async def authenticate(self, bearer_token: str) -> AuthenticatedPrincipal: + return AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse:actor-ja-read-purpose", + granted_scope_codes=frozenset( + {"orgmetra.job_architecture.read", "orgmetra.job_architecture.write"} + ), + ) + + +class _WritePort: + def persist_snapshot(self, **kwargs: object) -> JobAnalysisSnapshot: + snapshot = kwargs["snapshot"] + assert isinstance(snapshot, JobAnalysisSnapshot) + return snapshot + + +class _ReadPort: + def read_snapshot( + self, + *, + tenant_record_id: UUID, + analysis_record_id: UUID, + ) -> JobAnalysisSnapshot | None: + assert tenant_record_id == TENANT + assert analysis_record_id == ANALYSIS + return clinical_psychologist_snapshot() + + +class ReadPurposeHeaderTests(unittest.IsolatedAsyncioTestCase): + """Keep read purpose out of URLs and aligned with write-side authorization.""" + + async def _get( + self, + *, + headers: list[tuple[bytes, bytes]], + query_string: bytes = b"", + ) -> tuple[int, dict[str, object]]: + app = JobAnalysisAsgiApp( + authenticator=_Authenticator(), + write_policy=write_policy(), + read_policy=read_policy(), + write_port=_WritePort(), + read_port=_ReadPort(), + ) + scope = { + "type": "http", + "method": "GET", + "path": f"/v1/tenants/{TENANT}/job-analysis-snapshots/{ANALYSIS}", + "query_string": query_string, + "headers": headers, + } + sent: list[dict[str, object]] = [] + + async def receive() -> dict[str, object]: + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message: dict[str, object]) -> None: + sent.append(message) + + await app(scope, receive, send) + return int(sent[0]["status"]), json.loads(bytes(sent[1]["body"])) + + async def test_get_accepts_purpose_only_from_x_purpose_code_header(self) -> None: + status, payload = await self._get( + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"job_analysis_read"), + ] + ) + + self.assertEqual(status, 200) + self.assertEqual(payload["analysis_record_id"], str(ANALYSIS)) + + async def test_query_parameter_cannot_substitute_for_purpose_header(self) -> None: + status, payload = await self._get( + headers=[(b"authorization", b"Bearer opaque-token")], + query_string=b"purpose=job_analysis_read", + ) + + self.assertEqual((status, payload["error"]), (400, "invalid_request")) + + async def test_read_route_rejects_query_parameters_even_with_valid_purpose_header(self) -> None: + status, payload = await self._get( + headers=[ + (b"authorization", b"Bearer opaque-token"), + (b"x-purpose-code", b"job_analysis_read"), + ], + query_string=b"purpose=job_analysis_read", + ) + + self.assertEqual((status, payload["error"]), (400, "invalid_request")) diff --git a/services/job-analysis-api/tests/test_snapshot.py b/services/job-analysis-api/tests/test_snapshot.py new file mode 100644 index 000000000..0a887c9a4 --- /dev/null +++ b/services/job-analysis-api/tests/test_snapshot.py @@ -0,0 +1,345 @@ +"""Executable contracts for snapshot parse, persist, and read use cases.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +import unittest +from uuid import UUID + +from orgmetra_keyverse_adapter import AuthorizationDeniedError +from orgmetra_hris_kernel import AuditOutboxEvent, JobAnalysisSnapshot + +from orgmetra_job_analysis_api.snapshot import ( + JobAnalysisIntegrityError, + JobAnalysisSnapshotNotFound, + command_digest, + persist_job_analysis_snapshot, + read_job_analysis_snapshot, + snapshot_from_document, +) +from fixtures import ( + ANALYSIS, + CRITERION, + IDEMPOTENCY_KEY, + JOB, + OTHER_TENANT, + POSITION, + TENANT, + clinical_psychologist_document, + clinical_psychologist_snapshot, + read_policy, + read_principal, + write_policy, + write_principal, +) + + +class RecordingWritePort: + """Capture the exact write-port arguments, including Idempotency-Key.""" + + def __init__(self, result: JobAnalysisSnapshot | None = None, *, error: Exception | None = None) -> None: + self.result = result + self.error = error + self.calls: list[dict[str, object]] = [] + + def persist_snapshot( + self, + *, + snapshot: JobAnalysisSnapshot, + idempotency_key: str, + request_digest: str, + actor_reference: str, + purpose_code: str, + position_record_id: UUID | None, + criterion_blueprint_id: UUID | None, + audit_event: AuditOutboxEvent, + outbox_delivery_record_id: UUID, + write_command_id: UUID, + ) -> JobAnalysisSnapshot: + """Record every keyword the use case forwarded to persistence.""" + self.calls.append( + { + "snapshot": snapshot, + "idempotency_key": idempotency_key, + "request_digest": request_digest, + "actor_reference": actor_reference, + "purpose_code": purpose_code, + "position_record_id": position_record_id, + "criterion_blueprint_id": criterion_blueprint_id, + "audit_event": audit_event, + "outbox_delivery_record_id": outbox_delivery_record_id, + "write_command_id": write_command_id, + } + ) + if self.error is not None: + raise self.error + return self.result if self.result is not None else snapshot + + +class RecordingReadPort: + """Return a configured snapshot and capture protected-read attempts.""" + + def __init__(self, result: JobAnalysisSnapshot | None) -> None: + self.result = result + self.calls: list[tuple[UUID, UUID]] = [] + + def read_snapshot(self, *, tenant_record_id: UUID, analysis_record_id: UUID) -> JobAnalysisSnapshot | None: + """Return deterministic snapshot truth for use-case tests.""" + self.calls.append((tenant_record_id, analysis_record_id)) + return self.result + + +class SnapshotDocumentTests(unittest.TestCase): + """Prove posted 임상심리사 documents rebuild the kernel snapshot exactly.""" + + def test_clinical_psychologist_document_round_trips_through_kernel(self) -> None: + posted = clinical_psychologist_document() + rebuilt = snapshot_from_document(posted, tenant_record_id=TENANT) + self.assertEqual(rebuilt.to_snapshot(), posted) + self.assertIn("표준화된 심리검사를 실시하고", posted["tasks"][0]["task_statement"]) + + def test_rejects_malformed_documents_before_persistence(self) -> None: + posted = clinical_psychologist_document() + cases = ( + ("not-an-object", TENANT), + ({**posted, "tenant_record_id": str(OTHER_TENANT)}, TENANT), + ({**posted, "tenant_record_id": "not-a-uuid"}, TENANT), + ({**posted, "tenant_record_id": 12}, TENANT), + ({**posted, "job_record_id": str(UUID(int=0))}, TENANT), + ({**posted, "tasks": []}, TENANT), + ({**posted, "tasks": "nope"}, TENANT), + ({**posted, "tasks": ["bad"]}, TENANT), + ({**posted, "ksao_requirements": []}, TENANT), + ({**posted, "ksao_requirements": "nope"}, TENANT), + ({**posted, "ksao_requirements": ["bad"]}, TENANT), + ({**posted, "task_ksao_links": []}, TENANT), + ({**posted, "task_ksao_links": "nope"}, TENANT), + ({**posted, "task_ksao_links": ["bad"]}, TENANT), + ({**posted, "fja_profile": "nope"}, TENANT), + ({**posted, "effective_from": "not-a-date"}, TENANT), + ({**posted, "effective_from": 12}, TENANT), + ({**posted, "effective_from": datetime(2026, 8, 1, tzinfo=timezone.utc)}, TENANT), + ({**posted, "recorded_at": "not-a-time"}, TENANT), + ({**posted, "recorded_at": 12}, TENANT), + ({**posted, "recorded_at": "2026-08-18T05:00:00"}, TENANT), + ({**posted, "tasks": [{**posted["tasks"][0], "source": "nope"}]}, TENANT), + ({**posted, "analysis_record_id": "zzzz"}, TENANT), + ({**posted, "recorded_at": datetime(2026, 8, 18, 5, 0)}, TENANT), + ) + rebuilt = snapshot_from_document( + { + **posted, + "analysis_record_id": ANALYSIS, + "tenant_record_id": TENANT, + "job_record_id": JOB, + "effective_from": date(2026, 8, 1), + "recorded_at": datetime(2026, 8, 18, 5, 0, tzinfo=timezone.utc), + }, + tenant_record_id=TENANT, + ) + self.assertEqual(rebuilt.job_record_id, JOB) + for document, tenant in cases: + with self.subTest(document=document), self.assertRaises(ValueError): + snapshot_from_document(document, tenant_record_id=tenant) + + +class PersistUseCaseTests(unittest.TestCase): + """Prove authorization, Idempotency-Key forwarding, and payload equality.""" + + def test_persisted_clinical_psychologist_snapshot_equals_posted_payload(self) -> None: + posted = clinical_psychologist_document() + port = RecordingWritePort() + + view = persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=posted, + idempotency_key=IDEMPOTENCY_KEY, + purpose_code="job_analysis_write", + position_record_id=str(POSITION), + criterion_blueprint_id=str(CRITERION), + policy=write_policy(), + write_port=port, + ) + + self.assertEqual(view.snapshot, posted) + self.assertEqual(len(port.calls), 1) + call = port.calls[0] + self.assertEqual(call["idempotency_key"], IDEMPOTENCY_KEY) + self.assertEqual(call["position_record_id"], POSITION) + self.assertEqual(call["criterion_blueprint_id"], CRITERION) + + uuid_port = RecordingWritePort() + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=posted, + idempotency_key=IDEMPOTENCY_KEY, + purpose_code="job_analysis_write", + position_record_id=POSITION, + criterion_blueprint_id=CRITERION, + policy=write_policy(), + write_port=uuid_port, + ) + self.assertEqual(uuid_port.calls[0]["position_record_id"], POSITION) + self.assertEqual( + call["request_digest"], + command_digest( + snapshot=clinical_psychologist_snapshot(), + position_record_id=POSITION, + criterion_blueprint_id=CRITERION, + ), + ) + audit_event = call["audit_event"] + self.assertIsInstance(audit_event, AuditOutboxEvent) + self.assertEqual(audit_event.event_type, "orgmetra.job_architecture.snapshot_recorded") + self.assertFalse(audit_event.high_impact) + + def test_authorization_denial_never_reaches_write_port(self) -> None: + port = RecordingWritePort() + with self.assertRaises(AuthorizationDeniedError): + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=clinical_psychologist_document(), + idempotency_key=IDEMPOTENCY_KEY, + purpose_code="wrong_purpose", + policy=write_policy(), + write_port=port, + ) + self.assertEqual(port.calls, []) + + def test_integrity_mismatch_from_write_port_fails_closed(self) -> None: + other = clinical_psychologist_snapshot() + drifted = JobAnalysisSnapshot( + analysis_record_id=other.analysis_record_id, + tenant_record_id=other.tenant_record_id, + job_record_id=other.job_record_id, + analysis_version_code="clinical-psychologist:v2", + status_code=other.status_code, + effective_from=other.effective_from, + recorded_at=other.recorded_at, + tasks=other.tasks, + ksao_requirements=other.ksao_requirements, + task_ksao_links=other.task_ksao_links, + fja_profile=other.fja_profile, + reviewed_by_reference=other.reviewed_by_reference, + reviewed_at=other.reviewed_at, + ) + port = RecordingWritePort(result=drifted) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "escaped posted payload"): + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=clinical_psychologist_document(), + idempotency_key=IDEMPOTENCY_KEY, + purpose_code="job_analysis_write", + policy=write_policy(), + write_port=port, + ) + + def test_rejects_reserved_tenant_and_short_idempotency_key(self) -> None: + port = RecordingWritePort() + with self.assertRaises(ValueError): + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=UUID(int=0), + document=clinical_psychologist_document(), + idempotency_key=IDEMPOTENCY_KEY, + purpose_code="job_analysis_write", + policy=write_policy(), + write_port=port, + ) + with self.assertRaises(ValueError): + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=clinical_psychologist_document(), + idempotency_key="short-key", + purpose_code="job_analysis_write", + policy=write_policy(), + write_port=port, + ) + with self.assertRaises(ValueError): + persist_job_analysis_snapshot( + principal=write_principal(), + tenant_record_id=TENANT, + document=clinical_psychologist_document(), + idempotency_key="bad\x1fidempotency-key01", + purpose_code="job_analysis_write", + policy=write_policy(), + write_port=port, + ) + self.assertEqual(port.calls, []) + + +class ReadUseCaseTests(unittest.TestCase): + """Prove authorized reads return the exact persisted snapshot document.""" + + def test_read_returns_posted_clinical_psychologist_snapshot(self) -> None: + port = RecordingReadPort(clinical_psychologist_snapshot()) + view = read_job_analysis_snapshot( + principal=read_principal(), + tenant_record_id=TENANT, + analysis_record_id=ANALYSIS, + purpose_code="job_analysis_read", + policy=read_policy(), + read_port=port, + ) + self.assertEqual(view.snapshot, clinical_psychologist_document()) + self.assertEqual(port.calls, [(TENANT, ANALYSIS)]) + + def test_missing_snapshot_is_reported_without_persistence_details(self) -> None: + port = RecordingReadPort(None) + with self.assertRaisesRegex(JobAnalysisSnapshotNotFound, "unavailable"): + read_job_analysis_snapshot( + principal=read_principal(), + tenant_record_id=TENANT, + analysis_record_id=ANALYSIS, + purpose_code="job_analysis_read", + policy=read_policy(), + read_port=port, + ) + + def test_repository_target_mismatch_fails_closed(self) -> None: + drifted = clinical_psychologist_snapshot() + object.__setattr__(drifted, "tenant_record_id", OTHER_TENANT) + port = RecordingReadPort(drifted) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "does not match authorized target"): + read_job_analysis_snapshot( + principal=read_principal(), + tenant_record_id=TENANT, + analysis_record_id=ANALYSIS, + purpose_code="job_analysis_read", + policy=read_policy(), + read_port=port, + ) + wrong_id = clinical_psychologist_snapshot() + object.__setattr__(wrong_id, "analysis_record_id", JOB) + port = RecordingReadPort(wrong_id) + with self.assertRaisesRegex(JobAnalysisIntegrityError, "does not match authorized target"): + read_job_analysis_snapshot( + principal=read_principal(), + tenant_record_id=TENANT, + analysis_record_id=ANALYSIS, + purpose_code="job_analysis_read", + policy=read_policy(), + read_port=port, + ) + + def test_invalid_read_identity_fails_before_repository_access(self) -> None: + port = RecordingReadPort(clinical_psychologist_snapshot()) + with self.assertRaises(ValueError): + read_job_analysis_snapshot( + principal=read_principal(), + tenant_record_id=UUID(int=0), + analysis_record_id=ANALYSIS, + purpose_code="job_analysis_read", + policy=read_policy(), + read_port=port, + ) + self.assertEqual(port.calls, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_snapshot_cardinality.py b/services/job-analysis-api/tests/test_snapshot_cardinality.py new file mode 100644 index 000000000..ceb0e0399 --- /dev/null +++ b/services/job-analysis-api/tests/test_snapshot_cardinality.py @@ -0,0 +1,33 @@ +"""Regression contracts for bounded job-analysis snapshot cardinality.""" + +import pytest + +from fixtures import TENANT, clinical_psychologist_document +from orgmetra_job_analysis_api.snapshot import snapshot_from_document + + +def test_rejects_oversized_task_collection_before_duplicate_linkage_work() -> None: + """Bound task parsing explicitly instead of relying only on the HTTP byte ceiling.""" + posted = clinical_psychologist_document() + posted["tasks"] = posted["tasks"] * 501 + + with pytest.raises(ValueError, match="tasks must contain at most 500 items"): + snapshot_from_document(posted, tenant_record_id=TENANT) + + +def test_rejects_oversized_ksao_collection_before_duplicate_linkage_work() -> None: + """Bound KSAO parsing explicitly for predictable validation cost.""" + posted = clinical_psychologist_document() + posted["ksao_requirements"] = posted["ksao_requirements"] * 501 + + with pytest.raises(ValueError, match="ksao_requirements must contain at most 500 items"): + snapshot_from_document(posted, tenant_record_id=TENANT) + + +def test_rejects_oversized_task_ksao_link_collection_before_link_validation() -> None: + """Bound relation parsing so dense matrices cannot create unbounded validation work.""" + posted = clinical_psychologist_document() + posted["task_ksao_links"] = posted["task_ksao_links"] * 834 + + with pytest.raises(ValueError, match="task_ksao_links must contain at most 5000 items"): + snapshot_from_document(posted, tenant_record_id=TENANT) diff --git a/services/job-analysis-api/tests/test_snapshot_field_contract.py b/services/job-analysis-api/tests/test_snapshot_field_contract.py new file mode 100644 index 000000000..efabdd7d6 --- /dev/null +++ b/services/job-analysis-api/tests/test_snapshot_field_contract.py @@ -0,0 +1,43 @@ +"""Fail-closed field-shape regressions for posted job-analysis evidence.""" + +from __future__ import annotations + +from copy import deepcopy +import unittest + +from orgmetra_job_analysis_api.snapshot import snapshot_from_document +from fixtures import TENANT, clinical_psychologist_document + + +class JobAnalysisSnapshotFieldContractTests(unittest.TestCase): + """Keep runtime parsing aligned with OpenAPI ``additionalProperties: false``.""" + + def test_unknown_top_level_field_is_rejected_instead_of_silently_dropped(self) -> None: + """Do not collapse materially different requests to the same command digest.""" + document = clinical_psychologist_document() + document["candidate_name"] = "must-not-enter-job-evidence" + + with self.assertRaisesRegex(ValueError, "snapshot document contains unsupported fields"): + snapshot_from_document(document, tenant_record_id=TENANT) + + def test_unknown_nested_fields_are_rejected_at_every_evidence_boundary(self) -> None: + """Do not admit covert values beside Task, KSAO, link, FJA, or source contracts.""" + mutations = ( + ("task", lambda document: document["tasks"][0].__setitem__("model_output", "draft")), + ("task source", lambda document: document["tasks"][0]["source"].__setitem__("raw_excerpt", "value")), + ("KSAO", lambda document: document["ksao_requirements"][0].__setitem__("candidate_score", 99)), + ("task-KSAO link", lambda document: document["task_ksao_links"][0].__setitem__("comment", "hidden")), + ("FJA", lambda document: document["fja_profile"].__setitem__("free_text", "hidden")), + ("FJA source", lambda document: document["fja_profile"]["source"].__setitem__("credential", "secret")), + ) + original = clinical_psychologist_document() + for boundary, mutate in mutations: + with self.subTest(boundary=boundary): + document = deepcopy(original) + mutate(document) + with self.assertRaisesRegex(ValueError, "contains unsupported fields"): + snapshot_from_document(document, tenant_record_id=TENANT) + + +if __name__ == "__main__": + unittest.main() diff --git a/services/job-analysis-api/tests/test_workflow_contract.py b/services/job-analysis-api/tests/test_workflow_contract.py new file mode 100644 index 000000000..441ca21f9 --- /dev/null +++ b/services/job-analysis-api/tests/test_workflow_contract.py @@ -0,0 +1,26 @@ +"""Regression contracts for job-analysis quality-gate dispatch boundaries.""" + +from pathlib import Path + + +def _workflow(path: str) -> str: + """Read one reviewed repository-local workflow as UTF-8 text.""" + return Path(path).read_text(encoding="utf-8") + + +def test_job_analysis_api_quality_runs_on_current_default_branch_pull_requests() -> None: + """Keep service coverage evidence alive when Orgmetra's default branch changes.""" + assert " - develop\n" in _workflow(".github/workflows/job-analysis-api-quality.yml") + + +def test_job_analysis_quality_reruns_when_foundation_contract_changes() -> None: + """Revalidate the service when its asserted foundation workflow changes.""" + workflow = _workflow(".github/workflows/job-analysis-api-quality.yml") + assert ' - ".github/workflows/foundation-ci.yml"\n' in workflow + + +def test_foundation_ci_includes_job_analysis_postgres_contract() -> None: + """Keep the snapshot persistence contract in the PostgreSQL integrity matrix.""" + workflow = _workflow(".github/workflows/foundation-ci.yml") + assert "test_job_analysis_snapshot_postgres.sh" in workflow + assert " - develop\n" in workflow diff --git a/tests/foundation-contract.test.mjs b/tests/foundation-contract.test.mjs index fd8b90c76..72b18466f 100644 --- a/tests/foundation-contract.test.mjs +++ b/tests/foundation-contract.test.mjs @@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path'; import test from 'node:test'; import { DATABASE_OBJECT_NAMES, + MIGRATION_BACKED_DATABASE_OBJECT_NAMES, MATURITY_VALUES, REQUIRED_FILES, collectMarkdownFiles, @@ -40,6 +41,20 @@ function pythonRequiredFiles() { return [...match[1].matchAll(/^\s+"([^"]+)",$/gm)].map((item) => item[1]); } +function writeMigrationBackedTables(root) { + write( + root, + 'database/migrations/0013_job_analysis_snapshot.sql', + [ + 'CREATE TABLE job_analysis_snapshot (tenant_record_id uuid NOT NULL);', + 'CREATE TABLE job_analysis_task_item (tenant_record_id uuid NOT NULL);', + 'CREATE TABLE job_analysis_ksao_item (tenant_record_id uuid NOT NULL);', + 'CREATE TABLE job_analysis_task_ksao_link (tenant_record_id uuid NOT NULL);', + 'CREATE TABLE job_analysis_write_command (tenant_record_id uuid NOT NULL);' + ].join('\n') + '\n' + ); +} + function makeMinimalValidFoundation(root) { for (const filePath of REQUIRED_FILES) write(root, filePath); write( @@ -47,6 +62,7 @@ function makeMinimalValidFoundation(root) { 'database/migrations/0012_people_mutation_idempotency.sql', 'CREATE TABLE people_mutation_idempotency_record (tenant_record_id uuid NOT NULL);\n' ); + writeMigrationBackedTables(root); write( root, 'schemas/openapi.yaml', @@ -123,6 +139,7 @@ test('migration-backed database object validation detects table rename', () => { 'database/migrations/0012_people_mutation_idempotency.sql', 'CREATE TABLE people_mutation_idempotency_record (tenant_record_id uuid NOT NULL);\n' ); + writeMigrationBackedTables(root); assert.deepEqual(validateMigrationBackedDatabaseObjectNames(root), []); write( root, @@ -155,6 +172,7 @@ test('migration-backed validation ignores fake CREATE TABLE text in comments and 'SELECT $payload$CREATE TABLE people_mutation_idempotency_record (tenant_record_id uuid);$payload$;' ].join('\n') ); + writeMigrationBackedTables(root); assert.deepEqual(validateMigrationBackedDatabaseObjectNames(root), [ 'Migration-backed database object is missing from migrations: people_mutation_idempotency_record' ]); @@ -284,7 +302,7 @@ test('foundation validator reports every missing artifact', () => { const root = temporaryDirectory(); try { const errors = validateFoundation(root); - assert.equal(errors.length, REQUIRED_FILES.length + 1); + assert.equal(errors.length, REQUIRED_FILES.length + MIGRATION_BACKED_DATABASE_OBJECT_NAMES.length); assert.match(errors[0], /Missing required foundation artifact/); assert.ok(errors.some((error) => /Migration-backed database object is missing/.test(error))); } finally { diff --git a/tests/test_job_analysis_snapshot_postgres.sh b/tests/test_job_analysis_snapshot_postgres.sh new file mode 100755 index 000000000..f45508d1e --- /dev/null +++ b/tests/test_job_analysis_snapshot_postgres.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" + +for migration in \ + database/migrations/0001_foundation_schema.sql \ + database/migrations/0002_sealed_evidence_digest.sql \ + database/migrations/0003_audit_outbox_persistence.sql \ + database/migrations/0004_outbox_delivery_claim.sql \ + database/migrations/0005_outbox_delivery_finalization.sql \ + database/migrations/0006_outbox_delivery_dead_letter.sql \ + database/migrations/0007_outbox_retry_exhaustion.sql \ + database/migrations/0008_audit_outbox_review_hardening.sql \ + database/migrations/0009_candidate_worker_conversion_governance.sql \ + database/migrations/0010_validity_study_case_integrity.sql \ + database/migrations/0011_criterion_observation_scope.sql \ + database/migrations/0012_people_mutation_idempotency.sql \ + database/migrations/0013_job_analysis_snapshot.sql; do + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" +done + +TENANT_ID="10000000-0000-7000-8000-000000000001" +OTHER_TENANT_ID="20000000-0000-7000-8000-000000000002" +JOB_ID="00000000-0000-7000-8000-000000000021" +ANALYSIS_ID="00000000-0000-7000-8000-000000000081" +TASK_ID="00000000-0000-7000-8000-000000000082" +KSAO_ID="00000000-0000-7000-8000-000000000083" +POSITION_ID="00000000-0000-7000-8000-000000000084" +ORG_ID="00000000-0000-7000-8000-000000000085" +CRITERION_ID="00000000-0000-7000-8000-000000000086" +COMMAND_ID="00000000-0000-7000-8000-000000000087" +AUDIT_ID="00000000-0000-4000-8000-000000000088" +OUTBOX_ID="00000000-0000-4000-8000-000000000089" + + +tenant_psql() { + PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" command psql "$@" +} + +tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&1)" +missing_job_status=$? +set -e +if [[ ${missing_job_status} -eq 0 ]]; then + echo "job-analysis snapshot accepted a missing job_profile" >&2 + exit 1 +fi +if [[ "${missing_job_output}" != *"foreign key"* && "${missing_job_output}" != *"job_analysis_snapshot_job_tenant_fk"* ]]; then + echo "missing job failed for an unexpected reason: ${missing_job_output}" >&2 + exit 1 +fi + +set +e +missing_position_output="$({ tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO job_analysis_snapshot ( + tenant_record_id, analysis_record_id, job_profile_id, position_record_id, + analysis_version_code, status_code, effective_from, recorded_at, + content_digest_sha256, data_function_code, people_function_code, + things_function_code, fja_source_uri, fja_source_title, + fja_source_version_code, fja_retrieved_at, fja_content_digest_sha256, + fja_origin_code +) VALUES ( + '${TENANT_ID}', '${ANALYSIS_ID}', '${JOB_ID}', '${POSITION_ID}', + 'clinical-psychologist:v1', 'analysis_validated', DATE '2026-08-01', + TIMESTAMPTZ '2026-08-18 05:00:00+00', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 1, 0, 7, + 'https://www.dol.gov/agencies/oalj/PUBLIC/DOT/REFERENCES/DOTAPPB', + 'Dictionary of Occupational Titles Appendix B', 'dot:1991', + TIMESTAMPTZ '2026-08-18 03:00:00+00', + 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + 'authoritative_occupation_source' +); +" ; } 2>&1)" +missing_position_status=$? +set -e +if [[ ${missing_position_status} -eq 0 ]]; then + echo "job-analysis snapshot accepted a missing position_record" >&2 + exit 1 +fi +if [[ "${missing_position_output}" != *"foreign key"* \ + && "${missing_position_output}" != *"job_analysis_snapshot_position_tenant_fk"* ]]; then + echo "missing position failed for an unexpected reason: ${missing_position_output}" >&2 + exit 1 +fi + +tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + +ksao_statement="$(tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT requirement_statement FROM job_analysis_ksao_item +WHERE analysis_record_id = '${ANALYSIS_ID}'::uuid; +")" +if [[ "${ksao_statement}" != *"DSM-5-TR"* ]]; then + echo "persisted KSAO statement did not round-trip: ${ksao_statement}" >&2 + exit 1 +fi + +idempotency_key="$(tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT idempotency_key FROM job_analysis_write_command +WHERE analysis_record_id = '${ANALYSIS_ID}'::uuid; +")" +if [[ "${idempotency_key}" != "idempotency-clinical-psych-01" ]]; then + echo "Idempotency-Key did not persist on the write command" >&2 + exit 1 +fi + +audit_count="$(tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) FROM audit_event_record +WHERE audit_event_record_id = '${AUDIT_ID}'::uuid; +")" +if [[ "${audit_count}" != "1" ]]; then + echo "record_audit_outbox_event did not persist with the snapshot write" >&2 + exit 1 +fi + +set +e +update_output="$({ tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c \ + "UPDATE job_analysis_snapshot SET status_code = 'analysis_draft' WHERE analysis_record_id = '${ANALYSIS_ID}'::uuid;" ; } 2>&1)" +update_status=$? +set -e +if [[ ${update_status} -eq 0 || "${update_output}" != *"append-only"* ]]; then + echo "job-analysis snapshot was mutable: ${update_output}" >&2 + exit 1 +fi + +set +e +delete_output="$({ tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c \ + "DELETE FROM job_analysis_snapshot WHERE analysis_record_id = '${ANALYSIS_ID}'::uuid;" ; } 2>&1)" +delete_status=$? +set -e +if [[ ${delete_status} -eq 0 || "${delete_output}" != *"append-only"* ]]; then + echo "job-analysis snapshot was deletable: ${delete_output}" >&2 + exit 1 +fi + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +DO $role$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'orgmetra_job_analysis_reader') THEN + CREATE ROLE orgmetra_job_analysis_reader NOLOGIN NOSUPERUSER NOBYPASSRLS; + END IF; +END +$role$; +GRANT USAGE ON SCHEMA public TO orgmetra_job_analysis_reader; +GRANT SELECT ON job_analysis_snapshot TO orgmetra_job_analysis_reader; +SQL + +other_tenant_rows="$( + PGOPTIONS="-c orgmetra.tenant_record_id=${OTHER_TENANT_ID}" \ + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc \ + "SET ROLE orgmetra_job_analysis_reader; SELECT count(*) FROM job_analysis_snapshot;" +)" +if [[ "${other_tenant_rows}" != "0" ]]; then + echo "row-level security leaked job-analysis rows across tenants: ${other_tenant_rows}" >&2 + exit 1 +fi + +echo "job-analysis snapshot PostgreSQL contract passed" diff --git a/tests/test_job_analysis_snapshot_schema_hardening.sh b/tests/test_job_analysis_snapshot_schema_hardening.sh new file mode 100644 index 000000000..88f85288f --- /dev/null +++ b/tests/test_job_analysis_snapshot_schema_hardening.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" + +TENANT_ID="10000000-0000-7000-8000-000000000001" +OTHER_TENANT_ID="20000000-0000-7000-8000-000000000002" +ANALYSIS_ID="00000000-0000-7000-8000-000000000081" + +tenant_psql() { + PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" command psql "$@" +} + +set +e +delete_output="$({ tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c \ + "DELETE FROM job_analysis_snapshot WHERE analysis_record_id = '${ANALYSIS_ID}'::uuid;" ; } 2>&1)" +delete_status=$? +set -e +if [[ ${delete_status} -eq 0 || "${delete_output}" != *"append-only"* ]]; then + echo "job-analysis snapshot DELETE was not rejected by the append-only guard: ${delete_output}" >&2 + exit 1 +fi + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +CREATE ROLE orgmetra_rls_probe NOLOGIN; +GRANT SELECT ON job_analysis_snapshot TO orgmetra_rls_probe; +SQL + +other_tenant_rows="$(PGOPTIONS="-c orgmetra.tenant_record_id=${OTHER_TENANT_ID}" \ + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc \ + "SET ROLE orgmetra_rls_probe; SELECT count(*) FROM job_analysis_snapshot;")" +if [[ "${other_tenant_rows}" != "0" ]]; then + echo "row-level security leaked ${other_tenant_rows} job-analysis snapshot row(s) across tenants" >&2 + exit 1 +fi + +supporting_index_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) +FROM pg_indexes +WHERE schemaname = current_schema() + AND indexname IN ( + 'job_analysis_snapshot_position_idx', + 'job_analysis_snapshot_criterion_idx', + 'job_analysis_write_command_analysis_idx' + ); +")" +if [[ "${supporting_index_count}" != "3" ]]; then + echo "job-analysis parent/write-command supporting indexes are incomplete: ${supporting_index_count}/3" >&2 + exit 1 +fi + +redundant_unique_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) +FROM pg_constraint +WHERE conname IN ( + 'job_analysis_task_item_identity_unique', + 'job_analysis_ksao_item_identity_unique' +); +")" +if [[ "${redundant_unique_count}" != "0" ]]; then + echo "redundant task/KSAO UNIQUE constraints remain: ${redundant_unique_count}" >&2 + exit 1 +fi + +echo "job-analysis PostgreSQL schema hardening contract passed" diff --git a/tests/validate_repository.py b/tests/validate_repository.py index d29ad86d9..b1b85a2b3 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -31,6 +31,7 @@ "apps/hr-workspace/app.js", "apps/hr-workspace/workspace.stories.js", ".github/workflows/foundation-ci.yml", + ".github/workflows/job-analysis-api-quality.yml", "docs/PRD.md", "docs/TRD.md", "docs/USER_STORIES.md", @@ -54,8 +55,13 @@ "docs/adr/0004-employment-position-version-and-assignment-binding.md", "docs/adr/0005-exclusive-employment-and-staffable-seats.md", "docs/adr/0006-governed-audit-outbox-envelope.md", + "docs/adr/0007-governed-job-analysis-evidence.md", "docs/adr/0008-purpose-bound-pii-authorization.md", "docs/adr/0009-performance-criterion-observation-scope.md", + "docs/adr/0010-naruon-calendar-intent-boundary.md", + "docs/adr/0011-bitemporal-workforce-composition.md", + "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", @@ -73,6 +79,7 @@ "database/migrations/0010_validity_study_case_integrity.sql", "database/migrations/0011_criterion_observation_scope.sql", "database/migrations/0012_people_mutation_idempotency.sql", + "database/migrations/0013_job_analysis_snapshot.sql", "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", "packages/hris-kernel/tests/test_audit_outbox.py", "schemas/openapi.yaml", @@ -94,6 +101,7 @@ "tests/test_validity_study_case_postgres.sh", "tests/test_criterion_observation_scope_postgres.sh", "tests/test_people_mutation_idempotency_postgres.sh", + "tests/test_job_analysis_snapshot_postgres.sh", "tests/validate_repository.py", ] @@ -209,6 +217,12 @@ def _validate_database_contract() -> None: migration_paths = sorted((ROOT / "database/migrations").glob("*.sql")) if not migration_paths: _fail("No database migrations found") + migration_prefixes = [path.name.split("_", 1)[0] for path in migration_paths] + duplicate_prefixes = sorted( + prefix for prefix in set(migration_prefixes) if migration_prefixes.count(prefix) > 1 + ) + if duplicate_prefixes: + _fail(f"Duplicate migration number prefixes: {duplicate_prefixes}") sql = "\n".join(path.read_text(encoding="utf-8") for path in migration_paths) table_sql = sql @@ -351,6 +365,15 @@ def _validate_database_contract() -> None: "REVOKE TRUNCATE ON people_mutation_idempotency_record FROM PUBLIC", "ALTER TABLE people_mutation_idempotency_record FORCE ROW LEVEL SECURITY", "CREATE POLICY people_mutation_idempotency_scope_policy", + "CREATE TABLE job_analysis_snapshot", + "CREATE TABLE job_analysis_task_item", + "CREATE TABLE job_analysis_ksao_item", + "CREATE TABLE job_analysis_task_ksao_link", + "CREATE TABLE job_analysis_write_command", + "CONSTRAINT job_analysis_write_command_idempotency_unique", + "CREATE TRIGGER job_analysis_snapshot_append_only_guard", + "ALTER TABLE job_analysis_snapshot FORCE ROW LEVEL SECURITY", + "CREATE POLICY job_analysis_snapshot_scope_policy", ] for fragment in required_fragments: if fragment not in sql: @@ -486,6 +509,42 @@ def _validate_openapi_contract() -> None: _require_in_block(block, operation_id, response, f"response {response.strip()}") _require_in_block(block, operation_id, " Location:", "201 Location header") + job_analysis_block = _yaml_block( + openapi, " /tenants/{tenant_record_id}/job-analysis-snapshots:" + ) + if not job_analysis_block: + _fail("persistJobAnalysisSnapshot: path block is missing") + for fragment, description in ( + ("operationId: persistJobAnalysisSnapshot", "operationId"), + (" - orgmetra.job_architecture.write", "least-privilege write scope"), + ("$ref: '#/components/parameters/IdempotencyKey'", "Idempotency-Key"), + ("$ref: '#/components/parameters/PurposeCode'", "purpose parameter"), + ("$ref: '#/components/schemas/PersistJobAnalysisSnapshotCommand'", "request schema"), + (" '201':", "201 response"), + (" Location:", "201 Location header"), + (" '415':", "unsupported-media response"), + ): + _require_in_block(job_analysis_block, "persistJobAnalysisSnapshot", fragment, description) + + job_analysis_read_block = _yaml_block( + openapi, + " /tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}:", + ) + if not job_analysis_read_block: + _fail("readJobAnalysisSnapshot: path block is missing") + _require_in_block( + job_analysis_read_block, + "readJobAnalysisSnapshot", + "operationId: readJobAnalysisSnapshot", + "operationId", + ) + _require_in_block( + job_analysis_read_block, + "readJobAnalysisSnapshot", + " - orgmetra.job_architecture.read", + "least-privilege read scope", + ) + for schema_name in ( "CreateJobProfileCommand", "RecordSelectionDecisionCommand", From 1880d09222d7a4f158a784fb0f2a26fbfc0f74cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:47:12 +0900 Subject: [PATCH 033/110] docs: refresh product gap evidence --- docs/product-technical-gap-baseline.md | 15 ++++++++------- manifest.json | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8a4103a74..33ac7f13e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `9fccfd22717ee7f037cae34bce2f3086e7c61be0`; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `1d66f40778cc9900fd21df51a28b9109c600b754` 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. @@ -99,7 +99,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | | 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | | 54 | `feat/workforce-composition-change` / `db4ea77221e0318dc2ab1be26543e338683d7678` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | -| 53 | `codex/product-gap-baseline-workspace` / `9fccfd22717ee7f037cae34bce2f3086e7c61be0` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / `1d66f40778cc9900fd21df51a28b9109c600b754` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | | 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | | 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | | 48 | `feat/governed-compensation-change-review` / `f6cefeb64d214e020ed82840b64d7d0cca70ec6e` | Compensation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | @@ -109,7 +109,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 44 | `feat/governed-performance-review` / `b867f4cf1efdec198f34199e7e8c382338e94505` | Performance review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify criterion scope and human confirmation before protected merge. | | 43 | `feat/governed-offer-approval` / `8af2e3bf31bb9352033bded6530b9357405782af` | Offer approval packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify evidence-backed offer approval and audit/outbox behavior before protected merge. | | 42 | `feat/selection-outcome-monitoring-plan` / `a306d8199be475ba07682e31c1f9b4d9c701a50b` | Selection outcome monitoring | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify monitoring scope and temporal cohort semantics before protected merge. | -| 40 | `feat/structured-interview-plan` / `6e6cc4f6b9c1356773cb3d89a52bcbf4bdfcadba` | Governed structured interview plan | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify human review, evidence versioning, and current-head checks before protected merge. | +| 40 | `feat/structured-interview-plan` / `1f30abbd076a2844d9c0fa6cf053555baadfc591` | Governed structured interview plan | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify human review, evidence versioning, and current-head checks before protected merge. | No open GitHub Issue was returned by the current `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. @@ -148,9 +148,10 @@ The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. | Check | Result | |---|---| | Protected `develop` contents | `git ls-tree` at `33eff439df5c2ef58805c975108d156fd73799df` contains migration `0013`, Job Analysis API, ADR `0014`, candidate-evidence package, ADR `0025`, and their quality workflows. This proves repository presence, not deployment. | -| `npm run validate` on active PR #53 | Passed: foundation validation and 60 Node tests on exact local head `eedcf1c34bd6dc6e94fc0c70802c6f7d43090034`. | -| `packages/candidate-evidence` on active PR #53 | 75 passed; owned statement and branch coverage 100%. | -| Storybook and browser fixture | Storybook `10.5.10` production build passed; Playwright browser smoke passed for four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors. This is local fixture evidence, not protected deployment or People API integration. | -| PostgreSQL and full Python package matrix | Protected-branch contract evidence remains recorded in the merged package/workflow artifacts; a complete fresh matrix was not rerun in this documentation-only update and must be required by the corresponding hosted workflows. | +| `npm run validate` on active PR #53 | Passed: foundation validation and 60 Node tests on exact local head `1d66f40778cc9900fd21df51a28b9109c600b754`. | +| Python package matrix on active PR #53 | `packages/hris-kernel`: 171 passed; Job Analysis API: 69 passed plus 72 subtests; People API: 146 passed plus 235 subtests; candidate-evidence: 75 passed. All owned statement and branch coverage reports were 100%. | +| Storybook and browser fixture | Storybook `10.5.10` production build passed on exact local head `1d66f40778cc9900fd21df51a28b9109c600b754`. Prior local browser smoke covered four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors; this remains local fixture evidence, not current protected deployment, People API integration, or released browser E2E. | +| 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 | Current hosted workflows remain the authoritative exact-head PostgreSQL evidence; no fresh local database contract run is claimed in this snapshot. | These results prove the current foundation contracts. They do not prove that open PRs are merged, that a browser UI exists, that a statistical estimator exists, or that Orgmetra is certified under CSAP/SOC 2. diff --git a/manifest.json b/manifest.json index dd3290511..74dd086c3 100644 --- a/manifest.json +++ b/manifest.json @@ -371,9 +371,9 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "05057513e8f58661a16969a348cd5224cf6d5b6b1bb11f3515bb2e5eee9a5f57", - "bytes": 22005, - "lines": 156 + "sha256": "af9c2c22eddacd6a1838ca0be5597512c34b68245c51f1d09bfef04d8df5015c", + "bytes": 22443, + "lines": 157 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", From 48ce063d057f1ca2793f62c3e40cfcda9a84df52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:55:39 +0900 Subject: [PATCH 034/110] docs: record workforce review repair --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 33ac7f13e..214ee70a0 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -98,7 +98,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 57 | `feat/validation-analysis-handoff` / `f0c30c5e6cfd6cb90afcbef1efd0ba8825f3fd52` | Governed selection-validity analysis handoff | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflow runs queued/pending | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | | 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | | 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | -| 54 | `feat/workforce-composition-change` / `db4ea77221e0318dc2ab1be26543e338683d7678` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Re-fetch exact head, terminal checks, resolved threads, and qualifying independent approval before merge. | +| 54 | `feat/workforce-composition-change` / `4184e927f808f184dc52b2ffc6ba9c380337f915` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; current workflows queued; outdated review threads resolved; no qualifying approval | Re-fetch exact head, terminal checks, and qualifying independent approval before protected merge. | | 53 | `codex/product-gap-baseline-workspace` / `1d66f40778cc9900fd21df51a28b9109c600b754` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | | 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | | 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | diff --git a/manifest.json b/manifest.json index 74dd086c3..8a7d81ab9 100644 --- a/manifest.json +++ b/manifest.json @@ -371,8 +371,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "af9c2c22eddacd6a1838ca0be5597512c34b68245c51f1d09bfef04d8df5015c", - "bytes": 22443, + "sha256": "807b3c18430f9e225474d0aa89915b4d3e7b44720b25325ed2fcfdf8062a197f", + "bytes": 22474, "lines": 157 }, { From 9bae254d87d2e521b405debe1adee93128b5d7ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:59:26 +0900 Subject: [PATCH 035/110] docs: record local job analysis database evidence --- docs/product-technical-gap-baseline.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 214ee70a0..f351b38ea 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -152,6 +152,6 @@ The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. | Python package matrix on active PR #53 | `packages/hris-kernel`: 171 passed; Job Analysis API: 69 passed plus 72 subtests; People API: 146 passed plus 235 subtests; candidate-evidence: 75 passed. All owned statement and branch coverage reports were 100%. | | Storybook and browser fixture | Storybook `10.5.10` production build passed on exact local head `1d66f40778cc9900fd21df51a28b9109c600b754`. Prior local browser smoke covered four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors; this remains local fixture evidence, not current protected deployment, People API integration, or released browser E2E. | | 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 | Current hosted workflows remain the authoritative exact-head PostgreSQL evidence; no fresh local database contract run is claimed in this snapshot. | +| PostgreSQL contracts | `tests/test_job_analysis_snapshot_postgres.sh` passed on exact local head `48ce063d057f1ca2793f62c3e40cfcda9a84df52` against disposable PostgreSQL `16.14`; the full hosted PostgreSQL matrix remains the authoritative release gate. | These results prove the current foundation contracts. They do not prove that open PRs are merged, that a browser UI exists, that a statistical estimator exists, or that Orgmetra is certified under CSAP/SOC 2. diff --git a/manifest.json b/manifest.json index 8a7d81ab9..b1bd7bb52 100644 --- a/manifest.json +++ b/manifest.json @@ -371,8 +371,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "807b3c18430f9e225474d0aa89915b4d3e7b44720b25325ed2fcfdf8062a197f", - "bytes": 22474, + "sha256": "3452d0b524c78e3caf94944efbdd74993cab7a19fcde4b60826d1ff2ee6eb439", + "bytes": 22556, "lines": 157 }, { From 06c8841a0a809934aa3af756042eca46a40398d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:23:45 +0900 Subject: [PATCH 036/110] docs: refresh current product gap baseline --- docs/product-technical-gap-baseline.md | 44 +++++++++++++------------- manifest.json | 4 +-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index f351b38ea..1e311b49d 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `33eff439df5c2ef58805c975108d156fd73799df`; current workspace PR #53 at `1d66f40778cc9900fd21df51a28b9109c600b754` immediately before this documentation snapshot commit; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df`; current workspace PR #53 at `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f` 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. @@ -46,6 +46,7 @@ flowchart LR | 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. | @@ -91,25 +92,24 @@ independently before P0-1 can change maturity. ## Current open PR inventory and integration order -The following is the current GitHub inventory checked on 2026-08-21. All listed PRs target `develop@33eff439df5c2ef58805c975108d156fd73799df`. `REVIEW_REQUIRED` means the GitHub listing reported that review gate; it is not approval evidence. No self-approval or protection bypass is permitted. +The following is the current GitHub inventory checked on 2026-08-21. 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` / `f0c30c5e6cfd6cb90afcbef1efd0ba8825f3fd52` | Governed selection-validity analysis handoff | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflow runs queued/pending | Review the handoff boundary and keep estimator claims separate until exact-head checks and independent approval exist. | -| 56 | `feat/organization-hierarchy-snapshot` / `95e6ce7f5d4a07322129635b2adfd0f890b61d2b` | Bitemporal organization hierarchy snapshot evidence | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/time semantics and current-head checks before independent approval. | -| 55 | `fix/people-read-auth-backend-failure` / `a650d5b8040f3c8a0a11b516b99c814d89508d57` | People-read error normalization and pre-auth resource budgets | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued | Reproduce any review finding on this exact head, then wait for terminal People API/security/recovery checks and independent approval. | -| 54 | `feat/workforce-composition-change` / `4184e927f808f184dc52b2ffc6ba9c380337f915` | Same-cutoff workforce composition change evidence | Ready; mergeable; base `33eff43`; current workflows queued; outdated review threads resolved; no qualifying approval | Re-fetch exact head, terminal checks, and qualifying independent approval before protected merge. | -| 53 | `codex/product-gap-baseline-workspace` / `1d66f40778cc9900fd21df51a28b9109c600b754` | HR Home + Employee Profile fixture, Storybook, and baseline repair | Draft; mergeable; base `33eff43`; `REVIEW_REQUIRED`; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | -| 52 | `feat/tepp-analysis-adapter` / `fcbd800513c3605b78347daa283ec58291f0bc28` | Governed TEPP analysis request boundary | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Keep non-executing and evidence-bound; merge only after exact-head checks and qualifying review. | -| 51 | `docs/protected-truth-refresh` / `fa3b6e9c9cd449f577cf0b493aad561fb7376327` | Protected product-truth documentation repair | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Reconcile documentation with protected runtime truth, then use current-head checks and independent review. | -| 48 | `feat/governed-compensation-change-review` / `f6cefeb64d214e020ed82840b64d7d0cca70ec6e` | Compensation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify high-impact confirmation/evidence boundaries, then merge only with terminal checks and independent approval. | -| 47 | `feat/governed-employment-leave-review` / `088abb13c1f1b2aa69c6e68b1814ce13fb08e4d9` | Employment leave review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify temporal and authorization contracts before protected merge. | -| 46 | `feat/governed-employment-separation-review` / `add78f169a01d72ad2e027a3fae9f83ce795721f` | Employment separation review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify irreversible-action confirmation and audit evidence before protected merge. | -| 45 | `feat/governed-assignment-change-review` / `18ac3a733f775b67dbe0260b5b59699bc0d10301` | Assignment change review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify tenant/person/employment/position binding before protected merge. | -| 44 | `feat/governed-performance-review` / `b867f4cf1efdec198f34199e7e8c382338e94505` | Performance review packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify criterion scope and human confirmation before protected merge. | -| 43 | `feat/governed-offer-approval` / `8af2e3bf31bb9352033bded6530b9357405782af` | Offer approval packet | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify evidence-backed offer approval and audit/outbox behavior before protected merge. | -| 42 | `feat/selection-outcome-monitoring-plan` / `a306d8199be475ba07682e31c1f9b4d9c701a50b` | Selection outcome monitoring | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify monitoring scope and temporal cohort semantics before protected merge. | -| 40 | `feat/structured-interview-plan` / `1f30abbd076a2844d9c0fa6cf053555baadfc591` | Governed structured interview plan | Ready; mergeable; base `33eff43`; `REVIEW_REQUIRED` | Verify human review, evidence versioning, and current-head checks before protected merge. | +| 57 | `feat/validation-analysis-handoff` / `c38151852d95ff1256012d31c179a898bbc1c11e` | 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` / `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f` | HR Home + Employee Profile fixture, Storybook, offer-approval integration, and baseline repair | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued/pending | Keep the fixture/API boundary 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` / `047613278d7e6829651e4b32edbc663f2cf6e894` | 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 current `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. @@ -147,11 +147,11 @@ The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. | Check | Result | |---|---| -| Protected `develop` contents | `git ls-tree` at `33eff439df5c2ef58805c975108d156fd73799df` contains migration `0013`, Job Analysis API, ADR `0014`, candidate-evidence package, ADR `0025`, and their quality workflows. This proves repository presence, not deployment. | -| `npm run validate` on active PR #53 | Passed: foundation validation and 60 Node tests on exact local head `1d66f40778cc9900fd21df51a28b9109c600b754`. | -| Python package matrix on active PR #53 | `packages/hris-kernel`: 171 passed; Job Analysis API: 69 passed plus 72 subtests; People API: 146 passed plus 235 subtests; candidate-evidence: 75 passed. All owned statement and branch coverage reports were 100%. | -| Storybook and browser fixture | Storybook `10.5.10` production build passed on exact local head `1d66f40778cc9900fd21df51a28b9109c600b754`. Prior local browser smoke covered four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors; this remains local fixture evidence, not current protected deployment, People API integration, or released browser E2E. | +| 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 60 Node tests on exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f`. | +| Python package matrix on active PR #53 | Exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f`: HRIS kernel 171 passed; Job Analysis API 69 passed; People API 146 passed; candidate-evidence 75 passed; offer-approval 84 passed. All owned statement and branch coverage reports were 100%. | +| Storybook and browser fixture | Storybook `10.5.10` production build passed on prior exact workspace head `1d66f40778cc9900fd21df51a28b9109c600b754`; prior local browser smoke covered four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors. This remains local fixture evidence, not current protected deployment, People API integration, or released browser E2E; no current `c2d3a6c` Storybook rerun is claimed. | | 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 `48ce063d057f1ca2793f62c3e40cfcda9a84df52` against disposable PostgreSQL `16.14`; the full hosted PostgreSQL matrix remains the authoritative release gate. | +| 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 results prove the current foundation contracts. They do not prove that open PRs are merged, that a browser UI exists, that a statistical estimator exists, or that Orgmetra is certified under CSAP/SOC 2. diff --git a/manifest.json b/manifest.json index b1bd7bb52..2f9901547 100644 --- a/manifest.json +++ b/manifest.json @@ -371,8 +371,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "3452d0b524c78e3caf94944efbdd74993cab7a19fcde4b60826d1ff2ee6eb439", - "bytes": 22556, + "sha256": "8c556be73cd31fe42e22dd9301bd1acdf2e6fbd998afff671b75d312912824a9", + "bytes": 23413, "lines": 157 }, { From 0f51dc338501d10e5cd95824dbb3dbcc9b85600e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:31:14 +0900 Subject: [PATCH 037/110] feat(workspace): connect governed job analysis read --- apps/hr-workspace/app.js | 139 +++++++++++++++++++++++++++++++++++ apps/hr-workspace/index.html | 31 ++++++++ apps/hr-workspace/styles.css | 1 + docs/STORYBOOK.md | 9 ++- manifest.json | 30 ++++---- tests/hr-workspace.test.mjs | 33 ++++++++- 6 files changed, 224 insertions(+), 19 deletions(-) diff --git a/apps/hr-workspace/app.js b/apps/hr-workspace/app.js index 17659825f..f5d3ef094 100644 --- a/apps/hr-workspace/app.js +++ b/apps/hr-workspace/app.js @@ -7,6 +7,59 @@ const translations = { } }; +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', + jobAnalysisDigest: 'Content digest', + jobAnalysisTasks: 'Tasks', + jobAnalysisKsaos: 'KSAO requirements', + jobAnalysisNoValues: 'No snapshot 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: '기록 시각', + jobAnalysisDigest: '콘텐츠 다이제스트', + jobAnalysisTasks: '과업', + jobAnalysisKsaos: 'KSAO 요구사항', + jobAnalysisNoValues: '불러온 스냅샷 값이 없습니다.', +}); + export function isPurposeAuthorized(purpose) { return purpose === 'hr_operations'; } @@ -15,6 +68,40 @@ export function nextLocale(locale) { return locale === 'en' ? 'ko' : 'en'; } +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)}`; +} + +export async function fetchJobAnalysisSnapshot(config, fetchImpl = globalThis.fetch) { + 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.'); + } + 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(); +} + function setLocale(locale) { const dictionary = translations[locale]; document.documentElement.lang = locale; @@ -63,6 +150,31 @@ function closeDialog(id) { else dialog.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 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-digest').textContent = snapshot?.content_digest_sha256 || 'unknown'; + document.getElementById('job-analysis-task-count').textContent = String(tasks.length); + document.getElementById('job-analysis-ksao-count').textContent = String(ksaos.length); + result.hidden = false; +} + if (typeof document !== 'undefined') { let locale = 'en'; document.querySelectorAll('[data-view-link]').forEach((link) => link.addEventListener('click', () => activateView(link.dataset.viewLink))); @@ -89,5 +201,32 @@ if (typeof document !== 'undefined') { 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'); + } + jobAnalysisForm.addEventListener('submit', async (event) => { + event.preventDefault(); + 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); + renderJobAnalysisSnapshot(snapshot); + setJobAnalysisStatus('jobAnalysisLoaded', 'loaded'); + } catch (error) { + setJobAnalysisStatus(error.message === 'JOB_ANALYSIS_ACCESS_DENIED' ? 'jobAnalysisDenied' : 'jobAnalysisFailed', 'error'); + } + }); setLocale(locale); } diff --git a/apps/hr-workspace/index.html b/apps/hr-workspace/index.html index 03fb4e238..d048847a8 100644 --- a/apps/hr-workspace/index.html +++ b/apps/hr-workspace/index.html @@ -17,6 +17,7 @@ @@ -90,6 +91,36 @@

Credential boundary · No identity credential or passkey is stored in the person record.

+ + diff --git a/apps/hr-workspace/styles.css b/apps/hr-workspace/styles.css index cf1a608c1..08350c270 100644 --- a/apps/hr-workspace/styles.css +++ b/apps/hr-workspace/styles.css @@ -74,6 +74,7 @@ h2 { margin-bottom: 0; font-size: 20px; line-height: 1.2; letter-spacing: -0.015 .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); } .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; } diff --git a/docs/STORYBOOK.md b/docs/STORYBOOK.md index ba1d5e999..2ea48e1f0 100644 --- a/docs/STORYBOOK.md +++ b/docs/STORYBOOK.md @@ -55,8 +55,10 @@ Each interactive component requires at least these stories where applicable: Employee Profile and HR Home experience. It consumes the shared CSS tokens, keeps the Figma node IDs in the markup, and exercises evidence review, purpose-bound permission denial, high-impact confirmation, exact allocation -values, and English/Korean labels. It is fixture mode: it does not claim a -connected API or a deployed customer workflow. +values, and English/Korean labels. The Job Analysis view is an API-bound read +surface: a host may inject an API base URL and short-lived authorization +provider through `globalThis.__ORGMETRA_JOB_ANALYSIS__`; the fixture has no such +provider and therefore makes no connected-data claim or local-data fallback. ## Local Storybook runtime @@ -71,4 +73,5 @@ assignment values. Shared design tokens and workspace CSS are imported by 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; it does not replace connected People API or browser E2E -evidence. +evidence. The Job Analysis read surface also requires a real protected API +runtime before it can be called connected or released. diff --git a/manifest.json b/manifest.json index 2f9901547..8fa8f83fa 100644 --- a/manifest.json +++ b/manifest.json @@ -77,21 +77,21 @@ }, { "path": "apps/hr-workspace/app.js", - "sha256": "d2c069ebeaccbd344b10af52960f7ae23dbffb2a3c9d7650c6c26bee12022c38", - "bytes": 10978, - "lines": 93 + "sha256": "72f86f3a1e092ba3279607532fa0878807c140918fb6cb79d73536b20df65d13", + "bytes": 18374, + "lines": 232 }, { "path": "apps/hr-workspace/index.html", - "sha256": "fc0e31865d114834d214a86fc1b16e7d4f424ceb8f4919a2fc6700bb19bcfb8c", - "bytes": 13504, - "lines": 113 + "sha256": "43067618b6cb0ba60599c49b74bb269ac814f2d2d504939382ae002959fbbbfc", + "bytes": 17110, + "lines": 144 }, { "path": "apps/hr-workspace/styles.css", - "sha256": "fb44828e958e164e515da587256966f805ea115b172e4b584c4fef8e6b26322a", - "bytes": 10013, - "lines": 117 + "sha256": "09349ad43a12d015d7f6eee30fa45d794c4b66e813efbd741dd23bf35de5360b", + "bytes": 10110, + "lines": 118 }, { "path": "apps/hr-workspace/workspace.stories.js", @@ -221,9 +221,9 @@ }, { "path": "docs/STORYBOOK.md", - "sha256": "ddfbddea1a4cb82936ee72cd489195e271cc01556516f00d1ab369d8e372f388", - "bytes": 2511, - "lines": 74 + "sha256": "86a6fc47f41709ad014aa3562b00a3f3ccc55daa6eee0ab14906abfd88123f45", + "bytes": 2818, + "lines": 77 }, { "path": "docs/TEST_STRATEGY.md", @@ -443,9 +443,9 @@ }, { "path": "tests/hr-workspace.test.mjs", - "sha256": "03d395e3f62ab9178e7d9f9979926b186ca0f224c71e4368b23beffb9aecd940", - "bytes": 3206, - "lines": 66 + "sha256": "52fc7ccc7cd8fec27e138c024a89eaf58ce781207010c705fa1f48af990d82ee", + "bytes": 4499, + "lines": 97 }, { "path": "tests/openapi-contract.test.mjs", diff --git a/tests/hr-workspace.test.mjs b/tests/hr-workspace.test.mjs index 28bbaca49..d65d696cc 100644 --- a/tests/hr-workspace.test.mjs +++ b/tests/hr-workspace.test.mjs @@ -1,7 +1,12 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; -import { isPurposeAuthorized, nextLocale } from '../apps/hr-workspace/app.js'; +import { + fetchJobAnalysisSnapshot, + isPurposeAuthorized, + jobAnalysisSnapshotUrl, + nextLocale, +} 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'); @@ -16,6 +21,7 @@ test('workspace exposes the Figma role slice and existing design tokens', () => 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(css, /var\(--orgmetra-action-review\)/); assert.match(css, /var\(--orgmetra-focus-ring\)/); }); @@ -64,3 +70,28 @@ test('purpose and locale transitions preserve the trust boundary', () => { 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); +}); From 88f809b030bb4ea2568810f1fd68bdc27d74c230 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:33:50 +0900 Subject: [PATCH 038/110] docs: record connected job analysis boundary --- docs/product-technical-gap-baseline.md | 13 ++++++++----- manifest.json | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1e311b49d..a048c92da 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -19,7 +19,7 @@ This document is the buyer-facing work queue. It separates what a customer can u ## 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 and local Storybook state runtime, but protected truth has no connected or released workspace. +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. @@ -54,7 +54,7 @@ flowchart LR | 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 and Storybook runtime | Active PR #53 has the HR Home/Employee Profile fixture plus a local Storybook `10.5.10` build; protected `develop` still has no customer UI | `implemented_on_active_pr` | The local component/state runtime is reviewable, but there is no connected or released buyer workflow in protected truth. | +| Role workspaces and Storybook runtime | Active PR #53 has the HR Home/Employee Profile fixture, an API-bound read-only Job Analysis view, and a local Storybook `10.5.10` build; protected `develop` still has no customer UI | `implemented_on_active_pr` | The component/state runtime and host-injected API boundary 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. | @@ -66,12 +66,15 @@ flowchart LR ## Local candidate artifact outside protected truth 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`. It uses +and Employee Profile fixture based on Figma nodes `1:10` and `1:28`, plus an +API-bound read-only Job Analysis snapshot view. 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 same active PR includes a local Storybook runtime with tokenized stories; +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; the current head has a passed Storybook build and local Playwright browser smoke, but neither is connected People API integration, hosted deployment, or protected-develop truth. The artifact must be reviewed, checked, and merged @@ -82,7 +85,7 @@ independently before P0-1 can change maturity. | 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. Connect it to the buyer workflow, SME approval path, deployment, and browser evidence. | Protected People API evidence; active workspace path | +| 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 | diff --git a/manifest.json b/manifest.json index 8fa8f83fa..faa66bb85 100644 --- a/manifest.json +++ b/manifest.json @@ -371,9 +371,9 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "8c556be73cd31fe42e22dd9301bd1acdf2e6fbd998afff671b75d312912824a9", - "bytes": 23413, - "lines": 157 + "sha256": "601bc13819a75abdfe599eba7de6eced1f10be48adf895df828b32da79b04be2", + "bytes": 23830, + "lines": 160 }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", From eb036d24b602dd0dd89635bb582cc90515050dc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:37:55 +0900 Subject: [PATCH 039/110] docs: pin final current workspace snapshot --- docs/product-technical-gap-baseline.md | 8 ++++---- manifest.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a048c92da..0f0e9fe76 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df`; current workspace PR #53 at `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f` immediately before this documentation snapshot commit; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df`; current workspace PR #53 at `88f809b030bb4ea2568810f1fd68bdc27d74c230` 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. @@ -103,7 +103,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 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` / `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f` | HR Home + Employee Profile fixture, Storybook, offer-approval integration, and baseline repair | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / `88f809b030bb4ea2568810f1fd68bdc27d74c230` | HR Home + Employee Profile fixture, API-bound Job Analysis read view, Storybook, offer-approval integration, and baseline repair | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued/pending | Keep the fixture/API boundary 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. | @@ -112,7 +112,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 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` / `047613278d7e6829651e4b32edbc663f2cf6e894` | 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. | +| 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 current `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. @@ -151,7 +151,7 @@ The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. | 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 60 Node tests on exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f`. | +| `npm run validate` on active PR #53 | Passed: foundation validation and 61 Node tests on exact local head `88f809b030bb4ea2568810f1fd68bdc27d74c230`. | | Python package matrix on active PR #53 | Exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f`: HRIS kernel 171 passed; Job Analysis API 69 passed; People API 146 passed; candidate-evidence 75 passed; offer-approval 84 passed. All owned statement and branch coverage reports were 100%. | | Storybook and browser fixture | Storybook `10.5.10` production build passed on prior exact workspace head `1d66f40778cc9900fd21df51a28b9109c600b754`; prior local browser smoke covered four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors. This remains local fixture evidence, not current protected deployment, People API integration, or released browser E2E; no current `c2d3a6c` Storybook rerun is claimed. | | 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. | diff --git a/manifest.json b/manifest.json index faa66bb85..ef8688dd8 100644 --- a/manifest.json +++ b/manifest.json @@ -371,8 +371,8 @@ }, { "path": "docs/product-technical-gap-baseline.md", - "sha256": "601bc13819a75abdfe599eba7de6eced1f10be48adf895df828b32da79b04be2", - "bytes": 23830, + "sha256": "67636c43190e1d99e55edb2fedf5248c68dea5cd4591ec1cfdc8833f0b4e18be", + "bytes": 23864, "lines": 160 }, { From 8f21f69fdc06908cd23edafdd99cee41cf939589 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:43:12 +0900 Subject: [PATCH 040/110] docs: align product gap snapshot with current head --- docs/product-technical-gap-baseline.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0f0e9fe76..ff0807809 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,7 +1,7 @@ # Orgmetra product and technical gap baseline **Snapshot:** 2026-08-21, Asia/Seoul -**Evidence base:** protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df`; current workspace PR #53 at `88f809b030bb4ea2568810f1fd68bdc27d74c230` immediately before this documentation snapshot commit; current GitHub PR metadata and exact-head workflow state observed on 2026-08-21. +**Evidence base:** protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df`; current workspace PR #53 at `eb036d24b602dd0dd89635bb582cc90515050dc8` 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. @@ -103,7 +103,7 @@ The following is the current GitHub inventory checked on 2026-08-21. All listed | 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` / `88f809b030bb4ea2568810f1fd68bdc27d74c230` | HR Home + Employee Profile fixture, API-bound Job Analysis read view, Storybook, offer-approval integration, and baseline repair | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued/pending | Keep the fixture/API boundary explicit, wait for all current-head checks, obtain independent approval, then decide whether to merge the product-surface slice. | +| 53 | `codex/product-gap-baseline-workspace` / `eb036d24b602dd0dd89635bb582cc90515050dc8` | HR Home + Employee Profile fixture, API-bound Job Analysis read view, Storybook, offer-approval integration, and baseline repair | Draft; merge state `BLOCKED`; `REVIEW_REQUIRED`; no submitted review; current workflows queued/pending | Keep the fixture/API boundary 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. | @@ -151,7 +151,7 @@ The complete APA 7 bibliography is in `docs/doctoring/REFERENCES.md`. | 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 61 Node tests on exact local head `88f809b030bb4ea2568810f1fd68bdc27d74c230`. | +| `npm run validate` on active PR #53 | Passed: foundation validation and 61 Node tests on exact local head `eb036d24b602dd0dd89635bb582cc90515050dc8`. | | Python package matrix on active PR #53 | Exact local head `c2d3a6c2804ab5a3e17e454312eddc2f96f2a72f`: HRIS kernel 171 passed; Job Analysis API 69 passed; People API 146 passed; candidate-evidence 75 passed; offer-approval 84 passed. All owned statement and branch coverage reports were 100%. | | Storybook and browser fixture | Storybook `10.5.10` production build passed on prior exact workspace head `1d66f40778cc9900fd21df51a28b9109c600b754`; prior local browser smoke covered four action states, exact allocation values, locale switching, localized accessible names, and zero console/page errors. This remains local fixture evidence, not current protected deployment, People API integration, or released browser E2E; no current `c2d3a6c` Storybook rerun is claimed. | | 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. | From d66c7e8ba58193060d8ec42d367bdfe54cf02561 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:56:27 +0900 Subject: [PATCH 041/110] feat(workspace): connect protected People read boundary --- apps/hr-workspace/app.js | 128 +++++++++++++++++++++++++++++++++++ apps/hr-workspace/index.html | 14 ++++ docs/STORYBOOK.md | 13 ++-- manifest.json | 26 +++---- tests/hr-workspace.test.mjs | 29 ++++++++ 5 files changed, 191 insertions(+), 19 deletions(-) diff --git a/apps/hr-workspace/app.js b/apps/hr-workspace/app.js index f5d3ef094..9f8a6d07e 100644 --- a/apps/hr-workspace/app.js +++ b/apps/hr-workspace/app.js @@ -32,6 +32,24 @@ Object.assign(translations.en, { 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: '직무 분석', @@ -58,6 +76,24 @@ Object.assign(translations.ko, { 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 값이 없습니다.', }); export function isPurposeAuthorized(purpose) { @@ -102,6 +138,49 @@ export async function fetchJobAnalysisSnapshot(config, fetchImpl = globalThis.fe return response.json(); } +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}`; +} + +export async function fetchPeopleRecord(config, fetchImpl = globalThis.fetch) { + 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.'); + } + 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; @@ -175,6 +254,25 @@ function renderJobAnalysisSnapshot(snapshot) { 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 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'; document.querySelectorAll('[data-view-link]').forEach((link) => link.addEventListener('click', () => activateView(link.dataset.viewLink))); @@ -228,5 +326,35 @@ if (typeof document !== 'undefined') { 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'); + } + peopleApiForm.addEventListener('submit', async (event) => { + event.preventDefault(); + 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 { + renderPeopleRecord(await fetchPeopleRecord(config)); + setPeopleApiStatus('peopleApiLoaded', 'loaded'); + } catch (error) { + setPeopleApiStatus(error.message === 'PEOPLE_ACCESS_DENIED' ? 'peopleApiDenied' : 'peopleApiFailed', 'error'); + } + }); setLocale(locale); } diff --git a/apps/hr-workspace/index.html b/apps/hr-workspace/index.html index d048847a8..4ac106d10 100644 --- a/apps/hr-workspace/index.html +++ b/apps/hr-workspace/index.html @@ -90,6 +90,20 @@
Exact assignment allocation values
AssignmentAllocationNext action
Platform60.00%No action required
Governance40.00%No action required

Credential boundary · No identity credential or passkey is stored in the person record.

+
+

API-bound read

Protected People record

API-bound read
+

Read one authorized worker record from the protected People API.

+
+ + + + + +
+
+
+ +