Skip to content

promote(dev): fast-forward integration trunk from main - #860

Merged
jason931225 merged 15 commits into
devfrom
main
Aug 21, 2026
Merged

promote(dev): fast-forward integration trunk from main#860
jason931225 merged 15 commits into
devfrom
main

Conversation

@jason931225

Copy link
Copy Markdown
Collaborator

Integration trunk is dev. main is 15 commits ahead and is where merges have been landing. This PR is a no-diff promotion so dev becomes the tip. Next: default branch = dev; staging > canary > production only from the predecessor rung.

jason931225 and others added 15 commits August 19, 2026 23:26
…ed rows (#834)

`scripts/stage_coss_group_payroll_readiness.sql` is the **only** writer
of `payroll_draft_lines` — there is no production Rust INSERT — so it is
the sole path by which a payroll roster exists. Two holes, both measured
against a live PostgreSQL with the real migrations applied.

## 1. A re-stage rewound a PAID run

The run upsert ended with an unconditional `DO UPDATE SET status =
'BLOCKED_LEGAL_GATE'`, and the lifecycle drives runs through
`ATTENDANCE_CLOSED → CALCULATED → APPROVED → DISBURSEMENT_SCHEDULED →
ISSUED → PAID`. Re-running the script over any of those reset the
status, rewrote `source_summary`, and — because the line upsert joins
the returned run — rewrote every line's `attendance_source_row_count`
and `source_data_import_row_ids` **underneath the `close_receipt` that
attested them**, desynchronising the append-only
`payroll_line_calculations` from the lines they were computed from.

Measured, seeded run at `PAID`:

| | |
|---|---|
| guard present | PAID → **PAID** |
| guard removed | PAID → **BLOCKED_LEGAL_GATE** |

A run past the pre-close states is now skipped entirely — it returns no
row, so the line upsert joins nothing and its evidence is untouched.
Doing nothing is correct; rewinding a paid run is not. The three
admissible states are the ones `CLOSEABLE` already names in
`lifecycle.rs`.

## 2. Unapplied and errored import rows counted as payroll source
material

`data_import_runs.status` admits `PREVIEWED/DRY_RUN/APPLIED/FAILED`;
`data_import_rows.row_status` admits `CANDIDATE/PRESERVED/ERROR`
(migration 0070). The source CTE filtered on **neither** — the only
thing keeping unapplied rows out of a payroll roster was a human
hand-typing one vetted `source_filename LIKE '2026/5월/%'`.

Measured matrix, one org per case, **two positive controls** so the
negatives cannot be vacuous:

```
APPLIED   + CANDIDATE  -> 1 line   (positive control)
APPLIED   + PRESERVED  -> 1 line   (positive control)
DRY_RUN   + CANDIDATE  -> 0 lines
PREVIEWED + CANDIDATE  -> 0 lines
FAILED    + CANDIDATE  -> 0 lines
APPLIED   + ERROR      -> 0 lines

filter removed -> DRY_RUN org and ERROR-row org each materialise a line again
```

## Pinned so it cannot regress

Both fixes are pinned in the **G008 gate**, which already reads this
file and runs at `ci.yml:1646`. Mutation-proven — removing any one of
the three pinned clauses turns G008 red; restored, it passes 21 checks.

## Explicitly not fixed here

`is_attendance_source` is JSONB **key presence** (`raw_row ?|
array['출근', ...]`), so a row whose 출근 column is blank still counts as
attendance material. Provenance now guarantees the rows were *applied*;
it does not yet guarantee they *say anything*. That is the next piece,
tracked in `console-4eo`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…olumn (#836)

The five source-material flags were `raw_row ?| array['출근', ...]` — true
when the workbook merely **has** the column, whatever the cell contains.
`is_attendance_source` feeds `attendance_source_row_count`, which is
exactly what the close preflight's **근태 원천 확보** check reads. So a blank
attendance column satisfied the gate.

## Measured, two orgs identical but for one cell

| 출근 | before | after |
|---|---|---|
| `'09:00'` | count = 1 | **1 line, count = 1** |
| `''` (blank) | **count = 1** ← the hole | **0 lines** |

After the fix a blank row produces *no roster line at all*: every source
flag is false and there's no leave balance, so it fails the admission
predicate. Combined with #833's `roster_total > 0`, a run whose rows are
all blank now **cannot close at all** — rather than closing on evidence
that says nothing.

Mutation-proven: reverting `is_attendance_source` to the `?|` form
brings the blank row straight back at count 1.

## This completes a chain

| PR | stopped |
|---|---|
| #833 | an **empty** roster satisfying the gate |
| #834 | **unapplied / errored** import rows becoming roster material |
| this | **applied** rows that carry nothing |

Provenance said the rows were real; substance says they contain
something.

The allowlist is unchanged — same Korean headers, same application. Only
the test changed, from key presence to a non-blank value, with a
`jsonb_typeof(...) = 'object'` guard so a non-object `raw_row` cannot
make `jsonb_each_text` raise.

## A fence I had to look at first

G008 already pinned this line as *"stage SQL classifies
payroll/attendance source rows by allowlisted headers"* — but it pinned
the `raw_row?|array[` **idiom**, so it also, accidentally, pinned
key-presence as the test. The property it names is the **allowlist**,
which this change preserves exactly. The assertion now matches the
allowlist itself rather than the operator that consumed it.

## New pins, both mutation-proven

- **`?|array` must not reappear** — reverting *any one* of the five
flags to key presence turns G008 red.
- **All five flags must carry the non-blank test, counted.** A plain
`includes` check passed while four of five were weakened — the same
shape of hole as the bug itself, so it's pinned by count.

G008 passes 23 checks. Exit codes captured directly, not through a pipe.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… not (#838)

`load_payslip_issuance_in_tx` guards `run.status == "PAID"`
(`lifecycle.rs:1351`) before selecting money for payslips. **Nothing
pinned that guard** — deleting it left the whole suite green.

## A status assertion would not have caught it either

`record_payslip_deliveries_in_tx:1406` raises the identical
`invalid_state` error, so the caller still gets a clean **409** with the
guard gone. What doesn't survive is the vault: `issue_payslips` emits
inbox documents **outside the refusing transaction**, so a payslip for
an *unpaid* run reaches the employee's inbox while the API reports
refusal.

So the new assertion counts **documents**, not status codes.

## Measured against real PostgreSQL

| | |
|---|---|
| baseline | 4 passed / 0 failed |
| delete `lifecycle.rs:1351-1353` | **FAILED** — *"a run at APPROVED
must issue no payslip; the caller's 409 does not undo a document already
emitted outside the refusing transaction"* (the 409 assertion stayed
**green**) |
| restore | 4 passed / 0 failed |
| delete `validate_run_release_gate` | the pre-existing `legal_gate`
assertion at `:416` reddens — that path is still independently pinned |
| restore | 4 passed / 0 failed |

## Order is load-bearing, and the comment says so

`emit_inbox_doc` dedups on `payroll-run:{run}:line:{line}`, so a leaked
document created *after* the legitimate issuance collapses into it and
`leaked == 0` goes vacuously true. The block sits **above** the PAID
issuance for that reason.

The release gate is registered before the probe so the PAID check is the
guard actually under test — otherwise the request is refused by the
legal gate and the test proves nothing about status — then removed again
so the legal-gate refusal below still exercises its own path.

## What this deliberately does not do

`payable` is still not consulted on issuance. Two designs that would
have changed that were **refuted**:

- **Gating on `payable`** blocks 100% of issuance *permanently* — `0222`
re-grants INSERT on every column **except** `payable`, and no role in
the tree may set it, so no row is ever payable.
- **Giving it a writer** installs a second unwritable attestation that
cannot ship a mutation proof until a release-gate writer exists — a
false green.

Blocking statutory pay is a worse failure than the gap. The gap stays
open and tracked in `console-afw`.

## Correcting myself

I earlier recorded that `payable` is read by nothing. **That was wrong**
— my grep was truncated by `head`. It is read at `lifecycle.rs:744`
(`BOOL_AND(payable)`), surfaced as `RunCalcSummary.payable`, and
asserted at `run_lifecycle_api.rs:140`. It is *reported but not enforced
at issuance* — a narrower defect than "read by nothing".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
)

Audited **all twelve** gate binaries by *running* each from an empty
directory. Eleven resolve their root from `current_dir()`, so that is a
real test of what they do with nothing to examine. Two passed:

```
console-gate-migration-safety: checking workspace at /tmp/empty
console-gate-migration-safety: PASSED
console-gate-pii-no-logs: checking workspace at /tmp/empty
console-gate-pii-no-logs: PASSED
```

Both report *violations*, so zero files yield zero violations and the
gate prints PASSED. The workspace holds hundreds of migrations and
thousands of Rust files — zero means the scan didn't find them (moved
directory, wrong cwd), never that the migrations are all safe or that
nothing logs personal data. For a PII gate that is **absence of evidence
reported as evidence of absence**, which is the entire thing the gate
exists to assert.

## The rule already exists here

`topology.canonical_enforcement` refuses to claim enforcement over zero
tables. `tools/ci/gate-sweep.mjs` refuses a manifest declaring zero
gates. `fanout.py`'s `_in_slice` denies an empty allow list. Payroll's
close preflight did **not**, and an empty roster silently satisfied it
(#833).

This is that same defect — found by looking for it, rather than by being
bitten again.

## Measured

| | |
|---|---|
| empty tree, before | exit 0, `PASSED` |
| empty tree, after | exit 1, *"examined no migration files … a gate
that examines nothing cannot report safety"* / *"examined no Rust source
files … cannot report absence"* |
| real tree, after | exit 0 for both — **no legitimate run blocked** |

Both `main.rs` already exit 1 on `Err`, so no entry-point change was
needed.

Each floor ships a regression test **and a positive control**, because a
floor can be satisfied by a gate that refuses everything — which would
block every migration change and every backend change, worse than the
hole. Mutation-proven: deleting either `files.is_empty()` block turns
that gate's suite red; restored, **17** and **7** pass.

## Not fixed here, and worth naming

`console-gate-writer-ownership` resolves its root from
`env!("CARGO_MANIFEST_DIR")` — a compile-time constant — so it cannot be
pointed at a tree and this audit could not test it the same way. **A
gate that cannot be aimed cannot be tested over an empty tree.** Its
shared-authority ratchet (#829) does make it fail when nothing writes
the receipt store, so the empty case is covered incidentally rather than
by design.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…840)

`.config/nextest.toml` has pinned cargo-nextest **0.9.138** since it
landed, and `cargo_needs_postgres.sh` has accepted `--runner nextest`
since #814. Nothing installed it, so every shard still ran under cargo.

Measured on the heaviest multi-test target
(`identity-rest-org-setup-pg`): **43.472s cargo → 18.525s nextest, 39/39
tests identical — 2.35×.**

## The recorded blocker was wrong

The bead said the prebuilt tarball *"adds a network download the
hardening model has no precedent for."* It has **two**: `security.yml`
installs Trivy by fetching one exact URL and checking one exact sha256
before extracting — pinned byte-for-byte as `TRIVY_INSTALL` in
`check-workflow-hardening.mjs` — and kubectl uses the same `curl` +
`sha256sum --check` shape. This installer is modelled on Trivy's, so
it's the *existing* posture, not a new one.

`.config/nextest.toml`'s comment pointed at `taiki-e/install-action`,
which this repo has never used. This script is what actually implements
the pin it names.

Rolled out to **domain-b only**, as the bead asks — concurrency is
proven for one target, not all 209.

## What is pinned, and where

The ci.yml step digest proves only that the job **calls** the installer;
it says nothing about what the installer does. So the artifact is pinned
separately *against the script itself*: exact URL, exact sha256,
verification via `sha256sum --check`, and that it happens **before**
`tar -xzf`. Without that second pin the digest would look like
supply-chain protection while protecting nothing.

Comment lines are excluded from those checks deliberately — a plain
`includes("sha256sum --check")` was satisfied by the script's own prose,
so deleting the real verification left the pin green. Caught by
mutation; same shape of hole this gate exists to refuse.

## Mutation-proven (exit codes captured directly, not through a pipe)

| mutation | result |
|---|---|
| domain-b silently reverted to cargo | preflight exit 1 |
| installer sha256 swapped | exit 1 |
| pinned URL swapped for a moving "latest" | exit 1 |
| real verification line deleted | exit 1 |
| verification moved *after* extraction | exit 1 — "must verify the
sha256 BEFORE extracting" |
| restored | exit 0 |

Behaviour proven in a **linux/amd64 container**, not inferred: fetches,
verifies, extracts, reports `cargo-nextest 0.9.138`; with a wrong hash
it exits 1 and installs **no binary at all**.

## It cannot be a silent no-op

The installer asserts the installed binary reports the pinned version.
An installer that exits 0 having done nothing would drop the shard back
to cargo and report a green run that measured nothing — and an untracked
45-byte stub whose entire body was `exit 0` has been sitting in a
working tree since 2026-08-18. This replaces it with one that cannot
lie.

Ratchets moved **up**: run-step coverage 125 → 126, bypass matrix 375 →
378, because the new step goes through the same three mutations as every
other. 61 preflight contract tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#841)

`ont_action_command_receipts` (0177) is the authority record for **all
thirteen dispatch targets**, written by three production crates, and
carried **no attribution at all**. #829 bounded *which crates* may write
it; it could not express per-row attribution because there was no column
to attribute to.

`ReceiptOwner::owner_check_constraint_sql()` and
`target_check_constraint_sql()` have generated the exact constraint
bodies for this widening since the roster landed — but **nothing
executed them**. They were referenced only by unit tests asserting the
generated *string*, so they described columns PostgreSQL had never seen:
a control that looks present and is not.

This migration is the one specified in `canonical-domain/src/lib.rs`,
and its two CHECK bodies are **copied from those generators**, not
retyped.

## Why this shape

A plain `ADD COLUMN owner TEXT NOT NULL` fails on any database already
holding a receipt, and would break the live REST writer whose INSERT
names its columns explicitly. It couldn't be repaired by a backfill
`UPDATE` either — 0177's `BEFORE UPDATE OR DELETE` trigger RAISEs on
every row. `ADD COLUMN … DEFAULT` is DDL (no row trigger fires), and
`ADD CONSTRAINT` then validates pre-existing rows as they stand.

## Proven against real PostgreSQL, on a table holding a pre-existing
receipt

That's the case the naive form breaks — and the case an empty-table test
would miss. **My first attempt did miss it**: an aborted seed left the
table empty and the migration "passed" over nothing.

```
apply over 1 existing receipt -> exit 0; row becomes owner='ontology.action', target NULL
canonical owner + valid target   -> accepted
canonical owner, NO target       -> rejected
ontology.action WITH a target    -> rejected
owner outside the roster         -> rejected
canonical owner + unknown target -> rejected
```

Those five are now a **test**, not a transcript. It drives them over
`ReceiptOwner::ALL` and `DispatchTarget::ALL` rather than a hand-listed
set, so a seventh object key can't be added without either passing here
or failing loudly. Mutation-proven: dropping the `(owner =
'ontology.action') = (target IS NULL)` clause turns the test red.

## Wired, not merely written

`check-executed-tests.mjs` caught the new file as executing nowhere —
*"the repository has 1 more test that cannot fail"* — so it's declared
in `TEST_RESOURCE_REQUIREMENTS`, its BUCK target regenerated, and its
postgres-cargo-map entry added with a **measured** 3.3s rather than left
to imputation.

## Deliberately not here

Dropping the `DEFAULT`. That belongs with the change making the REST
writer pass `owner` explicitly; dropping it now breaks that writer on
the next deploy. Per-row attribution is now **expressible and
database-enforced**, but no writer sets a canonical owner yet, so
`console-yw0` stays open until they do.

Authored as a PR for review per your decision on migration ownership —
nothing here is applied to any live database.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#844)

`stage_coss_group_payroll_readiness.sql` scopes a payroll roster with
`source_filename LIKE '2026/5월/%'`. `source_filename` is whatever the
operator named the upload — `hr.rs` sets it from `upload.filename`, with
no convention documented or enforced. **That literal is the only thing
standing between a payroll roster and material from the wrong month.**

Migration 0224 adds `pay_period_start`/`pay_period_end` to
`data_import_runs`: NOT NULL, no DEFAULT, no sentinel, ordering CHECKed,
immutable after insert.

## Why the run and not the row

Three facts, each sufficient, all re-derived:

- `data_import_rows` is **append-only**
(`trg_data_import_rows_no_update`) — a mis-parsed per-row period could
never be corrected
- the only repair is re-import, and the staging script **SUMs across
runs**, so re-importing *doubles* every hour and day figure
- there's nothing reliable to parse: `지급일` is a **restricted** header,
masked in the very preview meant to review it

## Why NOT NULL with no default, and why immutable

No live data, nothing deployed — so no row to accommodate and no
backfill to invent. A DEFAULT would let an upload acquire a period
**nobody chose**, which is the fabricated provenance this column
removes.

`console_rt` holds table-wide UPDATE (0070:83, never revoked) and 0166's
guard only inspects `entity_type` and INSERT-with-APPLIED, so a
period-only UPDATE is permitted today. Without the trigger, the roster's
scope would be a mutable, unaudited pointer on top of append-only
material: the rows couldn't change, but *which rows a payroll run sees*
could — silently.

## The upload readers had to be restructured first

Both readers `return Ok(...)` from **inside** the `next_field()` loop
the moment they had the file. Any part sent *after* `file` was silently
discarded — a period field would have vanished with no error, and the
upload would have failed later citing the wrong thing. Both now drain
every part through one shared reader, then require the period. Missing
or unparseable → 422, never a default.

## Blast radius, and why `cargo check` couldn't find it

All thirteen `INSERT INTO data_import_runs` sites are plain
`sqlx::query`, so a missing NOT NULL column **compiles cleanly and fails
at runtime with 23502**. All thirteen updated — six in `hr.rs`, six in
tests, and `import_coss_group_workbooks.py`, which now takes an explicit
`--pay-period-start/--pay-period-end` rather than deriving them from the
folder path (deriving would rebuild the exact coupling this removes).

## Proven against real PostgreSQL

| case | result |
|---|---|
| declared period | accepted *(positive control)* |
| no period at all | refused, 23502 |
| end before start | refused by CHECK |
| period changed after insert | refused by trigger |
| status changed, period untouched | still allowed |

**Mutation-proven with forced recompilation** —
`#[sqlx::test(migrations=…)]` embeds the SQL at *compile* time, so
editing a `.sql` alone doesn't rebuild, and my first matrix reported two
false passes:

```
drop NOT NULL from both columns -> "no period" test FAILS
neuter the ordering CHECK       -> ordering test FAILS
detach the immutability trigger -> immutability test FAILS
restored                        -> 4 passed
```

Five existing PostgreSQL suites that seed import runs still pass.

## Scope

This is the **ledger contract only**. The roster writer that consumes
the period is deliberately not here: adversarial review found that
shipping both retires the G008 text pins that are currently the *only*
mechanical proof of the APPLIED / non-ERROR provenance filters — leaving
that gate green over a file nothing runs. The writer needs its own pins
and its own PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`payroll_draft_lines` had **no production writer at all** — its only
writer was a hand-run SQL script. So every payroll run production code
created had an empty roster, and once the close preflight required
`roster_total > 0` (#833), could never close. **PayRun, the last step of
the product order, was not runnable by the application.**

`roster::materialise_roster_in_tx` is a **port** of that script, called
from `stage_draft_run_inner` so a run and its roster are created in one
transaction. Deriving a second mapping is how the two drift — the
failure a previous bead was killed for.

## Four deliberate differences

1. **Scope is the declared pay period, by equality.** The script used
`source_filename LIKE '2026/5월/%'` — one operator's folder layout.
Migration 0224's `pay_period_*` replaces it. Equality, not overlap: an
import declared for May is material for the May run, not for one that
straddles May.
2. **No `leave_remaining` admission disjunct.** The script admitted
employees with leave and no imported rows — lines that carry no evidence
and can only block the close they're counted toward.
3. **No reconciliation DELETE.** 0222 revoked DELETE from `console_rt`
and asserts it, so a delete raises 42501 at *plan* time and would kill
every `payroll.create_run`.
4. **The employee-driven grouping is kept.** Review advised deleting it;
that's wrong, and the review's own residual-risk note says why —
`data_import_rows.source_key` is `filename:…|sheet:…|row:…`, so grouping
on it yields one line per **spreadsheet row**. A test pins it: two rows
for one person make one line.

## Called from all three success paths, never gated on `created`

A run whose header exists but whose roster was never written — a
previous attempt dying between the two — would otherwise never acquire
one, and be unclosable forever with no repair. A draft with **no**
declared period writes nothing: no scope, and guessing one is the
fabricated provenance 0224 removes.

**An empty roster is not an error.** The drain leaves a failed event
PENDING without incrementing `attempt_count`, so `Err` would be an
unbounded hot retry. `close_preflight` already refuses legibly with `명세
대상 없음(로스터 0명)`.

## Mutation-proven

| mutation | result |
|---|---|
| equality → overlap | different-period test **FAILS** |
| drop `run.status = 'APPLIED'` | unapplied test **FAILS** |
| drop `row_status <> 'ERROR'` | same test **FAILS** |
| non-blank → key presence (all four flags) | blank-cells test **FAILS**
|
| admit everyone | **4 of 7 FAIL** |
| restored | 7 passed |

The fixture **satisfies** 0166's writer guard rather than routing around
it — `console_leave_definer`, an armed `app.current_org`, and a
same-transaction `data_import.apply` audit row — and asserts the run
actually reached APPLIED, because without the org GUC the transition
matched zero rows and **succeeded silently**.

## Unproven after this lands

`attendance_event_count` still has no writer, so 근태 원천 확보 is attested
from payroll-workbook columns alone. The pay period is attributed and
frozen but never verified against the rows it scopes. And one near-miss
is weaker than I wanted: `employees.leave_remaining` can't be set from a
test (42501 `leave_write.command_required`), so the no-material case is
proven without the leave balance the deleted disjunct keyed on.

**The script is not retired here.** G008's three text pins remain the
only mechanical proof of the APPLIED / non-ERROR filters; retiring it
without moving them would leave that gate green over a file nothing
runs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
G008 carried **twelve pins on the staging script and none on
`roster.rs`**. That was right while the script was the only writer of
`payroll_draft_lines`. It stopped being right at #846 —
`roster::materialise_roster_in_tx` now runs on every
`payroll.create_run`, and the pinned file is no longer on the production
path.

So the mechanical proof of *"what may become a payroll roster"* sat
entirely on one encoding while a second encoding did the actual work.
Nothing stopped the two drifting, and G008 would have stayed green while
the executed one weakened.

The tests from #846 do bind these predicates — but from the outside. A
text pin is what makes a **reviewer** see the divergence in the diff.

## Six pins, each mutation-proven against the writer

| mutation | result |
|---|---|
| drop `run.status = 'APPLIED'` | exit 1 |
| drop `r.row_status <> 'ERROR'` | exit 1 |
| equality → overlap on the period | exit 1 |
| weaken **one of four** non-blank flags | exit 1 |
| reintroduce `?|array` | exit 1 |
| add a reconciliation DELETE | exit 1 |
| restored | exit 0, **29 checks** |

The non-blank pin is **counted**, not `includes` — a plain `includes`
passes while three of the four flags are weakened. That's the same hole
I shipped once already in #836 and had to fix.

## Deliberately not re-pointed from the script

Two of the script's pins would be actively harmful here.
`requireMatches(/raw_row\?\|array\[/)` asserts the **key-presence
idiom** — the exact fabrication vector where a blank `출근` cell counts as
attendance material. Re-pointing it at the writer would make CI *require
the bug*. The writer is pinned on the non-blank form instead, plus the
**absence** of `?|array`.

The DELETE pin isn't stylistic: 0222 revoked DELETE on this table from
`console_rt` and asserts the revocation, so a reconciliation delete
raises **42501 at plan time** — killing every `payroll.create_run`, not
just the re-stage that introduced it.

## The script keeps its twelve pins

It's still the operational hand-run path. Retiring it is a separate
change, and not a trivial one: reducing it to a read-only query would
leave all twelve pins passing over a file that writes nothing — a false
green rather than a removal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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:

```rust
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](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…853)

#840 put **one** shard on nextest to measure it. Measured on merge_group
run 32402124838, the pilot against its nearest neighbour:

| shard | targets | runner | wall |
|---|---|---|---|
| domain adapters **B** | 45 | **nextest** | **6m** |
| domain adapters A | 40 | cargo | 15m |

More tests, less than half the wall clock — the 2.35× benchmark
reproduced on real CI against a bigger shard. The pilot has done its
job, so the remaining four shards move to the same hash-pinned installer
and runner.

## What this does not do — and the measurement matters more than the
change

The critical path of a merge_group run is **not** the shards:

```
Backend — fmt / clippy / test / gates   17m   <- critical path
Test PostgreSQL — platform              15m
Test PostgreSQL — domain adapters A     15m
Test PostgreSQL — app                   15m
```

Taking the shards to ~6–7m leaves wall clock pinned near **17m by
Backend**, whose own 17m is an accumulation rather than a hot spot: 5m
dev-auth PG suites, 3m clippy, 3m console-app unit, ~4m setup, and 24
further steps at or under a minute. Splitting it is the next lever and a
much larger ci.yml change.

A second measurement, taken locally while checking isolation: the
**platform shard runs 154 tests in 263s, and 263s of that is one test**
— `console-gate-writer-ownership::census_executes_against_postgres`. No
runner change takes that shard below ~4.5m. Shards are not uniformly
runner-bound, and the remaining gains are not evenly distributed.

## Isolation — the actual risk, and why it was piloted

nextest runs test binaries in **parallel** where cargo ran them
serially, so a suite sharing state would newly fail. Verified locally
before pushing: the platform shard passes **154/154** under nextest,
with the cluster-global serial group in `.config/nextest.toml` doing its
job.

Ratchets moved **up**: run-step coverage 126 → 130, bypass matrix 378 →
390, because each new install step goes through the same three mutations
as every other. 61 preflight contract tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Measured on merge_group run 32402124838: CI wall clock ~19m, and this
one job was the **17m critical path**. After #853 moved every PostgreSQL
shard to nextest (now 4–8m each), Backend is the only thing holding the
number up.

Its 17m isn't a hot spot — it's an accumulation. Three steps are **757s
of ~1,070s** (dev-auth PG suites 355s, clippy 217s, console-app unit
185s); the other 25 steps are all under 82s. So the lever is
parallelism, not speed.

## Three legs

| leg | steps |
|---|---|
| `cargo` | rustfmt, clippy, the 11 cargo gates, mutation suites, PR 473
tests, boot smoke (16) |
| `buck-app` | platform-authz unit, console-app unit, OpenAPI drift,
console-app inline PG (4) |
| `buck-dev-auth` | the dev-auth feature PostgreSQL suites (1) |

Setup, topology reconcile and collect-failures run on every leg (7).
**Every one of the 28 steps lands on exactly one leg or on all three** —
verified by parsing the workflow, not by reading it. `fail-fast: false`,
so one red leg never hides the others.

## Why a matrix and not separate jobs

Separate jobs were designed and **refuted**: they touch every pinned
contract structure and create two false-green surfaces — a new job not
wired into `required-ci`'s `needs` runs and gates nothing; a
`steps.topology.outcome` condition carried into a job with no topology
step skips forever while the job stays green. A matrix keeps the job id,
its `required-ci` membership, and every step's place in the
bypass-mutation matrix exactly where they were. **Ratchets do not move**
(130 / 390 after #853): no step was added, removed, or renamed.

## The cache — and a pre-existing hole this closes

rust-cache has *one writer* by contract. **It had two.**
`migration-expand-contract` carried `save-if: main` under a comment
reading *"The ONLY writer"* — copied from `backend` when that job was
split out in #817, and false in place ever since — and was absent from
`cargoRustCacheJobs`, so the one-writer check never saw it. Now
restore-only and listed.

With a matrix, "one writer" also stops being enough: three legs saving
the same key race on every main push, and the textual `save-if: false`
test can't tell one leg from three. The writer is now exactly the
`cargo` leg, enforced by a new assertion.

## Two new assertions, both mutation-proven, both from the refutation

| mutation | result |
|---|---|
| rename a leg in the matrix, leave a step's `if:` stale | *"names
matrix leg 'buck-app', which is not in strategy.matrix.leg — that step
would run on ZERO legs"* |
| writer's `save-if` no longer leg-scoped | *"save-if must name exactly
ONE matrix leg"* |
| restored | preflight exit 0 |

Without the first, a coordinated matrix rename plus hash recompute could
leave a proof step running on **no leg at all** while every leg,
`required-ci`, and `check-executed-tests` stayed green.

Preflight exit 0 · contract suite **61/61** · gate sweep 13/13 ·
workflow-hardening and executed-tests exit 0 · clippy→first-gate
adjacency holds on the cargo leg.

## Predicted, not measured

Per-leg setup ~55s. Design estimate: cargo ~6.7m, buck-dev-auth ~7.0m,
buck-app ~8–10m — the last because app-unit's 185s was measured **warm**
after dev-auth had built the platform crates in the same daemon, and
buck-out doesn't cross runners. If that holds, Backend drops 17m →
~8–10m and wall clock lands near **10m**. That is a prediction until
this PR's own CI run measures it. **5m is not reachable by this change
alone**: the platform shard carries one 263s test, and the buck-app leg
pays a cold platform build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…hing (#857)

"Domain crates — unit tests" is CI's critical path at **12m** — one step
worth 714s. **It was never a slow test suite.** On merge_group run
32434156418 that step is **684.8s of rustc** (1434 `Compiling`, 0
`Fresh`) and ~29s of actual test execution: the 1156-test lib sweep in
1.1s, 18 doctests in 14.3s, everything else sub-second.

It was a cold build on every run, masquerading as a slow job.

## The cause

rust-cache logged `No cache found.` on **all 14 recent main runs**. The
key it asked for was `…-b587c171-`; the writer and all five PostgreSQL
shards restore `…-f3304cdf-` with an identical lockfile half. The env
half differed by exactly two variables — `CARGO_PROFILE_DEV_DEBUG=0` and
`CARGO_PROFILE_TEST_DEBUG=0` — which rust-cache folds into its hash,
which 03e7292 added to the shards on 2026-08-18 to fix **this same miss
there**, and which `check-ci-preflight.mjs` *forbade* on this job:

```js
if (/^    (?:env|defaults):/m.test(domainUnit) ...)
  failures.push("domain-unit must use the default shell with no job or step env/defaults overrides");
```

A blanket "no env" ban is what kept the miss in place. The contract was
protecting the shell; the side effect was a 685s build per run.

## The fix

Two lines of YAML and a narrower ban. The job gains the same env block
the shards carry, with the same rationale. The contract no longer
forbids job env — it **requires** it to be exactly those two variables,
and still forbids step-level env and any `defaults:`. Failure text
unchanged, so existing tests keep matching.

## I went in believing the lever was nextest

Measured locally on a warm build, the lib sweep is 57.4s under cargo vs
2.08s under nextest — a 27× ratio that turned out to be a **macOS
process-spawn artifact** applied to what is, on the runner, about one
second of execution. Honest nextest gain here: 10–20s. It would also
have opened `.config/nextest.toml` as a second, *unpinned* selection
surface for 141 binaries — a profile `default-filter` narrows what runs
while the verbatim-pinned ci.yml text is unchanged and
`check-executed-tests` keeps reporting everything executed. An
adversarial review found both. **No nextest, no new run step, no change
to the pinned cargo invocations, ratchets unchanged at 130/390.**

## Proven

Preflight exit 0 · contract suite **61/61** · gate sweep 13/13 ·
executed-tests 0.

The env requirement can **fail**: dropping `CARGO_PROFILE_DEV_DEBUG`,
appending `RUSTC_WRAPPER`, or re-adding `defaults:` each turns preflight
red with the pinned text; restored, exit 0. The envelope digest was
recomputed with the preflight's own serializer, not copied.

## Predicted, not measured

A warm restore should take this step from ~714s to near the ~30s of real
execution plus restore time. **That is the first run's job to confirm.**
If it holds, the 12m critical path drops out entirely and wall clock is
set by buck-app (11m) and the platform shard (9m).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Agent working directories were tracked and pushed, which the
no-dot-agent-directories rule forbids: `.grok` (41 files), `.claude`
(9), `.cursor` (53), `.codex` (2), `.beads` (10), `.omx` (4).

## Load-bearing content

Unlike oyatie's, console's `.grok` held no source that other code reads.
Its one code reference — `tools/ci/ingest-soft-reds.mjs` — **wrote** to
`.grok/harness/lane-board.live.json`, a generated artifact that was
already gitignored. That output is redirected to `ci/harness/` so no
tool points into an agent directory.

## Untracked, not deleted

`.claude`, `.cursor`, `.codex`, `.beads` and `.omx` remain **on disk** —
agent tools read those exact paths. The requirement is that they not
reach GitHub. `.grok` is gone; nothing consumed it.

Everything is preserved at
`refs/preserved/{grok,claude,codex,cursor,beads,omx}` on this remote.
Recover with `git checkout refs/preserved/<name> -- .`

`.omx` needed a second pass: the first sweep enumerated only the six
dot-directories then known, which is exactly how a stale allowlist lets
something through.

## Tracked hooks

`.githooks/` is tracked and reviewable, and delegates to local untracked
hooks (`.beads/hooks/<name>`, `.git/hooks/<name>.local`), propagating
their exit codes — so per-developer tooling keeps working while stopping
being the only thing between a mistake and the remote.

- `pre-commit` refuses staged agent-directory files and staged
gitignored files
  - `pre-push` refuses a workspace that does not compile

Enable with `git config core.hooksPath .githooks`.

Verified by firing them: a staged `.claude` file was refused, a local
delegate ran, and a delegate exiting 3 propagated 3 rather than 0.

## Note

`.beads/` carried executable hooks (`post-checkout`, `pre-commit`). They
remain on disk and work, but wiring them from a tracked location is
follow-up work — a hook that exists only on the machine that wrote it is
a local habit, not enforcement.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
- `generated-face-authority` moves off `macos-latest` (10×) onto Linux.
ADR-0039 already names deleting that macOS job; this is the cheap cut.
- Backend cargo leg also runs on `ubuntu-24.04-arm`. Buck legs stay
amd64 (graph honesty, not rustc of the product).
- Preflight envelope hashes updated; `node
scripts/check-ci-preflight.mjs` and its unit tests pass.

Cheap local vs this PR: rustfmt stays a local/backend step; image builds
stay `image-release.yml` (CD).
@jason931225
jason931225 merged commit 0305434 into dev Aug 21, 2026
78 of 79 checks passed
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