fix(storage): hydrate the label diff base inside the update transaction (ub-lp9.27) - #437
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the label diff base in
update_issue. Tracker issue: ub-lp9.27 (P1,data-integrity, v1.0.1 slot).The bug
update_issuebuilt itsIssuefrom aSELECTover theissuestable alone and never hydrated the label relation beforeapply_labelsseeded 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:
labels_removeof 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.labels_add/labels_setnaming an already-present label re-inserted against thelabelsprimary key and died with an opaque backend error, rolling the whole transaction back.labels_setwas purely additive and clearing the set was a no-op.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
SELECTseeds the diff base from thelabelstable, placed after the tombstone guard so the reject path pays nothing; and the stamping condition gains the label term so a real relation change advancesupdated_at.content_hashis 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:
updated_at, aligning labels with the two existing precedents: reparent (FR-1b) and the comment surface (D37'sFORK-S1, which advances it to feed thestalequery). Landed as an inline amendment riding FR-1b — no new D-id, no D-range cascade.Gates
Design Review passed. The Verify gate ran four adversarial rounds; each found real defects and each was fixed:
WHEREclause 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 intests/behaviour.rswhere backend behaviour belongs.LabelRemoved).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 reconcileDELETE.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_versionbump, noCONTRACT_HASHre-pin, no newErrorCode, no tool/command/schema field, and the 0–8 exit table is untouched.unblock-storageis the only crate that gains code.Follow-ups filed (scoped out deliberately)
crud.rsline citations across the PRD, spine, crate plan and an engine test, all pre-existing; needs an executable check, not another manual re-citation pass.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 fourscripts/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.jsonlis re-exported here (it previously carried this issue with zero comments).Remaining acceptance criterion
The issue's last criterion — removing the stale
v1-1label 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 onmaintoday:labels_removeon that issue returnsisError:falsewith the label set unchanged andupdated_atunmoved.🤖 Generated with Claude Code