Skip to content

feat(ontology): canonical receipts record whose they are - #850

Merged
jason931225 merged 1 commit into
mainfrom
feat/receipt-owner-attribution
Aug 20, 2026
Merged

feat(ontology): canonical receipts record whose they are#850
jason931225 merged 1 commit into
mainfrom
feat/receipt-owner-attribution

Conversation

@jason931225

Copy link
Copy Markdown
Collaborator

Migration 0223 gave ont_action_command_receipts an owner and a target, but no writer set them. owner DEFAULTs to 'ontology.action', so every canonical receipt — Company revise, HR appoint, PayRun decide — claimed to belong to the pre-existing instance-action path.

That's worse than the column not existing. An absent column is a known gap; a populated one reads as an answer, and this one answered wrongly for six of the seven writers.

Derived from the command's query, not action_key

Every canonical query already implements dispatch_target() — the same value the projected-dispatch path uses — so attribution is a lookup, not a second mapping that can drift:

let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());

action_key cannot do this job, and I tried it first. Its own doc says it is "unique only per object type": these commands carry a bare "revise", which names no target at all, and employment.rs reassigns org units under "internal.reassign_org_unit", which is not a dispatch target by construction. Parsing it would have failed closed on a real production path.

ontology/rest is deliberately unchanged — it is the instance-action path, the rows the DEFAULT was written for, so 'ontology.action' with a NULL target is the truth there, and 0223's CHECK requires exactly that pairing.

Proven against real PostgreSQL

a real Company port write owner='company', target='company.revise'
every DispatchTarget storable under its owning object — looped over DispatchTarget::ALL, so a fourteenth target can't arrive unattributed
canonical receipts none fall back to the instance-action default

Mutation-proven: removing the two binds from company.rs turns the port test red with left: "ontology.action" — the exact wrong attribution this fixes.

All five canonical port suites, the PayRun port suite and the widening suite pass.

What this does not do

The DEFAULT stays — dropping it belongs with the change that makes ontology/rest pass owner explicitly, and dropping it now breaks that writer on the next deploy.

Per-row attribution is now true where it is written, but nothing yet stops a crate writing a row it doesn't own. That's the receipt store's writer boundary — bounded to three crates by #829, still without per-row enforcement (console-yw0).

🤖 Generated with Claude Code

Migration 0223 gave `ont_action_command_receipts` an `owner` and a `target`,
but no writer set them. `owner` DEFAULTs to `'ontology.action'`, so every
canonical receipt — Company revise, HR appoint, PayRun decide — claimed to
belong to the pre-existing instance-action path.

That is worse than the column not existing. An absent column is a known gap;
a populated one reads as an answer, and this one answered wrongly for six of
the seven writers.

DERIVED FROM THE COMMAND'S QUERY, not from `action_key`. Every canonical
query already implements `dispatch_target()` — the same value the projected
dispatch path uses — so the attribution is a lookup, not a second mapping to
drift:

    let receipt_target = command.query.dispatch_target();
    let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());

`action_key` cannot do this job, and I tried it first. Its own doc says it is
"unique only per object type": this suite's commands carry a bare `"revise"`,
which names no target at all, and `employment.rs` reassigns org units under
`"internal.reassign_org_unit"`, which is not a dispatch target by
construction. Parsing it would have failed closed on a real production path.

`ontology/rest` is deliberately UNCHANGED. It is the instance-action path —
the rows the widening's DEFAULT was written for — so `'ontology.action'` with
a NULL target is the truth there, and 0223's CHECK requires exactly that
pairing.

Proven against real PostgreSQL:

  a real Company port write -> owner='company', target='company.revise'
  every DispatchTarget       -> storable under the object that owns it,
                                looped over DispatchTarget::ALL so a
                                fourteenth target cannot arrive unattributed
  no canonical receipt        -> falls back to the instance-action default

Mutation-proven: removing the two binds from `company.rs` turns the port test
red with `left: "ontology.action"` — the exact wrong attribution this fixes.

All five canonical port suites, the PayRun port suite and the widening suite
pass.

WHAT THIS DOES NOT DO. The DEFAULT stays. Dropping it belongs with the change
that makes `ontology/rest` pass `owner` explicitly, and dropping it now would
break that writer on the next deploy. Per-row attribution is now TRUE where it
is written, but nothing yet stops a crate writing a row it does not own —
that is the receipt store's writer boundary, bounded to three crates by #829
and still without per-row enforcement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jason931225

Copy link
Copy Markdown
Collaborator Author

Pull Request Review: #850 - feat(ontology): canonical receipts record whose they are

Executive Overview

  • Core Problem Addressed: Migration 0223 added owner and target columns to ont_action_command_receipts with a default owner of 'ontology.action'. Prior to this PR, canonical port writers (company, employment, job_position, org_unit, person, and pay_run) omitted these columns during command receipt insertion, resulting in canonical receipts (Company revise, HR appointments, PayRun decisions) being misattributed to the legacy instance-action path (ontology.action with NULL target).
  • Implementation Approach: Instead of brittle string parsing of action_key, the PR derives receipt attribution directly from the strongly-typed command query (command.query.dispatch_target()) and wraps the owning domain object into ReceiptOwner::Canonical(receipt_target.object()). All 6 canonical adapters now bind owner and target explicitly in parameterized SQL statements.
  • Verification & Test Coverage: Validated against live PostgreSQL migrations. Includes integration test a_company_receipt_is_attributed_to_company and exhaustive coverage across DispatchTarget::ALL in receipt_owner_widening.rs, ensuring future dispatch targets cannot be added without attribution support. Baseline test metrics in executed-tests-baseline.json are appropriately updated.

16-Lens Canonical Reasoning Framework Evaluation

# Lens Assessment Status
1 Cartesian Doubt Directly resolves the root issue: eliminates false data attribution where valid canonical actions were logged under the instance-action default. PASS
2 Essentialism / YAGNI Minimal and surgical. Reuses existing dispatch_target() contracts without introducing speculative abstractions or intermediary wrappers. PASS
3 Chesterton's Fence Respects why the 'ontology.action' default exists in migration 0223 (to support legacy ontology/rest instance-action path) and leaves the DB default intact to avoid breaking deployment order. PASS
4 Contrarian / Outside-the-box Evaluated deriving from action_key vs command.query.dispatch_target(). Choosing command.query.dispatch_target() correctly avoids ambiguities with non-dispatch action keys like internal.reassign_org_unit. PASS
5 Socratic Query interfaces and enum contracts are upheld. Exhaustive iteration over DispatchTarget::ALL in tests ensures compiler-level and database-level invariants hold. PASS
6 Pragmatism Uniformly updates all six canonical port adapters (company, employment, job_position, org_unit, person, pay_run) with zero unnecessary churn. PASS
7 Red Team No SQL injection risk: all values are strictly parameterized using $1..$10 sqlx bind parameters. Multi-tenant org_id scoping remains strictly intact. PASS
8 Systems Thinking Downstream receipt consumers, audit log projections, and idempotency replays will now accurately distinguish between canonical object actions and instance-actions. PASS
9 Operability / Day-2 Substantially improves on-call triage and incident investigation by allowing direct filtering on owner and target in ont_action_command_receipts. PASS
10 Opportunity Cost High ROI: fixes silent data corruption at minimal implementation and maintenance cost. PASS
11 Blast-radius / Cell-based Localized to receipt insertion within existing transactional boundaries (tx). No cross-service or cross-tenant side effects. PASS
12 Constant-work / Anti-fragility dispatch_target() and .object() are pure in-memory $O(1)$ enum lookups; no latency overhead or heap allocation hotpaths introduced. PASS
13 Shared-nothing / Eventual consistency Atomicity preserved: receipt recording executes in the exact same database transaction as the entity state mutation. PASS
14 FinOps / Unit-cost Negligible storage footprint for owner and target columns; zero added network hops or query round-trips. PASS
15 Telemetry-first Enables precise OpenTelemetry tracing and structured log aggregation correlated to concrete domain objects and targets. PASS
16 Zero-trust / Defense-in-depth Ensures attribution validity at write time. Acknowledges future boundary enforcement (console-yw0) while establishing row-level data truth. PASS

Strict Scrutiny Compliance Checklist

  • Type Safety & Contracts: Strongly typed enums (ReceiptOwner, DispatchTarget) converted to string representations at SQL bind boundary.
  • Concurrency & State: Executed within active transaction (*tx / tx.as_mut()).
  • Security & Access Control: Parameterized queries across all modified adapters.
  • Performance & Scalability: Zero extra database round-trips ($O(1)$ in-memory dispatch resolution).
  • Test Coverage & Quality: Comprehensive live PostgreSQL tests added, covering specific actions and full DispatchTarget::ALL domain space.
  • Architecture: Clean adherence to Hexagonal / Ports & Adapters architecture.

🤖 [Reviewed] by Oyatie Anvil

@jason931225

Copy link
Copy Markdown
Collaborator Author

❌ Blocked — 9 finding(s) across 68 gates; 6 gate(s) produced no measurement.

  • cell-isolation — failed: Cell isolation warnings (1 items): SQL Query without explicit tenant_id filter (multi-tenant bleed risk)
  • monorepo — failed: Monorepo pattern warnings (6 items): OVERSIZED_WHOLE_FILE: Total lines: 436; OVERSIZED_WHOLE_FILE: Total lines: 1121; OVERSIZED_WHOLE_FILE: Total lines: 617; OVERSIZED_WHOLE_FILE: Total lines: 540; OVERSIZED_WHOLE_FILE: Total lines: 475; OVERSIZED_WHOLE_FILE: Total lines: 945
  • modularization — failed: Modularization & depth findings (8 violations): backend/crates/ontology/canonical-adapter-postgres/src/company.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/ontology/canonical-adapter-postgres/src/employment.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/ontology/canonical-adapter-postgres/src/job_position.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/ontology/canonical-adapter-postgres/src/org_unit.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/ontology/canonical-adapter-postgres/src/person.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/ontology/canonical-adapter-postgres/tests/company_port_as_runtime_role.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/ontology/canonical-adapter-postgres/tests/receipt_owner_widening.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.; backend/crates/payroll/adapter-postgres/src/pay_run.rs: Directory depth 6 exceeds maximum allowed limit of 5 for category 'production_code'. Flatten module structure.
    • fix: split into submodules; ceiling is 300 effective lines
  • coverage — not measured: the synchronous entry point cannot run a coverage tool; call CoverageGuard::measure_diff_coverage
    • fix: add tests covering the lines this PR adds
    • note: this gate is aspirational fidelity and does not fully measure what its name implies
  • slo — not measured: no Prometheus or OpenTelemetry endpoint is configured, so error budget consumption over any window was never queried
    • note: this gate is aspirational fidelity and does not fully measure what its name implies
  • cluster-audit — not measured: no Kubernetes API or ArgoCD cluster access is configured, so no live state was read back and no comparison against Git was performed
    • note: this gate is aspirational fidelity and does not fully measure what its name implies
  • ci-wallclock — not measured: no GitHub Actions workflow-run timing API access is configured, so neither this PR's CI duration nor its billable compute was read
    • note: this gate is aspirational fidelity and does not fully measure what its name implies
  • remote-cache — not measured: no sccache or Buck2 CAS statistics endpoint is configured, so no cache hit rate was read and no lockfile was hashed
    • note: this gate is aspirational fidelity and does not fully measure what its name implies
  • shadow-traffic — not measured: no traffic mirror and no replay target are configured, so no production requests were sampled and no responses were compared
    • note: this gate is aspirational fidelity and does not fully measure what its name implies

🤖 [Blocked] by Oyatie Anvil

@jason931225
jason931225 added this pull request to the merge queue Aug 20, 2026
Merged via the queue into main with commit a7fe1fb Aug 20, 2026
32 checks passed
@jason931225
jason931225 deleted the feat/receipt-owner-attribution branch August 20, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant