COSI_RUBY_PKG_COST_BLESS is outside the bless contract, no test blesses through an env switch and inspects the bytes, and bless_registry's own doc and floor have drifted below their population #177
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#177
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during the #45.6 audit ("Bless requires reason, generation identities and proof that controls executed"). All three are fixed in
gate: #45.6 — one bless verdict, six refusals, and the vacuity that hid a straggler, each with its own run mutation. Filed as a record and because the audit half is still open — see the last section.Filed as one issue, and here is why. Defects 1 and 2 share a root — the bless wrote before the run was validated — and compose into a single worst case. Defect 3 is a genuinely different root (a refusal computed and never consulted). I chose one issue anyway because all three were found in one audit of one function, all three were closed by one change, and splitting a closed finding into two records helps no reader. The distinction is stated here rather than erased.
1. A bless taken with a repo unavailable SILENTLY DELETED that repo's row
rewrite_baselinebuilt the committed rows fromobservedalone, with head/tail splices, and never merged the previous baseline. A repo that could not be staged iscov.skippedand therefore never entersobserved.So: fetch fails for one of the seven pinned repos, an operator blesses an intended change on the other six, and that repo's row disappears from
tests/corpus/baseline.json. The next run has nothing to compare it against, and the ratchet that exists to notice a resolver change quietly stops covering a seventh of the corpus.The tell was an asymmetry, not a hunch:
corpus_cost's writer does merge —let mut merged = load_baseline(old)— so the cost baseline was immune and the content baseline was not. Two writers for two artifacts, one of which had thought about this.2. The artifact was written BEFORE the coverage panic
The bless arm ran
rewrite_baseline(...); cov.finish(ctl); return;— andCoverage::finishis whereCOSI_CORPUS_REQUIRE=1turns an unavailable repo into a failure. So even underREQUIRE, the file was already on disk when the failure fired. The guard that was supposed to prevent exactly scenario 1 ran after the damage.corpus_stagehad the ordering right: its verdict refuses beforefs::write. Again, two implementations, one of which was correct.3. An empty categorised diff was blessable
driftwas computed and the bless branch never consulted it, so a bless that moved nothing still rewrote the artifact and appended a reason. That is how a bless becomes a habit instead of an event — #45's anti-gaming list names it explicitly, and this switch had never enforced it.Why existing gates could not see any of the three
All three live in a path that only executes under
COSI_CORPUS_BLESS=1, which no CI job sets and no test exercised. The suite's own tests graded the comparison arm; the writer arm was reachable only by an operator typing the switch, and its failure mode is silent data loss in a committed file. There was no test that blessed anything and then looked at what was written.The fix
All bless paths now route through
corpus::projection::bless_verdict, which refuses before any write on all six of #45's clauses, and the writer merges rather than truncates. Mutations run for each: reinstating the truncating writer reddens with the dropped repo named; moving the write back before the refusal reddens; making the diff check vacuous reddens on all four suites.The parameter list also became a
BlessClaimstruct, which removed three#[allow(clippy::too_many_arguments)]— and the reason is in the struct's doc:unavailableanddegradedare both&[String]and adjacent, so transposing them compiles, and the refusal that then fires names the wrong rule.What is still open, and it is the reason this is filed rather than only committed
corpus_costmerges andcorpus_ratchetnow merges.corpus_stage,corpus_tier3_ratchetandruby_package_costwere brought onto the shared verdict but their writers were not individually re-read for this specific hazard. Any writer that constructs its output from the current run alone has defect 1.COSI_RUBY_PKG_COST_BLESSis still outside the contract — it enforces a non-empty reason only. Its waiver is written intobless_registryand is self-clearing (it reddens the moment that suite callsbless_verdict), but it is a real residual.ruby_package_cost::the_bless_writes_only_its_own_baselineis the model — it hashes foreign artifacts around a real bless.What must NOT be done
tests/corpus/baseline.jsonto test any of this. It is at md5534084b856c22566c48e386bc41ed67eand is mid-re-record on another branch. Every mutation above was run against a temp path or reverted with md5 verification.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Open item 1 answered, and it was not clean: two more writers had defect 1, and they share one function
Worktree
/tmp/cosi-lane-gatesatf6a878a. Staged, not pushed. The instruction "do not close item 1 by reading the writers and declaring them fine" is why this is a fix plus a graded property per artifact rather than a verdict.The full writer census — 6 switches, 7 registered artifacts
COSI_CORPUS_BLESScorpus_ratchetbaseline.jsonCOSI_COST_BLESScorpus_costcost-baseline.jsonlet mut merged = load_baseline(old))COSI_STAGE_BLESScorpus_stagestage-baseline.jsonCOSI_TIER3_BLESScorpus_tier3_ratchettier3-baseline.jsonCOSI_RUBY_PKG_COST_BLESSruby_package_costruby-package-cost.jsonCOSI_BLESS_RUBY_EXPECTruby_builtin_expectationsfixtures/*.expectedagent_task_benchbench/ratchet.jsonThe finding: the two hazardous writers are the two that share
projection::rendercorpus_ratchetandcorpus_costeach hand-rolled arender_baselineand each learned to merge.corpus_stageandcorpus_tier3_ratchetwere brought onto the shared verdict but their write isfs::write(baseline_path(), render(&text, &observed, &act, &Bless{…})), andprojection::renderbuiltrepo_blockfromobserved.iter()alone — taking fromoldonly the comment head. That is the pre-fixcorpus_ratchetshape, in a shared function, reached by two suites. Neither had a merge-property test:corpus_stage's nearest arm passesold = "{\n \"_comment\": [\"x\"],\n"— noreposblock at all — so it was structurally incapable of noticing.And the shrunken-universe refusal does not cover it.
bless_verdictrefuses whenunavailableis non-empty, which catches the repo that failed to stage. It cannot catch a repo that was never iterated:corpus::tier(n)ismanifest().filter(|r| r.tier == n), so a repo whose tier is edited incorpus.toml, or which is removed from it, is absent fromobservedand fromunavailable— the verdict passes clean and the row goes. I verified that filter directly. One of those is a rule the operator can be told about; the other is a property of the writer, and it has to hold however the writer is called. Ontier3-baseline.jsonthe population is two rows, so one silent deletion halves the artifact.The fix, in one place
projection::rendernow merges:load_baseline(old).0, thenobservedinserted over it.Per-repo replace, not per-key union, and the difference is load-bearing.
corpus_ratchet/corpus_costmerge per key over a closedDIMSlist;render's rows are open key sets (rule.*,stage.*,influence.*,producer.*). A key union would resurrect a key that has stopped firing — destroying theKEY GONEsignalcategorised_diffexists to raise, which is the whole point of the stage artifact. A measured repo's row is replaced whole; an unmeasured repo's row is kept whole. Both halves are asserted.The
activationblock is deliberately not merged:activation_facts()is a fixed global set (fact_abi,engine), not a per-repo map, so it has no member a partial run can drop. Written down so the omission is a decision, not a gap.Item 3, partially: a test that renders and inspects the bytes
a_partial_bless_keeps_the_rows_it_did_not_measureadded to bothcorpus_stageandcorpus_tier3_ratchet, modelled oncorpus_cost's. It builds a two-repo artifact through the real writer (so the fixture cannot drift from the format), asserts the fixture holds two rows, then blesses with only one measured — the proof minted through the realbless_verdict, becauseBlessProof's only field is private toprojectionand there is no other route.MUTATION (RUN): revert
projection::rendertolet mut merged: BTreeMap<String, Row> = BTreeMap::new().corpus_stage's existingthe_writer_and_the_reader_round_trip_a_baselinestayed ok under that same mutation — the blindness, measured rather than asserted. Restored bycp+ md5 verify; the committed baselines never moved.What remains open on item 3. No test drives the env switch through
fs::write(baseline_path(), …)with a repo genuinely unavailable. The write path is anchored to the repo root and there is no path-injection seam; adding one is a production-shaped change I did not want to bundle into a workflow lane. The bytes are now graded; the switch is not.Defect 2 was still live in
ruby_package_cost, and is now fixedcov.finishis whereCOSI_CORPUS_REQUIRE=1turns an unavailable repo into a failure, so under REQUIRE the artifact was already on disk when the failure fired — this issue's defect 2, unchanged, in a sixth suite. The order is nowcov.finish(ctl)thenfs::write.Latent rather than live today only because that suite returns early when its single repo cannot be staged. That is a property of the current control flow, not of the writer — and closing open item 2 will change that control flow. The other five switches cannot have this bug for a structural reason worth stating: their writers take a
&Blesscarrying aBlessProofwhose only field is private toprojection, so "write before the verdict" is a compile error.ruby_package_cost::render_baselinetakes no proof, so ordering is all it has.A finding inside the test this issue names as the model
ruby_package_cost::the_bless_writes_only_its_own_baselinehashesforeign_baselines()around a bless — 2 of the 4 sibling artifacts.stage-baseline.jsonandtier3-baseline.jsonwere not hashed, so this suite could have written either undetected. Widened to four, and the two hand-indexedassert_ne!s replaced by a loop over the whole set, because widening the array while leaving[0]/[1]asserts behind would have left the two new members ungraded while the code still looked like it covered them.MUTATION (RUN): point
baseline_path()attests/corpus/stage-baseline.json— a sibling the old 2-member set did not cover.Related, and left as a note rather than acted on: the hash loop in that test writes to a tempdir
dst, never tobaseline_path(), so it can only ever catch a hidden side effect ofrender_baseline. Under its own recorded mutation the test does go red — but on the path-discrimination arm, not the hash arm, and its doc quotes a third failure string ("which belongs to corpus_cost") that appears nowhere in the file. The recorded mutation result does not match the code that would produce it.Item 2 (
COSI_RUBY_PKG_COST_BLESSoutside the contract) — scoped, not doneThe waiver in
bless_registryis self-clearing and correct. What bringing it ontobless_verdictcosts, field by field:reasonanddegraded_allowlistwork today (*_DEGRADEDalready reduces viaDERIVED);repos_gradedis literal1;controlsneeds a parallelVec<String>becauseControls::lenis private, exactly ascorpus_stagedoes and says why;identity_afteranddegradedboth needmeasure_both_legsto return the package-leg db path it currently drops;diffneedsload_baseline(&prior)wrapped as a one-keyBTreeMap<String, Row>for the sharedcategorised_diff(which also lands defect 3 there). The one that is not plumbing:unavailableis vacuous while the suite returns early onrepo_pathErr — making the shrunken-universe refusal real requires replacing that early return withunavailable.push(fx::REPO)and falling through. Thenrender_baselinemust take&Blessso the type-level ordering guarantee applies.Two incidental drifts in
bless_registry: its module doc still says "Six ratcheted artifacts … Five of them are unlocked by an environment variable" (it holds 7 entries and 6 switches —COSI_BLESS_RUBY_EXPECTwas added without updating the prose), andevery_bless_switch_in_the_tree_is_registeredfloors at>= 5against an actual 6, so one switch deleted outright would not move it.Gates
fmt0 ·clippy -D warnings0 ·rustdoc -D warnings0 ·COSI_CORPUS_DIR=… COSI_CORPUS_REQUIRE=1 cargo test --workspace --no-fail-fast0 (301 suites ok, 0 failed) ·COSI_E2E_LEG=daemon0 ·precision_gate7/7phantoms=0· windows-gnu cross clippy 0.Corpus ratchets re-run in release with the corpus env,
--nocapture, so the floor is visible rather than assumed:Nothing was blessed.
baseline.json914dda1a,tier3-baseline.json94abf592,stage-baseline.json7d9695be,ruby-package-cost.jsonf5a1a8f2— all unmoved, verified after every mutation.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
STAYING OPEN, NARROWED — item 1 is discharged, items 2 and 3 are verbatim untouched
Close-out lane, master
552e3a2. Title corrected to the residual only.Done — strike this clause from the issue
Item 1, the audit of the other bless writers, is complete.
a_partial_bless_keeps_the_rows_it_did_not_measurenow exists in all four suites:All green —
corpus_stage11/11,corpus_tier3_ratchet5/5, EXIT=0. The census found 6 switches over 7 artifacts and two further hazardous writers, both fixed; defect 2 inruby_package_costis fixed atcrates/indexer/tests/ruby_package_cost.rs:690-691(cov.finish(ctl);thenfs::write).Still open, verbatim
crates/indexer/tests/bless_registry.rs:134-147still carriesswitch: "COSI_RUBY_PKG_COST_BLESS"withwaiver: "PENDING #45.6: this suite still enforces only a NON-EMPTY reason …". That switch is outside the bless contract.fs::write(baseline_path(), …)with a repo genuinely unavailable — i.e. nothing blesses and then inspects the bytes, which is the only thing that would have caught the original three defects.bless_registry.rs:3-4reads "Six ratcheted artifacts … Five of them are unlocked by an environment variable" against an actual 7 entries / 6 switches.bless_registry.rs:323floors atfound.len() >= 5against an actual 6 — so one switch deleted outright would not move it.Corrected scope
Retitle as above; strike "the audit of the other writers is open"; keep items 2, 3 and the two drifts as the remaining work.
corpus_ratchet's bless path could delete a repo's row, wrote before its own refusal, and never consulted the diff it computed (found + fixed; audit of the other writers is open)toCOSI_RUBY_PKG_COST_BLESSis outside the bless contract, no test blesses through an env switch and inspects the bytes, andbless_registry's own doc and floor have drifted below their populationAll three residuals are closed on master
1d81180. Closing.This issue's own last section listed three open items. All three are in the tree, and none of them was closed the way the issue forbade.
Item 2 —
COSI_RUBY_PKG_COST_BLESSoutside the contract.crates/indexer/tests/bless_registry.rs:139-160now carriesenforces: ALL, waiver: "", and the deleted waiver's replacement comment records why the waiver was not merely unimplemented:identity_afteranddegradedwere unreachable, because they are properties of the package leg's database andLegsdropped the path.Legskeeps it now. The self-clearing mechanism the issue described did its job —every_registered_switch_routes_through_the_shared_verdictdemands the waiver go the moment the suite starts callingbless_verdict.Item 1 — the other writers were not audited for truncate-vs-merge. Not closed by reading them and declaring them fine, which the issue explicitly ruled out. Closed structurally:
corpus::projection::renderis now the shared writer and it merges (let mut merged = load_baseline(old).0;), so the asymmetry that exposed the original defect cannot exist between two implementations any more.Item 3 — no test blesses and then inspects the bytes.
corpus_tier3_ratchet.rs::a_partial_bless_keeps_the_rows_it_did_not_measuredoes exactly that: it builds a two-repo artifact through the real writer (so the fixture cannot drift from the format), blesses a run that measured one of the two, and reads the bytes back — the measured repo carries its new number, the unmeasured repo keeps its blessed row and every one of its dimensions.Two details in it are better than the issue asked for:
observed, which the shrunken-universe refusal does not cover: the repo was never iterated because someone edited a tier or removed a row fromcorpus.toml. Nothing is unavailable, the verdict passes clean, and a truncating writer drops the row in silence. That is why this has to be a property of the writer rather than a rule an operator can be told.categorised_diff's KEY GONE signal is unreachable. That is the failure mode a naive fix for item 1 would have introduced.The declared mutation (truncating writer) is recorded as run and RED, and
tests/corpus/baseline.jsonwas not touched.