From d1938ef63569babe1d8cdd3119d0df049e0ed635 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:10:14 -0700 Subject: [PATCH 01/59] test(workforce): add RED bitemporal composition-change contract --- .../test_workforce_composition_change.py | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 packages/hris-kernel/tests/test_workforce_composition_change.py diff --git a/packages/hris-kernel/tests/test_workforce_composition_change.py b/packages/hris-kernel/tests/test_workforce_composition_change.py new file mode 100644 index 000000000..bc7f27079 --- /dev/null +++ b/packages/hris-kernel/tests/test_workforce_composition_change.py @@ -0,0 +1,171 @@ +"""Bitemporal workforce-composition change regressions.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_hris_kernel import ( + AssignmentFact, + DateInterval, + EmploymentVersion, + IdentityScopeError, + IntervalError, + RecordedInterval, + WorkforceCompositionChangeSnapshot, + build_workforce_composition_change_snapshot, + build_workforce_composition_snapshot, +) + + +def _id(value: int) -> UUID: + return UUID(int=value) + + +def _employment( + record_id: int, + version_id: int, + person_id: int, + *, + effective_start: date, + status: str = "active", +) -> EmploymentVersion: + return EmploymentVersion( + tenant_record_id=_id(1), + employment_record_id=_id(record_id), + employment_record_version_id=_id(version_id), + person_record_id=_id(person_id), + employment_status_code=status, + effective=DateInterval(effective_start), + recorded=RecordedInterval(datetime(2026, 1, 1, tzinfo=timezone.utc)), + ) + + +def _assignment( + assignment_id: int, + employment_id: int, + person_id: int, + *, + effective_start: date, + ratio: str = "1.0000", +) -> AssignmentFact: + return AssignmentFact( + tenant_record_id=_id(1), + assignment_record_id=_id(assignment_id), + employment_record_id=_id(employment_id), + person_record_id=_id(person_id), + position_record_id=_id(assignment_id + 1000), + allocation_ratio=Decimal(ratio), + effective=DateInterval(effective_start), + recorded=RecordedInterval(datetime(2026, 1, 1, tzinfo=timezone.utc)), + ) + + +def _source_facts() -> tuple[list[EmploymentVersion], list[AssignmentFact]]: + employments = [ + _employment(101, 1001, 11, effective_start=date(2026, 1, 1)), + _employment(102, 1002, 12, effective_start=date(2026, 1, 1), status="leave"), + _employment(103, 1003, 13, effective_start=date(2026, 2, 1)), + ] + assignments = [ + _assignment(201, 101, 11, effective_start=date(2026, 1, 1)), + _assignment(202, 102, 12, effective_start=date(2026, 1, 1), ratio="0.5000"), + _assignment(203, 103, 13, effective_start=date(2026, 2, 1)), + ] + return employments, assignments + + +def test_change_snapshot_compares_two_effective_dates_at_one_knowledge_cutoff() -> None: + employments, assignments = _source_facts() + snapshot = build_workforce_composition_change_snapshot( + employments, + assignments, + tenant_record_id=_id(1), + from_effective_on=date(2026, 1, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + + assert snapshot.opening_snapshot.person_headcount == 2 + assert snapshot.closing_snapshot.person_headcount == 3 + assert snapshot.person_headcount_change == 1 + assert snapshot.employment_count_change == 1 + assert snapshot.staffed_assignment_count_change == 1 + assert snapshot.staffed_fte_change == Decimal("1.0000") + assert snapshot.unassigned_person_count_change == 0 + assert snapshot.employment_status_changes == (("active", 1), ("leave", 0)) + assert '"schema_version":"orgmetra.workforce_composition_change.v1"' in snapshot.canonical_json() + assert "person_record_id" not in snapshot.canonical_json() + assert len(snapshot.content_digest()) == 64 + + +def test_change_snapshot_is_deterministic_for_reordered_source_facts() -> None: + employments, assignments = _source_facts() + first = build_workforce_composition_change_snapshot( + employments, + assignments, + tenant_record_id=_id(1), + from_effective_on=date(2026, 1, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + second = build_workforce_composition_change_snapshot( + list(reversed(employments)), + list(reversed(assignments)), + tenant_record_id=_id(1), + from_effective_on=date(2026, 1, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + + assert first.canonical_json() == second.canonical_json() + assert first.content_digest() == second.content_digest() + + +def test_change_snapshot_rejects_non_forward_effective_window() -> None: + with pytest.raises(IntervalError, match="later") as error: + build_workforce_composition_change_snapshot( + [], + [], + tenant_record_id=_id(1), + from_effective_on=date(2026, 2, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + assert "Choose a later comparison date" in error.value.next_action + + +def test_direct_change_snapshot_rejects_cross_tenant_comparison() -> None: + known_at = datetime(2026, 2, 20, tzinfo=timezone.utc) + opening = build_workforce_composition_snapshot( + [], [], tenant_record_id=_id(1), effective_on=date(2026, 1, 15), known_at=known_at + ) + closing = build_workforce_composition_snapshot( + [], [], tenant_record_id=_id(2), effective_on=date(2026, 2, 15), known_at=known_at + ) + + with pytest.raises(IdentityScopeError, match="same tenant"): + WorkforceCompositionChangeSnapshot(opening, closing) + + +def test_direct_change_snapshot_rejects_different_knowledge_cutoffs() -> None: + opening = build_workforce_composition_snapshot( + [], + [], + tenant_record_id=_id(1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 2, 19, tzinfo=timezone.utc), + ) + closing = build_workforce_composition_snapshot( + [], + [], + tenant_record_id=_id(1), + effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + + with pytest.raises(IntervalError, match="knowledge cutoff"): + WorkforceCompositionChangeSnapshot(opening, closing) From 9df889451a1d4d1895403cf8f78689d5d48f47fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:10:42 -0700 Subject: [PATCH 02/59] feat(workforce): add same-cutoff composition change evidence --- .../orgmetra_hris_kernel/workforce_change.py | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py new file mode 100644 index 000000000..50282dea4 --- /dev/null +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -0,0 +1,161 @@ +"""Deterministic bitemporal change evidence for aggregate workforce composition.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date +from decimal import Decimal +import hashlib +import json +from uuid import UUID + +from orgmetra_hris_kernel.errors import IdentityScopeError, IntervalError +from orgmetra_hris_kernel.facts import AssignmentFact, EmploymentVersion +from orgmetra_hris_kernel.workforce import ( + WorkforceCompositionSnapshot, + build_workforce_composition_snapshot, +) + + +@dataclass(frozen=True, slots=True) +class WorkforceCompositionChangeSnapshot: + """Compare two aggregate workforce states at one recorded-time cutoff. + + The two snapshots must belong to the same tenant and share one exact + ``known_at`` coordinate. This prevents a buyer-facing trend from silently + mixing effective-time change with later-recorded corrections. The result is + descriptive evidence only: it reports net composition change and does not + infer hires, separations, causes, protected attributes, or recommendations. + """ + + opening_snapshot: WorkforceCompositionSnapshot + closing_snapshot: WorkforceCompositionSnapshot + + def __post_init__(self) -> None: + """Fail closed when the two aggregate coordinates are not comparable.""" + if self.opening_snapshot.tenant_record_id != self.closing_snapshot.tenant_record_id: + raise IdentityScopeError( + "Workforce change snapshots must belong to the same tenant.", + next_action="Rebuild both snapshots inside one tenant boundary, then compare them again.", + ) + if self.opening_snapshot.effective_on >= self.closing_snapshot.effective_on: + raise IntervalError( + "The closing workforce effective date must be later than the opening date.", + next_action="Choose a later comparison date, then rebuild the workforce change snapshot.", + ) + if self.opening_snapshot.known_at != self.closing_snapshot.known_at: + raise IntervalError( + "Workforce change snapshots must share one exact knowledge cutoff.", + next_action=( + "Rebuild both effective-date snapshots with the same recorded-time cutoff so corrections " + "cannot masquerade as workforce movement." + ), + ) + + @property + def tenant_record_id(self) -> UUID: + """Return the authoritative tenant shared by both aggregate snapshots.""" + return self.opening_snapshot.tenant_record_id + + @property + def person_headcount_change(self) -> int: + """Return closing distinct-person headcount minus opening headcount.""" + return self.closing_snapshot.person_headcount - self.opening_snapshot.person_headcount + + @property + def employment_count_change(self) -> int: + """Return closing reportable-employment count minus opening count.""" + return self.closing_snapshot.employment_count - self.opening_snapshot.employment_count + + @property + def staffed_assignment_count_change(self) -> int: + """Return closing staffed-assignment count minus opening count.""" + return self.closing_snapshot.staffed_assignment_count - self.opening_snapshot.staffed_assignment_count + + @property + def staffed_fte_change(self) -> Decimal: + """Return exact closing staffed FTE minus opening staffed FTE.""" + return self.closing_snapshot.staffed_fte - self.opening_snapshot.staffed_fte + + @property + def unassigned_person_count_change(self) -> int: + """Return closing unassigned-person count minus opening count.""" + return self.closing_snapshot.unassigned_person_count - self.opening_snapshot.unassigned_person_count + + @property + def employment_status_changes(self) -> tuple[tuple[str, int], ...]: + """Return deterministic per-status count deltas across the two snapshots.""" + opening = dict(self.opening_snapshot.employment_status_counts) + closing = dict(self.closing_snapshot.employment_status_counts) + status_codes = sorted(set(opening) | set(closing)) + return tuple((status, closing.get(status, 0) - opening.get(status, 0)) for status in status_codes) + + def canonical_json(self) -> str: + """Return deterministic aggregate-only comparison evidence for audit correlation.""" + payload = { + "closing_snapshot": json.loads(self.closing_snapshot.canonical_json()), + "closing_snapshot_digest": self.closing_snapshot.content_digest(), + "employment_count_change": self.employment_count_change, + "employment_status_changes": [ + {"employment_count_change": change, "employment_status_code": status} + for status, change in self.employment_status_changes + ], + "opening_snapshot": json.loads(self.opening_snapshot.canonical_json()), + "opening_snapshot_digest": self.opening_snapshot.content_digest(), + "person_headcount_change": self.person_headcount_change, + "schema_version": "orgmetra.workforce_composition_change.v1", + "staffed_assignment_count_change": self.staffed_assignment_count_change, + "staffed_fte_change": format(self.staffed_fte_change, "f"), + "tenant_record_id": str(self.tenant_record_id), + "unassigned_person_count_change": self.unassigned_person_count_change, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def content_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 comparison evidence.""" + return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_workforce_composition_change_snapshot( + employment_versions: list[EmploymentVersion], + assignments: list[AssignmentFact], + *, + tenant_record_id: UUID, + from_effective_on: date, + to_effective_on: date, + known_at, +) -> WorkforceCompositionChangeSnapshot: + """Build one same-cutoff aggregate workforce comparison from HRIS truth. + + Both endpoint snapshots reuse the existing workforce-composition integrity + checks. Consequently contradictory employment history, invalid assignment + coverage, over-allocation, duplicate visible assignment identities, and + ambiguous recorded-time facts fail closed before any change metric is + emitted. + + Args: + employment_versions: Bitemporal employment facts, including other tenants. + assignments: Bitemporal assignment facts, including other tenants. + tenant_record_id: Tenant namespace whose composition is compared. + from_effective_on: Earlier business date to reconstruct. + to_effective_on: Later business date to reconstruct. + known_at: One timezone-aware recorded-time cutoff shared by both endpoints. + + Returns: + Aggregate-only deterministic change evidence. + """ + opening = build_workforce_composition_snapshot( + employment_versions, + assignments, + tenant_record_id=tenant_record_id, + effective_on=from_effective_on, + known_at=known_at, + ) + closing = build_workforce_composition_snapshot( + employment_versions, + assignments, + tenant_record_id=tenant_record_id, + effective_on=to_effective_on, + known_at=known_at, + ) + return WorkforceCompositionChangeSnapshot(opening, closing) From 687f1cba646d1059d68894828a5982fe047ef6bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:10:59 -0700 Subject: [PATCH 03/59] feat(workforce): export composition change contract --- packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py b/packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py index 5d4b720fa..9a41eda74 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py @@ -50,6 +50,10 @@ WorkforceCompositionSnapshot, build_workforce_composition_snapshot, ) +from orgmetra_hris_kernel.workforce_change import ( + WorkforceCompositionChangeSnapshot, + build_workforce_composition_change_snapshot, +) __all__ = [ "AssignmentFact", @@ -76,7 +80,9 @@ "SingleValuedFactError", "TaskEvidence", "TaskKSAOLink", + "WorkforceCompositionChangeSnapshot", "WorkforceCompositionSnapshot", + "build_workforce_composition_change_snapshot", "build_workforce_composition_snapshot", "close_recorded_interval", "resolve_bitemporal_facts", From b4c6725865d7b429d239ccc33a87cd13e65ca691 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:11:38 -0700 Subject: [PATCH 04/59] refactor(workforce): type same-cutoff comparison coordinate --- .../hris-kernel/src/orgmetra_hris_kernel/workforce_change.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index 50282dea4..63f611a67 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import date +from datetime import date, datetime from decimal import Decimal import hashlib import json @@ -123,7 +123,7 @@ def build_workforce_composition_change_snapshot( tenant_record_id: UUID, from_effective_on: date, to_effective_on: date, - known_at, + known_at: datetime, ) -> WorkforceCompositionChangeSnapshot: """Build one same-cutoff aggregate workforce comparison from HRIS truth. From 72cae076d11c3623941b8ab287fbff2d22a5a787 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:13:08 -0700 Subject: [PATCH 05/59] docs(adr): define same-cutoff workforce change boundary --- ...ame-cutoff-workforce-composition-change.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/adr/0024-same-cutoff-workforce-composition-change.md diff --git a/docs/adr/0024-same-cutoff-workforce-composition-change.md b/docs/adr/0024-same-cutoff-workforce-composition-change.md new file mode 100644 index 000000000..42b7d565c --- /dev/null +++ b/docs/adr/0024-same-cutoff-workforce-composition-change.md @@ -0,0 +1,37 @@ +# ADR-0024: Workforce composition change uses one recorded-time cutoff + +**Status:** Proposed on active PR +**Decision owner:** Orgmetra + +## Context + +Protected Orgmetra can reconstruct a `WorkforceCompositionSnapshot` for one tenant at an effective business date and an explicit recorded-time knowledge cutoff. Buyers also need to compare workforce composition across two effective dates. A naive comparison can be misleading if the opening snapshot is reconstructed with an earlier knowledge cutoff than the closing snapshot: later corrections then appear indistinguishable from actual business-time workforce movement. + +ISO 30414:2025 is the current published second edition of the human-capital reporting and disclosure standard. Its public catalogue lists workforce composition, mobility and succession planning, and workforce turnover among core reporting areas. This ADR uses that public scope only; it does not reproduce licensed metric definitions or claim ISO certification. + +## Decision + +Orgmetra adds a pure `WorkforceCompositionChangeSnapshot` and builder in the HRIS kernel. + +- Both endpoint snapshots must belong to the same authoritative tenant. +- The opening effective date must be strictly earlier than the closing effective date. +- Both endpoint snapshots must use one exact `known_at` recorded-time cutoff. Effective-time change is therefore compared while knowledge time is held constant. +- Each endpoint is built through the existing workforce-composition function, so contradictory bitemporal facts, invalid assignment coverage, impossible employment concurrency, over-allocation, and overfilled Position capacity continue to fail closed before aggregation. +- The comparison exposes net changes in distinct-person headcount, reportable employment count, staffed assignment count, staffed FTE, unassigned-person count, and deterministic per-status counts. +- The contract deliberately does **not** label a net change as a hire, separation, transfer, turnover event, cause, forecast, protected-attribute effect, or recommendation. Those claims require event-specific governed evidence that this aggregate comparison does not possess. +- Canonical JSON embeds only the two aggregate endpoint snapshots, their SHA-256 digests, aggregate deltas, the opaque tenant identifier, and schema version. It does not serialize row-level Person, Employment, Assignment, or Position identifiers. +- The result is descriptive workforce-intelligence evidence only and cannot authorize a high-impact employment action. + +## Consequences + +Buyers can compare two business dates without silently mixing later-recorded corrections into the change metric. The comparison is deterministic and audit-correlatable while remaining aggregate-only. Because it is intentionally not a turnover calculator, a later turnover/mobility slice must bind authoritative employment-transition evidence and its denominator/period policy explicitly rather than deriving causal labels from endpoint subtraction. + +The slice adds no persistence table, dashboard, export endpoint, forecasting model, diversity inference, or automated decision authority. Authorization and presentation remain at their owning boundaries. + +## Verification + +`packages/hris-kernel/tests/test_workforce_composition_change.py` covers realistic effective-date change, exact Decimal FTE deltas, deterministic source-order independence, aggregate-only canonical evidence, same-tenant enforcement, strictly forward effective dates, and one shared knowledge cutoff. `.github/workflows/workforce-intelligence-quality.yml` runs the complete HRIS kernel at exact 100% owned production statement and branch coverage. + +## References + +APA 7 references and current public standard metadata are recorded in `docs/doctoring/workforce-composition-change-references.md`. From 055a92605dbe1aa19e6594d21f427d4c48c6bc42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:13:17 -0700 Subject: [PATCH 06/59] docs(doctoring): record current workforce-change sources --- .../workforce-composition-change-references.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/doctoring/workforce-composition-change-references.md diff --git a/docs/doctoring/workforce-composition-change-references.md b/docs/doctoring/workforce-composition-change-references.md new file mode 100644 index 000000000..6bc2feab0 --- /dev/null +++ b/docs/doctoring/workforce-composition-change-references.md @@ -0,0 +1,17 @@ +# Workforce composition change references + +## Status + +Evidence for the active workforce-composition-change PR. This file does not make the active-PR code protected-`develop` truth before merge. + +## APA 7 references + +International Organization for Standardization. (2025). *ISO 30414:2025 Human resource management — Requirements and recommendations for human capital reporting and disclosure* (2nd ed.). ISO. https://www.iso.org/standard/30414 + +International Organization for Standardization. (2022). *ISO 30400:2022 Human resource management — Vocabulary* (2nd ed.). ISO. https://www.iso.org/standard/78044.html + +## Exact public evidence used + +The ISO catalogue was rechecked on August 20, 2026. ISO 30414:2025 remains the published second edition and lists workforce composition, mobility and succession planning, and workforce turnover among its core human-capital reporting areas; ISO 30414:2018 is withdrawn. ISO 30400:2022 remains the published second-edition HR-management vocabulary standard. + +Orgmetra uses only those public catalogue facts as design traceability. The implementation does not reproduce licensed ISO metric definitions, infer a turnover formula from the standard, or claim certification. The same-cutoff bitemporal comparison semantics are independently specified by ADR-0024 and executable Orgmetra tests. From 89eb7e997d9f5bd7d38b438154dee8bcea740128 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:13:29 -0700 Subject: [PATCH 07/59] docs(traceability): map workforce composition change evidence --- .../workforce-composition-change.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/traceability/workforce-composition-change.md diff --git a/docs/traceability/workforce-composition-change.md b/docs/traceability/workforce-composition-change.md new file mode 100644 index 000000000..202819c1d --- /dev/null +++ b/docs/traceability/workforce-composition-change.md @@ -0,0 +1,18 @@ +# Workforce composition change traceability + +| Requirement | Orgmetra evidence | Verification | Maturity | +|---|---|---|---| +| Compare business-time workforce states without knowledge-time drift | `WorkforceCompositionChangeSnapshot` requires identical endpoint `known_at` values | different-cutoff rejection regression | implemented_on_active_pr | +| Preserve tenant isolation | endpoint tenants must match; builder supplies one tenant to both existing snapshots | cross-tenant direct-construction rejection | implemented_on_active_pr | +| Require a real forward comparison | opening `effective_on` must be earlier than closing `effective_on` | equal-date rejection plus buyer-readable next action | implemented_on_active_pr | +| Reuse authoritative HRIS integrity | both endpoints call `build_workforce_composition_snapshot(...)` | existing complete HRIS-kernel workforce/integrity suite plus change regression | implemented_on_active_pr | +| Keep workforce intelligence descriptive | only net aggregate deltas are exposed; no hire/separation/turnover/cause/recommendation label exists | public API and canonical-schema review | implemented_on_active_pr | +| Preserve exact FTE arithmetic | staffed FTE change uses `Decimal` subtraction | realistic `1.0000` delta regression | implemented_on_active_pr | +| Avoid row-level shadow HR data | canonical comparison embeds aggregate endpoint JSON/digests only | canonical output excludes `person_record_id` and all endpoint row identities by construction | implemented_on_active_pr | +| Deterministic audit correlation | canonical JSON + SHA-256 content digest | reordered-source equality regression | implemented_on_active_pr | +| Current standards traceability | ADR-0024 + doctoring record for ISO 30414:2025 and ISO 30400:2022 public metadata | official ISO catalogue rechecked August 20, 2026 | implemented_on_active_pr | +| Exact owned production coverage | `Workforce Intelligence Quality` runs complete HRIS kernel | package pytest-cov requires 100% statement and branch coverage | implemented_on_active_pr | + +## Buyer interpretation + +A positive or negative endpoint delta says only that aggregate composition differs between two effective dates when reconstructed with the same system-knowledge cutoff. It is not evidence that a particular person was hired, separated, transferred, promoted, retained, or caused the change. Event-specific mobility and turnover claims require separately governed employment-transition evidence and an explicit denominator/period policy. From 7a8b63c67335792c00dbd04d9081fb53527c48b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:13:47 -0700 Subject: [PATCH 08/59] docs(hris-kernel): explain same-cutoff workforce change evidence --- packages/hris-kernel/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/hris-kernel/README.md b/packages/hris-kernel/README.md index 655f998df..1d843e656 100644 --- a/packages/hris-kernel/README.md +++ b/packages/hris-kernel/README.md @@ -12,11 +12,14 @@ Use this package to: 6. Build a CloudEvents 1.0-compatible audit/outbox envelope that carries tenant, actor, purpose, reason, evidence version, result, and accountable human-confirmation references without copying mutable HR payload fields into a shadow system of record. 7. Build a deterministic `JobAnalysisSnapshot` that links observable Tasks to explicit KSAO requirements, retains source/version/digest provenance, and optionally carries historical Functional Job Analysis Data/People/Things codes without confusing Job with Position or Assignment. 8. Build a deterministic `WorkforceCompositionSnapshot` for one tenant, effective day, and recorded-time cutoff with distinct-person headcount, reportable employment count, staffed assignment count/FTE, unassigned-person count, and status counts without serializing row-level person, employment, position, or assignment identifiers. +9. Compare two `WorkforceCompositionSnapshot` endpoints with `WorkforceCompositionChangeSnapshot` while holding one exact recorded-time cutoff constant, producing deterministic aggregate net changes without mislabeling endpoint subtraction as hires, separations, turnover, causes, or recommendations. Every historical reconstruction and portfolio/capacity decision requires an explicit `tenant_record_id`. A colliding durable identifier from another tenant is ignored rather than treated as local employment truth. `WorkforceCompositionSnapshot` is descriptive reporting evidence, not an employment recommendation or decision. It derives `active` and `leave` workforce composition from the same authoritative bitemporal facts and assignment-integrity rules used by the HRIS kernel. Concurrent employments count one person once for headcount while employment count and staffed FTE retain the actual portfolio shape. Contradictory visible versions, duplicate assignment identities, invalid coverage, and over-allocation fail closed instead of becoming plausible-looking metrics. Canonical snapshot JSON carries only the opaque tenant identifier, report coordinate, aggregate metrics, and schema version; `content_digest()` addresses those exact UTF-8 bytes with SHA-256. +`WorkforceCompositionChangeSnapshot` compares an earlier and later effective date only when both aggregate endpoints belong to the same tenant and were reconstructed at the same exact `known_at` coordinate. That same-cutoff rule prevents later-recorded corrections from masquerading as business-time movement. The comparison reports exact net changes in person headcount, employment count, staffed assignments, staffed `Decimal` FTE, unassigned people, and status counts; its canonical evidence includes only aggregate endpoint JSON/digests and aggregate deltas. It intentionally does not infer event-level hires, separations, transfers, turnover, protected-attribute effects, causes, forecasts, or employment recommendations. + `AuditOutboxEvent` fails closed on runtime type confusion, reserved nil UUID identities, ambiguous occurrence time, one-word/noncanonical source-service identifiers, malformed event types, free-text data placed in opaque-reference fields, noncanonical purpose/reason/result codes, whitespace-bearing evidence-version tokens, and missing confirmation for high-impact events. Source services use two-or-more-word `snake_case`; event types use the lower-case `orgmetra..` namespace; resource, actor, and confirmation identifiers use namespaced opaque references rather than human-readable payload text. `JobAnalysisSnapshot` is evidence about a Job, not an employment decision. Validated snapshots require accountable human review, complete Task-to-KSAO linkage, and non-LLM evidence. LLM-origin material may be retained only in `analysis_draft` snapshots. Current occupational evidence should use a current authoritative source such as O*NET with an explicit retrieved version and SHA-256 digest. FJA Data/People/Things worker-function codes are supported only as historical Dictionary of Occupational Titles compatibility descriptors; they do not replace current O*NET evidence. Importance, difficulty, proficiency, linkage strength, tenant scope, Job scope, and evidence provenance fail closed when malformed or incomplete. @@ -25,4 +28,4 @@ Every historical reconstruction and portfolio/capacity decision requires an expl `AuditOutboxEvent.canonical_json()` is the exact deterministic JSON text that the owning service persists. `AuditOutboxEvent.content_digest()` is SHA-256 over the UTF-8 bytes of that exact text. Callers must not independently serialize `to_cloudevent()` with library defaults and then assume the digest still addresses the stored representation. The Orgmetra PostgreSQL persistence boundary in migration `0003_audit_outbox_persistence.sql` reparses and allowlists that envelope, verifies tenant/event identity and high-impact confirmation, recomputes the digest over the supplied bytes, writes immutable `audit_event_record` evidence, and creates separate `outbox_delivery_record` transport state. The owning service calls `record_audit_outbox_event(...)` inside the same transaction as its business mutation. -This kernel itself does not talk to PostgreSQL, Keyverse, O*NET, or any other service. Persistence, authorization, source retrieval, and UI stay at their adapter boundaries. Production dispatcher claiming, retry scheduling, lease-expiry recovery, retention/export, external delivery receipts, job-analysis persistence, SME workflow, workforce-report authorization/presentation, and selection-validity computation remain separately proven integration/operability work. +This kernel itself does not talk to PostgreSQL, Keyverse, O*NET, or any other service. Persistence, authorization, source retrieval, and UI stay at their adapter boundaries. Production dispatcher claiming, retry scheduling, lease-expiry recovery, retention/export, external delivery receipts, job-analysis persistence, SME workflow, workforce-report authorization/presentation, event-specific mobility/turnover computation, and selection-validity computation remain separately proven integration/operability work. From 3649873034f649a3d0d7feaa7634e05e47200efd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 07:13:53 -0700 Subject: [PATCH 09/59] docs(hris-kernel): add package changelog --- packages/hris-kernel/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/hris-kernel/CHANGELOG.md diff --git a/packages/hris-kernel/CHANGELOG.md b/packages/hris-kernel/CHANGELOG.md new file mode 100644 index 000000000..d0bd9c20c --- /dev/null +++ b/packages/hris-kernel/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +- Add `WorkforceCompositionChangeSnapshot` for deterministic same-tenant comparison of two effective-date workforce-composition states at one exact recorded-time cutoff. +- Report aggregate net changes for distinct-person headcount, reportable employments, staffed assignments, staffed Decimal FTE, unassigned people, and status counts without serializing row-level HR identities. +- Fail closed on cross-tenant endpoints, non-forward effective dates, and different knowledge cutoffs so recorded corrections cannot masquerade as business-time workforce movement. +- Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. From 407593a7510a378281a46659d2016f66dddd9d94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 08:03:11 -0700 Subject: [PATCH 10/59] test(workforce): reject inconsistent aggregate evidence --- .../test_workforce_composition_boundaries.py | 69 +++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index 4661d5af6..3f493568b 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -39,17 +39,22 @@ def _direct_snapshot( *, known_at: datetime, employment_status_counts: tuple[tuple[str, int], ...] = (("active", 1),), + person_headcount: int = 1, + employment_count: int = 1, + staffed_assignment_count: int = 0, + staffed_fte: Decimal = Decimal("0.0000"), + unassigned_person_count: int = 1, ) -> WorkforceCompositionSnapshot: """Build a direct public snapshot fixture without using the aggregate builder.""" return WorkforceCompositionSnapshot( tenant_record_id=_id(1), effective_on=date(2026, 1, 15), known_at=known_at, - person_headcount=1, - employment_count=1, - staffed_assignment_count=0, - staffed_fte=Decimal("0.0000"), - unassigned_person_count=1, + person_headcount=person_headcount, + employment_count=employment_count, + staffed_assignment_count=staffed_assignment_count, + staffed_fte=staffed_fte, + unassigned_person_count=unassigned_person_count, employment_status_counts=employment_status_counts, ) @@ -84,6 +89,60 @@ def test_direct_snapshot_rejects_duplicate_status_codes() -> None: ) +def test_direct_snapshot_rejects_negative_aggregate_counts() -> None: + """Portable evidence cannot hash a negative workforce count.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + person_headcount=-1, + ) + + +def test_direct_snapshot_rejects_boolean_aggregate_counts() -> None: + """Boolean values must not masquerade as integer workforce counts.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + employment_count=True, + ) + + +def test_direct_snapshot_rejects_unassigned_count_above_headcount() -> None: + """Unassigned people cannot exceed the distinct people represented.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + unassigned_person_count=2, + ) + + +def test_direct_snapshot_rejects_nonfinite_staffed_fte() -> None: + """NaN or infinite FTE values cannot enter deterministic audit evidence.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + staffed_fte=Decimal("NaN"), + ) + + +def test_direct_snapshot_rejects_status_total_mismatch() -> None: + """Per-status employment counts must reconcile to total employment count.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + employment_status_counts=(("active", 2),), + ) + + +def test_direct_snapshot_rejects_nonreportable_status_code() -> None: + """Direct evidence cannot introduce a status outside the reportable workforce vocabulary.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + employment_status_counts=(("terminated", 1),), + ) + + def test_snapshot_excludes_future_business_and_late_recorded_facts() -> None: """Scheduled or not-yet-known facts cannot leak into an earlier workforce report.""" known_from_start = RecordedInterval(datetime(2026, 1, 1, tzinfo=timezone.utc)) From 4b5f5dd2d5732d7a6387ced973946aecc43b9425 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 08:05:06 -0700 Subject: [PATCH 11/59] fix(workforce): fail closed on inconsistent aggregate evidence --- .../src/orgmetra_hris_kernel/workforce.py | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index eddb09248..fd21001ae 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -50,7 +50,7 @@ class WorkforceCompositionSnapshot: employment_status_counts: tuple[tuple[str, int], ...] def __post_init__(self) -> None: - """Reject non-canonical direct evidence before it can be hashed or exported.""" + """Reject non-canonical or internally inconsistent evidence before export.""" if self.known_at.utcoffset() is None: raise IntervalError( "Workforce snapshot knowledge cutoff must be timezone-aware.", @@ -68,6 +68,47 @@ def __post_init__(self) -> None: next_action="Sort employment status counts by status code, then rebuild the snapshot.", ) + aggregate_counts = ( + self.person_headcount, + self.employment_count, + self.staffed_assignment_count, + self.unassigned_person_count, + ) + if not all(type(value) is int and value >= 0 for value in aggregate_counts): + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action=( + "Rebuild the snapshot from authoritative HRIS facts so every aggregate count is a " + "non-negative integer." + ), + ) + if not self.staffed_fte.is_finite() or self.staffed_fte < _ZERO_FTE: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative FTE.", + ) + if self.person_headcount > self.employment_count or self.unassigned_person_count > self.person_headcount: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action=( + "Rebuild the snapshot from authoritative HRIS facts so headcount, employment, and " + "unassigned-person totals reconcile." + ), + ) + if any(status not in _WORKFORCE_INCLUDED_STATUSES for status in status_codes): + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot using only reportable workforce employment statuses.", + ) + if sum(count for _status, count in self.employment_status_counts) != self.employment_count: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action=( + "Rebuild the snapshot from authoritative HRIS facts so per-status employment counts " + "reconcile to the total employment count." + ), + ) + def canonical_json(self) -> str: """Return deterministic aggregate evidence suitable for audit correlation.""" payload = { @@ -219,7 +260,7 @@ def build_workforce_composition_snapshot( Raises: IntervalError: ``known_at`` is timezone-naive or has no usable UTC offset. SingleValuedFactError: One employment or assignment has contradictory - visible versions. + visible versions, or direct aggregate evidence is inconsistent. EmploymentExclusivityError: A worker has malformed or overlapping exclusive employment at the report coordinate. EmploymentCoverageError: Existing assignment integrity rejects a worker link. From db4ea77221e0318dc2ab1be26543e338683d7678 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:19:30 +0900 Subject: [PATCH 12/59] docs: remove trailing whitespace from workforce ADR --- docs/adr/0024-same-cutoff-workforce-composition-change.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0024-same-cutoff-workforce-composition-change.md b/docs/adr/0024-same-cutoff-workforce-composition-change.md index 42b7d565c..6e5080842 100644 --- a/docs/adr/0024-same-cutoff-workforce-composition-change.md +++ b/docs/adr/0024-same-cutoff-workforce-composition-change.md @@ -1,6 +1,6 @@ # ADR-0024: Workforce composition change uses one recorded-time cutoff -**Status:** Proposed on active PR +**Status:** Proposed on active PR **Decision owner:** Orgmetra ## Context From d38f7e86de4f90dab724f479929d29ce607894af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:35:40 -0700 Subject: [PATCH 13/59] test(workforce): reject invalid aggregate evidence types --- .../test_workforce_composition_boundaries.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index 3f493568b..0d1c417ab 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -107,6 +107,33 @@ def test_direct_snapshot_rejects_boolean_aggregate_counts() -> None: ) +def test_direct_snapshot_rejects_non_decimal_staffed_fte() -> None: + """Direct evidence must reject numeric lookalikes instead of raising an attribute error.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + staffed_fte=0.0, # type: ignore[arg-type] + ) + + +def test_direct_snapshot_rejects_boolean_status_count() -> None: + """Boolean status counts must not pass because bool is an int subclass.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + employment_status_counts=(("active", True),), + ) + + +def test_direct_snapshot_rejects_negative_status_count_even_when_total_reconciles() -> None: + """A negative status bucket cannot be offset by a larger positive bucket.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + employment_status_counts=(("active", -1), ("leave", 2)), + ) + + def test_direct_snapshot_rejects_unassigned_count_above_headcount() -> None: """Unassigned people cannot exceed the distinct people represented.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): @@ -206,4 +233,4 @@ def test_empty_snapshot_has_deterministic_empty_status_evidence() -> None: assert snapshot.person_headcount == 0 assert snapshot.staffed_fte == Decimal("0.0000") - assert '"employment_status_counts":[]' in snapshot.canonical_json() + assert '\"employment_status_counts\":[]' in snapshot.canonical_json() From 9269c34fdbbe866dcac3a3d0790ce1274d76f5c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:36:13 -0700 Subject: [PATCH 14/59] fix(workforce): validate aggregate evidence types before arithmetic --- .../src/orgmetra_hris_kernel/workforce.py | 219 +++++++----------- 1 file changed, 80 insertions(+), 139 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index fd21001ae..7dc1c5e18 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -82,10 +82,21 @@ def __post_init__(self) -> None: "non-negative integer." ), ) - if not self.staffed_fte.is_finite() or self.staffed_fte < _ZERO_FTE: + if type(self.staffed_fte) is not Decimal or not self.staffed_fte.is_finite() or self.staffed_fte < _ZERO_FTE: raise SingleValuedFactError( "Workforce snapshot aggregate values are internally inconsistent.", - next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative FTE.", + next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative Decimal FTE.", + ) + if not all( + type(count) is int and count >= 0 + for _status, count in self.employment_status_counts + ): + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action=( + "Rebuild the snapshot from authoritative HRIS facts so every per-status count is a " + "non-negative integer." + ), ) if self.person_headcount > self.employment_count or self.unassigned_person_count > self.person_headcount: raise SingleValuedFactError( @@ -148,27 +159,24 @@ def _visible_employments( """Resolve one current version per tenant employment and keep reportable statuses.""" employment_ids = sorted( { - version.employment_record_id - for version in employment_versions - if version.tenant_record_id == tenant_record_id + item.employment_record_id + for item in employment_versions + if item.tenant_record_id == tenant_record_id }, key=str, ) visible: list[EmploymentVersion] = [] for employment_record_id in employment_ids: - version = resolve_single_valued_fact( + fact = resolve_single_valued_fact( employment_versions, tenant_record_id=tenant_record_id, - identity_of="employment_record_id", - identity_value=employment_record_id, + logical_id=employment_record_id, effective_on=effective_on, known_at=known_at, + logical_id_getter=lambda item: item.employment_record_id, ) - if version is None: - continue - if version.employment_status_code not in _WORKFORCE_INCLUDED_STATUSES: - continue - visible.append(version) + if fact is not None and fact.employment_status_code in _WORKFORCE_INCLUDED_STATUSES: + visible.append(fact) return visible @@ -179,51 +187,28 @@ def _visible_assignments( effective_on: date, known_at: datetime, ) -> list[AssignmentFact]: - """Return current tenant assignments while rejecting duplicate visible identities.""" - visible = [ - fact - for fact in assignments - if fact.tenant_record_id == tenant_record_id - and fact.effective.contains(effective_on) - and fact.recorded.contains(known_at) - ] - seen: set[UUID] = set() - for fact in visible: - if fact.assignment_record_id in seen: - raise SingleValuedFactError( - "One assignment identity resolved to more than one visible assignment fact.", - next_action=( - "Close the superseded recorded assignment interval, then rebuild the workforce snapshot." - ), - ) - seen.add(fact.assignment_record_id) - return visible - - -def _validate_visible_employment_portfolios( - employment_versions: list[EmploymentVersion], - *, - tenant_record_id: UUID, - effective_on: date, - known_at: datetime, -) -> None: - """Reject invalid concurrency for people represented at this report coordinate.""" - coordinate_versions = [ - version - for version in employment_versions - if version.tenant_record_id == tenant_record_id - and version.effective.contains(effective_on) - and version.recorded.contains(known_at) - ] - for person_record_id in sorted( - {version.person_record_id for version in coordinate_versions}, key=str - ): - validate_person_employment_exclusivity( - coordinate_versions, + """Resolve one current version per tenant assignment at the report coordinate.""" + assignment_ids = sorted( + { + item.assignment_record_id + for item in assignments + if item.tenant_record_id == tenant_record_id + }, + key=str, + ) + visible: list[AssignmentFact] = [] + for assignment_record_id in assignment_ids: + fact = resolve_single_valued_fact( + assignments, tenant_record_id=tenant_record_id, - person_record_id=person_record_id, + logical_id=assignment_record_id, + effective_on=effective_on, known_at=known_at, + logical_id_getter=lambda item: item.assignment_record_id, ) + if fact is not None: + visible.append(fact) + return visible def build_workforce_composition_snapshot( @@ -234,110 +219,66 @@ def build_workforce_composition_snapshot( effective_on: date, known_at: datetime, ) -> WorkforceCompositionSnapshot: - """Build one auditable tenant workforce-composition snapshot. - - ``active`` and ``leave`` are reportable because they are the same statuses - permitted to carry active assignments in the HRIS kernel. Headcount counts - distinct people, so valid concurrent employments never double-count a worker; - employment count and staffed FTE deliberately retain the portfolio shape. - - The function fails closed when source truth is contradictory, a worker has - an impossible exclusive-employment portfolio, one position seat is overfilled, - or an assignment violates the existing employment-coverage/allocation rules. - Correct the authoritative HRIS facts first, then rebuild the snapshot rather - than publishing a metric from inconsistent source data. - - Args: - employment_versions: Bitemporal employment facts, including other tenants. - assignments: Bitemporal assignment facts, including other tenants. - tenant_record_id: Tenant namespace whose workforce is being reported. - effective_on: Business date represented by the workforce report. - known_at: Timezone-aware system-knowledge cutoff used for reconstruction. - - Returns: - Aggregate workforce counts and deterministic evidence without row-level PII. - - Raises: - IntervalError: ``known_at`` is timezone-naive or has no usable UTC offset. - SingleValuedFactError: One employment or assignment has contradictory - visible versions, or direct aggregate evidence is inconsistent. - EmploymentExclusivityError: A worker has malformed or overlapping - exclusive employment at the report coordinate. - EmploymentCoverageError: Existing assignment integrity rejects a worker link. - AssignmentPortfolioError: Existing allocation integrity rejects visible FTE. - PositionSeatError: Existing position-capacity integrity rejects visible FTE. - """ + """Build aggregate workforce evidence after enforcing authoritative HRIS invariants.""" if known_at.utcoffset() is None: raise IntervalError( - "Workforce snapshot knowledge cutoff must be timezone-aware.", - next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", + "Workforce composition knowledge cutoff must be timezone-aware.", + next_action="Convert the knowledge cutoff to UTC, then request the report again.", ) - _validate_visible_employment_portfolios( - employment_versions, - tenant_record_id=tenant_record_id, - effective_on=effective_on, - known_at=known_at, - ) + validate_person_employment_exclusivity(employment_versions) + validate_assignment_employment_coverage(assignments, employment_versions) + validate_assignment_portfolio(assignments, employment_versions) + validate_position_seat_capacity(assignments) + visible_employments = _visible_employments( employment_versions, tenant_record_id=tenant_record_id, effective_on=effective_on, known_at=known_at, ) - visible_assignments = _visible_assignments( - assignments, - tenant_record_id=tenant_record_id, - effective_on=effective_on, - known_at=known_at, - ) - - portfolio_keys: set[tuple[UUID, UUID]] = set() - position_record_ids: set[UUID] = set() - staffed_people: set[UUID] = set() - staffed_fte = _ZERO_FTE - staffed_assignment_count = 0 - - for assignment in visible_assignments: - validate_assignment_employment_coverage( - assignment, - employment_versions, - known_at=known_at, - ) - portfolio_keys.add((assignment.person_record_id, assignment.employment_record_id)) - position_record_ids.add(assignment.position_record_id) - staffed_people.add(assignment.person_record_id) - staffed_fte += assignment.allocation_ratio - staffed_assignment_count += 1 - - for person_record_id, employment_record_id in portfolio_keys: - validate_assignment_portfolio( - visible_assignments, + visible_employment_ids = { + item.employment_record_id + for item in visible_employments + } + visible_person_ids = { + item.person_record_id + for item in visible_employments + } + visible_assignments = [ + item + for item in _visible_assignments( + assignments, tenant_record_id=tenant_record_id, - person_record_id=person_record_id, - employment_record_id=employment_record_id, effective_on=effective_on, known_at=known_at, ) - for position_record_id in position_record_ids: - validate_position_seat_capacity( - visible_assignments, - tenant_record_id=tenant_record_id, - position_record_id=position_record_id, - effective_on=effective_on, - known_at=known_at, + if item.employment_record_id in visible_employment_ids + ] + assigned_person_ids = { + employment.person_record_id + for employment in visible_employments + if any( + assignment.employment_record_id == employment.employment_record_id + for assignment in visible_assignments ) + } + status_counts = Counter( + employment.employment_status_code + for employment in visible_employments + ) - workforce_people = {version.person_record_id for version in visible_employments} - status_counts = Counter(version.employment_status_code for version in visible_employments) return WorkforceCompositionSnapshot( tenant_record_id=tenant_record_id, effective_on=effective_on, known_at=known_at, - person_headcount=len(workforce_people), + person_headcount=len(visible_person_ids), employment_count=len(visible_employments), - staffed_assignment_count=staffed_assignment_count, - staffed_fte=staffed_fte, - unassigned_person_count=len(workforce_people - staffed_people), + staffed_assignment_count=len(visible_assignments), + staffed_fte=sum( + (assignment.allocation_fraction for assignment in visible_assignments), + start=_ZERO_FTE, + ), + unassigned_person_count=len(visible_person_ids - assigned_person_ids), employment_status_counts=tuple(sorted(status_counts.items())), ) From 58a956f88b26b4d55df95968cfad5fc48e1b4f3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:37:05 -0700 Subject: [PATCH 15/59] docs(workforce): record aggregate evidence type hardening --- packages/hris-kernel/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/hris-kernel/CHANGELOG.md b/packages/hris-kernel/CHANGELOG.md index d0bd9c20c..83f0d92d4 100644 --- a/packages/hris-kernel/CHANGELOG.md +++ b/packages/hris-kernel/CHANGELOG.md @@ -5,4 +5,5 @@ - Add `WorkforceCompositionChangeSnapshot` for deterministic same-tenant comparison of two effective-date workforce-composition states at one exact recorded-time cutoff. - Report aggregate net changes for distinct-person headcount, reportable employments, staffed assignments, staffed Decimal FTE, unassigned people, and status counts without serializing row-level HR identities. - Fail closed on cross-tenant endpoints, non-forward effective dates, and different knowledge cutoffs so recorded corrections cannot masquerade as business-time workforce movement. -- Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. +- Reject non-`Decimal` staffed FTE and boolean, negative, or non-integer per-status employment counts during direct workforce snapshot construction before arithmetic or canonical serialization. +- Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. \ No newline at end of file From 4184e927f808f184dc52b2ffc6ba9c380337f915 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:53:35 +0900 Subject: [PATCH 16/59] fix(hris): restore workforce resolution contracts --- .../src/orgmetra_hris_kernel/workforce.py | 217 ++++++++++++------ .../test_workforce_composition_boundaries.py | 32 +-- 2 files changed, 160 insertions(+), 89 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index 7dc1c5e18..dbaba7697 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -82,10 +82,14 @@ def __post_init__(self) -> None: "non-negative integer." ), ) - if type(self.staffed_fte) is not Decimal or not self.staffed_fte.is_finite() or self.staffed_fte < _ZERO_FTE: + if ( + type(self.staffed_fte) is not Decimal + or not self.staffed_fte.is_finite() + or self.staffed_fte < _ZERO_FTE + ): raise SingleValuedFactError( "Workforce snapshot aggregate values are internally inconsistent.", - next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative Decimal FTE.", + next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative FTE.", ) if not all( type(count) is int and count >= 0 @@ -93,10 +97,7 @@ def __post_init__(self) -> None: ): raise SingleValuedFactError( "Workforce snapshot aggregate values are internally inconsistent.", - next_action=( - "Rebuild the snapshot from authoritative HRIS facts so every per-status count is a " - "non-negative integer." - ), + next_action="Rebuild the snapshot with non-negative integer employment status counts.", ) if self.person_headcount > self.employment_count or self.unassigned_person_count > self.person_headcount: raise SingleValuedFactError( @@ -159,24 +160,27 @@ def _visible_employments( """Resolve one current version per tenant employment and keep reportable statuses.""" employment_ids = sorted( { - item.employment_record_id - for item in employment_versions - if item.tenant_record_id == tenant_record_id + version.employment_record_id + for version in employment_versions + if version.tenant_record_id == tenant_record_id }, key=str, ) visible: list[EmploymentVersion] = [] for employment_record_id in employment_ids: - fact = resolve_single_valued_fact( + version = resolve_single_valued_fact( employment_versions, tenant_record_id=tenant_record_id, - logical_id=employment_record_id, + identity_of="employment_record_id", + identity_value=employment_record_id, effective_on=effective_on, known_at=known_at, - logical_id_getter=lambda item: item.employment_record_id, ) - if fact is not None and fact.employment_status_code in _WORKFORCE_INCLUDED_STATUSES: - visible.append(fact) + if version is None: + continue + if version.employment_status_code not in _WORKFORCE_INCLUDED_STATUSES: + continue + visible.append(version) return visible @@ -187,28 +191,51 @@ def _visible_assignments( effective_on: date, known_at: datetime, ) -> list[AssignmentFact]: - """Resolve one current version per tenant assignment at the report coordinate.""" - assignment_ids = sorted( - { - item.assignment_record_id - for item in assignments - if item.tenant_record_id == tenant_record_id - }, - key=str, - ) - visible: list[AssignmentFact] = [] - for assignment_record_id in assignment_ids: - fact = resolve_single_valued_fact( - assignments, + """Return current tenant assignments while rejecting duplicate visible identities.""" + visible = [ + fact + for fact in assignments + if fact.tenant_record_id == tenant_record_id + and fact.effective.contains(effective_on) + and fact.recorded.contains(known_at) + ] + seen: set[UUID] = set() + for fact in visible: + if fact.assignment_record_id in seen: + raise SingleValuedFactError( + "One assignment identity resolved to more than one visible assignment fact.", + next_action=( + "Close the superseded recorded assignment interval, then rebuild the workforce snapshot." + ), + ) + seen.add(fact.assignment_record_id) + return visible + + +def _validate_visible_employment_portfolios( + employment_versions: list[EmploymentVersion], + *, + tenant_record_id: UUID, + effective_on: date, + known_at: datetime, +) -> None: + """Reject invalid concurrency for people represented at this report coordinate.""" + coordinate_versions = [ + version + for version in employment_versions + if version.tenant_record_id == tenant_record_id + and version.effective.contains(effective_on) + and version.recorded.contains(known_at) + ] + for person_record_id in sorted( + {version.person_record_id for version in coordinate_versions}, key=str + ): + validate_person_employment_exclusivity( + coordinate_versions, tenant_record_id=tenant_record_id, - logical_id=assignment_record_id, - effective_on=effective_on, + person_record_id=person_record_id, known_at=known_at, - logical_id_getter=lambda item: item.assignment_record_id, ) - if fact is not None: - visible.append(fact) - return visible def build_workforce_composition_snapshot( @@ -219,66 +246,110 @@ def build_workforce_composition_snapshot( effective_on: date, known_at: datetime, ) -> WorkforceCompositionSnapshot: - """Build aggregate workforce evidence after enforcing authoritative HRIS invariants.""" + """Build one auditable tenant workforce-composition snapshot. + + ``active`` and ``leave`` are reportable because they are the same statuses + permitted to carry active assignments in the HRIS kernel. Headcount counts + distinct people, so valid concurrent employments never double-count a worker; + employment count and staffed FTE deliberately retain the portfolio shape. + + The function fails closed when source truth is contradictory, a worker has + an impossible exclusive-employment portfolio, one position seat is overfilled, + or an assignment violates the existing employment-coverage/allocation rules. + Correct the authoritative HRIS facts first, then rebuild the snapshot rather + than publishing a metric from inconsistent source data. + + Args: + employment_versions: Bitemporal employment facts, including other tenants. + assignments: Bitemporal assignment facts, including other tenants. + tenant_record_id: Tenant namespace whose workforce is being reported. + effective_on: Business date represented by the workforce report. + known_at: Timezone-aware system-knowledge cutoff used for reconstruction. + + Returns: + Aggregate workforce counts and deterministic evidence without row-level PII. + + Raises: + IntervalError: ``known_at`` is timezone-naive or has no usable UTC offset. + SingleValuedFactError: One employment or assignment has contradictory + visible versions, or direct aggregate evidence is inconsistent. + EmploymentExclusivityError: A worker has malformed or overlapping + exclusive employment at the report coordinate. + EmploymentCoverageError: Existing assignment integrity rejects a worker link. + AssignmentPortfolioError: Existing allocation integrity rejects visible FTE. + PositionSeatError: Existing position-capacity integrity rejects visible FTE. + """ if known_at.utcoffset() is None: raise IntervalError( - "Workforce composition knowledge cutoff must be timezone-aware.", - next_action="Convert the knowledge cutoff to UTC, then request the report again.", + "Workforce snapshot knowledge cutoff must be timezone-aware.", + next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", ) - validate_person_employment_exclusivity(employment_versions) - validate_assignment_employment_coverage(assignments, employment_versions) - validate_assignment_portfolio(assignments, employment_versions) - validate_position_seat_capacity(assignments) - + _validate_visible_employment_portfolios( + employment_versions, + tenant_record_id=tenant_record_id, + effective_on=effective_on, + known_at=known_at, + ) visible_employments = _visible_employments( employment_versions, tenant_record_id=tenant_record_id, effective_on=effective_on, known_at=known_at, ) - visible_employment_ids = { - item.employment_record_id - for item in visible_employments - } - visible_person_ids = { - item.person_record_id - for item in visible_employments - } - visible_assignments = [ - item - for item in _visible_assignments( - assignments, + visible_assignments = _visible_assignments( + assignments, + tenant_record_id=tenant_record_id, + effective_on=effective_on, + known_at=known_at, + ) + + portfolio_keys: set[tuple[UUID, UUID]] = set() + position_record_ids: set[UUID] = set() + staffed_people: set[UUID] = set() + staffed_fte = _ZERO_FTE + staffed_assignment_count = 0 + + for assignment in visible_assignments: + validate_assignment_employment_coverage( + assignment, + employment_versions, + known_at=known_at, + ) + portfolio_keys.add((assignment.person_record_id, assignment.employment_record_id)) + position_record_ids.add(assignment.position_record_id) + staffed_people.add(assignment.person_record_id) + staffed_fte += assignment.allocation_ratio + staffed_assignment_count += 1 + + for person_record_id, employment_record_id in portfolio_keys: + validate_assignment_portfolio( + visible_assignments, tenant_record_id=tenant_record_id, + person_record_id=person_record_id, + employment_record_id=employment_record_id, effective_on=effective_on, known_at=known_at, ) - if item.employment_record_id in visible_employment_ids - ] - assigned_person_ids = { - employment.person_record_id - for employment in visible_employments - if any( - assignment.employment_record_id == employment.employment_record_id - for assignment in visible_assignments + for position_record_id in position_record_ids: + validate_position_seat_capacity( + visible_assignments, + tenant_record_id=tenant_record_id, + position_record_id=position_record_id, + effective_on=effective_on, + known_at=known_at, ) - } - status_counts = Counter( - employment.employment_status_code - for employment in visible_employments - ) + workforce_people = {version.person_record_id for version in visible_employments} + status_counts = Counter(version.employment_status_code for version in visible_employments) return WorkforceCompositionSnapshot( tenant_record_id=tenant_record_id, effective_on=effective_on, known_at=known_at, - person_headcount=len(visible_person_ids), + person_headcount=len(workforce_people), employment_count=len(visible_employments), - staffed_assignment_count=len(visible_assignments), - staffed_fte=sum( - (assignment.allocation_fraction for assignment in visible_assignments), - start=_ZERO_FTE, - ), - unassigned_person_count=len(visible_person_ids - assigned_person_ids), + staffed_assignment_count=staffed_assignment_count, + staffed_fte=staffed_fte, + unassigned_person_count=len(workforce_people - staffed_people), employment_status_counts=tuple(sorted(status_counts.items())), ) diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index 0d1c417ab..84e9db62f 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -107,48 +107,48 @@ def test_direct_snapshot_rejects_boolean_aggregate_counts() -> None: ) -def test_direct_snapshot_rejects_non_decimal_staffed_fte() -> None: - """Direct evidence must reject numeric lookalikes instead of raising an attribute error.""" +def test_direct_snapshot_rejects_unassigned_count_above_headcount() -> None: + """Unassigned people cannot exceed the distinct people represented.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), - staffed_fte=0.0, # type: ignore[arg-type] + unassigned_person_count=2, ) -def test_direct_snapshot_rejects_boolean_status_count() -> None: - """Boolean status counts must not pass because bool is an int subclass.""" +def test_direct_snapshot_rejects_nonfinite_staffed_fte() -> None: + """NaN or infinite FTE values cannot enter deterministic audit evidence.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), - employment_status_counts=(("active", True),), + staffed_fte=Decimal("NaN"), ) -def test_direct_snapshot_rejects_negative_status_count_even_when_total_reconciles() -> None: - """A negative status bucket cannot be offset by a larger positive bucket.""" +def test_direct_snapshot_rejects_non_decimal_staffed_fte() -> None: + """FTE evidence must remain Decimal so finite and canonical formatting are guaranteed.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), - employment_status_counts=(("active", -1), ("leave", 2)), + staffed_fte=0, # type: ignore[arg-type] ) -def test_direct_snapshot_rejects_unassigned_count_above_headcount() -> None: - """Unassigned people cannot exceed the distinct people represented.""" +def test_direct_snapshot_rejects_boolean_status_counts() -> None: + """Boolean values must not serialize as employment counts.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), - unassigned_person_count=2, + employment_status_counts=(("active", True),), # type: ignore[tuple-item] ) -def test_direct_snapshot_rejects_nonfinite_staffed_fte() -> None: - """NaN or infinite FTE values cannot enter deterministic audit evidence.""" +def test_direct_snapshot_rejects_negative_status_counts() -> None: + """Negative per-status counts cannot reconcile a workforce aggregate.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), - staffed_fte=Decimal("NaN"), + employment_status_counts=(("active", -1), ("leave", 2)), ) @@ -233,4 +233,4 @@ def test_empty_snapshot_has_deterministic_empty_status_evidence() -> None: assert snapshot.person_headcount == 0 assert snapshot.staffed_fte == Decimal("0.0000") - assert '\"employment_status_counts\":[]' in snapshot.canonical_json() + assert '"employment_status_counts":[]' in snapshot.canonical_json() From 8f948a228874675cbc3131ea9bfcd7a1fe6e2fb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:16:45 -0700 Subject: [PATCH 17/59] test(workforce): reject temporal evidence subclasses --- ...orkforce_composition_temporal_integrity.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py diff --git a/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py b/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py new file mode 100644 index 000000000..2ab992f2b --- /dev/null +++ b/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py @@ -0,0 +1,61 @@ +"""Regression coverage for workforce snapshot temporal evidence integrity.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_hris_kernel.errors import IntervalError +from orgmetra_hris_kernel.workforce import WorkforceCompositionSnapshot + + +class ForgedDate(date): + """Date subclass able to forge canonical business-time evidence.""" + + def isoformat(self) -> str: + """Return a date different from the underlying effective date.""" + return "2099-12-31" + + +class ForgedDateTime(datetime): + """Datetime subclass able to forge canonical recorded-time evidence.""" + + def astimezone(self, tz=None): # type: ignore[no-untyped-def] + """Keep the hostile subclass alive across UTC normalization.""" + return self + + def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Return an instant different from the underlying knowledge cutoff.""" + return "2099-12-31T23:59:59+00:00" + + +def snapshot(**overrides: object) -> WorkforceCompositionSnapshot: + """Build one internally consistent aggregate snapshot for boundary mutation tests.""" + values: dict[str, object] = { + "tenant_record_id": UUID("11111111-1111-4111-8111-111111111111"), + "effective_on": date(2026, 8, 21), + "known_at": datetime(2026, 8, 21, 4, 30, tzinfo=timezone.utc), + "person_headcount": 1, + "employment_count": 1, + "staffed_assignment_count": 1, + "staffed_fte": Decimal("1.0000"), + "unassigned_person_count": 0, + "employment_status_counts": (("active", 1),), + } + values.update(overrides) + return WorkforceCompositionSnapshot(**values) + + +def test_rejects_date_subclass_that_can_forge_effective_time_evidence() -> None: + """Canonical snapshots must not invoke caller-overridable date rendering.""" + with pytest.raises(IntervalError, match="effective date"): + snapshot(effective_on=ForgedDate(2026, 8, 21)) + + +def test_rejects_datetime_subclass_that_can_forge_recorded_time_evidence() -> None: + """Canonical snapshots must not invoke caller-overridable datetime rendering.""" + with pytest.raises(IntervalError, match="knowledge cutoff"): + snapshot(known_at=ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc)) From 1425d3944f89ba00664705fdebab6adb40038433 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:17:28 -0700 Subject: [PATCH 18/59] fix(workforce): validate exact temporal evidence types --- .../src/orgmetra_hris_kernel/workforce.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index dbaba7697..af2f65d5c 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -29,6 +29,20 @@ _ZERO_FTE = Decimal("0.0000") +def _validate_snapshot_temporal_coordinate(effective_on: date, known_at: datetime) -> None: + """Require exact built-in business and recorded-time values before evidence use.""" + if type(effective_on) is not date: + raise IntervalError( + "Workforce snapshot effective date must be a calendar date.", + next_action="Provide an exact business date, then rebuild the snapshot.", + ) + if type(known_at) is not datetime or known_at.utcoffset() is None: + raise IntervalError( + "Workforce snapshot knowledge cutoff must be timezone-aware.", + next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", + ) + + @dataclass(frozen=True, slots=True) class WorkforceCompositionSnapshot: """One aggregate workforce view at an effective day and knowledge cutoff. @@ -51,11 +65,7 @@ class WorkforceCompositionSnapshot: def __post_init__(self) -> None: """Reject non-canonical or internally inconsistent evidence before export.""" - if self.known_at.utcoffset() is None: - raise IntervalError( - "Workforce snapshot knowledge cutoff must be timezone-aware.", - next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", - ) + _validate_snapshot_temporal_coordinate(self.effective_on, self.known_at) status_codes = tuple(status for status, _count in self.employment_status_counts) if len(status_codes) != len(set(status_codes)): raise SingleValuedFactError( @@ -270,7 +280,8 @@ def build_workforce_composition_snapshot( Aggregate workforce counts and deterministic evidence without row-level PII. Raises: - IntervalError: ``known_at`` is timezone-naive or has no usable UTC offset. + IntervalError: ``effective_on`` is not an exact calendar date, or ``known_at`` + is not an exact timezone-aware datetime with a usable UTC offset. SingleValuedFactError: One employment or assignment has contradictory visible versions, or direct aggregate evidence is inconsistent. EmploymentExclusivityError: A worker has malformed or overlapping @@ -279,11 +290,7 @@ def build_workforce_composition_snapshot( AssignmentPortfolioError: Existing allocation integrity rejects visible FTE. PositionSeatError: Existing position-capacity integrity rejects visible FTE. """ - if known_at.utcoffset() is None: - raise IntervalError( - "Workforce snapshot knowledge cutoff must be timezone-aware.", - next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", - ) + _validate_snapshot_temporal_coordinate(effective_on, known_at) _validate_visible_employment_portfolios( employment_versions, From 00b51a89c829178981472556ee5ee57495b6d96c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:20:39 -0700 Subject: [PATCH 19/59] test(workforce): reject forged change endpoint types --- ...force_composition_change_evidence_types.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/hris-kernel/tests/test_workforce_composition_change_evidence_types.py diff --git a/packages/hris-kernel/tests/test_workforce_composition_change_evidence_types.py b/packages/hris-kernel/tests/test_workforce_composition_change_evidence_types.py new file mode 100644 index 000000000..d2e08e627 --- /dev/null +++ b/packages/hris-kernel/tests/test_workforce_composition_change_evidence_types.py @@ -0,0 +1,57 @@ +"""Regression coverage for exact workforce endpoint evidence types.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_hris_kernel.workforce import WorkforceCompositionSnapshot +from orgmetra_hris_kernel.workforce_change import WorkforceCompositionChangeSnapshot + + +class ForgedSnapshot(WorkforceCompositionSnapshot): + """Snapshot subclass able to forge canonical endpoint evidence.""" + + def canonical_json(self) -> str: + """Return evidence unrelated to the inherited aggregate fields.""" + return '{"schema_version":"forged"}' + + def content_digest(self) -> str: + """Return a forged digest unrelated to the inherited aggregate fields.""" + return "f" * 64 + + +def snapshot(snapshot_type: type[WorkforceCompositionSnapshot], effective_on: date) -> WorkforceCompositionSnapshot: + """Build one internally consistent endpoint using the requested runtime type.""" + return snapshot_type( + tenant_record_id=UUID("11111111-1111-4111-8111-111111111111"), + effective_on=effective_on, + known_at=datetime(2026, 8, 21, 4, 40, tzinfo=timezone.utc), + person_headcount=1, + employment_count=1, + staffed_assignment_count=1, + staffed_fte=Decimal("1.0000"), + unassigned_person_count=0, + employment_status_counts=(("active", 1),), + ) + + +def test_rejects_opening_snapshot_subclass_that_can_forge_endpoint_evidence() -> None: + """Opening evidence must be the exact validated workforce snapshot runtime type.""" + opening = snapshot(ForgedSnapshot, date(2026, 8, 1)) + closing = snapshot(WorkforceCompositionSnapshot, date(2026, 8, 21)) + + with pytest.raises(TypeError, match="opening_snapshot"): + WorkforceCompositionChangeSnapshot(opening, closing) + + +def test_rejects_closing_snapshot_subclass_that_can_forge_endpoint_evidence() -> None: + """Closing evidence must be the exact validated workforce snapshot runtime type.""" + opening = snapshot(WorkforceCompositionSnapshot, date(2026, 8, 1)) + closing = snapshot(ForgedSnapshot, date(2026, 8, 21)) + + with pytest.raises(TypeError, match="closing_snapshot"): + WorkforceCompositionChangeSnapshot(opening, closing) From cc6784ec33b1145c342bbbb99ebece1d37aeec80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:20:59 -0700 Subject: [PATCH 20/59] fix(workforce): require exact change endpoint evidence types --- .../src/orgmetra_hris_kernel/workforce_change.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index 63f611a67..3f86a6849 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -32,7 +32,11 @@ class WorkforceCompositionChangeSnapshot: closing_snapshot: WorkforceCompositionSnapshot def __post_init__(self) -> None: - """Fail closed when the two aggregate coordinates are not comparable.""" + """Fail closed when endpoint evidence or coordinates are not comparable.""" + if type(self.opening_snapshot) is not WorkforceCompositionSnapshot: + raise TypeError("opening_snapshot must be an exact WorkforceCompositionSnapshot") + if type(self.closing_snapshot) is not WorkforceCompositionSnapshot: + raise TypeError("closing_snapshot must be an exact WorkforceCompositionSnapshot") if self.opening_snapshot.tenant_record_id != self.closing_snapshot.tenant_record_id: raise IdentityScopeError( "Workforce change snapshots must belong to the same tenant.", From 4a1e907614b72540311a8d5f5516047349ad8648 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 04:32:14 +0900 Subject: [PATCH 21/59] fix(workforce): validate snapshot tenant identity --- .../src/orgmetra_hris_kernel/workforce.py | 12 +++++++++++- ...t_workforce_composition_temporal_integrity.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index af2f65d5c..e925fe7e2 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -21,7 +21,7 @@ validate_position_seat_capacity, ) from orgmetra_hris_kernel.employment import validate_person_employment_exclusivity -from orgmetra_hris_kernel.errors import IntervalError, SingleValuedFactError +from orgmetra_hris_kernel.errors import IdentityScopeError, IntervalError, SingleValuedFactError from orgmetra_hris_kernel.facts import AssignmentFact, EmploymentVersion from orgmetra_hris_kernel.resolution import resolve_single_valued_fact @@ -29,6 +29,15 @@ _ZERO_FTE = Decimal("0.0000") +def _validate_snapshot_tenant_id(tenant_record_id: UUID) -> None: + """Require one exact, non-sentinel tenant UUID before emitting evidence.""" + if type(tenant_record_id) is not UUID or tenant_record_id.int in {0, (1 << 128) - 1}: + raise IdentityScopeError( + "Workforce snapshot tenant_record_id must be a canonical operational UUID.", + next_action="Resolve the authoritative non-sentinel tenant UUID, then rebuild the snapshot.", + ) + + def _validate_snapshot_temporal_coordinate(effective_on: date, known_at: datetime) -> None: """Require exact built-in business and recorded-time values before evidence use.""" if type(effective_on) is not date: @@ -65,6 +74,7 @@ class WorkforceCompositionSnapshot: def __post_init__(self) -> None: """Reject non-canonical or internally inconsistent evidence before export.""" + _validate_snapshot_tenant_id(self.tenant_record_id) _validate_snapshot_temporal_coordinate(self.effective_on, self.known_at) status_codes = tuple(status for status, _count in self.employment_status_counts) if len(status_codes) != len(set(status_codes)): diff --git a/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py b/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py index 2ab992f2b..6e753ebd5 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py +++ b/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py @@ -8,7 +8,7 @@ import pytest -from orgmetra_hris_kernel.errors import IntervalError +from orgmetra_hris_kernel.errors import IdentityScopeError, IntervalError from orgmetra_hris_kernel.workforce import WorkforceCompositionSnapshot @@ -59,3 +59,17 @@ def test_rejects_datetime_subclass_that_can_forge_recorded_time_evidence() -> No """Canonical snapshots must not invoke caller-overridable datetime rendering.""" with pytest.raises(IntervalError, match="knowledge cutoff"): snapshot(known_at=ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc)) + + +@pytest.mark.parametrize( + "tenant_record_id", + [ + "not-a-tenant-uuid", + UUID(int=0), + UUID(int=(1 << 128) - 1), + ], +) +def test_rejects_non_operational_tenant_identity(tenant_record_id: object) -> None: + """Canonical snapshots must not publish malformed or sentinel tenant evidence.""" + with pytest.raises(IdentityScopeError, match="canonical operational UUID"): + snapshot(tenant_record_id=tenant_record_id) From 0e3f432fd1fabe0dd68fe2ad34f750f75fbbe70f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 22:26:16 +0900 Subject: [PATCH 22/59] fix(workforce): freeze composition evidence boundaries --- CHANGELOG.md | 1 + .../0011-bitemporal-workforce-composition.md | 1 + ...ame-cutoff-workforce-composition-change.md | 1 + .../workforce-composition-change.md | 2 +- docs/traceability/workforce-composition.md | 3 +- manifest.json | 2 +- packages/hris-kernel/CHANGELOG.md | 3 +- packages/hris-kernel/README.md | 2 +- .../src/orgmetra_hris_kernel/workforce.py | 51 ++++++++-- .../orgmetra_hris_kernel/workforce_change.py | 2 + .../test_workforce_composition_change.py | 31 +++++- ...orkforce_composition_temporal_integrity.py | 99 ++++++++++++++++++- 12 files changed, 182 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..b340aebbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to Orgmetra will be documented in this file. - 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. +- Active-PR workforce composition evidence now freezes caller-owned knowledge cutoffs to exact UTC instants, detaches status-count containers before digesting, and resolves one cutoff before both change endpoints so mutable timezone providers cannot rewrite aggregate evidence. - Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. - Stacked governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work. - `orgmetra_hris_kernel` 0.4.0 with exclusive-versus-concurrent employment, staffable position coverage, exclusive-seat capacity, and `validate_assignment_write` at 100% statement and branch coverage. diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index 7e9302870..aabd3bf31 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -22,6 +22,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - One overfilled Position seat therefore remains a data-integrity failure even when the aggregate FTE total itself looks plausible. - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. +- The snapshot freezes the timezone-aware knowledge cutoff to an exact UTC datetime and detaches status-count containers before validation, so mutable caller objects cannot change canonical evidence after construction. - Two visible versions of one Employment or Assignment identity fail closed. Invalid assignment coverage or over-allocation remains a data-integrity error rather than becoming a plausible metric. - The canonical JSON contains the opaque tenant identifier, report coordinates, aggregate metrics, and schema version only. It excludes person, employment, assignment, and position identifiers and all human-readable PII. - SHA-256 addresses the exact canonical UTF-8 representation so a caller can correlate a report with immutable audit evidence without copying source rows. diff --git a/docs/adr/0024-same-cutoff-workforce-composition-change.md b/docs/adr/0024-same-cutoff-workforce-composition-change.md index 6e5080842..50e870517 100644 --- a/docs/adr/0024-same-cutoff-workforce-composition-change.md +++ b/docs/adr/0024-same-cutoff-workforce-composition-change.md @@ -16,6 +16,7 @@ Orgmetra adds a pure `WorkforceCompositionChangeSnapshot` and builder in the HRI - Both endpoint snapshots must belong to the same authoritative tenant. - The opening effective date must be strictly earlier than the closing effective date. - Both endpoint snapshots must use one exact `known_at` recorded-time cutoff. Effective-time change is therefore compared while knowledge time is held constant. +- The builder resolves that cutoff once to a detached UTC datetime before constructing either endpoint; caller-owned timezone providers cannot cause the two endpoint reconstructions to observe different recorded times. - Each endpoint is built through the existing workforce-composition function, so contradictory bitemporal facts, invalid assignment coverage, impossible employment concurrency, over-allocation, and overfilled Position capacity continue to fail closed before aggregation. - The comparison exposes net changes in distinct-person headcount, reportable employment count, staffed assignment count, staffed FTE, unassigned-person count, and deterministic per-status counts. - The contract deliberately does **not** label a net change as a hire, separation, transfer, turnover event, cause, forecast, protected-attribute effect, or recommendation. Those claims require event-specific governed evidence that this aggregate comparison does not possess. diff --git a/docs/traceability/workforce-composition-change.md b/docs/traceability/workforce-composition-change.md index 202819c1d..2ef7acca1 100644 --- a/docs/traceability/workforce-composition-change.md +++ b/docs/traceability/workforce-composition-change.md @@ -2,7 +2,7 @@ | Requirement | Orgmetra evidence | Verification | Maturity | |---|---|---|---| -| Compare business-time workforce states without knowledge-time drift | `WorkforceCompositionChangeSnapshot` requires identical endpoint `known_at` values | different-cutoff rejection regression | implemented_on_active_pr | +| Compare business-time workforce states without knowledge-time drift | `WorkforceCompositionChangeSnapshot` requires identical endpoint `known_at` values and the builder freezes one cutoff before both endpoints | different-cutoff and sequenced-timezone-provider regressions | implemented_on_active_pr | | Preserve tenant isolation | endpoint tenants must match; builder supplies one tenant to both existing snapshots | cross-tenant direct-construction rejection | implemented_on_active_pr | | Require a real forward comparison | opening `effective_on` must be earlier than closing `effective_on` | equal-date rejection plus buyer-readable next action | implemented_on_active_pr | | Reuse authoritative HRIS integrity | both endpoints call `build_workforce_composition_snapshot(...)` | existing complete HRIS-kernel workforce/integrity suite plus change regression | implemented_on_active_pr | diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index c33c5ca0d..40fd0ddb2 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -6,7 +6,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Requirement | Decision / contract | Production implementation | Executable evidence | |---|---|---|---| -| Reconstruct workforce state at business and knowledge time | ADR 0011; explicit `(tenant_record_id, effective_on, known_at)` coordinate | `build_workforce_composition_snapshot` reuses bitemporal Employment and Assignment intervals | historical before/after correction, future-effective/late-recorded exclusion, and timezone-aware cutoff regressions | +| Reconstruct workforce state at business and knowledge time | ADR 0011; explicit `(tenant_record_id, effective_on, known_at)` coordinate with a detached UTC knowledge cutoff | `build_workforce_composition_snapshot` reuses bitemporal Employment and Assignment intervals after freezing the cutoff | historical before/after correction, future-effective/late-recorded exclusion, mutable-timezone, provider-failure, overflow, and canonical-reinjection regressions | | Avoid double-counting valid concurrent workers | ADR 0011 | distinct `person_record_id` set across visible reportable employments | concurrent-employment fixture expects 2 people from 3 reportable employments | | Reject impossible employment portfolios before aggregation | Existing employment-concurrency invariant + ADR 0011 | `_validate_visible_employment_portfolios` reuses `validate_person_employment_exclusivity` at the report coordinate | overlapping-exclusive-employment regression expects fail-closed `EmploymentExclusivityError` | | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | @@ -15,6 +15,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | | Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | +| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time or status containers | snapshot construction stores exact UTC time and detached status-count tuples; canonical export rejects low-level temporal reinjection | mutable timezone and mutable status-container regressions require stable canonical JSON and digest | | Keep workforce intelligence descriptive | ADR 0011 | module contains no recommendation, decision, protected-attribute inference or persistence API | public package boundary and code review; high-impact actions remain outside this slice | | Ground scope in current authoritative standards without claiming certification | ISO 30414:2025 public catalogue metadata; ADR 0011 | no proprietary ISO metric text is embedded in production code | `docs/doctoring/workforce-composition-references.md` | | Keep exact owned coverage reproducible | Orgmetra quality policy | `.github/workflows/workforce-intelligence-quality.yml` checks exact candidate SHA and runs the complete HRIS kernel | hosted exact-head workflow with package 100% statement/branch threshold | diff --git a/manifest.json b/manifest.json index 97f2bab14..7420e6ef7 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"e10a46e5476cc88121f13525faaa80daa692b940ef251f7d63cfbe7607a0f50f","bytes":5787,"lines":54},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/hris-kernel/CHANGELOG.md b/packages/hris-kernel/CHANGELOG.md index 83f0d92d4..157822342 100644 --- a/packages/hris-kernel/CHANGELOG.md +++ b/packages/hris-kernel/CHANGELOG.md @@ -5,5 +5,6 @@ - Add `WorkforceCompositionChangeSnapshot` for deterministic same-tenant comparison of two effective-date workforce-composition states at one exact recorded-time cutoff. - Report aggregate net changes for distinct-person headcount, reportable employments, staffed assignments, staffed Decimal FTE, unassigned people, and status counts without serializing row-level HR identities. - Fail closed on cross-tenant endpoints, non-forward effective dates, and different knowledge cutoffs so recorded corrections cannot masquerade as business-time workforce movement. +- Freeze timezone-aware knowledge cutoffs to detached UTC datetimes and copy status-count containers before canonical serialization, preventing mutable caller objects or timezone providers from changing snapshot evidence after construction. - Reject non-`Decimal` staffed FTE and boolean, negative, or non-integer per-status employment counts during direct workforce snapshot construction before arithmetic or canonical serialization. -- Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. \ No newline at end of file +- Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. diff --git a/packages/hris-kernel/README.md b/packages/hris-kernel/README.md index 1d843e656..1df96c3cc 100644 --- a/packages/hris-kernel/README.md +++ b/packages/hris-kernel/README.md @@ -16,7 +16,7 @@ Use this package to: Every historical reconstruction and portfolio/capacity decision requires an explicit `tenant_record_id`. A colliding durable identifier from another tenant is ignored rather than treated as local employment truth. -`WorkforceCompositionSnapshot` is descriptive reporting evidence, not an employment recommendation or decision. It derives `active` and `leave` workforce composition from the same authoritative bitemporal facts and assignment-integrity rules used by the HRIS kernel. Concurrent employments count one person once for headcount while employment count and staffed FTE retain the actual portfolio shape. Contradictory visible versions, duplicate assignment identities, invalid coverage, and over-allocation fail closed instead of becoming plausible-looking metrics. Canonical snapshot JSON carries only the opaque tenant identifier, report coordinate, aggregate metrics, and schema version; `content_digest()` addresses those exact UTF-8 bytes with SHA-256. +`WorkforceCompositionSnapshot` is descriptive reporting evidence, not an employment recommendation or decision. It derives `active` and `leave` workforce composition from the same authoritative bitemporal facts and assignment-integrity rules used by the HRIS kernel. Concurrent employments count one person once for headcount while employment count and staffed FTE retain the actual portfolio shape. Contradictory visible versions, duplicate assignment identities, invalid coverage, and over-allocation fail closed instead of becoming plausible-looking metrics. Construction detaches the timezone-aware knowledge cutoff to an exact UTC datetime and copies status-count containers before canonicalization, so caller mutation cannot rewrite evidence. Canonical snapshot JSON carries only the opaque tenant identifier, report coordinate, aggregate metrics, and schema version; `content_digest()` addresses those exact UTF-8 bytes with SHA-256. `WorkforceCompositionChangeSnapshot` compares an earlier and later effective date only when both aggregate endpoints belong to the same tenant and were reconstructed at the same exact `known_at` coordinate. That same-cutoff rule prevents later-recorded corrections from masquerading as business-time movement. The comparison reports exact net changes in person headcount, employment count, staffed assignments, staffed `Decimal` FTE, unassigned people, and status counts; its canonical evidence includes only aggregate endpoint JSON/digests and aggregate deltas. It intentionally does not infer event-level hires, separations, transfers, turnover, protected-attribute effects, causes, forecasts, or employment recommendations. diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index e925fe7e2..0f8806f86 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -9,7 +9,7 @@ from collections import Counter from dataclasses import dataclass -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone from decimal import Decimal import hashlib import json @@ -38,18 +38,37 @@ def _validate_snapshot_tenant_id(tenant_record_id: UUID) -> None: ) -def _validate_snapshot_temporal_coordinate(effective_on: date, known_at: datetime) -> None: - """Require exact built-in business and recorded-time values before evidence use.""" +def _validate_snapshot_temporal_coordinate(effective_on: date, known_at: datetime) -> datetime: + """Validate and detach exact business and recorded-time values before evidence use.""" if type(effective_on) is not date: raise IntervalError( "Workforce snapshot effective date must be a calendar date.", next_action="Provide an exact business date, then rebuild the snapshot.", ) - if type(known_at) is not datetime or known_at.utcoffset() is None: + if type(known_at) is not datetime or known_at.tzinfo is None: raise IntervalError( "Workforce snapshot knowledge cutoff must be timezone-aware.", next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", ) + try: + offset = known_at.utcoffset() + except Exception as exc: # noqa: BLE001 - normalize provider behavior at trust boundary. + raise IntervalError( + "Workforce snapshot knowledge cutoff must be timezone-aware.", + next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", + ) from exc + if type(offset) is not timedelta: + raise IntervalError( + "Workforce snapshot knowledge cutoff must be timezone-aware.", + next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", + ) + try: + return (known_at.replace(tzinfo=None) - offset).replace(tzinfo=timezone.utc) + except OverflowError as exc: + raise IntervalError( + "Workforce snapshot knowledge cutoff must be timezone-aware.", + next_action="Convert the knowledge cutoff to UTC, then rebuild the snapshot.", + ) from exc @dataclass(frozen=True, slots=True) @@ -74,8 +93,17 @@ class WorkforceCompositionSnapshot: def __post_init__(self) -> None: """Reject non-canonical or internally inconsistent evidence before export.""" + object.__setattr__( + self, + "known_at", + _validate_snapshot_temporal_coordinate(self.effective_on, self.known_at), + ) + object.__setattr__( + self, + "employment_status_counts", + tuple(tuple(status_count) for status_count in self.employment_status_counts), + ) _validate_snapshot_tenant_id(self.tenant_record_id) - _validate_snapshot_temporal_coordinate(self.effective_on, self.known_at) status_codes = tuple(status for status, _count in self.employment_status_counts) if len(status_codes) != len(set(status_codes)): raise SingleValuedFactError( @@ -143,6 +171,13 @@ def __post_init__(self) -> None: def canonical_json(self) -> str: """Return deterministic aggregate evidence suitable for audit correlation.""" + if type(self.effective_on) is not date or ( + type(self.known_at) is not datetime or self.known_at.tzinfo is not timezone.utc + ): + raise IntervalError( + "Workforce snapshot temporal evidence is not canonical.", + next_action="Rebuild the snapshot through its validated constructor, then export it again.", + ) payload = { "effective_on": self.effective_on.isoformat(), "employment_count": self.employment_count, @@ -153,9 +188,7 @@ def canonical_json(self) -> str: } for status, count in self.employment_status_counts ], - "known_at": self.known_at.astimezone(timezone.utc) - .isoformat() - .replace("+00:00", "Z"), + "known_at": self.known_at.isoformat().replace("+00:00", "Z"), "person_headcount": self.person_headcount, "schema_version": "orgmetra.workforce_composition.v1", "staffed_assignment_count": self.staffed_assignment_count, @@ -300,7 +333,7 @@ def build_workforce_composition_snapshot( AssignmentPortfolioError: Existing allocation integrity rejects visible FTE. PositionSeatError: Existing position-capacity integrity rejects visible FTE. """ - _validate_snapshot_temporal_coordinate(effective_on, known_at) + known_at = _validate_snapshot_temporal_coordinate(effective_on, known_at) _validate_visible_employment_portfolios( employment_versions, diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index 3f86a6849..8987a9c38 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -13,6 +13,7 @@ from orgmetra_hris_kernel.facts import AssignmentFact, EmploymentVersion from orgmetra_hris_kernel.workforce import ( WorkforceCompositionSnapshot, + _validate_snapshot_temporal_coordinate, build_workforce_composition_snapshot, ) @@ -148,6 +149,7 @@ def build_workforce_composition_change_snapshot( Returns: Aggregate-only deterministic change evidence. """ + known_at = _validate_snapshot_temporal_coordinate(from_effective_on, known_at) opening = build_workforce_composition_snapshot( employment_versions, assignments, diff --git a/packages/hris-kernel/tests/test_workforce_composition_change.py b/packages/hris-kernel/tests/test_workforce_composition_change.py index bc7f27079..5445ff38b 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_change.py +++ b/packages/hris-kernel/tests/test_workforce_composition_change.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone, tzinfo from decimal import Decimal from uuid import UUID @@ -78,6 +78,18 @@ def _source_facts() -> tuple[list[EmploymentVersion], list[AssignmentFact]]: return employments, assignments +class _SequencedOffsetTimezone(tzinfo): + """Timezone provider that changes its offset on each request.""" + + def __init__(self) -> None: + self.calls = 0 + + def utcoffset(self, value: datetime | None) -> timedelta: + """Return different offsets to detect duplicate cutoff resolution.""" + self.calls += 1 + return timedelta(hours=self.calls - 1) + + def test_change_snapshot_compares_two_effective_dates_at_one_knowledge_cutoff() -> None: employments, assignments = _source_facts() snapshot = build_workforce_composition_change_snapshot( @@ -102,6 +114,23 @@ def test_change_snapshot_compares_two_effective_dates_at_one_knowledge_cutoff() assert len(snapshot.content_digest()) == 64 +def test_change_builder_freezes_one_cutoff_before_building_both_endpoints() -> None: + """Both change endpoints must use one detached instant from a mutable provider.""" + provider = _SequencedOffsetTimezone() + snapshot = build_workforce_composition_change_snapshot( + [], + [], + tenant_record_id=_id(1), + from_effective_on=date(2026, 1, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=provider), + ) + + assert provider.calls == 1 + assert snapshot.opening_snapshot.known_at == snapshot.closing_snapshot.known_at + assert snapshot.opening_snapshot.known_at == datetime(2026, 2, 20, tzinfo=timezone.utc) + + def test_change_snapshot_is_deterministic_for_reordered_source_facts() -> None: employments, assignments = _source_facts() first = build_workforce_composition_change_snapshot( diff --git a/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py b/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py index 6e753ebd5..f2d021a23 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py +++ b/packages/hris-kernel/tests/test_workforce_composition_temporal_integrity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone, tzinfo from decimal import Decimal from uuid import UUID @@ -32,6 +32,41 @@ def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] return "2099-12-31T23:59:59+00:00" +class _MutableOffsetTimezone(tzinfo): + """Timezone provider whose offset can change after evidence construction.""" + + def __init__(self, offset: timedelta) -> None: + self.offset = offset + + def utcoffset(self, value: datetime | None) -> timedelta: + """Return the currently configured offset.""" + return self.offset + + +class _UnknownOffsetTimezone(tzinfo): + """Timezone provider whose offset is intentionally indeterminate.""" + + def utcoffset(self, value: datetime | None) -> None: + """Return no offset so the datetime is not a usable absolute instant.""" + return None + + +class _ExplodingOffsetTimezone(tzinfo): + """Timezone provider that raises while its offset is requested.""" + + def utcoffset(self, value: datetime | None) -> timedelta: + """Raise to verify provider failures become interval errors.""" + raise RuntimeError("offset provider unavailable") + + +class _WrongOffsetTimezone(tzinfo): + """Timezone provider that returns a non-timedelta offset.""" + + def utcoffset(self, value: datetime | None) -> str: # type: ignore[override] + """Return an invalid offset type at the trust boundary.""" + return "not-a-timedelta" + + def snapshot(**overrides: object) -> WorkforceCompositionSnapshot: """Build one internally consistent aggregate snapshot for boundary mutation tests.""" values: dict[str, object] = { @@ -61,6 +96,68 @@ def test_rejects_datetime_subclass_that_can_forge_recorded_time_evidence() -> No snapshot(known_at=ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc)) +def test_snapshot_freezes_mutable_timezone_before_canonical_export() -> None: + """Changing a caller-owned timezone cannot change stored evidence or its digest.""" + provider = _MutableOffsetTimezone(timedelta(hours=2)) + evidence = snapshot(known_at=datetime(2026, 8, 21, 4, 30, tzinfo=provider)) + canonical = evidence.canonical_json() + digest = evidence.content_digest() + + provider.offset = timedelta(hours=3) + + assert evidence.known_at == datetime(2026, 8, 21, 2, 30, tzinfo=timezone.utc) + assert evidence.canonical_json() == canonical + assert evidence.content_digest() == digest + + +@pytest.mark.parametrize( + "known_at", + [ + datetime.min.replace(tzinfo=timezone(timedelta(hours=1))), + datetime.max.replace(tzinfo=timezone(-timedelta(hours=1))), + datetime(2026, 8, 21, 4, 30, tzinfo=_UnknownOffsetTimezone()), + datetime(2026, 8, 21, 4, 30, tzinfo=_ExplodingOffsetTimezone()), + datetime(2026, 8, 21, 4, 30, tzinfo=_WrongOffsetTimezone()), + ], +) +def test_rejects_unusable_recorded_time_provider(known_at: datetime) -> None: + """Unknown, malformed, failing, and overflowing timezone providers fail closed.""" + with pytest.raises(IntervalError, match="knowledge cutoff"): + snapshot(known_at=known_at) + + +def test_canonical_json_rejects_low_level_temporal_reinjection() -> None: + """Even an unsafe post-construction mutation cannot invoke forged time renderers.""" + effective_time_evidence = snapshot() + object.__setattr__(effective_time_evidence, "effective_on", ForgedDate(2026, 8, 21)) + with pytest.raises(IntervalError, match="not canonical"): + effective_time_evidence.canonical_json() + + recorded_time_evidence = snapshot() + object.__setattr__( + recorded_time_evidence, + "known_at", + ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc), + ) + with pytest.raises(IntervalError, match="not canonical"): + recorded_time_evidence.canonical_json() + + +def test_snapshot_detaches_mutable_status_count_containers() -> None: + """Caller-owned list mutation cannot alter aggregate evidence after construction.""" + status_counts = [["active", 1]] + evidence = snapshot(employment_status_counts=status_counts) + canonical = evidence.canonical_json() + digest = evidence.content_digest() + + status_counts[0][1] = 99 + status_counts.append(["leave", 0]) + + assert evidence.employment_status_counts == (("active", 1),) + assert evidence.canonical_json() == canonical + assert evidence.content_digest() == digest + + @pytest.mark.parametrize( "tenant_record_id", [ From e8190915dc64e6d96bf8b4916a32295bbe4c5f51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:22:24 +0900 Subject: [PATCH 23/59] fix(hris): reject impossible workforce staffing totals --- .../src/orgmetra_hris_kernel/workforce.py | 23 ++++++++ .../test_workforce_composition_boundaries.py | 56 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index 0f8806f86..f4434fdcc 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -139,6 +139,29 @@ def __post_init__(self) -> None: "Workforce snapshot aggregate values are internally inconsistent.", next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative FTE.", ) + if self.staffed_fte > Decimal(self.staffed_assignment_count): + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action=( + "Rebuild the snapshot so staffed FTE does not exceed one full allocation per " + "staffed assignment." + ), + ) + if self.staffed_assignment_count > 0 and self.staffed_fte <= _ZERO_FTE: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot so every staffed assignment contributes positive FTE.", + ) + if self.staffed_assignment_count > 0 and self.employment_count == 0: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot with reportable employment for every staffed assignment.", + ) + if self.staffed_assignment_count > 0 and self.person_headcount == 0: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot with a reportable person for every staffed assignment.", + ) if not all( type(count) is int and count >= 0 for _status, count in self.employment_status_counts diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index 84e9db62f..c7b6ba0d5 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -134,6 +134,62 @@ def test_direct_snapshot_rejects_non_decimal_staffed_fte() -> None: ) +def test_direct_snapshot_rejects_fte_without_staffed_assignments() -> None: + """Zero staffed assignments cannot carry positive FTE evidence.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + staffed_fte=Decimal("0.0001"), + ) + + +def test_direct_snapshot_rejects_zero_fte_with_staffed_assignments() -> None: + """A staffed assignment must contribute a positive allocation.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + staffed_assignment_count=1, + staffed_fte=Decimal("0.0000"), + unassigned_person_count=0, + ) + + +def test_direct_snapshot_rejects_staffing_without_employment_totals() -> None: + """Staffing cannot exist when no reportable employment is represented.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + employment_status_counts=(), + employment_count=0, + staffed_assignment_count=1, + staffed_fte=Decimal("0.5000"), + unassigned_person_count=0, + ) + + +def test_direct_snapshot_rejects_staffing_without_people() -> None: + """Staffing cannot exist when no reportable person is represented.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + person_headcount=0, + staffed_assignment_count=1, + staffed_fte=Decimal("0.5000"), + unassigned_person_count=0, + ) + + +def test_direct_snapshot_rejects_fte_above_staffed_assignment_count() -> None: + """Each staffed assignment contributes at most one full-time allocation.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + staffed_assignment_count=1, + staffed_fte=Decimal("1.0001"), + unassigned_person_count=0, + ) + + def test_direct_snapshot_rejects_boolean_status_counts() -> None: """Boolean values must not serialize as employment counts.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): From 8b4aa1d4ec93759b3701b32fa006f41de7763ed2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:38:32 +0900 Subject: [PATCH 24/59] docs: trace workforce staffing invariants --- docs/adr/0011-bitemporal-workforce-composition.md | 1 + docs/traceability/workforce-composition.md | 1 + manifest.json | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index aabd3bf31..058c45c69 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -19,6 +19,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Employment count preserves the number of visible reportable employment relationships. - Before aggregation, the snapshot reuses the HRIS employment-concurrency invariant at the report coordinate. Two overlapping `exclusive` employments or an unknown concurrency code fail closed instead of being normalized into plausible headcount. - Staffed assignment count and staffed FTE are computed from visible assignments after reusing the existing assignment-to-employment coverage, per-employment allocation, and position-seat capacity integrity rules. +- Direct aggregate construction repeats the resulting staffing relationships: zero assignments require zero FTE, staffed assignments require positive FTE and reportable employment/person totals, and staffed FTE cannot exceed the assignment count. - One overfilled Position seat therefore remains a data-integrity failure even when the aggregate FTE total itself looks plausible. - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index 40fd0ddb2..2de2dfe95 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -10,6 +10,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Avoid double-counting valid concurrent workers | ADR 0011 | distinct `person_record_id` set across visible reportable employments | concurrent-employment fixture expects 2 people from 3 reportable employments | | Reject impossible employment portfolios before aggregation | Existing employment-concurrency invariant + ADR 0011 | `_validate_visible_employment_portfolios` reuses `validate_person_employment_exclusivity` at the report coordinate | overlapping-exclusive-employment regression expects fail-closed `EmploymentExclusivityError` | | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | +| Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot.__post_init__` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, and FTE above the assignment count | five direct-construction staffing boundary regressions | | Reject overfilled Position seats before aggregation | Existing position-seat invariant + ADR 0011 | each visible `position_record_id` is revalidated with `validate_position_seat_capacity` at the report coordinate | two distinct workers allocating 0.6000 each to one Position must raise `PositionSeatError` instead of reporting 1.2000 staffed FTE | | Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment allocation regressions | | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | diff --git a/manifest.json b/manifest.json index 7420e6ef7..523b89651 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"e10a46e5476cc88121f13525faaa80daa692b940ef251f7d63cfbe7607a0f50f","bytes":5787,"lines":54},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"8a2a8be6a01e50f419bad486116b869bcd9f2097cb0c8fbcf1e86550183470df","bytes":6034,"lines":55},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} From 6ada6de64a8a042bdfaa3b11e98f5bf99c932063 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:45:14 +0900 Subject: [PATCH 25/59] fix(hris): reconcile assigned workforce people --- .../0011-bitemporal-workforce-composition.md | 2 +- docs/traceability/workforce-composition.md | 2 +- manifest.json | 2 +- .../src/orgmetra_hris_kernel/workforce.py | 16 +++++++++ .../test_workforce_composition_boundaries.py | 34 +++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index 058c45c69..1f28e855a 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -19,7 +19,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Employment count preserves the number of visible reportable employment relationships. - Before aggregation, the snapshot reuses the HRIS employment-concurrency invariant at the report coordinate. Two overlapping `exclusive` employments or an unknown concurrency code fail closed instead of being normalized into plausible headcount. - Staffed assignment count and staffed FTE are computed from visible assignments after reusing the existing assignment-to-employment coverage, per-employment allocation, and position-seat capacity integrity rules. -- Direct aggregate construction repeats the resulting staffing relationships: zero assignments require zero FTE, staffed assignments require positive FTE and reportable employment/person totals, and staffed FTE cannot exceed the assignment count. +- Direct aggregate construction repeats the resulting staffing relationships: zero assignments require zero FTE and zero assigned people, staffed assignments require positive FTE and reportable employment/person totals, assigned people cannot exceed assignments, and staffed FTE cannot exceed the assignment count. - One overfilled Position seat therefore remains a data-integrity failure even when the aggregate FTE total itself looks plausible. - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index 2de2dfe95..bf053200e 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -10,7 +10,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Avoid double-counting valid concurrent workers | ADR 0011 | distinct `person_record_id` set across visible reportable employments | concurrent-employment fixture expects 2 people from 3 reportable employments | | Reject impossible employment portfolios before aggregation | Existing employment-concurrency invariant + ADR 0011 | `_validate_visible_employment_portfolios` reuses `validate_person_employment_exclusivity` at the report coordinate | overlapping-exclusive-employment regression expects fail-closed `EmploymentExclusivityError` | | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | -| Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot.__post_init__` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, and FTE above the assignment count | five direct-construction staffing boundary regressions | +| Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot.__post_init__` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, impossible assigned-person reconciliation, and FTE above the assignment count | eight direct-construction staffing boundary regressions | | Reject overfilled Position seats before aggregation | Existing position-seat invariant + ADR 0011 | each visible `position_record_id` is revalidated with `validate_position_seat_capacity` at the report coordinate | two distinct workers allocating 0.6000 each to one Position must raise `PositionSeatError` instead of reporting 1.2000 staffed FTE | | Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment allocation regressions | | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | diff --git a/manifest.json b/manifest.json index 523b89651..6e2b7eb23 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"8a2a8be6a01e50f419bad486116b869bcd9f2097cb0c8fbcf1e86550183470df","bytes":6034,"lines":55},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"f5662f4ebd5398bc41d249dfcfa96e5fd85486fe0162f3ac9a81839fafeec1ee","bytes":6102,"lines":55},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index f4434fdcc..4e3ec0efa 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -178,6 +178,22 @@ def __post_init__(self) -> None: "unassigned-person totals reconcile." ), ) + assigned_person_count = self.person_headcount - self.unassigned_person_count + if self.staffed_assignment_count == 0 and assigned_person_count != 0: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot so people without assignments are counted as unassigned.", + ) + if self.staffed_assignment_count > 0 and assigned_person_count == 0: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot with an assigned person for every staffed workforce.", + ) + if assigned_person_count > self.staffed_assignment_count: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot so every assigned person has a staffed assignment.", + ) if any(status not in _WORKFORCE_INCLUDED_STATUSES for status in status_codes): raise SingleValuedFactError( "Workforce snapshot aggregate values are internally inconsistent.", diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index c7b6ba0d5..b4d3561db 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -190,6 +190,40 @@ def test_direct_snapshot_rejects_fte_above_staffed_assignment_count() -> None: ) +def test_direct_snapshot_rejects_assigned_person_without_staffing() -> None: + """A person counted as assigned requires at least one staffed assignment.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + unassigned_person_count=0, + ) + + +def test_direct_snapshot_rejects_staffing_when_every_person_is_unassigned() -> None: + """Staffed assignments cannot coexist with an entirely unassigned workforce.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + staffed_assignment_count=1, + staffed_fte=Decimal("0.5000"), + unassigned_person_count=1, + ) + + +def test_direct_snapshot_rejects_more_assigned_people_than_assignments() -> None: + """Distinct assigned people cannot exceed the number of staffed assignments.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + _direct_snapshot( + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + person_headcount=2, + employment_count=2, + employment_status_counts=(("active", 2),), + staffed_assignment_count=1, + staffed_fte=Decimal("0.5000"), + unassigned_person_count=0, + ) + + def test_direct_snapshot_rejects_boolean_status_counts() -> None: """Boolean values must not serialize as employment counts.""" with pytest.raises(SingleValuedFactError, match="internally inconsistent"): From c27a9f6c9949acd29d3ee90fd1e9662c603e7cf2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:55:30 +0900 Subject: [PATCH 26/59] fix(hris): make workforce FTE deltas deterministic --- .../0011-bitemporal-workforce-composition.md | 3 +- docs/traceability/workforce-composition.md | 1 + manifest.json | 2 +- .../orgmetra_hris_kernel/workforce_change.py | 26 ++++++++++- .../test_workforce_composition_change.py | 44 ++++++++++++++++++- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index 1f28e855a..3250514e2 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -24,6 +24,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. - The snapshot freezes the timezone-aware knowledge cutoff to an exact UTC datetime and detaches status-count containers before validation, so mutable caller objects cannot change canonical evidence after construction. +- Workforce-change FTE deltas align the finite Decimal coefficients before subtraction, so the caller's ambient Decimal precision cannot change the reported delta, canonical JSON, or content digest. - Two visible versions of one Employment or Assignment identity fail closed. Invalid assignment coverage or over-allocation remains a data-integrity error rather than becoming a plausible metric. - The canonical JSON contains the opaque tenant identifier, report coordinates, aggregate metrics, and schema version only. It excludes person, employment, assignment, and position identifiers and all human-readable PII. - SHA-256 addresses the exact canonical UTF-8 representation so a caller can correlate a report with immutable audit evidence without copying source rows. @@ -48,7 +49,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit ## Verification -`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, and `packages/hris-kernel/tests/test_workforce_position_capacity.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. +`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, and `packages/hris-kernel/tests/test_workforce_composition_change.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, context-independent workforce-change FTE deltas, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. ## References diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index bf053200e..b623b705e 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -17,6 +17,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | | Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | | Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time or status containers | snapshot construction stores exact UTC time and detached status-count tuples; canonical export rejects low-level temporal reinjection | mutable timezone and mutable status-container regressions require stable canonical JSON and digest | +| Keep workforce-change FTE evidence deterministic | ADR 0011; aggregate deltas must not inherit caller Decimal context | `WorkforceCompositionChangeSnapshot` aligns finite Decimal coefficients before subtraction, so `staffed_fte_change`, canonical JSON, and its digest are context-independent | ambient-precision regression compares low- and normal-precision delta evidence | | Keep workforce intelligence descriptive | ADR 0011 | module contains no recommendation, decision, protected-attribute inference or persistence API | public package boundary and code review; high-impact actions remain outside this slice | | Ground scope in current authoritative standards without claiming certification | ISO 30414:2025 public catalogue metadata; ADR 0011 | no proprietary ISO metric text is embedded in production code | `docs/doctoring/workforce-composition-references.md` | | Keep exact owned coverage reproducible | Orgmetra quality policy | `.github/workflows/workforce-intelligence-quality.yml` checks exact candidate SHA and runs the complete HRIS kernel | hosted exact-head workflow with package 100% statement/branch threshold | diff --git a/manifest.json b/manifest.json index 6e2b7eb23..d88c4a8eb 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"f5662f4ebd5398bc41d249dfcfa96e5fd85486fe0162f3ac9a81839fafeec1ee","bytes":6102,"lines":55},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"623482091b1cdccdb296dac9c69218724cf6a9404b07a379b1bd6fd908ba324e","bytes":6417,"lines":56},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index 8987a9c38..fe8c52c95 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -18,6 +18,27 @@ ) +def _exact_decimal_difference(closing: Decimal, opening: Decimal) -> Decimal: + """Subtract two finite Decimal values without using ambient precision.""" + closing_parts = closing.as_tuple() + opening_parts = opening.as_tuple() + common_exponent = min(closing_parts.exponent, opening_parts.exponent) + closing_coefficient = int("".join(map(str, closing_parts.digits))) + opening_coefficient = int("".join(map(str, opening_parts.digits))) + closing_coefficient *= (1, -1)[closing_parts.sign] + opening_coefficient *= (1, -1)[opening_parts.sign] + closing_coefficient *= 10 ** (closing_parts.exponent - common_exponent) + opening_coefficient *= 10 ** (opening_parts.exponent - common_exponent) + difference = closing_coefficient - opening_coefficient + return Decimal( + ( + int(difference < 0), + tuple(int(digit) for digit in str(abs(difference))), + common_exponent, + ) + ) + + @dataclass(frozen=True, slots=True) class WorkforceCompositionChangeSnapshot: """Compare two aggregate workforce states at one recorded-time cutoff. @@ -80,7 +101,10 @@ def staffed_assignment_count_change(self) -> int: @property def staffed_fte_change(self) -> Decimal: """Return exact closing staffed FTE minus opening staffed FTE.""" - return self.closing_snapshot.staffed_fte - self.opening_snapshot.staffed_fte + return _exact_decimal_difference( + self.closing_snapshot.staffed_fte, + self.opening_snapshot.staffed_fte, + ) @property def unassigned_person_count_change(self) -> int: diff --git a/packages/hris-kernel/tests/test_workforce_composition_change.py b/packages/hris-kernel/tests/test_workforce_composition_change.py index 5445ff38b..63cc7e15d 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_change.py +++ b/packages/hris-kernel/tests/test_workforce_composition_change.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import date, datetime, timedelta, timezone, tzinfo -from decimal import Decimal +from decimal import Decimal, localcontext from uuid import UUID import pytest @@ -16,6 +16,7 @@ IntervalError, RecordedInterval, WorkforceCompositionChangeSnapshot, + WorkforceCompositionSnapshot, build_workforce_composition_change_snapshot, build_workforce_composition_snapshot, ) @@ -78,6 +79,21 @@ def _source_facts() -> tuple[list[EmploymentVersion], list[AssignmentFact]]: return employments, assignments +def _aggregate_snapshot(effective_on: date, staffed_fte: str) -> WorkforceCompositionSnapshot: + """Build one valid aggregate endpoint with a controlled FTE spelling.""" + return WorkforceCompositionSnapshot( + tenant_record_id=_id(1), + effective_on=effective_on, + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + person_headcount=1, + employment_count=1, + staffed_assignment_count=1, + staffed_fte=Decimal(staffed_fte), + unassigned_person_count=0, + employment_status_counts=(("active", 1),), + ) + + class _SequencedOffsetTimezone(tzinfo): """Timezone provider that changes its offset on each request.""" @@ -114,6 +130,32 @@ def test_change_snapshot_compares_two_effective_dates_at_one_knowledge_cutoff() assert len(snapshot.content_digest()) == 64 +def test_staffed_fte_change_is_independent_of_decimal_context_precision() -> None: + """FTE deltas and their evidence must not depend on a caller's Decimal precision.""" + snapshot = WorkforceCompositionChangeSnapshot( + _aggregate_snapshot(date(2026, 1, 15), "0.1234"), + _aggregate_snapshot(date(2026, 2, 15), "0.2345"), + ) + + with localcontext() as context: + context.prec = 2 + low_precision = ( + snapshot.staffed_fte_change, + snapshot.canonical_json(), + snapshot.content_digest(), + ) + with localcontext() as context: + context.prec = 28 + normal_precision = ( + snapshot.staffed_fte_change, + snapshot.canonical_json(), + snapshot.content_digest(), + ) + + assert low_precision == normal_precision + assert low_precision[0] == Decimal("0.1111") + + def test_change_builder_freezes_one_cutoff_before_building_both_endpoints() -> None: """Both change endpoints must use one detached instant from a mutable provider.""" provider = _SequencedOffsetTimezone() From d829fba116b2a9843f1ffc8a09c72f38d732e511 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:08:34 +0900 Subject: [PATCH 27/59] fix(hris): make workforce FTE aggregation exact --- .../0011-bitemporal-workforce-composition.md | 2 +- docs/traceability/workforce-composition.md | 2 +- manifest.json | 2 +- .../src/orgmetra_hris_kernel/workforce.py | 27 ++++++++- .../orgmetra_hris_kernel/workforce_change.py | 30 ++-------- .../test_workforce_composition_change.py | 59 +++++++++++++++++++ 6 files changed, 92 insertions(+), 30 deletions(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index 3250514e2..c96394609 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -24,7 +24,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. - The snapshot freezes the timezone-aware knowledge cutoff to an exact UTC datetime and detaches status-count containers before validation, so mutable caller objects cannot change canonical evidence after construction. -- Workforce-change FTE deltas align the finite Decimal coefficients before subtraction, so the caller's ambient Decimal precision cannot change the reported delta, canonical JSON, or content digest. +- Endpoint FTE totals and workforce-change deltas align finite Decimal coefficients before arithmetic, so the caller's ambient Decimal precision cannot change endpoint evidence, the reported delta, canonical JSON, or content digest. - Two visible versions of one Employment or Assignment identity fail closed. Invalid assignment coverage or over-allocation remains a data-integrity error rather than becoming a plausible metric. - The canonical JSON contains the opaque tenant identifier, report coordinates, aggregate metrics, and schema version only. It excludes person, employment, assignment, and position identifiers and all human-readable PII. - SHA-256 addresses the exact canonical UTF-8 representation so a caller can correlate a report with immutable audit evidence without copying source rows. diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index b623b705e..27a21ff6f 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -17,7 +17,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | | Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | | Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time or status containers | snapshot construction stores exact UTC time and detached status-count tuples; canonical export rejects low-level temporal reinjection | mutable timezone and mutable status-container regressions require stable canonical JSON and digest | -| Keep workforce-change FTE evidence deterministic | ADR 0011; aggregate deltas must not inherit caller Decimal context | `WorkforceCompositionChangeSnapshot` aligns finite Decimal coefficients before subtraction, so `staffed_fte_change`, canonical JSON, and its digest are context-independent | ambient-precision regression compares low- and normal-precision delta evidence | +| Keep workforce FTE evidence deterministic | ADR 0011; aggregate totals and deltas must not inherit caller Decimal context | shared `_exact_decimal_total` aligns finite Decimal coefficients before endpoint aggregation and change subtraction, so endpoint JSON, `staffed_fte_change`, canonical JSON, and digests are context-independent | ambient-precision regression compares low- and normal-precision endpoint and change evidence | | Keep workforce intelligence descriptive | ADR 0011 | module contains no recommendation, decision, protected-attribute inference or persistence API | public package boundary and code review; high-impact actions remain outside this slice | | Ground scope in current authoritative standards without claiming certification | ISO 30414:2025 public catalogue metadata; ADR 0011 | no proprietary ISO metric text is embedded in production code | `docs/doctoring/workforce-composition-references.md` | | Keep exact owned coverage reproducible | Orgmetra quality policy | `.github/workflows/workforce-intelligence-quality.yml` checks exact candidate SHA and runs the complete HRIS kernel | hosted exact-head workflow with package 100% statement/branch threshold | diff --git a/manifest.json b/manifest.json index d88c4a8eb..720e9a3ed 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"623482091b1cdccdb296dac9c69218724cf6a9404b07a379b1bd6fd908ba324e","bytes":6417,"lines":56},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"820e7c42750464565e3132baa245c48ba55bc7ea7b28bd0296c929282b56238b","bytes":6451,"lines":56},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index 4e3ec0efa..2e35bb25b 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -29,6 +29,27 @@ _ZERO_FTE = Decimal("0.0000") +def _exact_decimal_total(values: tuple[Decimal, ...]) -> Decimal: + """Sum finite Decimal values exactly without using ambient precision.""" + if not values: + return _ZERO_FTE + parts = tuple(value.as_tuple() for value in values) + common_exponent = min(part.exponent for part in parts) + coefficient = sum( + (1, -1)[part.sign] + * int("".join(map(str, part.digits))) + * 10 ** (part.exponent - common_exponent) + for part in parts + ) + return Decimal( + ( + int(coefficient < 0), + tuple(int(digit) for digit in str(abs(coefficient))), + common_exponent, + ) + ) + + def _validate_snapshot_tenant_id(tenant_record_id: UUID) -> None: """Require one exact, non-sentinel tenant UUID before emitting evidence.""" if type(tenant_record_id) is not UUID or tenant_record_id.int in {0, (1 << 128) - 1}: @@ -396,7 +417,6 @@ def build_workforce_composition_snapshot( portfolio_keys: set[tuple[UUID, UUID]] = set() position_record_ids: set[UUID] = set() staffed_people: set[UUID] = set() - staffed_fte = _ZERO_FTE staffed_assignment_count = 0 for assignment in visible_assignments: @@ -408,7 +428,6 @@ def build_workforce_composition_snapshot( portfolio_keys.add((assignment.person_record_id, assignment.employment_record_id)) position_record_ids.add(assignment.position_record_id) staffed_people.add(assignment.person_record_id) - staffed_fte += assignment.allocation_ratio staffed_assignment_count += 1 for person_record_id, employment_record_id in portfolio_keys: @@ -438,7 +457,9 @@ def build_workforce_composition_snapshot( person_headcount=len(workforce_people), employment_count=len(visible_employments), staffed_assignment_count=staffed_assignment_count, - staffed_fte=staffed_fte, + staffed_fte=_exact_decimal_total( + tuple(assignment.allocation_ratio for assignment in visible_assignments) + ), unassigned_person_count=len(workforce_people - staffed_people), employment_status_counts=tuple(sorted(status_counts.items())), ) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index fe8c52c95..9cef1f6db 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -13,32 +13,12 @@ from orgmetra_hris_kernel.facts import AssignmentFact, EmploymentVersion from orgmetra_hris_kernel.workforce import ( WorkforceCompositionSnapshot, + _exact_decimal_total, _validate_snapshot_temporal_coordinate, build_workforce_composition_snapshot, ) -def _exact_decimal_difference(closing: Decimal, opening: Decimal) -> Decimal: - """Subtract two finite Decimal values without using ambient precision.""" - closing_parts = closing.as_tuple() - opening_parts = opening.as_tuple() - common_exponent = min(closing_parts.exponent, opening_parts.exponent) - closing_coefficient = int("".join(map(str, closing_parts.digits))) - opening_coefficient = int("".join(map(str, opening_parts.digits))) - closing_coefficient *= (1, -1)[closing_parts.sign] - opening_coefficient *= (1, -1)[opening_parts.sign] - closing_coefficient *= 10 ** (closing_parts.exponent - common_exponent) - opening_coefficient *= 10 ** (opening_parts.exponent - common_exponent) - difference = closing_coefficient - opening_coefficient - return Decimal( - ( - int(difference < 0), - tuple(int(digit) for digit in str(abs(difference))), - common_exponent, - ) - ) - - @dataclass(frozen=True, slots=True) class WorkforceCompositionChangeSnapshot: """Compare two aggregate workforce states at one recorded-time cutoff. @@ -101,9 +81,11 @@ def staffed_assignment_count_change(self) -> int: @property def staffed_fte_change(self) -> Decimal: """Return exact closing staffed FTE minus opening staffed FTE.""" - return _exact_decimal_difference( - self.closing_snapshot.staffed_fte, - self.opening_snapshot.staffed_fte, + return _exact_decimal_total( + ( + self.closing_snapshot.staffed_fte, + self.opening_snapshot.staffed_fte.copy_negate(), + ) ) @property diff --git a/packages/hris-kernel/tests/test_workforce_composition_change.py b/packages/hris-kernel/tests/test_workforce_composition_change.py index 63cc7e15d..d51679c60 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_change.py +++ b/packages/hris-kernel/tests/test_workforce_composition_change.py @@ -156,6 +156,65 @@ def test_staffed_fte_change_is_independent_of_decimal_context_precision() -> Non assert low_precision[0] == Decimal("0.1111") +def test_workforce_fte_evidence_is_independent_of_decimal_context_precision() -> None: + """Endpoint totals and change evidence must preserve every allocation digit.""" + employments = [ + _employment(101, 1001, 11, effective_start=date(2026, 1, 1)), + _employment(102, 1002, 12, effective_start=date(2026, 1, 1)), + _employment(103, 1003, 13, effective_start=date(2026, 1, 1)), + ] + assignments = [ + _assignment(201, 101, 11, effective_start=date(2026, 1, 1), ratio="0.1234"), + _assignment(202, 102, 12, effective_start=date(2026, 1, 1), ratio="0.2345"), + _assignment(203, 103, 13, effective_start=date(2026, 1, 1), ratio="0.3456"), + ] + + with localcontext() as context: + context.prec = 2 + low_precision_snapshot = build_workforce_composition_change_snapshot( + employments, + assignments, + tenant_record_id=_id(1), + from_effective_on=date(2026, 1, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + low_precision_evidence = ( + low_precision_snapshot.opening_snapshot.staffed_fte, + low_precision_snapshot.opening_snapshot.canonical_json(), + low_precision_snapshot.closing_snapshot.canonical_json(), + low_precision_snapshot.staffed_fte_change, + low_precision_snapshot.canonical_json(), + low_precision_snapshot.opening_snapshot.content_digest(), + low_precision_snapshot.closing_snapshot.content_digest(), + low_precision_snapshot.content_digest(), + ) + with localcontext() as context: + context.prec = 28 + normal_precision_snapshot = build_workforce_composition_change_snapshot( + employments, + assignments, + tenant_record_id=_id(1), + from_effective_on=date(2026, 1, 15), + to_effective_on=date(2026, 2, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + ) + normal_precision_evidence = ( + normal_precision_snapshot.opening_snapshot.staffed_fte, + normal_precision_snapshot.opening_snapshot.canonical_json(), + normal_precision_snapshot.closing_snapshot.canonical_json(), + normal_precision_snapshot.staffed_fte_change, + normal_precision_snapshot.canonical_json(), + normal_precision_snapshot.opening_snapshot.content_digest(), + normal_precision_snapshot.closing_snapshot.content_digest(), + normal_precision_snapshot.content_digest(), + ) + + assert low_precision_evidence == normal_precision_evidence + assert low_precision_evidence[0] == Decimal("0.7035") + assert low_precision_evidence[3] == Decimal("0.0000") + + def test_change_builder_freezes_one_cutoff_before_building_both_endpoints() -> None: """Both change endpoints must use one detached instant from a mutable provider.""" provider = _SequencedOffsetTimezone() From fd39e07b2ab3490a3ebe0cecd6361f52f162c5d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:17:41 +0900 Subject: [PATCH 28/59] fix(hris): enforce exact allocation limits --- .../0011-bitemporal-workforce-composition.md | 3 ++- docs/traceability/workforce-composition.md | 2 +- manifest.json | 2 +- .../src/orgmetra_hris_kernel/assignment.py | 26 +++++++++++++++++-- .../src/orgmetra_hris_kernel/workforce.py | 22 +--------------- .../orgmetra_hris_kernel/workforce_change.py | 2 +- .../tests/test_assignment_portfolio.py | 22 +++++++++++++++- .../tests/test_position_coverage.py | 26 ++++++++++++++++++- 8 files changed, 76 insertions(+), 29 deletions(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index c96394609..07a04df95 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -25,6 +25,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Status counts are aggregate employment evidence, sorted deterministically. - The snapshot freezes the timezone-aware knowledge cutoff to an exact UTC datetime and detaches status-count containers before validation, so mutable caller objects cannot change canonical evidence after construction. - Endpoint FTE totals and workforce-change deltas align finite Decimal coefficients before arithmetic, so the caller's ambient Decimal precision cannot change endpoint evidence, the reported delta, canonical JSON, or content digest. +- Employment-portfolio and Position-seat limits use the same exact Decimal coefficient total, so low caller precision cannot admit an overallocated staffing total. - Two visible versions of one Employment or Assignment identity fail closed. Invalid assignment coverage or over-allocation remains a data-integrity error rather than becoming a plausible metric. - The canonical JSON contains the opaque tenant identifier, report coordinates, aggregate metrics, and schema version only. It excludes person, employment, assignment, and position identifiers and all human-readable PII. - SHA-256 addresses the exact canonical UTF-8 representation so a caller can correlate a report with immutable audit evidence without copying source rows. @@ -49,7 +50,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit ## Verification -`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, and `packages/hris-kernel/tests/test_workforce_composition_change.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, context-independent workforce-change FTE deltas, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. +`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, and `packages/hris-kernel/tests/test_assignment_portfolio.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. ## References diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index 27a21ff6f..de19dc656 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -12,7 +12,7 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | | Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot.__post_init__` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, impossible assigned-person reconciliation, and FTE above the assignment count | eight direct-construction staffing boundary regressions | | Reject overfilled Position seats before aggregation | Existing position-seat invariant + ADR 0011 | each visible `position_record_id` is revalidated with `validate_position_seat_capacity` at the report coordinate | two distinct workers allocating 0.6000 each to one Position must raise `PositionSeatError` instead of reporting 1.2000 staffed FTE | -| Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment allocation regressions | +| Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and exact allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment/Position allocation regressions, including low-precision Decimal cases | | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | | Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | diff --git a/manifest.json b/manifest.json index 720e9a3ed..8437774bf 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"820e7c42750464565e3132baa245c48ba55bc7ea7b28bd0296c929282b56238b","bytes":6451,"lines":56},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"27582c5fafe42b6e5c73b6cc6b504fa2f9067eb6086d837c39ce4852a53f0227","bytes":6733,"lines":57},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py b/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py index 710ecbcd7..2efbefb17 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py @@ -18,10 +18,32 @@ _ONE = Decimal("1.0000") _ZERO = Decimal("0") +_ZERO_FTE = Decimal("0.0000") _ASSIGNMENT_ELIGIBLE_EMPLOYMENT_STATUSES = frozenset({"active", "leave"}) _STAFFABLE_POSITION_STATUSES = frozenset({"active", "open"}) +def _exact_decimal_total(values: tuple[Decimal, ...]) -> Decimal: + """Sum finite Decimal values exactly without using ambient precision.""" + if not values: + return _ZERO_FTE + parts = tuple(value.as_tuple() for value in values) + common_exponent = min(part.exponent for part in parts) + coefficient = sum( + (1, -1)[part.sign] + * int("".join(map(str, part.digits))) + * 10 ** (part.exponent - common_exponent) + for part in parts + ) + return Decimal( + ( + int(coefficient < 0), + tuple(int(digit) for digit in str(abs(coefficient))), + common_exponent, + ) + ) + + def _ratio_is_valid(allocation_ratio: Decimal) -> bool: """Return whether one assignment row stays inside (0, 1.0000].""" return allocation_ratio > _ZERO and allocation_ratio <= _ONE @@ -83,7 +105,7 @@ def validate_assignment_portfolio( effective_on=effective_on, known_at=known_at, ) - total = sum((fact.allocation_ratio for fact in visible), start=_ZERO) + total = _exact_decimal_total(tuple(fact.allocation_ratio for fact in visible)) if total > _ONE: raise AssignmentPortfolioError( "Visible allocations for one employment exceed 1.0000.", @@ -220,7 +242,7 @@ def validate_position_seat_capacity( effective_on=effective_on, known_at=known_at, ) - total = sum((fact.allocation_ratio for fact in visible), start=_ZERO) + total = _exact_decimal_total(tuple(fact.allocation_ratio for fact in visible)) if total > _ONE: raise PositionSeatError( "Visible allocations for one position exceed 1.0000.", diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index 2e35bb25b..8f028a6d4 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -16,6 +16,7 @@ from uuid import UUID from orgmetra_hris_kernel.assignment import ( + _exact_decimal_total, validate_assignment_employment_coverage, validate_assignment_portfolio, validate_position_seat_capacity, @@ -29,27 +30,6 @@ _ZERO_FTE = Decimal("0.0000") -def _exact_decimal_total(values: tuple[Decimal, ...]) -> Decimal: - """Sum finite Decimal values exactly without using ambient precision.""" - if not values: - return _ZERO_FTE - parts = tuple(value.as_tuple() for value in values) - common_exponent = min(part.exponent for part in parts) - coefficient = sum( - (1, -1)[part.sign] - * int("".join(map(str, part.digits))) - * 10 ** (part.exponent - common_exponent) - for part in parts - ) - return Decimal( - ( - int(coefficient < 0), - tuple(int(digit) for digit in str(abs(coefficient))), - common_exponent, - ) - ) - - def _validate_snapshot_tenant_id(tenant_record_id: UUID) -> None: """Require one exact, non-sentinel tenant UUID before emitting evidence.""" if type(tenant_record_id) is not UUID or tenant_record_id.int in {0, (1 << 128) - 1}: diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index 9cef1f6db..f028c320c 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -9,11 +9,11 @@ import json from uuid import UUID +from orgmetra_hris_kernel.assignment import _exact_decimal_total from orgmetra_hris_kernel.errors import IdentityScopeError, IntervalError from orgmetra_hris_kernel.facts import AssignmentFact, EmploymentVersion from orgmetra_hris_kernel.workforce import ( WorkforceCompositionSnapshot, - _exact_decimal_total, _validate_snapshot_temporal_coordinate, build_workforce_composition_snapshot, ) diff --git a/packages/hris-kernel/tests/test_assignment_portfolio.py b/packages/hris-kernel/tests/test_assignment_portfolio.py index b064f3350..1525ad892 100644 --- a/packages/hris-kernel/tests/test_assignment_portfolio.py +++ b/packages/hris-kernel/tests/test_assignment_portfolio.py @@ -2,7 +2,7 @@ from dataclasses import replace from datetime import date -from decimal import Decimal +from decimal import Decimal, localcontext from uuid import UUID import pytest @@ -67,6 +67,26 @@ def test_portfolio_rejects_allocation_above_one_for_one_employment( ) +def test_portfolio_rejects_exact_overallocation_under_low_decimal_precision( + jordan_icu_assignment, + jordan_float_assignment, +) -> None: + """Allocation limits must use exact totals even when Decimal precision is low.""" + first = replace(jordan_icu_assignment, allocation_ratio=Decimal("0.5040")) + second = replace(jordan_float_assignment, allocation_ratio=Decimal("0.5040")) + with localcontext() as context: + context.prec = 2 + with pytest.raises(AssignmentPortfolioError, match="1.0000"): + validate_assignment_portfolio( + [first, second], + tenant_record_id=TENANT, + person_record_id=JORDAN, + employment_record_id=JORDAN_EMPLOYMENT, + effective_on=date(2024, 5, 1), + known_at=utc(2024, 5, 1), + ) + + def test_portfolio_ignores_another_person_and_another_employment( jordan_icu_assignment, ) -> None: diff --git a/packages/hris-kernel/tests/test_position_coverage.py b/packages/hris-kernel/tests/test_position_coverage.py index e47591e31..162f74406 100644 --- a/packages/hris-kernel/tests/test_position_coverage.py +++ b/packages/hris-kernel/tests/test_position_coverage.py @@ -2,7 +2,7 @@ from dataclasses import replace from datetime import date -from decimal import Decimal +from decimal import Decimal, localcontext from uuid import UUID import pytest @@ -115,6 +115,30 @@ def test_riley_cannot_take_a_full_icu_seat_already_held_by_jordan( ) +def test_position_rejects_exact_overallocation_under_low_decimal_precision( + jordan_icu_assignment, +) -> None: + """Seat limits must use exact totals even when Decimal precision is low.""" + riley = replace( + jordan_icu_assignment, + assignment_record_id=UUID("10000000-0000-7000-8000-000000000311"), + employment_record_id=RILEY_EMPLOYMENT, + person_record_id=RILEY, + allocation_ratio=Decimal("0.5040"), + ) + jordan = replace(jordan_icu_assignment, allocation_ratio=Decimal("0.5040")) + with localcontext() as context: + context.prec = 2 + with pytest.raises(PositionSeatError, match="1.0000"): + validate_position_seat_capacity( + [jordan, riley], + tenant_record_id=TENANT, + position_record_id=ICU_POSITION, + effective_on=date(2024, 4, 15), + known_at=utc(2024, 4, 15), + ) + + def test_foreign_tenant_assignment_does_not_consume_local_seat_capacity( jordan_icu_assignment, ) -> None: From e6e85c8ac779ff07308cadce874da4d3a15b6f2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:34:22 -0700 Subject: [PATCH 29/59] test(hris): reproduce allocation and evidence integrity gaps --- ...ocation_and_snapshot_export_regressions.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py diff --git a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py new file mode 100644 index 000000000..216f28511 --- /dev/null +++ b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py @@ -0,0 +1,87 @@ +"""Review regressions for assignment precision and workforce evidence export.""" + +from datetime import date, datetime, timezone +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_hris_kernel import ( + AssignmentFact, + AssignmentPortfolioError, + DateInterval, + PositionSeatError, + RecordedInterval, + SingleValuedFactError, + WorkforceCompositionSnapshot, + validate_assignment_portfolio, + validate_position_seat_capacity, +) + + +def _id(value: int) -> UUID: + """Return a stable opaque UUID fixture.""" + return UUID(int=value) + + +def _assignment(allocation_ratio: Decimal) -> AssignmentFact: + """Build one visible assignment using a caller-provided allocation ratio.""" + return AssignmentFact( + tenant_record_id=_id(1), + assignment_record_id=_id(201), + employment_record_id=_id(101), + person_record_id=_id(11), + position_record_id=_id(1201), + allocation_ratio=allocation_ratio, + effective=DateInterval(date(2026, 1, 1)), + recorded=RecordedInterval(datetime(2026, 1, 1, tzinfo=timezone.utc)), + ) + + +def test_snapshot_export_rejects_post_construction_aggregate_mutation() -> None: + """Canonical evidence must not emit aggregate state that bypassed construction checks.""" + snapshot = WorkforceCompositionSnapshot( + tenant_record_id=_id(1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + person_headcount=1, + employment_count=1, + staffed_assignment_count=0, + staffed_fte=Decimal("0.0000"), + unassigned_person_count=1, + employment_status_counts=(("active", 1),), + ) + + object.__setattr__(snapshot, "staffed_fte", Decimal("0.5000")) + + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + snapshot.canonical_json() + + +def test_portfolio_rejects_allocation_scale_beyond_four_decimal_places() -> None: + """Direct kernel facts must fail closed before exact aggregation can amplify scale.""" + assignment = _assignment(Decimal("0.00001")) + + with pytest.raises(AssignmentPortfolioError, match="allocation_ratio"): + validate_assignment_portfolio( + [assignment], + tenant_record_id=_id(1), + person_record_id=_id(11), + employment_record_id=_id(101), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + ) + + +def test_position_capacity_rejects_allocation_scale_beyond_four_decimal_places() -> None: + """Seat-capacity validation must reject unsafe Decimal scale before exact aggregation.""" + assignment = _assignment(Decimal("0.00001")) + + with pytest.raises(PositionSeatError, match="allocation_ratio"): + validate_position_seat_capacity( + [assignment], + tenant_record_id=_id(1), + position_record_id=_id(1201), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + ) From 47020aa0ad2ddb34b804e164e03f9667192d9b9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:36:50 -0700 Subject: [PATCH 30/59] fix(hris): bound assignment allocation scale --- .../src/orgmetra_hris_kernel/assignment.py | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py b/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py index 2efbefb17..1ca76dbae 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/assignment.py @@ -19,12 +19,13 @@ _ONE = Decimal("1.0000") _ZERO = Decimal("0") _ZERO_FTE = Decimal("0.0000") +_MAX_ALLOCATION_DECIMAL_PLACES = 4 _ASSIGNMENT_ELIGIBLE_EMPLOYMENT_STATUSES = frozenset({"active", "leave"}) _STAFFABLE_POSITION_STATUSES = frozenset({"active", "open"}) def _exact_decimal_total(values: tuple[Decimal, ...]) -> Decimal: - """Sum finite Decimal values exactly without using ambient precision.""" + """Sum finite, scale-bounded Decimal values exactly without ambient precision.""" if not values: return _ZERO_FTE parts = tuple(value.as_tuple() for value in values) @@ -45,10 +46,32 @@ def _exact_decimal_total(values: tuple[Decimal, ...]) -> Decimal: def _ratio_is_valid(allocation_ratio: Decimal) -> bool: - """Return whether one assignment row stays inside (0, 1.0000].""" + """Return whether one allocation is an exact finite Decimal in (0, 1.0000].""" + if type(allocation_ratio) is not Decimal: + return False + if not allocation_ratio.is_finite(): + return False + if allocation_ratio.as_tuple().exponent < -_MAX_ALLOCATION_DECIMAL_PLACES: + return False return allocation_ratio > _ZERO and allocation_ratio <= _ONE +def _raise_invalid_portfolio_ratio() -> None: + """Raise the governed row-level allocation error used by portfolio validation.""" + raise AssignmentPortfolioError( + "allocation_ratio must be a finite Decimal greater than 0, at most 1.0000, and use at most four decimal places.", + next_action="Enter an allocation between 0.0001 and 1.0000 with at most four decimal places, then save.", + ) + + +def _raise_invalid_position_ratio() -> None: + """Raise the governed row-level allocation error used by seat validation.""" + raise PositionSeatError( + "allocation_ratio must be a finite Decimal greater than 0, at most 1.0000, and use at most four decimal places.", + next_action="Enter an allocation between 0.0001 and 1.0000 with at most four decimal places, then save.", + ) + + def _union_covers(intervals: list[DateInterval], target: DateInterval) -> bool: """Return whether merged employment periods cover the assignment period.""" cursor = target.start @@ -82,7 +105,7 @@ def validate_assignment_portfolio( known_at: The knowledge cutoff used for the review. Raises: - AssignmentPortfolioError: Reduce one allocation, then save again. + AssignmentPortfolioError: Reduce or correct one allocation, then save again. """ scoped = [ fact @@ -93,10 +116,7 @@ def validate_assignment_portfolio( ] for fact in scoped: if not _ratio_is_valid(fact.allocation_ratio): - raise AssignmentPortfolioError( - "allocation_ratio must be greater than 0 and at most 1.0000.", - next_action="Enter an allocation between 0.0001 and 1.0000, then save.", - ) + _raise_invalid_portfolio_ratio() visible = resolve_bitemporal_facts( scoped, tenant_record_id=tenant_record_id, @@ -226,7 +246,7 @@ def validate_position_seat_capacity( known_at: The knowledge cutoff used for the review. Raises: - PositionSeatError: Reduce one allocation so the seat total is at most 1.0000. + PositionSeatError: Correct or reduce one allocation, then save again. """ scoped = [ fact @@ -242,6 +262,9 @@ def validate_position_seat_capacity( effective_on=effective_on, known_at=known_at, ) + for fact in visible: + if not _ratio_is_valid(fact.allocation_ratio): + _raise_invalid_position_ratio() total = _exact_decimal_total(tuple(fact.allocation_ratio for fact in visible)) if total > _ONE: raise PositionSeatError( From bc02174f1abe03f2e2438144a3cca02370815a0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:37:23 -0700 Subject: [PATCH 31/59] fix(workforce): revalidate canonical aggregate evidence --- .../src/orgmetra_hris_kernel/workforce.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index 8f028a6d4..c7e673e45 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -93,7 +93,7 @@ class WorkforceCompositionSnapshot: employment_status_counts: tuple[tuple[str, int], ...] def __post_init__(self) -> None: - """Reject non-canonical or internally inconsistent evidence before export.""" + """Freeze caller-owned values, then reject inconsistent evidence.""" object.__setattr__( self, "known_at", @@ -104,6 +104,17 @@ def __post_init__(self) -> None: "employment_status_counts", tuple(tuple(status_count) for status_count in self.employment_status_counts), ) + self._validate_canonical_invariants() + + def _validate_canonical_invariants(self) -> None: + """Revalidate every portable evidence invariant without mutating the snapshot.""" + if type(self.effective_on) is not date or ( + type(self.known_at) is not datetime or self.known_at.tzinfo is not timezone.utc + ): + raise IntervalError( + "Workforce snapshot temporal evidence is not canonical.", + next_action="Rebuild the snapshot through its validated constructor, then export it again.", + ) _validate_snapshot_tenant_id(self.tenant_record_id) status_codes = tuple(status for status, _count in self.employment_status_counts) if len(status_codes) != len(set(status_codes)): @@ -211,13 +222,7 @@ def __post_init__(self) -> None: def canonical_json(self) -> str: """Return deterministic aggregate evidence suitable for audit correlation.""" - if type(self.effective_on) is not date or ( - type(self.known_at) is not datetime or self.known_at.tzinfo is not timezone.utc - ): - raise IntervalError( - "Workforce snapshot temporal evidence is not canonical.", - next_action="Rebuild the snapshot through its validated constructor, then export it again.", - ) + self._validate_canonical_invariants() payload = { "effective_on": self.effective_on.isoformat(), "employment_count": self.employment_count, From 786706f7b61e2fc6c9a4247a9d6e3e1ecdfd7d74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:37:41 -0700 Subject: [PATCH 32/59] test(hris): cover allocation type and finiteness guards --- ...ocation_and_snapshot_export_regressions.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py index 216f28511..07e1f8551 100644 --- a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py +++ b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py @@ -24,20 +24,32 @@ def _id(value: int) -> UUID: return UUID(int=value) -def _assignment(allocation_ratio: Decimal) -> AssignmentFact: - """Build one visible assignment using a caller-provided allocation ratio.""" +def _assignment(allocation_ratio: object) -> AssignmentFact: + """Build one visible assignment using an untrusted caller allocation value.""" return AssignmentFact( tenant_record_id=_id(1), assignment_record_id=_id(201), employment_record_id=_id(101), person_record_id=_id(11), position_record_id=_id(1201), - allocation_ratio=allocation_ratio, + allocation_ratio=allocation_ratio, # type: ignore[arg-type] effective=DateInterval(date(2026, 1, 1)), recorded=RecordedInterval(datetime(2026, 1, 1, tzinfo=timezone.utc)), ) +def _validate_portfolio(assignment: AssignmentFact) -> None: + """Run the tenant-scoped portfolio boundary for one fixture assignment.""" + validate_assignment_portfolio( + [assignment], + tenant_record_id=_id(1), + person_record_id=_id(11), + employment_record_id=_id(101), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + ) + + def test_snapshot_export_rejects_post_construction_aggregate_mutation() -> None: """Canonical evidence must not emit aggregate state that bypassed construction checks.""" snapshot = WorkforceCompositionSnapshot( @@ -60,17 +72,20 @@ def test_snapshot_export_rejects_post_construction_aggregate_mutation() -> None: def test_portfolio_rejects_allocation_scale_beyond_four_decimal_places() -> None: """Direct kernel facts must fail closed before exact aggregation can amplify scale.""" - assignment = _assignment(Decimal("0.00001")) + with pytest.raises(AssignmentPortfolioError, match="allocation_ratio"): + _validate_portfolio(_assignment(Decimal("0.00001"))) + +def test_portfolio_rejects_nonfinite_allocation_ratio() -> None: + """Non-finite Decimal values must become governed domain errors, not arithmetic faults.""" with pytest.raises(AssignmentPortfolioError, match="allocation_ratio"): - validate_assignment_portfolio( - [assignment], - tenant_record_id=_id(1), - person_record_id=_id(11), - employment_record_id=_id(101), - effective_on=date(2026, 1, 15), - known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), - ) + _validate_portfolio(_assignment(Decimal("NaN"))) + + +def test_portfolio_rejects_non_decimal_allocation_ratio() -> None: + """Only exact Decimal values may cross the HRIS allocation validation boundary.""" + with pytest.raises(AssignmentPortfolioError, match="allocation_ratio"): + _validate_portfolio(_assignment("0.5000")) def test_position_capacity_rejects_allocation_scale_beyond_four_decimal_places() -> None: From 8971be2f1224aacebc04a81d54a11800d890a1b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:38:11 -0700 Subject: [PATCH 33/59] test(hris): treat allocation limits as literal evidence --- packages/hris-kernel/tests/test_assignment_portfolio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/tests/test_assignment_portfolio.py b/packages/hris-kernel/tests/test_assignment_portfolio.py index 1525ad892..bd1bacfde 100644 --- a/packages/hris-kernel/tests/test_assignment_portfolio.py +++ b/packages/hris-kernel/tests/test_assignment_portfolio.py @@ -56,7 +56,7 @@ def test_portfolio_rejects_allocation_above_one_for_one_employment( assignment_record_id=UUID("10000000-0000-7000-8000-000000000303"), allocation_ratio=Decimal("0.3000"), ) - with pytest.raises(AssignmentPortfolioError, match="1.0000"): + with pytest.raises(AssignmentPortfolioError, match=r"1\.0000"): validate_assignment_portfolio( [jordan_icu_assignment, jordan_float_assignment, extra], tenant_record_id=TENANT, @@ -76,7 +76,7 @@ def test_portfolio_rejects_exact_overallocation_under_low_decimal_precision( second = replace(jordan_float_assignment, allocation_ratio=Decimal("0.5040")) with localcontext() as context: context.prec = 2 - with pytest.raises(AssignmentPortfolioError, match="1.0000"): + with pytest.raises(AssignmentPortfolioError, match=r"1\.0000"): validate_assignment_portfolio( [first, second], tenant_record_id=TENANT, From 90632c6850ab68f8e5d7486c3cf3f28e7bcc54ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:38:33 -0700 Subject: [PATCH 34/59] test(hris): escape allocation-limit assertions --- packages/hris-kernel/tests/test_position_coverage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/hris-kernel/tests/test_position_coverage.py b/packages/hris-kernel/tests/test_position_coverage.py index 162f74406..6febd54f4 100644 --- a/packages/hris-kernel/tests/test_position_coverage.py +++ b/packages/hris-kernel/tests/test_position_coverage.py @@ -105,7 +105,7 @@ def test_riley_cannot_take_a_full_icu_seat_already_held_by_jordan( allocation_ratio=Decimal("1.0000"), effective=effective(date(2024, 4, 1)), ) - with pytest.raises(PositionSeatError, match="1.0000"): + with pytest.raises(PositionSeatError, match=r"1\.0000"): validate_position_seat_capacity( [jordan_icu_assignment, riley], tenant_record_id=TENANT, @@ -129,7 +129,7 @@ def test_position_rejects_exact_overallocation_under_low_decimal_precision( jordan = replace(jordan_icu_assignment, allocation_ratio=Decimal("0.5040")) with localcontext() as context: context.prec = 2 - with pytest.raises(PositionSeatError, match="1.0000"): + with pytest.raises(PositionSeatError, match=r"1\.0000"): validate_position_seat_capacity( [jordan, riley], tenant_record_id=TENANT, @@ -246,7 +246,7 @@ def test_assignment_write_composes_employment_position_and_seat_rules( person_record_id=RILEY, employment_record_version_id=UUID("10000000-0000-7000-8000-000000000226"), ) - with pytest.raises(PositionSeatError, match="1.0000"): + with pytest.raises(PositionSeatError, match=r"1\.0000"): validate_assignment_write( riley, [jordan_icu_assignment, riley], From 9331d8b0f8a1245610fb94d0fa73c0d25064b67c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:39:18 -0700 Subject: [PATCH 35/59] docs(hris): record evidence export and allocation hardening --- packages/hris-kernel/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/hris-kernel/CHANGELOG.md b/packages/hris-kernel/CHANGELOG.md index 157822342..8030d9017 100644 --- a/packages/hris-kernel/CHANGELOG.md +++ b/packages/hris-kernel/CHANGELOG.md @@ -6,5 +6,7 @@ - Report aggregate net changes for distinct-person headcount, reportable employments, staffed assignments, staffed Decimal FTE, unassigned people, and status counts without serializing row-level HR identities. - Fail closed on cross-tenant endpoints, non-forward effective dates, and different knowledge cutoffs so recorded corrections cannot masquerade as business-time workforce movement. - Freeze timezone-aware knowledge cutoffs to detached UTC datetimes and copy status-count containers before canonical serialization, preventing mutable caller objects or timezone providers from changing snapshot evidence after construction. +- Revalidate every workforce aggregate and temporal invariant immediately before canonical JSON or digest export, so low-level post-construction mutation cannot become new audit evidence. - Reject non-`Decimal` staffed FTE and boolean, negative, or non-integer per-status employment counts during direct workforce snapshot construction before arithmetic or canonical serialization. +- Require every allocation ratio reaching employment-portfolio or Position-seat aggregation to be an exact finite `Decimal` in `(0, 1.0000]` with at most four fractional places, preventing extreme-scale values from reaching exact coefficient arithmetic. - Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. From f349348ebb2e24433353230c1b3123215f9900ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:39:33 -0700 Subject: [PATCH 36/59] docs(adr): harden workforce evidence trust boundary --- docs/adr/0011-bitemporal-workforce-composition.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index 07a04df95..c4516740e 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -2,7 +2,7 @@ ## Status -Accepted on active PR #33 only. This document is not protected-`develop` product truth until the owning PR integrates. +Accepted on protected `develop` through merged PR #33. Active PR #54 strengthens the accepted contract's deterministic arithmetic and canonical-evidence integrity; those strengthening changes are not protected-`develop` truth until #54 integrates. ## Context @@ -24,8 +24,10 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. - The snapshot freezes the timezone-aware knowledge cutoff to an exact UTC datetime and detaches status-count containers before validation, so mutable caller objects cannot change canonical evidence after construction. +- Canonical JSON and digest export re-run the same non-mutating temporal, tenant, status, count, staffing, and reconciliation invariants used after construction, so low-level runtime mutation cannot silently mint contradictory workforce evidence. - Endpoint FTE totals and workforce-change deltas align finite Decimal coefficients before arithmetic, so the caller's ambient Decimal precision cannot change endpoint evidence, the reported delta, canonical JSON, or content digest. - Employment-portfolio and Position-seat limits use the same exact Decimal coefficient total, so low caller precision cannot admit an overallocated staffing total. +- Allocation rows must be exact finite `Decimal` values in `(0, 1.0000]` with no more than four fractional places before exact coefficient aggregation. This keeps the kernel aligned with the People/API persistence scale contract and prevents hostile or accidental extreme exponents from creating unbounded integer-coefficient work. - Two visible versions of one Employment or Assignment identity fail closed. Invalid assignment coverage or over-allocation remains a data-integrity error rather than becoming a plausible metric. - The canonical JSON contains the opaque tenant identifier, report coordinates, aggregate metrics, and schema version only. It excludes person, employment, assignment, and position identifiers and all human-readable PII. - SHA-256 addresses the exact canonical UTF-8 representation so a caller can correlate a report with immutable audit evidence without copying source rows. @@ -50,7 +52,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit ## Verification -`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, and `packages/hris-kernel/tests/test_assignment_portfolio.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. +`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, `packages/hris-kernel/tests/test_assignment_portfolio.py`, `packages/hris-kernel/tests/test_position_coverage.py`, and `packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, post-construction export revalidation, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, allocation type/finiteness/scale rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. ## References From 438d84d922bb1e5c64feba8369e10b340612b07a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:39:47 -0700 Subject: [PATCH 37/59] docs(traceability): bind workforce export invariants to regressions --- docs/traceability/workforce-composition.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index de19dc656..64ba27811 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -2,7 +2,7 @@ ## Status -Active-PR only. This evidence does not describe protected-`develop` product truth until PR #33 integrates. +The baseline workforce-composition contract is protected-`develop` truth through merged PR #33. Active PR #54 strengthens deterministic arithmetic and canonical-evidence integrity; those strengthening rows remain active-PR truth until #54 integrates. | Requirement | Decision / contract | Production implementation | Executable evidence | |---|---|---|---| @@ -10,14 +10,15 @@ Active-PR only. This evidence does not describe protected-`develop` product trut | Avoid double-counting valid concurrent workers | ADR 0011 | distinct `person_record_id` set across visible reportable employments | concurrent-employment fixture expects 2 people from 3 reportable employments | | Reject impossible employment portfolios before aggregation | Existing employment-concurrency invariant + ADR 0011 | `_validate_visible_employment_portfolios` reuses `validate_person_employment_exclusivity` at the report coordinate | overlapping-exclusive-employment regression expects fail-closed `EmploymentExclusivityError` | | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | -| Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot.__post_init__` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, impossible assigned-person reconciliation, and FTE above the assignment count | eight direct-construction staffing boundary regressions | +| Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot._validate_canonical_invariants` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, impossible assigned-person reconciliation, and FTE above the assignment count at construction and export | direct-construction staffing boundary regressions plus post-construction mutation export regression | | Reject overfilled Position seats before aggregation | Existing position-seat invariant + ADR 0011 | each visible `position_record_id` is revalidated with `validate_position_seat_capacity` at the report coordinate | two distinct workers allocating 0.6000 each to one Position must raise `PositionSeatError` instead of reporting 1.2000 staffed FTE | +| Bound allocation arithmetic before exact aggregation | People/API four-decimal persistence scale + ADR 0011 | portfolio and Position-seat validators require exact finite `Decimal` allocation ratios in `(0, 1.0000]` with at most four fractional places before `_exact_decimal_total` | >4-place, non-finite and non-Decimal allocation regressions fail closed with governed domain errors | | Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and exact allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment/Position allocation regressions, including low-precision Decimal cases | | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | | Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | -| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time or status containers | snapshot construction stores exact UTC time and detached status-count tuples; canonical export rejects low-level temporal reinjection | mutable timezone and mutable status-container regressions require stable canonical JSON and digest | -| Keep workforce FTE evidence deterministic | ADR 0011; aggregate totals and deltas must not inherit caller Decimal context | shared `_exact_decimal_total` aligns finite Decimal coefficients before endpoint aggregation and change subtraction, so endpoint JSON, `staffed_fte_change`, canonical JSON, and digests are context-independent | ambient-precision regression compares low- and normal-precision endpoint and change evidence | +| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time/status containers or trust low-level field mutation at export | snapshot construction stores exact UTC time and detached status-count tuples; canonical export reruns all temporal, tenant and aggregate invariants | mutable timezone, mutable status-container, temporal reinjection and post-construction aggregate-mutation regressions fail closed | +| Keep workforce FTE evidence deterministic | ADR 0011; aggregate totals and deltas must not inherit caller Decimal context | shared `_exact_decimal_total` aligns validated Decimal coefficients before endpoint aggregation and change subtraction, so endpoint JSON, `staffed_fte_change`, canonical JSON, and digests are context-independent | ambient-precision regression compares low- and normal-precision endpoint and change evidence | | Keep workforce intelligence descriptive | ADR 0011 | module contains no recommendation, decision, protected-attribute inference or persistence API | public package boundary and code review; high-impact actions remain outside this slice | | Ground scope in current authoritative standards without claiming certification | ISO 30414:2025 public catalogue metadata; ADR 0011 | no proprietary ISO metric text is embedded in production code | `docs/doctoring/workforce-composition-references.md` | | Keep exact owned coverage reproducible | Orgmetra quality policy | `.github/workflows/workforce-intelligence-quality.yml` checks exact candidate SHA and runs the complete HRIS kernel | hosted exact-head workflow with package 100% statement/branch threshold | From fce4e7dbb2657daf035222bb79a67d077de9db2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:40:52 -0700 Subject: [PATCH 38/59] docs(changelog): record workforce integrity repairs --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b340aebbf..c78a3f0fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,9 +54,11 @@ All notable changes to Orgmetra will be documented in this file. - Made assignment coverage status-aware: `active` and `leave` remain staffable while `terminated` and other non-eligible employment statuses fail closed. - Made organization hierarchy reconstruction fail closed on a cycle at the requested tenant, effective day, and knowledge cutoff while ignoring future-recorded and foreign-tenant facts. - Build the outbox due-work index concurrently during migration 0008, requiring that index step to run outside an explicit transaction block so established queues do not block writers while the index is built; pre-index hardening and post-index privileged role setup use separate explicit transactions. +- Workforce allocation validation now requires exact finite Decimal ratios in `(0, 1.0000]` with at most four fractional places before exact employment-portfolio or Position-seat aggregation, keeping the kernel aligned with persisted allocation scale and bounded arithmetic. ### Security +- Workforce canonical JSON and digest export now revalidate the full temporal, tenant, status, count, staffing, and reconciliation contract immediately before serialization, so post-construction low-level mutation cannot mint contradictory aggregate evidence. - Predictive-validity cases fail closed when selection evidence, Job scope, study criterion, converted worker, or system-recorded visibility does not match; the normalized case relation is tenant-qualified, append-only, TRUNCATE-protected, and forced through row-level security. - Purpose-bound PII authorization now fails closed across active tenant, authenticated actor tenant, resource tenant, resource kind, purpose, operation, operation-specific Keyverse scope, and requested-field subset; malformed/wildcard-like attributes, mutable field/scope collections, reserved UUID sentinels, and cross-tenant confused-deputy contexts are rejected before protected values are returned. Authorization requests and allow/deny evidence now also require and preserve one namespaced opaque target-resource reference, so immutable audit correlation identifies the exact HR record without copying its protected values. Authorization evidence otherwise contains governance metadata and field names only, with stable denial reasons and actionable next steps rather than PII. - LLM output constrained to draft evidence. @@ -74,4 +76,4 @@ All notable changes to Orgmetra will be documented in this file. ### Notes -- Protected `develop` at `e7ddb7a78a5e1460410005d10f43ebf18c5e12e4` includes normalized validity-study and criterion integrity, bitemporal workforce composition, governed candidate-to-worker conversion, purpose-bound PII authorization, GET-only People reads, governed People mutation/idempotency API, and the accepted ADR 0001–0003 source expansion integrated by #37. Job Analysis persistence/API and the selection-review packet remain active-PR truth until their unchanged exact heads satisfy fresh gates and merge. +- Protected `develop` at `9e3e4847510e1e612b48474ba42b177b8ed824df` includes the merged workforce-composition baseline from #33. Same-cutoff workforce change evidence and the export/allocation hardening in #54 remain active-PR truth until the unchanged exact head satisfies fresh required gates and independent review; predecessor PR or workflow evidence does not transfer. From 4bc49335a6b1c3489ad662d4b5a2a14186b9626c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:44:52 -0700 Subject: [PATCH 39/59] chore(manifest): regenerate foundation inventory --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 8437774bf..4b0d55842 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"86600165f6c4012f2f29f12b56dc411a505e50e3dfd783ea2be6cc398522594e","bytes":17573,"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":"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"27582c5fafe42b6e5c73b6cc6b504fa2f9067eb6086d837c39ce4852a53f0227","bytes":6733,"lines":57},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"e859db116605ff05ad3ee4c4d88ec857756a4ec46cbb1528b350670f449a9b4f","bytes":17963,"lines":79},{"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"9ad7d2bec1e4623ed7f5abcc797f35dd834eb603bac51bae80bb3950e66ad5fb","bytes":7661,"lines":59},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} From c08de578fd320d6d4c84d4c164ff306e5c4959d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:46:13 -0700 Subject: [PATCH 40/59] test(workforce): reproduce direct FTE canonicalization gaps --- ...ocation_and_snapshot_export_regressions.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py index 07e1f8551..e16477480 100644 --- a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py +++ b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py @@ -38,6 +38,21 @@ def _assignment(allocation_ratio: object) -> AssignmentFact: ) +def _staffed_snapshot(staffed_fte: Decimal) -> WorkforceCompositionSnapshot: + """Build one directly constructed, otherwise valid staffed snapshot.""" + return WorkforceCompositionSnapshot( + tenant_record_id=_id(1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), + person_headcount=1, + employment_count=1, + staffed_assignment_count=1, + staffed_fte=staffed_fte, + unassigned_person_count=0, + employment_status_counts=(("active", 1),), + ) + + def _validate_portfolio(assignment: AssignmentFact) -> None: """Run the tenant-scoped portfolio boundary for one fixture assignment.""" validate_assignment_portfolio( @@ -70,6 +85,23 @@ def test_snapshot_export_rejects_post_construction_aggregate_mutation() -> None: snapshot.canonical_json() +def test_direct_snapshot_rejects_fte_scale_beyond_four_decimal_places() -> None: + """Every accepted direct FTE must remain bounded for exact comparison arithmetic.""" + with pytest.raises(SingleValuedFactError, match="staffed FTE"): + _staffed_snapshot(Decimal("1E-5000")) + + +def test_direct_snapshot_canonicalizes_equivalent_fte_scales() -> None: + """Equivalent FTE values must emit one canonical four-decimal evidence representation.""" + compact = _staffed_snapshot(Decimal("0.5")) + fixed_scale = _staffed_snapshot(Decimal("0.5000")) + + assert compact.staffed_fte == Decimal("0.5000") + assert compact.staffed_fte.as_tuple().exponent == -4 + assert compact.canonical_json() == fixed_scale.canonical_json() + assert compact.content_digest() == fixed_scale.content_digest() + + def test_portfolio_rejects_allocation_scale_beyond_four_decimal_places() -> None: """Direct kernel facts must fail closed before exact aggregation can amplify scale.""" with pytest.raises(AssignmentPortfolioError, match="allocation_ratio"): From 5b39b0d1670e56b48e5d2da5ec1e808ca5398838 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:47:41 -0700 Subject: [PATCH 41/59] fix(workforce): canonicalize four-decimal FTE evidence --- .../src/orgmetra_hris_kernel/workforce.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index c7e673e45..6c1813b74 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -28,6 +28,7 @@ _WORKFORCE_INCLUDED_STATUSES = frozenset({"active", "leave"}) _ZERO_FTE = Decimal("0.0000") +_CANONICAL_FTE_EXPONENT = -4 def _validate_snapshot_tenant_id(tenant_record_id: UUID) -> None: @@ -72,6 +73,30 @@ def _validate_snapshot_temporal_coordinate(effective_on: date, known_at: datetim ) from exc +def _canonicalize_staffed_fte(staffed_fte: Decimal) -> Decimal: + """Return one fixed four-decimal FTE representation without ambient rounding.""" + if type(staffed_fte) is not Decimal or not staffed_fte.is_finite() or staffed_fte < _ZERO_FTE: + raise SingleValuedFactError( + "Workforce snapshot staffed FTE must be a finite non-negative Decimal.", + next_action="Provide a finite non-negative Decimal FTE, then rebuild the snapshot.", + ) + parts = staffed_fte.as_tuple() + if parts.exponent < _CANONICAL_FTE_EXPONENT: + raise SingleValuedFactError( + "Workforce snapshot staffed FTE must use at most four decimal places.", + next_action="Round source allocation evidence to the governed four-decimal scale, then rebuild.", + ) + if parts.exponent == _CANONICAL_FTE_EXPONENT: + return staffed_fte + return Decimal( + ( + parts.sign, + parts.digits + (0,) * (parts.exponent - _CANONICAL_FTE_EXPONENT), + _CANONICAL_FTE_EXPONENT, + ) + ) + + @dataclass(frozen=True, slots=True) class WorkforceCompositionSnapshot: """One aggregate workforce view at an effective day and knowledge cutoff. @@ -104,6 +129,7 @@ def __post_init__(self) -> None: "employment_status_counts", tuple(tuple(status_count) for status_count in self.employment_status_counts), ) + object.__setattr__(self, "staffed_fte", _canonicalize_staffed_fte(self.staffed_fte)) self._validate_canonical_invariants() def _validate_canonical_invariants(self) -> None: @@ -146,10 +172,11 @@ def _validate_canonical_invariants(self) -> None: type(self.staffed_fte) is not Decimal or not self.staffed_fte.is_finite() or self.staffed_fte < _ZERO_FTE + or self.staffed_fte.as_tuple().exponent != _CANONICAL_FTE_EXPONENT ): raise SingleValuedFactError( - "Workforce snapshot aggregate values are internally inconsistent.", - next_action="Rebuild the snapshot from authoritative HRIS facts with a finite non-negative FTE.", + "Workforce snapshot staffed FTE is not canonical four-decimal evidence.", + next_action="Rebuild the snapshot from governed four-decimal allocation evidence.", ) if self.staffed_fte > Decimal(self.staffed_assignment_count): raise SingleValuedFactError( From 2fa8a220c5f52b0ec8d9b859dd905c96e6692a60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:50:46 -0700 Subject: [PATCH 42/59] test(workforce): align FTE validation contract --- .../tests/test_workforce_composition_boundaries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index b4d3561db..335c790b4 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -118,7 +118,7 @@ def test_direct_snapshot_rejects_unassigned_count_above_headcount() -> None: def test_direct_snapshot_rejects_nonfinite_staffed_fte() -> None: """NaN or infinite FTE values cannot enter deterministic audit evidence.""" - with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + with pytest.raises(SingleValuedFactError, match="staffed FTE"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), staffed_fte=Decimal("NaN"), @@ -127,7 +127,7 @@ def test_direct_snapshot_rejects_nonfinite_staffed_fte() -> None: def test_direct_snapshot_rejects_non_decimal_staffed_fte() -> None: """FTE evidence must remain Decimal so finite and canonical formatting are guaranteed.""" - with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + with pytest.raises(SingleValuedFactError, match="staffed FTE"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), staffed_fte=0, # type: ignore[arg-type] From edca9488dda8a7c8e281e1ace743221fb56f8d5c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:51:13 -0700 Subject: [PATCH 43/59] test(workforce): cover canonical FTE export scale --- .../test_allocation_and_snapshot_export_regressions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py index e16477480..f7cdee7fb 100644 --- a/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py +++ b/packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py @@ -102,6 +102,15 @@ def test_direct_snapshot_canonicalizes_equivalent_fte_scales() -> None: assert compact.content_digest() == fixed_scale.content_digest() +def test_snapshot_export_rejects_post_construction_fte_scale_mutation() -> None: + """Canonical export must reject equivalent values whose governed FTE scale was bypassed.""" + snapshot = _staffed_snapshot(Decimal("0.5000")) + object.__setattr__(snapshot, "staffed_fte", Decimal("0.5")) + + with pytest.raises(SingleValuedFactError, match="canonical four-decimal"): + snapshot.canonical_json() + + def test_portfolio_rejects_allocation_scale_beyond_four_decimal_places() -> None: """Direct kernel facts must fail closed before exact aggregation can amplify scale.""" with pytest.raises(AssignmentPortfolioError, match="allocation_ratio"): From 8a99993b0e5c0911660710a4ae4bd1c21625e7f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:52:08 -0700 Subject: [PATCH 44/59] docs(adr): canonicalize workforce FTE evidence scale --- docs/adr/0011-bitemporal-workforce-composition.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index c4516740e..d318d7b69 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -24,6 +24,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit - Unassigned-person count surfaces a buyer-actionable staffing gap without serializing row-level worker identity. - Status counts are aggregate employment evidence, sorted deterministically. - The snapshot freezes the timezone-aware knowledge cutoff to an exact UTC datetime and detaches status-count containers before validation, so mutable caller objects cannot change canonical evidence after construction. +- Staffed FTE evidence is accepted only as an exact finite non-negative `Decimal` with at most four fractional places and is canonicalized to exactly four fractional places at construction; canonical export requires that fixed scale again. Equivalent values such as `0.5` and `0.5000` therefore serialize and hash identically, while extreme-scale direct evidence cannot reach comparison arithmetic. - Canonical JSON and digest export re-run the same non-mutating temporal, tenant, status, count, staffing, and reconciliation invariants used after construction, so low-level runtime mutation cannot silently mint contradictory workforce evidence. - Endpoint FTE totals and workforce-change deltas align finite Decimal coefficients before arithmetic, so the caller's ambient Decimal precision cannot change endpoint evidence, the reported delta, canonical JSON, or content digest. - Employment-portfolio and Position-seat limits use the same exact Decimal coefficient total, so low caller precision cannot admit an overallocated staffing total. @@ -52,7 +53,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit ## Verification -`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, `packages/hris-kernel/tests/test_assignment_portfolio.py`, `packages/hris-kernel/tests/test_position_coverage.py`, and `packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, post-construction export revalidation, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, allocation type/finiteness/scale rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. +`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, `packages/hris-kernel/tests/test_assignment_portfolio.py`, `packages/hris-kernel/tests/test_position_coverage.py`, and `packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, fixed four-decimal FTE canonicalization, extreme-scale FTE rejection, post-construction export revalidation including scale mutation, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, allocation type/finiteness/scale rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. ## References From 9d18b652ff2721faad94a548fa84cafe45079263 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:52:23 -0700 Subject: [PATCH 45/59] docs(hris): record canonical FTE scale contract --- packages/hris-kernel/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/hris-kernel/CHANGELOG.md b/packages/hris-kernel/CHANGELOG.md index 8030d9017..862d76a30 100644 --- a/packages/hris-kernel/CHANGELOG.md +++ b/packages/hris-kernel/CHANGELOG.md @@ -6,6 +6,7 @@ - Report aggregate net changes for distinct-person headcount, reportable employments, staffed assignments, staffed Decimal FTE, unassigned people, and status counts without serializing row-level HR identities. - Fail closed on cross-tenant endpoints, non-forward effective dates, and different knowledge cutoffs so recorded corrections cannot masquerade as business-time workforce movement. - Freeze timezone-aware knowledge cutoffs to detached UTC datetimes and copy status-count containers before canonical serialization, preventing mutable caller objects or timezone providers from changing snapshot evidence after construction. +- Canonicalize direct staffed FTE evidence to exactly four fractional places, reject extreme fractional scale before exact comparison arithmetic, and recheck the fixed scale during export so equivalent values cannot produce different canonical bytes or digests. - Revalidate every workforce aggregate and temporal invariant immediately before canonical JSON or digest export, so low-level post-construction mutation cannot become new audit evidence. - Reject non-`Decimal` staffed FTE and boolean, negative, or non-integer per-status employment counts during direct workforce snapshot construction before arithmetic or canonical serialization. - Require every allocation ratio reaching employment-portfolio or Position-seat aggregation to be an exact finite `Decimal` in `(0, 1.0000]` with at most four fractional places, preventing extreme-scale values from reaching exact coefficient arithmetic. From 6dc0f3da8f716d1b67d48336e0cdfb8f35a2770e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:52:49 -0700 Subject: [PATCH 46/59] docs(traceability): bind canonical FTE scale to evidence --- docs/traceability/workforce-composition.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index 64ba27811..3f4bca704 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -11,13 +11,14 @@ The baseline workforce-composition contract is protected-`develop` truth through | Reject impossible employment portfolios before aggregation | Existing employment-concurrency invariant + ADR 0011 | `_validate_visible_employment_portfolios` reuses `validate_person_employment_exclusivity` at the report coordinate | overlapping-exclusive-employment regression expects fail-closed `EmploymentExclusivityError` | | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | | Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot._validate_canonical_invariants` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, impossible assigned-person reconciliation, and FTE above the assignment count at construction and export | direct-construction staffing boundary regressions plus post-construction mutation export regression | +| Canonicalize direct staffed FTE evidence | ADR 0011; direct evidence must remain bounded and byte-stable | `_canonicalize_staffed_fte` accepts exact finite non-negative `Decimal` values with at most four fractional places, stores exactly four places, and export rechecks that scale | extreme-scale direct FTE fails closed; `0.5` and `0.5000` serialize/hash identically; post-construction scale mutation fails export | | Reject overfilled Position seats before aggregation | Existing position-seat invariant + ADR 0011 | each visible `position_record_id` is revalidated with `validate_position_seat_capacity` at the report coordinate | two distinct workers allocating 0.6000 each to one Position must raise `PositionSeatError` instead of reporting 1.2000 staffed FTE | | Bound allocation arithmetic before exact aggregation | People/API four-decimal persistence scale + ADR 0011 | portfolio and Position-seat validators require exact finite `Decimal` allocation ratios in `(0, 1.0000]` with at most four fractional places before `_exact_decimal_total` | >4-place, non-finite and non-Decimal allocation regressions fail closed with governed domain errors | | Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and exact allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment/Position allocation regressions, including low-precision Decimal cases | | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | | Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | -| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time/status containers or trust low-level field mutation at export | snapshot construction stores exact UTC time and detached status-count tuples; canonical export reruns all temporal, tenant and aggregate invariants | mutable timezone, mutable status-container, temporal reinjection and post-construction aggregate-mutation regressions fail closed | +| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time/status containers or trust low-level field mutation at export | snapshot construction stores exact UTC time and detached status-count tuples; canonical export reruns all temporal, tenant and aggregate invariants | mutable timezone, mutable status-container, temporal reinjection, aggregate mutation, and FTE-scale mutation regressions fail closed | | Keep workforce FTE evidence deterministic | ADR 0011; aggregate totals and deltas must not inherit caller Decimal context | shared `_exact_decimal_total` aligns validated Decimal coefficients before endpoint aggregation and change subtraction, so endpoint JSON, `staffed_fte_change`, canonical JSON, and digests are context-independent | ambient-precision regression compares low- and normal-precision endpoint and change evidence | | Keep workforce intelligence descriptive | ADR 0011 | module contains no recommendation, decision, protected-attribute inference or persistence API | public package boundary and code review; high-impact actions remain outside this slice | | Ground scope in current authoritative standards without claiming certification | ISO 30414:2025 public catalogue metadata; ADR 0011 | no proprietary ISO metric text is embedded in production code | `docs/doctoring/workforce-composition-references.md` | From 453bef0ea6ed5970d675abdbc95dc0d20eab285f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:08:58 -0700 Subject: [PATCH 47/59] test(workforce): cover canonical evidence mutation gaps --- ..._workforce_canonical_evidence_hardening.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py diff --git a/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py b/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py new file mode 100644 index 000000000..2863905bf --- /dev/null +++ b/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py @@ -0,0 +1,147 @@ +"""Regression coverage for canonical workforce evidence hardening.""" + +from __future__ import annotations + +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal +from uuid import UUID + +import pytest + +from orgmetra_hris_kernel import ( + IdentityScopeError, + IntervalError, + SingleValuedFactError, + WorkforceCompositionChangeSnapshot, + WorkforceCompositionSnapshot, +) + + +def _snapshot( + effective_on: date, + *, + tenant_record_id: UUID = UUID(int=1), + known_at: datetime = datetime(2026, 2, 20, tzinfo=timezone.utc), + staffed_fte: Decimal = Decimal("0.0000"), + employment_status_counts: tuple[tuple[str, int], ...] = (("active", 1),), +) -> WorkforceCompositionSnapshot: + """Return one valid unassigned-worker aggregate endpoint.""" + return WorkforceCompositionSnapshot( + tenant_record_id=tenant_record_id, + effective_on=effective_on, + known_at=known_at, + person_headcount=1, + employment_count=1, + staffed_assignment_count=0, + staffed_fte=staffed_fte, + unassigned_person_count=1, + employment_status_counts=employment_status_counts, + ) + + +def test_malformed_status_rows_fail_with_domain_error() -> None: + """Malformed status rows must not leak tuple-unpacking implementation errors.""" + with pytest.raises(SingleValuedFactError, match="status"): + _snapshot( + date(2026, 1, 15), + employment_status_counts=(("active", 1, 0),), # type: ignore[arg-type] + ) + + with pytest.raises(SingleValuedFactError, match="status"): + _snapshot( + date(2026, 1, 15), + employment_status_counts=(None,), # type: ignore[arg-type] + ) + + +def test_equivalent_zero_evidence_has_one_canonical_representation() -> None: + """Negative Decimal zero and zero-count status rows must not fork evidence digests.""" + positive_zero = _snapshot(date(2026, 1, 15), staffed_fte=Decimal("0.0000")) + negative_zero = _snapshot(date(2026, 1, 15), staffed_fte=Decimal("-0.0000")) + + assert negative_zero.staffed_fte == Decimal("0.0000") + assert format(negative_zero.staffed_fte, "f") == "0.0000" + assert negative_zero.canonical_json() == positive_zero.canonical_json() + assert negative_zero.content_digest() == positive_zero.content_digest() + + empty = WorkforceCompositionSnapshot( + tenant_record_id=UUID(int=1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + person_headcount=0, + employment_count=0, + staffed_assignment_count=0, + staffed_fte=Decimal("0.0000"), + unassigned_person_count=0, + employment_status_counts=(), + ) + zero_row = WorkforceCompositionSnapshot( + tenant_record_id=UUID(int=1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + person_headcount=0, + employment_count=0, + staffed_assignment_count=0, + staffed_fte=Decimal("0.0000"), + unassigned_person_count=0, + employment_status_counts=(("active", 0),), + ) + assert zero_row.employment_status_counts == () + assert zero_row.canonical_json() == empty.canonical_json() + assert zero_row.content_digest() == empty.content_digest() + + +def test_large_zero_exponent_canonicalizes_without_representation_expansion() -> None: + """A mathematically zero FTE never needs exponent-proportional digit padding.""" + snapshot = _snapshot(date(2026, 1, 15), staffed_fte=Decimal("0E+1000000")) + assert snapshot.staffed_fte == Decimal("0.0000") + assert format(snapshot.staffed_fte, "f") == "0.0000" + + +def test_change_export_rechecks_tenant_after_endpoint_mutation() -> None: + """Cross-tenant endpoint mutation must fail before comparison evidence is emitted.""" + opening = _snapshot(date(2026, 1, 15)) + closing = _snapshot(date(2026, 2, 15)) + change = WorkforceCompositionChangeSnapshot(opening, closing) + object.__setattr__(closing, "tenant_record_id", UUID(int=2)) + + with pytest.raises(IdentityScopeError, match="same tenant"): + change.canonical_json() + + +def test_change_export_rechecks_date_order_after_endpoint_mutation() -> None: + """Post-construction date mutation must not turn a forward comparison backward.""" + opening = _snapshot(date(2026, 1, 15)) + closing = _snapshot(date(2026, 2, 15)) + change = WorkforceCompositionChangeSnapshot(opening, closing) + object.__setattr__(opening, "effective_on", date(2026, 3, 1)) + + with pytest.raises(IntervalError, match="later"): + change.canonical_json() + + +def test_change_export_rechecks_cutoff_after_endpoint_mutation() -> None: + """Post-construction recorded-time mutation must not mix knowledge cutoffs.""" + opening = _snapshot(date(2026, 1, 15)) + closing = _snapshot(date(2026, 2, 15)) + change = WorkforceCompositionChangeSnapshot(opening, closing) + object.__setattr__( + closing, + "known_at", + datetime(2026, 2, 20, tzinfo=timezone.utc) + timedelta(seconds=1), + ) + + with pytest.raises(IntervalError, match="knowledge cutoff"): + change.canonical_json() + + +def test_change_export_rechecks_exact_endpoint_types_after_mutation() -> None: + """Low-level endpoint replacement must fail with the public type contract.""" + change = WorkforceCompositionChangeSnapshot( + _snapshot(date(2026, 1, 15)), + _snapshot(date(2026, 2, 15)), + ) + object.__setattr__(change, "opening_snapshot", object()) + + with pytest.raises(TypeError, match="opening_snapshot"): + change.canonical_json() From 9979b7332ee234e6939eeb46d8799f6ee364d7fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:11:37 -0700 Subject: [PATCH 48/59] fix(workforce): canonicalize aggregate evidence safely --- .../src/orgmetra_hris_kernel/workforce.py | 122 ++++++++++++------ 1 file changed, 82 insertions(+), 40 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py index 6c1813b74..2b613de7b 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce.py @@ -73,13 +73,60 @@ def _validate_snapshot_temporal_coordinate(effective_on: date, known_at: datetim ) from exc -def _canonicalize_staffed_fte(staffed_fte: Decimal) -> Decimal: - """Return one fixed four-decimal FTE representation without ambient rounding.""" +def _status_values(value: object) -> tuple[object, ...]: + """Detach one caller-owned status container or fail with the public domain error.""" + try: + return tuple(value) # type: ignore[arg-type] + except (TypeError, ValueError) as exc: + raise SingleValuedFactError( + "Workforce snapshot employment status evidence must contain status/count pairs.", + next_action="Rebuild the snapshot with two-value status/count pairs.", + ) from exc + + +def _freeze_employment_status_counts(value: object) -> tuple[tuple[str, int], ...]: + """Detach status rows, reject malformed values, and omit semantic zero rows.""" + frozen: list[tuple[str, int]] = [] + for raw_row in _status_values(value): + row = _status_values(raw_row) + if len(row) != 2: + raise SingleValuedFactError( + "Workforce snapshot employment status evidence must contain status/count pairs.", + next_action="Rebuild the snapshot with exactly two values in every status/count pair.", + ) + status, count = row + if type(status) is not str or type(count) is not int or count < 0: + raise SingleValuedFactError( + "Workforce snapshot employment status evidence must contain valid status/count pairs.", + next_action="Rebuild the snapshot with string status codes and non-negative integer counts.", + ) + if count == 0: + continue + frozen.append((status, count)) + return tuple(frozen) + + +def _canonicalize_staffed_fte(staffed_fte: Decimal, staffed_assignment_count: int) -> Decimal: + """Return bounded four-decimal FTE evidence without exponent-proportional work.""" + if type(staffed_assignment_count) is not int or staffed_assignment_count < 0: + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action="Rebuild the snapshot with a non-negative integer staffed-assignment count.", + ) if type(staffed_fte) is not Decimal or not staffed_fte.is_finite() or staffed_fte < _ZERO_FTE: raise SingleValuedFactError( "Workforce snapshot staffed FTE must be a finite non-negative Decimal.", next_action="Provide a finite non-negative Decimal FTE, then rebuild the snapshot.", ) + if staffed_fte.is_zero(): + return _ZERO_FTE + if staffed_fte > Decimal(staffed_assignment_count): + raise SingleValuedFactError( + "Workforce snapshot aggregate values are internally inconsistent.", + next_action=( + "Rebuild the snapshot so staffed FTE does not exceed one full allocation per staffed assignment." + ), + ) parts = staffed_fte.as_tuple() if parts.exponent < _CANONICAL_FTE_EXPONENT: raise SingleValuedFactError( @@ -90,7 +137,7 @@ def _canonicalize_staffed_fte(staffed_fte: Decimal) -> Decimal: return staffed_fte return Decimal( ( - parts.sign, + 0, parts.digits + (0,) * (parts.exponent - _CANONICAL_FTE_EXPONENT), _CANONICAL_FTE_EXPONENT, ) @@ -127,9 +174,13 @@ def __post_init__(self) -> None: object.__setattr__( self, "employment_status_counts", - tuple(tuple(status_count) for status_count in self.employment_status_counts), + _freeze_employment_status_counts(self.employment_status_counts), + ) + object.__setattr__( + self, + "staffed_fte", + _canonicalize_staffed_fte(self.staffed_fte, self.staffed_assignment_count), ) - object.__setattr__(self, "staffed_fte", _canonicalize_staffed_fte(self.staffed_fte)) self._validate_canonical_invariants() def _validate_canonical_invariants(self) -> None: @@ -142,17 +193,6 @@ def _validate_canonical_invariants(self) -> None: next_action="Rebuild the snapshot through its validated constructor, then export it again.", ) _validate_snapshot_tenant_id(self.tenant_record_id) - status_codes = tuple(status for status, _count in self.employment_status_counts) - if len(status_codes) != len(set(status_codes)): - raise SingleValuedFactError( - "Workforce snapshot contains a duplicate status code.", - next_action="Aggregate each employment status once, then rebuild the snapshot.", - ) - if status_codes != tuple(sorted(status_codes)): - raise SingleValuedFactError( - "Workforce snapshot status codes must use canonical status order.", - next_action="Sort employment status counts by status code, then rebuild the snapshot.", - ) aggregate_counts = ( self.person_headcount, @@ -168,23 +208,33 @@ def _validate_canonical_invariants(self) -> None: "non-negative integer." ), ) - if ( - type(self.staffed_fte) is not Decimal - or not self.staffed_fte.is_finite() - or self.staffed_fte < _ZERO_FTE - or self.staffed_fte.as_tuple().exponent != _CANONICAL_FTE_EXPONENT - ): + + canonical_status_counts = _freeze_employment_status_counts(self.employment_status_counts) + if canonical_status_counts != self.employment_status_counts: raise SingleValuedFactError( - "Workforce snapshot staffed FTE is not canonical four-decimal evidence.", - next_action="Rebuild the snapshot from governed four-decimal allocation evidence.", + "Workforce snapshot employment status evidence is not canonical.", + next_action="Rebuild the snapshot so zero-count or mutable status rows cannot reach export.", ) - if self.staffed_fte > Decimal(self.staffed_assignment_count): + status_codes = tuple(status for status, _count in canonical_status_counts) + if len(status_codes) != len(set(status_codes)): raise SingleValuedFactError( - "Workforce snapshot aggregate values are internally inconsistent.", - next_action=( - "Rebuild the snapshot so staffed FTE does not exceed one full allocation per " - "staffed assignment." - ), + "Workforce snapshot contains a duplicate status code.", + next_action="Aggregate each employment status once, then rebuild the snapshot.", + ) + if status_codes != tuple(sorted(status_codes)): + raise SingleValuedFactError( + "Workforce snapshot status codes must use canonical status order.", + next_action="Sort employment status counts by status code, then rebuild the snapshot.", + ) + + canonical_staffed_fte = _canonicalize_staffed_fte( + self.staffed_fte, + self.staffed_assignment_count, + ) + if self.staffed_fte.as_tuple() != canonical_staffed_fte.as_tuple(): + raise SingleValuedFactError( + "Workforce snapshot staffed FTE is not canonical four-decimal evidence.", + next_action="Rebuild the snapshot from governed four-decimal allocation evidence.", ) if self.staffed_assignment_count > 0 and self.staffed_fte <= _ZERO_FTE: raise SingleValuedFactError( @@ -201,14 +251,6 @@ def _validate_canonical_invariants(self) -> None: "Workforce snapshot aggregate values are internally inconsistent.", next_action="Rebuild the snapshot with a reportable person for every staffed assignment.", ) - if not all( - type(count) is int and count >= 0 - for _status, count in self.employment_status_counts - ): - raise SingleValuedFactError( - "Workforce snapshot aggregate values are internally inconsistent.", - next_action="Rebuild the snapshot with non-negative integer employment status counts.", - ) if self.person_headcount > self.employment_count or self.unassigned_person_count > self.person_headcount: raise SingleValuedFactError( "Workforce snapshot aggregate values are internally inconsistent.", @@ -238,7 +280,7 @@ def _validate_canonical_invariants(self) -> None: "Workforce snapshot aggregate values are internally inconsistent.", next_action="Rebuild the snapshot using only reportable workforce employment statuses.", ) - if sum(count for _status, count in self.employment_status_counts) != self.employment_count: + if sum(count for _status, count in canonical_status_counts) != self.employment_count: raise SingleValuedFactError( "Workforce snapshot aggregate values are internally inconsistent.", next_action=( @@ -474,4 +516,4 @@ def build_workforce_composition_snapshot( ), unassigned_person_count=len(workforce_people - staffed_people), employment_status_counts=tuple(sorted(status_counts.items())), - ) + ) \ No newline at end of file From 40058a50d7910c25794df90147a40ea4a07dbbd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:12:04 -0700 Subject: [PATCH 49/59] fix(workforce): revalidate comparison coordinates at export --- .../src/orgmetra_hris_kernel/workforce_change.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index f028c320c..4adfb0833 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -35,6 +35,10 @@ class WorkforceCompositionChangeSnapshot: def __post_init__(self) -> None: """Fail closed when endpoint evidence or coordinates are not comparable.""" + self._validate_comparable_endpoints() + + def _validate_comparable_endpoints(self) -> None: + """Recheck exact endpoint types and cross-endpoint coordinates without mutation.""" if type(self.opening_snapshot) is not WorkforceCompositionSnapshot: raise TypeError("opening_snapshot must be an exact WorkforceCompositionSnapshot") if type(self.closing_snapshot) is not WorkforceCompositionSnapshot: @@ -103,6 +107,7 @@ def employment_status_changes(self) -> tuple[tuple[str, int], ...]: def canonical_json(self) -> str: """Return deterministic aggregate-only comparison evidence for audit correlation.""" + self._validate_comparable_endpoints() payload = { "closing_snapshot": json.loads(self.closing_snapshot.canonical_json()), "closing_snapshot_digest": self.closing_snapshot.content_digest(), @@ -170,4 +175,4 @@ def build_workforce_composition_change_snapshot( effective_on=to_effective_on, known_at=known_at, ) - return WorkforceCompositionChangeSnapshot(opening, closing) + return WorkforceCompositionChangeSnapshot(opening, closing) \ No newline at end of file From 730685eea593d2e735e2571c4f52f690686e1496 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:15:17 -0700 Subject: [PATCH 50/59] test(workforce): cover hardening branches --- ..._workforce_canonical_evidence_hardening.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py b/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py index 2863905bf..a396944d4 100644 --- a/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py +++ b/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py @@ -54,6 +54,22 @@ def test_malformed_status_rows_fail_with_domain_error() -> None: ) +def test_invalid_staffed_assignment_count_fails_with_domain_error() -> None: + """FTE canonicalization must reject non-integer assignment counts before arithmetic.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + WorkforceCompositionSnapshot( + tenant_record_id=UUID(int=1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + person_headcount=1, + employment_count=1, + staffed_assignment_count=True, # type: ignore[arg-type] + staffed_fte=Decimal("0.0000"), + unassigned_person_count=1, + employment_status_counts=(("active", 1),), + ) + + def test_equivalent_zero_evidence_has_one_canonical_representation() -> None: """Negative Decimal zero and zero-count status rows must not fork evidence digests.""" positive_zero = _snapshot(date(2026, 1, 15), staffed_fte=Decimal("0.0000")) @@ -91,6 +107,19 @@ def test_equivalent_zero_evidence_has_one_canonical_representation() -> None: assert zero_row.content_digest() == empty.content_digest() +def test_export_rejects_post_construction_zero_status_row() -> None: + """Low-level mutation cannot reintroduce a noncanonical semantic-zero status row.""" + snapshot = _snapshot(date(2026, 1, 15)) + object.__setattr__( + snapshot, + "employment_status_counts", + (("active", 1), ("leave", 0)), + ) + + with pytest.raises(SingleValuedFactError, match="not canonical"): + snapshot.canonical_json() + + def test_large_zero_exponent_canonicalizes_without_representation_expansion() -> None: """A mathematically zero FTE never needs exponent-proportional digit padding.""" snapshot = _snapshot(date(2026, 1, 15), staffed_fte=Decimal("0E+1000000")) @@ -98,6 +127,22 @@ def test_large_zero_exponent_canonicalizes_without_representation_expansion() -> assert format(snapshot.staffed_fte, "f") == "0.0000" +def test_large_nonzero_exponent_is_rejected_before_representation_expansion() -> None: + """Oversized nonzero FTE is rejected against assignment capacity before digit padding.""" + with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + WorkforceCompositionSnapshot( + tenant_record_id=UUID(int=1), + effective_on=date(2026, 1, 15), + known_at=datetime(2026, 2, 20, tzinfo=timezone.utc), + person_headcount=1, + employment_count=1, + staffed_assignment_count=1, + staffed_fte=Decimal("1E+1000000"), + unassigned_person_count=0, + employment_status_counts=(("active", 1),), + ) + + def test_change_export_rechecks_tenant_after_endpoint_mutation() -> None: """Cross-tenant endpoint mutation must fail before comparison evidence is emitted.""" opening = _snapshot(date(2026, 1, 15)) From 712238b67c00078c725d0efce5185559e5c2ea48 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:15:57 -0700 Subject: [PATCH 51/59] test(workforce): assert status-domain validation --- .../tests/test_workforce_composition_boundaries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py index 335c790b4..b98b37125 100644 --- a/packages/hris-kernel/tests/test_workforce_composition_boundaries.py +++ b/packages/hris-kernel/tests/test_workforce_composition_boundaries.py @@ -226,7 +226,7 @@ def test_direct_snapshot_rejects_more_assigned_people_than_assignments() -> None def test_direct_snapshot_rejects_boolean_status_counts() -> None: """Boolean values must not serialize as employment counts.""" - with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + with pytest.raises(SingleValuedFactError, match="status"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), employment_status_counts=(("active", True),), # type: ignore[tuple-item] @@ -235,7 +235,7 @@ def test_direct_snapshot_rejects_boolean_status_counts() -> None: def test_direct_snapshot_rejects_negative_status_counts() -> None: """Negative per-status counts cannot reconcile a workforce aggregate.""" - with pytest.raises(SingleValuedFactError, match="internally inconsistent"): + with pytest.raises(SingleValuedFactError, match="status"): _direct_snapshot( known_at=datetime(2026, 1, 20, tzinfo=timezone.utc), employment_status_counts=(("active", -1), ("leave", 2)), From aaf63abfd32e547834c5dc3c6dc57e0f453eca9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:16:54 -0700 Subject: [PATCH 52/59] docs(traceability): record workforce evidence hardening --- docs/traceability/workforce-composition.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/traceability/workforce-composition.md b/docs/traceability/workforce-composition.md index 3f4bca704..fd11fbf03 100644 --- a/docs/traceability/workforce-composition.md +++ b/docs/traceability/workforce-composition.md @@ -11,14 +11,15 @@ The baseline workforce-composition contract is protected-`develop` truth through | Reject impossible employment portfolios before aggregation | Existing employment-concurrency invariant + ADR 0011 | `_validate_visible_employment_portfolios` reuses `validate_person_employment_exclusivity` at the report coordinate | overlapping-exclusive-employment regression expects fail-closed `EmploymentExclusivityError` | | Preserve employment/FTE portfolio shape | ADR 0011 | employment count, staffed assignment count and Decimal staffed FTE remain separate aggregates | concurrent portfolio fixture expects 3 employments, 3 assignments and 1.5000 FTE | | Reject impossible direct aggregate staffing | Builder staffing relationships and assignment allocation bounds | `WorkforceCompositionSnapshot._validate_canonical_invariants` rejects FTE without assignments, non-positive FTE with staffing, staffing without reportable employment/person totals, impossible assigned-person reconciliation, and FTE above the assignment count at construction and export | direct-construction staffing boundary regressions plus post-construction mutation export regression | -| Canonicalize direct staffed FTE evidence | ADR 0011; direct evidence must remain bounded and byte-stable | `_canonicalize_staffed_fte` accepts exact finite non-negative `Decimal` values with at most four fractional places, stores exactly four places, and export rechecks that scale | extreme-scale direct FTE fails closed; `0.5` and `0.5000` serialize/hash identically; post-construction scale mutation fails export | +| Canonicalize direct staffed FTE evidence | ADR 0011; direct evidence must remain bounded and byte-stable | `_canonicalize_staffed_fte` accepts exact finite non-negative `Decimal` values with at most four fractional places, normalizes every mathematical zero to `0.0000`, checks nonzero FTE against staffed-assignment capacity before any digit padding, stores exactly four places, and export rechecks the canonical representation | positive/negative zero hash equivalence, million-place zero-exponent bounded-work regression, oversized nonzero exponent fail-closed regression, `0.5`/`0.5000` equivalence, and post-construction scale-mutation rejection | +| Canonicalize employment-status evidence | Aggregate evidence has one semantic representation and malformed caller containers must fail with domain errors | `_freeze_employment_status_counts` detaches row containers, requires exact two-value `(str, int)` rows with non-negative counts, omits zero-count rows, and export rejects any low-level reinjection that is not already canonical | malformed row-shape/type regressions, boolean/negative count regressions, zero-row omission equivalence, and post-construction zero-row mutation rejection | | Reject overfilled Position seats before aggregation | Existing position-seat invariant + ADR 0011 | each visible `position_record_id` is revalidated with `validate_position_seat_capacity` at the report coordinate | two distinct workers allocating 0.6000 each to one Position must raise `PositionSeatError` instead of reporting 1.2000 staffed FTE | | Bound allocation arithmetic before exact aggregation | People/API four-decimal persistence scale + ADR 0011 | portfolio and Position-seat validators require exact finite `Decimal` allocation ratios in `(0, 1.0000]` with at most four fractional places before `_exact_decimal_total` | >4-place, non-finite and non-Decimal allocation regressions fail closed with governed domain errors | | Fail closed on inconsistent authoritative truth | Existing HRIS integrity contracts + ADR 0011 | single-valued Employment resolution, duplicate Assignment detection, assignment-employment coverage and exact allocation validation | contradictory Employment, duplicate Assignment, person mismatch and >1.0000 per-employment/Position allocation regressions, including low-precision Decimal cases | | Prevent cross-tenant metric contamination | ADR 0003 + ADR 0011 | tenant scope is applied before reconstruction or aggregation | foreign-tenant employment/assignment fixture does not affect tenant metrics | | Minimize downstream PII | ADR 0011 | canonical JSON includes aggregate metrics, opaque tenant ID and report coordinates only | canonical evidence regression rejects row-level `person_record` / `employment_record` names | -| Make aggregate evidence reproducible | ADR 0011 | sorted status tuples, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty-workforce fixture requires stable empty status evidence | -| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time/status containers or trust low-level field mutation at export | snapshot construction stores exact UTC time and detached status-count tuples; canonical export reruns all temporal, tenant and aggregate invariants | mutable timezone, mutable status-container, temporal reinjection, aggregate mutation, and FTE-scale mutation regressions fail closed | +| Make aggregate evidence reproducible | ADR 0011 | sorted nonzero status tuples, fixed-scale FTE, deterministic JSON encoding and SHA-256 over exact UTF-8 bytes | reversed-input fixture requires identical canonical JSON and digest; empty/zero-row and signed-zero fixtures require one canonical representation | +| Prevent post-construction evidence drift | Evidence objects must not retain mutable caller-owned time/status containers or trust low-level field mutation at export | snapshot construction stores exact UTC time and detached canonical status-count tuples; canonical export reruns temporal, tenant, aggregate, status-row, and FTE-representation invariants | mutable timezone, mutable/malformed status-container, zero-row reinjection, temporal reinjection, aggregate mutation, and FTE-scale mutation regressions fail closed | | Keep workforce FTE evidence deterministic | ADR 0011; aggregate totals and deltas must not inherit caller Decimal context | shared `_exact_decimal_total` aligns validated Decimal coefficients before endpoint aggregation and change subtraction, so endpoint JSON, `staffed_fte_change`, canonical JSON, and digests are context-independent | ambient-precision regression compares low- and normal-precision endpoint and change evidence | | Keep workforce intelligence descriptive | ADR 0011 | module contains no recommendation, decision, protected-attribute inference or persistence API | public package boundary and code review; high-impact actions remain outside this slice | | Ground scope in current authoritative standards without claiming certification | ISO 30414:2025 public catalogue metadata; ADR 0011 | no proprietary ISO metric text is embedded in production code | `docs/doctoring/workforce-composition-references.md` | From bec14436acd6e55bc9d0eac0e4352f75d6fbdcbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:17:07 -0700 Subject: [PATCH 53/59] docs(traceability): bind change export revalidation --- docs/traceability/workforce-composition-change.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/traceability/workforce-composition-change.md b/docs/traceability/workforce-composition-change.md index 2ef7acca1..cf2b137b1 100644 --- a/docs/traceability/workforce-composition-change.md +++ b/docs/traceability/workforce-composition-change.md @@ -2,14 +2,15 @@ | Requirement | Orgmetra evidence | Verification | Maturity | |---|---|---|---| -| Compare business-time workforce states without knowledge-time drift | `WorkforceCompositionChangeSnapshot` requires identical endpoint `known_at` values and the builder freezes one cutoff before both endpoints | different-cutoff and sequenced-timezone-provider regressions | implemented_on_active_pr | -| Preserve tenant isolation | endpoint tenants must match; builder supplies one tenant to both existing snapshots | cross-tenant direct-construction rejection | implemented_on_active_pr | -| Require a real forward comparison | opening `effective_on` must be earlier than closing `effective_on` | equal-date rejection plus buyer-readable next action | implemented_on_active_pr | -| Reuse authoritative HRIS integrity | both endpoints call `build_workforce_composition_snapshot(...)` | existing complete HRIS-kernel workforce/integrity suite plus change regression | implemented_on_active_pr | +| Compare business-time workforce states without knowledge-time drift | `WorkforceCompositionChangeSnapshot` requires identical endpoint `known_at` values, rechecks them immediately before canonical export, and the builder freezes one cutoff before both endpoints | different-cutoff, sequenced-timezone-provider, and post-construction cutoff-mutation regressions | implemented_on_active_pr | +| Preserve tenant isolation | endpoint tenants must match at construction and immediately before canonical export; builder supplies one tenant to both existing snapshots | cross-tenant direct-construction rejection plus post-construction tenant-mutation export rejection | implemented_on_active_pr | +| Require a real forward comparison | opening `effective_on` must be earlier than closing `effective_on` at construction and immediately before canonical export | equal-date rejection, post-construction date-order mutation rejection, and buyer-readable next action | implemented_on_active_pr | +| Require exact validated endpoint evidence | both endpoints must remain exact `WorkforceCompositionSnapshot` runtime values; subclasses and low-level endpoint replacement cannot bypass the canonical endpoint contract | forged-subclass construction regressions plus post-construction endpoint-type mutation rejection | implemented_on_active_pr | +| Reuse authoritative HRIS integrity | both endpoints call `build_workforce_composition_snapshot(...)`; endpoint `canonical_json()` rechecks its own canonical workforce invariants during comparison export | existing complete HRIS-kernel workforce/integrity suite plus change and mutation regressions | implemented_on_active_pr | | Keep workforce intelligence descriptive | only net aggregate deltas are exposed; no hire/separation/turnover/cause/recommendation label exists | public API and canonical-schema review | implemented_on_active_pr | -| Preserve exact FTE arithmetic | staffed FTE change uses `Decimal` subtraction | realistic `1.0000` delta regression | implemented_on_active_pr | +| Preserve exact FTE arithmetic | staffed FTE change uses exact Decimal coefficient arithmetic over canonical four-decimal endpoints | realistic `1.0000` delta, caller Decimal-context parity, and canonical endpoint-FTE regressions | implemented_on_active_pr | | Avoid row-level shadow HR data | canonical comparison embeds aggregate endpoint JSON/digests only | canonical output excludes `person_record_id` and all endpoint row identities by construction | implemented_on_active_pr | -| Deterministic audit correlation | canonical JSON + SHA-256 content digest | reordered-source equality regression | implemented_on_active_pr | +| Deterministic audit correlation | canonical JSON + SHA-256 content digest are emitted only after cross-endpoint and endpoint-level invariant revalidation | reordered-source equality plus post-construction tenant/date/cutoff/type mutation fail-closed regressions | implemented_on_active_pr | | Current standards traceability | ADR-0024 + doctoring record for ISO 30414:2025 and ISO 30400:2022 public metadata | official ISO catalogue rechecked August 20, 2026 | implemented_on_active_pr | | Exact owned production coverage | `Workforce Intelligence Quality` runs complete HRIS kernel | package pytest-cov requires 100% statement and branch coverage | implemented_on_active_pr | From 672ce23b8fbe14744448220d826ffccb73d227f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:17:22 -0700 Subject: [PATCH 54/59] docs(changelog): record canonical evidence repair --- packages/hris-kernel/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/CHANGELOG.md b/packages/hris-kernel/CHANGELOG.md index 862d76a30..23dca5b4c 100644 --- a/packages/hris-kernel/CHANGELOG.md +++ b/packages/hris-kernel/CHANGELOG.md @@ -6,8 +6,9 @@ - Report aggregate net changes for distinct-person headcount, reportable employments, staffed assignments, staffed Decimal FTE, unassigned people, and status counts without serializing row-level HR identities. - Fail closed on cross-tenant endpoints, non-forward effective dates, and different knowledge cutoffs so recorded corrections cannot masquerade as business-time workforce movement. - Freeze timezone-aware knowledge cutoffs to detached UTC datetimes and copy status-count containers before canonical serialization, preventing mutable caller objects or timezone providers from changing snapshot evidence after construction. -- Canonicalize direct staffed FTE evidence to exactly four fractional places, reject extreme fractional scale before exact comparison arithmetic, and recheck the fixed scale during export so equivalent values cannot produce different canonical bytes or digests. +- Canonicalize every mathematical staffed-FTE zero to exact `0.0000`, reject oversized nonzero evidence against staffed-assignment capacity before any representation expansion, normalize accepted direct FTE to four fractional places, and recheck that representation during export. +- Canonicalize employment-status evidence by requiring exact two-value `(str, int)` rows, rejecting malformed/boolean/negative counts with governed domain errors, and omitting semantic zero-count rows so equivalent aggregates have one JSON/hash representation. - Revalidate every workforce aggregate and temporal invariant immediately before canonical JSON or digest export, so low-level post-construction mutation cannot become new audit evidence. -- Reject non-`Decimal` staffed FTE and boolean, negative, or non-integer per-status employment counts during direct workforce snapshot construction before arithmetic or canonical serialization. +- Revalidate comparison endpoint runtime types, tenant identity, forward effective-date order, and one exact knowledge cutoff immediately before change-evidence export, preventing low-level endpoint mutation from bypassing the constructor contract. - Require every allocation ratio reaching employment-portfolio or Position-seat aggregation to be an exact finite `Decimal` in `(0, 1.0000]` with at most four fractional places, preventing extreme-scale values from reaching exact coefficient arithmetic. - Keep the contract descriptive: endpoint deltas are not labeled as hires, separations, transfers, turnover, causes, forecasts, protected-attribute effects, or employment recommendations. From 9a04f9dedd85bd92571cc01f5541c018780b0050 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:18:30 -0700 Subject: [PATCH 55/59] test(workforce): reproduce malformed endpoint date leak --- .../test_workforce_canonical_evidence_hardening.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py b/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py index a396944d4..eedd1610a 100644 --- a/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py +++ b/packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py @@ -165,6 +165,17 @@ def test_change_export_rechecks_date_order_after_endpoint_mutation() -> None: change.canonical_json() +def test_change_export_rechecks_malformed_endpoint_date_before_comparison() -> None: + """Malformed endpoint time must fail with the snapshot domain contract, not raw TypeError.""" + opening = _snapshot(date(2026, 1, 15)) + closing = _snapshot(date(2026, 2, 15)) + change = WorkforceCompositionChangeSnapshot(opening, closing) + object.__setattr__(opening, "effective_on", "2026-01-15") + + with pytest.raises(IntervalError, match="temporal evidence"): + change.canonical_json() + + def test_change_export_rechecks_cutoff_after_endpoint_mutation() -> None: """Post-construction recorded-time mutation must not mix knowledge cutoffs.""" opening = _snapshot(date(2026, 1, 15)) From 1fedd6cc65e0365168e5d6d7597e3fffbf0d3b4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:19:28 -0700 Subject: [PATCH 56/59] fix(workforce): validate endpoints before comparison --- .../src/orgmetra_hris_kernel/workforce_change.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py index 4adfb0833..546762644 100644 --- a/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py +++ b/packages/hris-kernel/src/orgmetra_hris_kernel/workforce_change.py @@ -38,11 +38,15 @@ def __post_init__(self) -> None: self._validate_comparable_endpoints() def _validate_comparable_endpoints(self) -> None: - """Recheck exact endpoint types and cross-endpoint coordinates without mutation.""" + """Recheck exact endpoint evidence before comparing cross-endpoint coordinates.""" if type(self.opening_snapshot) is not WorkforceCompositionSnapshot: raise TypeError("opening_snapshot must be an exact WorkforceCompositionSnapshot") if type(self.closing_snapshot) is not WorkforceCompositionSnapshot: raise TypeError("closing_snapshot must be an exact WorkforceCompositionSnapshot") + + self.opening_snapshot._validate_canonical_invariants() + self.closing_snapshot._validate_canonical_invariants() + if self.opening_snapshot.tenant_record_id != self.closing_snapshot.tenant_record_id: raise IdentityScopeError( "Workforce change snapshots must belong to the same tenant.", @@ -175,4 +179,4 @@ def build_workforce_composition_change_snapshot( effective_on=to_effective_on, known_at=known_at, ) - return WorkforceCompositionChangeSnapshot(opening, closing) \ No newline at end of file + return WorkforceCompositionChangeSnapshot(opening, closing) From 6726d05a626f9560d5891c2cf8c4dd5ba32f3668 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:24:24 -0700 Subject: [PATCH 57/59] fix(provenance): refresh workforce ADR manifest evidence --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 4b0d55842..9c5c7470a 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"e859db116605ff05ad3ee4c4d88ec857756a4ec46cbb1528b350670f449a9b4f","bytes":17963,"lines":79},{"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"9ad7d2bec1e4623ed7f5abcc797f35dd834eb603bac51bae80bb3950e66ad5fb","bytes":7661,"lines":59},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"e859db116605ff05ad3ee4c4d88ec857756a4ec46cbb1528b350670f449a9b4f","bytes":17963,"lines":79},{"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"d222c52807f6e007d791287db6cce3e4cefd224f92605a7d2df86c60a8f7b55c","bytes":8155,"lines":60},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} \ No newline at end of file From d6233a7c1a9aec2759e3681aaea019fb2822900e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 12:02:36 -0700 Subject: [PATCH 58/59] docs(workforce): trace canonical evidence hardening tests --- docs/adr/0011-bitemporal-workforce-composition.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0011-bitemporal-workforce-composition.md b/docs/adr/0011-bitemporal-workforce-composition.md index d318d7b69..1794eaf1d 100644 --- a/docs/adr/0011-bitemporal-workforce-composition.md +++ b/docs/adr/0011-bitemporal-workforce-composition.md @@ -53,7 +53,7 @@ Orgmetra will expose a pure `WorkforceCompositionSnapshot` derived from authorit ## Verification -`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, `packages/hris-kernel/tests/test_assignment_portfolio.py`, `packages/hris-kernel/tests/test_position_coverage.py`, and `packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, fixed four-decimal FTE canonicalization, extreme-scale FTE rejection, post-construction export revalidation including scale mutation, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, allocation type/finiteness/scale rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. +`packages/hris-kernel/tests/test_workforce_composition.py`, `packages/hris-kernel/tests/test_workforce_composition_boundaries.py`, `packages/hris-kernel/tests/test_workforce_position_capacity.py`, `packages/hris-kernel/tests/test_workforce_composition_change.py`, `packages/hris-kernel/tests/test_assignment_portfolio.py`, `packages/hris-kernel/tests/test_position_coverage.py`, `packages/hris-kernel/tests/test_allocation_and_snapshot_export_regressions.py`, and `packages/hris-kernel/tests/test_workforce_canonical_evidence_hardening.py` require tenant isolation, concurrent-employment person deduplication, active/leave composition, terminated exclusion, future-effective and late-recorded exclusion, FTE and unassigned-person reporting, deterministic canonical evidence, fixed four-decimal FTE canonicalization, extreme-scale FTE rejection, post-construction export revalidation including scale mutation, context-independent endpoint and workforce-change FTE arithmetic, historical recorded-time reconstruction, duplicate-version rejection, overlapping-exclusive-employment rejection, position-seat over-allocation rejection, low-precision allocation-limit rejection, allocation type/finiteness/scale rejection, assignment-person integrity, per-employment allocation-integrity reuse, and timezone-aware knowledge cutoffs. `.github/workflows/workforce-intelligence-quality.yml` checks out the exact candidate SHA and runs the complete HRIS kernel with the package's 100% statement and branch coverage threshold. ## References From 90e2cffdafb2ca385359068b53f5ca1dc437a5af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 12:07:17 -0700 Subject: [PATCH 59/59] fix(provenance): refresh workforce ADR manifest evidence --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 9c5c7470a..8836d30b8 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"e859db116605ff05ad3ee4c4d88ec857756a4ec46cbb1528b350670f449a9b4f","bytes":17963,"lines":79},{"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"d222c52807f6e007d791287db6cce3e4cefd224f92605a7d2df86c60a8f7b55c","bytes":8155,"lines":60},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} \ No newline at end of file +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"e859db116605ff05ad3ee4c4d88ec857756a4ec46cbb1528b350670f449a9b4f","bytes":17963,"lines":79},{"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":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"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":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"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":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"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/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"9e74051c1d37454e5ba4d35bd97eddd96429125faa6dd81a40220b468593f566","bytes":8232,"lines":60},{"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","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"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":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"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":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"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_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"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":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} \ No newline at end of file