Skip to content

fix(storage): hydrate the label diff base inside the update transaction (ub-lp9.27) - #437

Merged
miguelramos merged 18 commits into
mainfrom
ub-lp9.27-label-diff-base
Aug 4, 2026
Merged

fix(storage): hydrate the label diff base inside the update transaction (ub-lp9.27)#437
miguelramos merged 18 commits into
mainfrom
ub-lp9.27-label-diff-base

Conversation

@miguelramos

Copy link
Copy Markdown
Member

Fixes the label diff base in update_issue. Tracker issue: ub-lp9.27 (P1, data-integrity, v1.0.1 slot).

The bug

update_issue built its Issue from a SELECT over the issues table alone and never hydrated the label relation before apply_labels seeded its diff base — so the before-set was always empty. The post-transaction hydrated re-read then returned a correct-looking label set, which is why it went unnoticed until ordinary tracker upkeep hit it.

Four observable consequences over the shipped MCP surface:

  1. labels_remove of a present label removed nothing and returned success. In a label-only patch the equal diff took the empty-diff full skip; in a mixed patch the row update still landed and only the label op vanished.
  2. labels_add / labels_set naming an already-present label re-inserted against the labels primary key and died with an opaque backend error, rolling the whole transaction back.
  3. The removal loop was unreachable (an empty before-set makes the removal diff empty by construction), so labels_set was purely additive and clearing the set was a no-op.
  4. A real label change never stamped updated_at.

Net effect: a label could be added but never removed, and a set overlapping the current one could not be applied at all.

The fix

One in-transaction SELECT seeds the diff base from the labels table, placed after the tombstone guard so the reject path pays nothing; and the stamping condition gains the label term so a real relation change advances updated_at.

content_hash is unaffected in every scenario — the spine excludes relations and all timestamps from the hash — so import idempotency is untouched.

Decisions

Both were ruled explicitly and are recorded on the issue:

  • A real label add/remove/set advances updated_at, aligning labels with the two existing precedents: reparent (FR-1b) and the comment surface (D37's FORK-S1, which advances it to feed the stale query). Landed as an inline amendment riding FR-1b — no new D-id, no D-range cascade.
  • The reconcile INSERT stays strict (never ignore-on-conflict), keeping the uniqueness constraint as a loud tripwire if the diff base ever regresses. The global backend-message posture is GA-frozen and out of scope.

Gates

Design Review passed. The Verify gate ran four adversarial rounds; each found real defects and each was fixed:

  • Round 1 → FAIL. The contract suite asserted a label-event order the interface contract explicitly declines to guarantee, so a conformant backend reconciling additions first would have failed conformance. And the per-issue scope of the newly added query was pinned by nothing — widening its WHERE clause to match every row left the whole suite green on both backends. Ruled: loosen the conformance suite to a set comparison, and pin the libsql-specific ordering fact in tests/behaviour.rs where backend behaviour belongs.
  • Round 2 → FAIL. A false sentence in release-notes text that contradicted its own bullet twice.
  • Round 3 → FAIL. Two false statements in the interface contract itself, including a claim about pre-fix behaviour that was impossible (no pre-fix update could emit a LabelRemoved).
  • Round 4 → PASS, with three sweep-up defects landed here rather than looped back.

Mutation proof was required for every new or changed test, not merely asserted. Notably: reverting the fix reddens the label cells on both backend legs, and a workspace-wide run showed the storage contract suite is the only target in the repo that catches it — the empirical explanation for why this shipped.

Coverage added

Six contract_label_* cells in the NFR-16 backend-independent suite, all registered so they execute on both the in-memory and temp-file legs, plus one libsql-specific behaviour test. Each of the four consequences is now graded, as are the per-issue read base and the per-issue write scope of the reconcile DELETE.

What this does not do

It newly rejects nothing — no input GA accepted now fails. One changed input moves error→success (the already-present add). Everything else it changes was accepted and silently mishandled.

No published byte moves: no contract_version bump, no CONTRACT_HASH re-pin, no new ErrorCode, no tool/command/schema field, and the 0–8 exit table is untouched. unblock-storage is the only crate that gains code.

Follow-ups filed (scoped out deliberately)

  • ub-lp9.29 — ~12 stale crud.rs line citations across the PRD, spine, crate plan and an engine test, all pre-existing; needs an executable check, not another manual re-citation pass.
  • ub-lp9.30 — the shared-cache in-memory store can redden a required CI job for reasons unrelated to the change under test.
  • ub-lp9.31 — decide whether the strict-INSERT rule is testable at all, and if not, say so where the only guard lives.

Verification

cargo fmt --check, clippy --workspace -D warnings (plus the testkit legs), cargo test --workspace, cargo insta test --check, cargo xtask doc-lint / check-layering / knowledge-lint, all four scripts/checks/*.sh, and the run-report gate — all green.

Run-report: .knowledge/wiki/runs/2026-08-04-label-diff-base-hydration.md. The tracker record .unblock/issues.jsonl is re-exported here (it previously carried this issue with zero comments).

Remaining acceptance criterion

The issue's last criterion — removing the stale v1-1 label from ub-lp9.13 through the shipped API — needs a binary built from this branch, so it is verified after merge. The bug is confirmed live on main today: labels_remove on that issue returns isError:false with the label set unchanged and updated_at unmoved.

🤖 Generated with Claude Code

Miguel Ramos added 18 commits August 4, 2026 07:45
update_issue built its Issue from a SELECT over the issues table alone, so
apply_labels always diffed against an EMPTY before-set. Over the shipped MCP
surface that produced three defects, all masked by the post-transaction
hydrated re-read (which returns a correct-looking label set either way):

  - labels_remove removed from nothing, the diff compared equal and the whole
    patch was skipped — a silent no-op returned with isError:false;
  - labels_add of an already-present label was seen as new and re-INSERTed an
    existing row, hitting the labels UNIQUE constraint and surfacing
    DATABASE_ERROR with raw SQLite text;
  - labels_set overlapping the current set failed identically, so it never
    performed the replacement it advertises.

The update transaction now seeds issue.labels from the labels table with one
in-tx SELECT (the read-path hydrate shape, ORDER BY label ASC), placed after
the tombstone guard so the reject path pays for no wasted query. Labels only:
deps/comments are not diffed in-tx and the re-read hydrates them.

A real label add/remove/set now also advances updated_at, matching the two
existing relation precedents (the FR-1b reparent, and comments per D37).
content_hash is unaffected in every label scenario — spine 1.8 excludes
relations and all timestamps from the hash. A label no-op patch still takes
the empty-diff full skip: no updated_at, no event.

The reconcile INSERT stays STRICT (no INSERT OR IGNORE): with a correct diff
base a duplicate insert is unreachable, so the UNIQUE constraint remains a
loud tripwire if the diff base ever regresses.

Four NFR-16 contract cases, registered in run_storage_contract_suite so they
execute on both the in-memory and the temp-file backend, all proven red before
this fix: remove-then-read; add-of-an-already-present-label is idempotent (no
error, no event, updated_at exactly equal); labels_set over an overlapping set
replaces and announces the deltas only; and the updated_at semantics of a real
label-only change versus a label no-op.

Task: ub-lp9.27
…at rule

The update_issue clause stated, absolutely, that updated_at advances only when
at least one stored column changes. That absolute was ALREADY false: the
shipped reparent stamp (commit 42470c5) changed code only, and the spine stated
the reparent exception nowhere.

The clause now states the rule as "a stored column OR a real relation change"
and names both exceptions as normative: the reparent (FR-1b) and, riding this
amendment, a real label add/remove/set. The label half is an INLINE AMENDMENT —
a consequence of the same update_issue decision, so no new D-id and no D-range
bump. The empty-diff full-skip semantics are kept exactly as they were, with
"real" spelled out (a reparent to the current parent, or a label patch whose
net set equals the current one, still skips the whole UPDATE).

Also records, as normative, the label diff base itself: the before-set is the
issue's actually persisted label set read inside the same transaction, with its
three observable consequences (a remove really removes; an add of a present
label is an idempotent Ok; labels_set announces the deltas only), the strict
INSERT ruling, and the within-one-patch event ordering (relation events precede
the scalar per-field events; order AMONG label events is not guaranteed).

The unblock-storage crate plan's crud.rs row carries the same rules spec-first
alongside the existing D44/D45 cells, plus the four new NFR-16 contract cells.

Task: ub-lp9.27
…ct suite

Two NFR-16 contract cells asserted the EXACT ordered sequence
label_removed-then-label_added within one patch, which the spine amendment
shipped alongside them explicitly declines to promise: the label diff is a set,
so relative order AMONG the label events of one patch is not guaranteed. A
mutation swapping the two reconcile loops in `apply_labels` reddened exactly
those assertions, so a future backend reconciling additions first would fail the
conformance suite while fully honouring the contract it is graded against.

Separate the two questions:

- the contract suite compares the label events of one patch as a SET
  (canonicalised by sorting). Everything the contract DOES promise stays exact:
  the event count, both types, both payloads, and — in the combined
  label+scalar case — the scalar event pinned LAST, that half of the order being
  spine-guaranteed;
- libsql's own determinism (removals reconciled before additions) is pinned
  where implementation-specific behaviour belongs, as a new `tests/behaviour.rs`
  case. It patches two removals AND two additions so the assertion is about the
  two GROUPS, and compares event TYPES because the order WITHIN each group comes
  from a HashSet difference iteration and is genuinely unspecified.

The spine text stands as written on this point; it only gains a pointer to
where the backend fact is pinned (that pointer lands with the doc commit).

Mutation-proved: swapping the two reconcile loops reddens the new behaviour
case and leaves the contract suite green on BOTH backend legs (before this
commit the same mutant reddened testkit.rs's ordered assertion). Deleting the
in-transaction label seed still reddens all four round-one cases, so the
set-compare loosened nothing load-bearing.

Refs: ub-lp9.27
The `WHERE issue_id = ?1` of the in-transaction label seed was pinned by
nothing. Widening it to match every row (`WHERE (issue_id = ?1 OR 1=1)`) left
the entire contract suite green on BOTH backend legs, and the whole workspace
suite green with it: every existing label case holds exactly one issue in the
database when its label patch runs, so a workspace-wide diff base is
indistinguishable there from a per-issue one.

A polluted base is not cosmetic — it re-creates the very defect this cell set
closes. An add of a label some OTHER issue carries diffs EQUAL and is skipped
(the silent no-op, returned as success with the label absent), and a remove of
a label only some other issue carries diffs as a REAL removal, inventing a
LabelRemoved event and an `updated_at` stamp for a label the target never had.

Add `contract_label_diff_base_is_per_issue`: it seeds a SECOND issue carrying
two labels the target does not, BEFORE any label op runs, then exercises both
directions on the target and asserts the polluting issue is untouched
throughout. It is registered in `run_storage_contract_suite`, so it executes on
the in-memory and the temp-file backend alike.

Mutation-proved: the OR-1=1 widening now reddens the add half
(`left: ["own"], right: ["foreign-add", "own"]`) on both backend legs, and an
isolated copy of the remove half reddens on the spurious `updated_at` stamp —
the same mutant that was green before this commit.

Refs: ub-lp9.27
`contract_label_change_updated_at_semantics` compared the loaded issue's
`content_hash` against `compute_content_hash()` of that same loaded issue,
under a message claiming to observe that the STORED hash still matches the
recompute. It observes no such thing: `mappers::issue_from_row` recomputes
`content_hash` from the loaded fields on every load and never reads the stored
column (spine §1.8 — the column is a dedup cache, never trusted), so the
assertion compares a value to itself. A mutant writing a garbage hash into the
column left the whole suite green.

No backend-independent test can observe that obligation, because the contract
itself forbids trusting the column. Delete the assertion and leave a comment
saying why it cannot be written here; the sibling assertion above it — the hash
does not move across a label change — carries the real obligation and stays.

Refs: ub-lp9.27
Prose-only pass over the claims round one shipped, plus the two the label rule
never had. No behaviour changes.

crates/unblock-storage/src/libsql/crud.rs — two false claims in the seed
comment. It said the pre-fix `labels_remove` skipped "the whole patch", which
held only for a label-ONLY patch (in a mixed patch the row UPDATE still ran and
only the label op vanished); and it called `apply_labels` "the sole consumer of
a relation inside this transaction", which `apply_reparent` contradicts by
diffing the parent-child edge in the same tx. The true, narrower claim is that
labels are the only relation whose diff base comes from the in-memory `Issue` —
`apply_reparent` re-reads its own base from the tx. The comment now also states
that the `issue_id` scope is load-bearing and names the case that pins it.

crates/unblock-storage/src/trait_def.rs — the `Storage::update_issue` doc is
what every backend implementor reads and what the conformance suite grades, and
it stated only that a no-op writes no event and leaves `updated_at` unchanged.
It never stated the rule the suite now ENFORCES: that a real RELATION change (a
reparent, or a label add/remove/set) stamps `updated_at` on its own. Added,
with the label diff-base semantics and the ordering guarantee (label events
before the scalar ones; no order among themselves), worded to match the spine.

docs/plans/01-design-spine.md — three corrections, none touching the normative
label-ordering sentence: the empty-diff parenthetical carried the ORIGINAL
tool's `if set_clauses.is_empty() { return Ok }` mechanism, which read as a
description of OUR guard describes exactly the skip this change fixed (our
guard is the three-term condition); the reparent exception back-referenced an
absolute this same amendment had DELETED, so "above" pointed at nothing; and
the tombstone-patch guard was cited at crud.rs:332-334, which is now the D45
dependency-target existence pass. The ordering clause additionally points at
where the libsql-specific order is pinned.

docs/plans/00-roadmap.md — the v1.0.1 slot enumerated every defect the cut
carries but had no bullet for this one, whose user-visible changes ship in the
same release. Added in the style of its siblings, stating that it mints no
D-id (an inline spine amendment on the same decision), moves no published byte
and newly rejects nothing.

docs/plans/crates/unblock-storage.md — the same two false claims as crud.rs,
the new fifth contract cell, and the note that the STORED hash column is not
assertable in a backend-independent test. The cell count is dropped rather
than incremented (a list carries no count — PROCESS.md §3).

Refs: ub-lp9.27
`contract_label_diff_base_is_per_issue` ended with a step whose comment
claimed the per-issue scoping "holds for WRITES too" and that "neither the
add nor the remove reached" the polluting issue. With a correct diff base
that half was vacuous: the add step diffs to an addition and the remove
step diffs equal, so `apply_labels` returns early and NO `DELETE` ever
executes in that fixture. The comment asserted more than the assertions
graded, and as a direct consequence the reconcile `DELETE`'s own
`WHERE issue_id = ?1` predicate was pinned by nothing.

Give the target and the polluting issue a SHARED label and have the target
remove it. A real `DELETE` now runs while a second issue holds the same
label, which makes the closing assertion load-bearing: a `DELETE` matching
on the label alone strips it from the other issue too.

Mutation-proved: with the previous fixture, a `DELETE FROM labels WHERE
label = ?1` mutant passes every `unblock-storage` test target (lib,
behaviour, contract on both backends, contention_lab, scale, migrations,
heavy_corpus_stress); with this fixture the contract suite fails on BOTH
backends at the "keeps ALL THREE labels" assertion.

The step comments now describe exactly what is graded, and the four steps
carry an explicit `too_many_lines` allow with its rationale: they share one
fixture and read in sequence, so splitting them would sever each assertion
from the writes it is about.

Refs: ub-lp9.27
Nothing in the workspace exercised `labels_set` with an empty vector: the
three call sites that pass a value (`filters.rs`, `behaviour.rs` and the
overlap contract cell) all pass a non-empty set, so a mutant treating the
empty set as a no-op survived everything. That left the last of the four
observable ub-lp9.27 consequences uncovered — pre-fix the removal half of
the set diff was unreachable, which made `labels_set` purely ADDITIVE and
clearing a silent no-op.

`contract_labels_set_empty_clears` asserts that clearing really clears: the
returned issue AND a fresh `get_issue` both carry no labels, one
`LabelRemoved` per previously-carried label is emitted, and `updated_at`
ADVANCES because a clear is a real relation change. The removal events are
compared as a SET — two or more removals come out of a `HashSet`
difference, so their relative order is genuinely unspecified — while the
count and both payloads stay exact.

Registered in `run_storage_contract_suite`, so it executes on both the
in-memory and the temp-file backend; an unregistered cell is a dead test.

Mutation-proved: an `if !set_labels.is_empty()` guard around the set
assignment survives every `unblock-storage` and `unblock-engine` test
target without this cell, and fails only this cell with it.

Refs: ub-lp9.27
…tion

The v1.0.1 bullet closed with "every input it changes was previously
accepted and silently mishandled". That clause was FALSE, and the bullet
contradicted itself twice within nine lines: it already said an
already-present label add was "turning an idempotent call into an opaque
backend error". Those inputs were REJECTED, loudly; only the silent
no-op removal and the additive-only `labels_set` were mishandled quietly.

The CONCLUSION was and stays true — this files with the bug fixes, not
with the ratified behavioural breaks — so the justification is repaired
rather than the conclusion. The true predicate is that no input GA
ACCEPTED now fails, which is exactly what separates it from those breaks.
The one loud change goes the other way, error->success, and is named
first: a release note owes its reader that transition rather than a
bucket it can be buried in.

Also under-enumerated: "User-visible in this cut" listed only the removal
taking effect and the duplicate add ceasing to error. It omitted that
`labels_set` now genuinely REPLACES — a caller's set drops the labels it
does not list, and the empty set really clears.

Refs: ub-lp9.27
Three prose corrections, all of the same class — a sentence that claims
more than its subject does.

Crate plan, the label contract-cell enumeration:

- "The scalar event stays pinned LAST, and the length + both payloads stay
  exact" read as covering BOTH halves of the overlap cell. Only the first
  half is payload-exact; the combined label+scalar half compares event
  TYPES only. Split into what each half grades.
- "the other four cannot grade the WHERE issue_id scope" was a DERIVED
  COUNT inside an enumeration — the failure mode this repo has hit five
  times. Restated as a property of the list, with no numeral to derive.
  The count would have rotted in this very commit: the enumeration gains
  the empty-set cell, which the same commit documents.
- The per-issue cell entry now records that it also grades the reconcile
  DELETE's scope, and why that needed a third step of its own.

Spine, the amended empty-diff passage: the aside said a row-column-only
guard "would skip a relation-only patch — which is precisely the ub-lp9.27
defect". Over-general in one direction and silently wrong in another. Such
a guard skips EVERY relation-only patch; the defect skipped only those
whose diff had been mis-computed into an equal one. Verified against the
pre-fix code (4ab02ab crud.rs:925/933): the skip guard DID carry
`label_changed` while the stamping condition did NOT, so a pre-fix
label-only patch that really moved the set was never skipped — its rows and
events landed and it lost only the `updated_at` stamp. That is the fourth
observable consequence, and the passage now states it instead of the
plausible-but-false "still stamped updated_at".

Refs: ub-lp9.27
…count

The Verify gate failed two prose statements in the `update_issue` clause of
the interface contract. Both fixes are prose in `docs/plans/01-design-spine.md`;
no code and no test changes.

(1) The empty-diff passage claimed that a pre-fix label-only patch which
really moved the set landed "its label rows and `LabelAdded`/`LabelRemoved`
events". The `LabelRemoved` half was impossible: the reconcile iterates
before-minus-current, and against the always-empty pre-fix diff base that set
is always empty, so the removal loop was unreachable and no pre-fix update
ever wrote a label `DELETE` or a `LabelRemoved` event — which is what
`00-roadmap.md` and `crates/unblock-storage.md` already state on this branch.
Deleting the `LabelRemoved` mention alone would still imply every moving patch
landed, which is false too, so the passage now separates the pre-fix shapes:
an add of genuinely ABSENT labels landed its rows and its `LabelAdded` events
and lost only the `updated_at` stamp; a patch naming an ALREADY-PRESENT label
died on the `labels` `(issue_id, label)` primary key and rolled its whole
transaction back, landing nothing; a removal never ran, which is why
`labels_set` was purely additive and clearing was a no-op. The passage's point
— that the defect was NARROWER than a row-column-only guard would be — is
preserved.

(2) The normative "Label diff base" paragraph said "Three observable
consequences" and then enumerated three. This branch added a fourth
contract-suite-pinned consequence — `contract_labels_set_empty_clears`,
registered in `run_storage_contract_suite`, so it runs on both the in-memory
and the temp-file backend. The numeral is removed (the consequences are stated
as a list, never as a count of it, because the list grows), and the two graded
behaviours the enumeration did not cover are now stated: `labels_set` REPLACES
the whole set, so a currently-carried label the caller does not list is
DROPPED, and `labels_set` to the EMPTY set really CLEARS — one `LabelRemoved`
per carried label, `updated_at` advancing like any other real relation change.

Checks: `cargo xtask doc-lint` (19 docs, 6 classes clean), `cargo xtask
knowledge-lint` (59 pages, 6 checks clean), all four `scripts/checks/*.sh`
green, `cargo fmt --all --check` clean. No test run — no code changed.
…cope

Four prose repairs in the `update_issue` clause of the interface contract,
from the final gate. No code and no test changes.

(1) "A pre-fix label-only patch that really DID move the set was not skipped
at all" is true only of a patch whose COMPUTED diff came out non-equal. Read
plainly it collides with the preceding sentence: a `labels_remove` of a
present label genuinely moved the set in truth and WAS skipped, because
against the empty base its diff came out net-zero. The qualifier is now on
the computed diff; the following sentences already disambiguate, so nothing
else in the passage moves.

(2) A reflow artifact left the sentence beginning "`updated_at` advances"
dangling on its own line. Rewrapped so it reads whole. The same rewrap
closes two more dangling lines in the same paragraph group (the event-order
clause and the tombstone-guard heading).

(3) "The relation exceptions are NORMATIVE and there are exactly TWO" sits
twenty lines above "stated as a LIST and never as a count of it". Both are
right and they are reconcilable, but the clause stated no rule for which
case gets which, so it read as an oversight. It now says why THIS one is
counted: it is a CLOSED normative set whose count is itself the rule,
because it forbids a third exception, unlike the OPEN consequence list
below, which is stated as a list precisely because it grows.

(4) The "Label diff base" paragraph stated only the READ base while the
contract suite hard-gates the reconcile DELETE's per-issue WRITE scope as
well (a label shared with another issue must survive on that other issue
when the target removes it). One clause closes the parity, matching the
framing `docs/plans/crates/unblock-storage.md` already carries.

Checks: `cargo xtask doc-lint` (19 docs, 6 classes clean), all four
`scripts/checks/*.sh` green, `git diff --word-diff` confirming the reflow
changed no word outside the three repaired clauses.

Refs: ub-lp9.27
The v1.0.1 bullet for this defect said "The post-transaction hydrated
re-read masked both, returning a correct-looking label set either way".
That is false, and it contradicts its own immediately preceding clause,
which correctly says the already-present add/set turned an idempotent call
into an opaque backend error. An error is the LOUDEST possible outcome, not
a masked one: that path died on the `labels` primary key, rolled its whole
transaction back, and no post-transaction re-read ever ran on it.

The masking applies ONLY to the removal shapes — the label-only skip and
the mixed patch — and the carve-out is now explicit, naming the add/set as
the loud half. The code comment this sentence descends from
(`crates/unblock-storage/src/libsql/crud.rs`, the seed block) keeps its
"The remove is the SILENT one" disambiguator and was already correct; it is
untouched.

Also rewraps the paragraph, which closes the dangling "**`unblock-storage`
(L2)" line an earlier hand edit left mid-sentence. The rewrap was generated
mechanically and verified with `git diff --word-diff`: no word outside the
repaired sentence changed.

Checks: `cargo xtask doc-lint` (19 docs, 6 classes clean), all four
`scripts/checks/*.sh` green.

Refs: ub-lp9.27
The `crud.rs` row carried the same defect the roadmap bullet did, condensed:
"both are hidden by the post-tx hydrated re-read, which returns a
correct-looking label set either way". Only the two REMOVE shapes are
hidden. The already-present `labels_add`/`labels_set` re-INSERT died on the
`labels` `(issue_id, label)` primary key and rolled its whole transaction
back, so it never reached a post-transaction re-read at all — it is the
loud half, which is why the fix ships as an error-to-success change rather
than a silent one.

The code comment this row was condensed from keeps its "The remove is the
SILENT one" disambiguator and is unchanged.

Checks: `cargo xtask doc-lint` (19 docs, 6 classes clean), all four
`scripts/checks/*.sh` green.

Refs: ub-lp9.27
`contract_update_issue`'s doc comment said the label half lives in "the four
`contract_label_*` cases below". A rotted derived count: six label cells are
registered in `run_storage_contract_suite` (remove_then_read,
add_existing_is_idempotent, labels_set_overlap_replaces,
labels_set_empty_clears, label_change_updated_at_semantics,
label_diff_base_is_per_issue), and the literal glob also matches
`contract_label_and_or`, an unrelated query case elsewhere in the file. This
branch wrote the numeral and then grew the list past it.

Without this the pull request would ship, in one diff, the rule and its
violation: the spine text on this very branch now says these consequences
are "stated as a LIST and never as a count of it, because the list GROWS".
The comment now names the block, points at its registration site — the only
place where membership is decidable, since an unregistered case is a dead
test — and says why the glob is not the definition.

Doc comment only: no assertion, no fixture, no behaviour.

Checks: `cargo fmt --all --check` clean, `cargo clippy -p unblock-storage
--all-targets --features testkit -- -D warnings` clean, `cargo test -p
unblock-storage --features testkit` green (13 targets, 0 failures).

Refs: ub-lp9.27
The implementor-facing `Storage::update_issue` doc said only that
`labels_set` "overlapping the current set replaces it while announcing only
the deltas". It omitted the two halves of the same replacement that the
spine now states normatively and the contract suite hard-gates: a
currently-carried label the caller does NOT list is DROPPED, and the EMPTY
set is that same replacement at its boundary rather than a special case —
it really CLEARS.

The old wording said LESS, never something else, but this is the surface
every backend author reads before writing an implementation, and both
behaviours are graded on both backend legs. Worded to match the spine.

Checks: `cargo fmt --all --check` clean, `cargo clippy -p unblock-storage
--all-targets --features testkit -- -D warnings` clean, `cargo test -p
unblock-storage --features testkit` green (13 targets, 0 failures).

Refs: ub-lp9.27
The wiki run-report for the label-hydration run, indexed under Runs
(PROCESS.md §8): context, what and why, outcome, gotchas, glossary, links.

Written to be act-on-able cold. The Gotchas carry the part that lives in no
diff: the production code was right after round one and never moved again,
so every defect the three later gate rounds found was prose claiming more
than was true; a conformance suite must not grade an order its own contract
declines to promise; a positive-only corpus cannot grade a query predicate,
because every existing label case held ONE issue and widening the WHERE
clause to match all rows left the whole suite green; a read fixture still
cannot grade the reconcile DELETE, whose scope only becomes observable when
two issues share the removed label; an assertion comparing a loaded content
hash to a recompute of the same loaded issue compares a value to itself,
because the loader recomputes and never reads the column; and a derived
count rotted twenty lines from where this same branch removed the identical
defect, in the higher-authority document, where no lint reaches.

The `.unblock/issues.jsonl` re-export does NOT land in this commit and was
not hand-written. The workspace database is gitignored, so it exists only in
the session's shared checkout, and this isolated worktree has no unblock
tool access; a hand-edited export would not be byte-identical to a real one.
It lands with the orchestrator, in the same pull request. Note for that
commit: the glossary token check scans the linked issue's COMMENTS, which
were not readable here, so it must re-run `cargo xtask knowledge-lint` and
add a glossary row for any uppercase session-local code the thread carries.

Checks: `cargo xtask knowledge-lint` (60 pages, 6 checks clean),
`scripts/knowledge/tests/run-report-gate-selftest.sh` (38 cases + the
single-sourcing pin clean), `scripts/knowledge/run-report-gate.sh main`
(pass — the substantive diff carries this report).

Refs: ub-lp9.27
The committed record carried ub-lp9.27 with zero comments while the whole
gate narrative — root cause, both rulings, four adversarial gate rounds and
their must-fixes — lived only in the local database. This is the D5 model-B
export that snapshots it into git.

Produced by the `sync` tool's `export` action against the live store and
installed verbatim: md5 806c6e939f382290e54f7c8ed2f7c73b on both the export
and the committed file. Not regenerated, not hand-edited.

Also carries the three follow-up issues this task's gates raised and Miguel
scoped out of it: the stale crud.rs line citations with their executable
check, the shared-cache in-memory store that can redden a required job for
unrelated reasons, and whether the strict label INSERT rule is testable at all.

The run-report landed separately in the preceding commit because the export
needs MCP access the implement worktree does not have; both are in this PR,
which is what the same-PR rule requires. Verified after installing: the
knowledge lint's glossary check now scans the exported comment thread and
stays clean (60 pages, 6 checks), doc-lint clean (19 docs, 6 classes), the
run-report gate passes against main, and all four required check scripts
exit zero.
@miguelramos
miguelramos merged commit bd39fa2 into main Aug 4, 2026
23 checks passed
@miguelramos
miguelramos deleted the ub-lp9.27-label-diff-base branch August 4, 2026 16:11
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