From d3d19cc5781c618563b7016c77e6a3b057184f7f Mon Sep 17 00:00:00 2001 From: Jason Lee <56489493+jason931225@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:44:24 -0400 Subject: [PATCH] ci: pin the provenance filters on the writer that actually runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G008 carried twelve pins on `scripts/stage_coss_group_payroll_readiness.sql` 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 added with #846 do bind those predicates — but they bind them from the outside, and a text pin is what makes a REVIEWER see the divergence in the diff. Six pins on the writer, each mutation-proven against it: drop `run.status = 'APPLIED'` -> exit 1 drop `r.row_status <> 'ERROR'` -> exit 1 equality -> overlap on the period -> exit 1 weaken ONE of the four non-blank source-material flags -> exit 1 (counted, not `includes`: a plain includes passes while three of the four are weakened) reintroduce `?|array` -> exit 1 add a reconciliation DELETE -> exit 1 restored -> exit 0, 29 checks DELIBERATELY NOT RE-POINTED FROM THE SCRIPT. Two of the script's pins would be actively harmful on the writer. `requireMatches(/raw_row\?\|array\[/)` asserts the key-presence idiom — the exact fabrication vector where a blank `출근` cell counts as attendance material — so re-pointing it would make CI REQUIRE the bug. The writer is pinned on the non-blank form instead, and on the ABSENCE of `?|array`. The DELETE pin is not stylistic: 0222 revoked DELETE on this table from `console_rt` and asserts the revocation, so a reconciliation delete raises 42501 at PLAN time and kills every `payroll.create_run` — not just the re-stage that introduced it. The script keeps its own twelve pins. It is still the operational hand-run path, and retiring it is a separate change: reducing it to a read-only query would leave all twelve passing over a file that writes nothing, which is a false green rather than a removal. Co-Authored-By: Claude Opus 5 --- scripts/check-g008-payroll-readiness.mjs | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/scripts/check-g008-payroll-readiness.mjs b/scripts/check-g008-payroll-readiness.mjs index dfd8c27f9..7c6e0cbf7 100644 --- a/scripts/check-g008-payroll-readiness.mjs +++ b/scripts/check-g008-payroll-readiness.mjs @@ -143,6 +143,56 @@ requireMatches( "all five stage SQL source-material flags require a non-blank value", ); +// THE PRODUCTION WRITER, not only the script. +// +// Until PR #846 `payroll_draft_lines` had no production writer, so pinning the +// staging SQL pinned the only path a roster could come from. It is not any more: +// `roster::materialise_roster_in_tx` runs on every `payroll.create_run`, and the +// twelve pins above sit on a file production no longer needs. Left alone, the two +// encodings of "what may become a payroll roster" could drift, and the gate would +// stay green while the executed one weakened. +// +// These pin the SAME provenance properties on the writer that actually runs. +// Deliberately NOT re-pointed from the script verbatim: the script's +// `raw_row ?| array[...]` idiom is the key-presence fabrication vector, and a +// gate that REQUIRED it on the writer would mandate the bug. +const rosterWriter = "backend/crates/payroll/adapter-postgres/src/roster.rs"; +requireIncludes( + rosterWriter, + "run.status = 'APPLIED'", + "production roster writer admits only APPLIED import runs", +); +requireIncludes( + rosterWriter, + "r.row_status <> 'ERROR'", + "production roster writer never treats an ERROR import row as material", +); +requireIncludes( + rosterWriter, + "run.pay_period_start = $3", + "production roster writer scopes by the declared pay period, by equality", +); +// Counted, not merely present: there are four source-material flags, and a plain +// `includes` passes while three of them are weakened. +requireMatches( + rosterWriter, + /(?:btrim\(kv\.value\) <> ''[\s\S]*?){4}/, + "all four production source-material flags require a non-blank value", +); +requireNotIncludes( + rosterWriter, + "?|array", + "production roster writer never treats a merely PRESENT column as material", +); +// 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. +requireNotIncludes( + rosterWriter, + "DELETE FROM payroll_draft_lines", + "production roster writer never deletes: console_rt holds no DELETE on this table", +); + requireIncludes( "package.json", '"check:g008-payroll-readiness"',