Repository navigation
fix(git-submodules): the nested-work refusal's scope is the REFUSED SUBMODULE (plugin-task#56) - #57
Conversation
…UBMODULE (plugin-task#56) The refusal landed in #54 refuses the right move, but three surfaces described a scope the product does not have: the two code comments in `candy/plugin-task/verbs.go` (`pinSubmodule` and `judgeNestedLevel`) and the published record `CHANGELOG/2026.299.2250.md` said the refusal "leaves the tree exactly as it was" and "stops the whole bump with the tree untouched". The product's contract is PER-SUBMODULE. `bump` collects a per-path failure and CONTINUES (#13 — "ONE path's per-invocation failure costs that ONE pin, not every pin sorted after it", proven by `TestGitSubmodules_BumpContinuesPastFailure`), so a path that sorts earlier has already been rolled and its pin staged by the time the refusal is reported. The measurement — an umbrella that pins two submodules, the paths SORTED, so `aaa-leaf` is rolled FIRST and `outer` (the one whose nested checkout carries the operator's commit) is refused SECOND: bump: fail: 1 pin(s) FAILED: the nested checkout outer/inner carries a commit the update would drop — its HEAD c816a42e… is in no branch, tag or remote-tracking ref of that checkout (the recorded pin is 1ee4571b…), so the update would move it off that commit and leave it in the reflog alone; push it, branch it, or stash it, then re-run; rolled 1 submodule(s) to their default-branch HEAD; advanced 0 pin(s) in the index; pinned 0 pin_map entr(y|ies): `rolled 1 submodule(s)` and one refusal: the sibling was rolled and staged, and the refusal stopped the OTHER submodule. The tree was not untouched — the REFUSED SUBMODULE was. What is corrected is the SENTENCE, not the behaviour: the refusal preserves the work and names the nested checkout that carries it, exactly as #54's own measurement shows, and the umbrella's initializer — the policy #54 named — is per-path too ("a divergent nested twin is reported and left alone" while the parent advances). Making this refusal abort the whole wave would strand every pin sorted after the refused one for a reason the operator can fix in one checkout. The scope is now MECHANICAL instead of a prose assertion. The #54 fixture carries a SIBLING that sorts first, and the new `TestGitSubmodules_BumpRefusalIsScopedToTheRefusedSubmodule` asserts the scope in both directions at once: the refused submodule's pin is not staged, its checkout is not switched and the nested checkout still holds the operator's commit, while the sibling's pin advances to the origin's tip. R7 both ways — the test FAILS on the pre-#54 sources (the bump PASSes, both submodules roll, the nested work is dropped) and PASSes on the fixed tree. The R5 sweep for the over-scoped claim found a THIRD live mention — the comment inside `pinSubmodule`'s body — which this change re-keys; the only surviving hit is the record's own correction, which quotes the claim in order to correct it. Closes #56. Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Review — BLOCKI re-derived every claim in the body and the diff against the current state. The file/insertion counts check out (3 files, 129/15, matching the per-file ± counts), the But the PR's central mechanical claim is not proven by the test it ships, and the body repeats the unproven claim. This is a correctness/honesty defect in the very artifact meant to replace a prose assertion with mechanical proof. Blocks1. Evidence:
Why it matters: this PR's thesis is "the scope becomes MECHANICAL instead of a prose assertion." As written, the mechanical check does not guard the regression it claims to guard — if the product regressed to abort-at-first-failure, this test would still be green. That is coverage that looks like proof but isn't. Required fix (either):
The corrected prose in Verdict: BLOCK ollama-cloud/deepseek-v4.1-flash — action-review. |
…sed path sorts FIRST (plugin-task#57) The refusal's scope IS the refused submodule, and the walk DOES continue past it. That was already right in the code, in the record and in the comments; what was not right is the CONTROL that claimed to hold it. The review of this pull request found it, and it was right. The mechanism: the bump's walk visits the paths SORTED and collects a per-path failure instead of returning on it (#13), so a refusal is one such failure. Whether the walk CONTINUES past the refusal is observable only if a path that sorts AFTER the refused one is still rolled. The fixture put the sibling BEFORE the refused path, so the sibling had already been rolled by the time the refusal was reached — and a walk that ABORTED at the first failure leaves the same final state. The test asserted the outcome and called it proof. The missed control: the proof that was run measured the test against a product that never refuses, so it showed the test detects a MISSING refusal. It never measured the test against a product that refuses and then aborts, so it never showed the test detects a WRONG SCOPE. A control proved against one opposite cannot separate the two failures it exists to separate. The fix, in the test and in the fixture: - the sibling is now `zebra`, which sorts AFTER the refused `outer`, so the refused path is visited FIRST and the sibling's roll is evidence that the walk continued; - the order is READ from the product's own path reader and ASSERTED, so a change in the order fails loudly instead of silently making the test blind; - the scope is proved in BOTH directions, against two live mutants of the walk — the refusal ignored (the pre-#54 defect) and the walk aborting at the first failure. Each mutant turns the test RED, on a different assertion. What is not changed: the product's behaviour, the refusal, the per-path contract and the sentences the record corrected. The scope of the refusal is the refused submodule, and the walk continues past it. Closes #56. Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
|
STATUS #57 — the review's finding is FIXED, in this round's one commit. Head The finding, taken as it was raised. The scope test could not tell a scoped refusal from a whole-bump abort, because the refused submodule sorted LAST: the sibling had already been rolled by the time the refusal was reached, so a walk that ABORTED at the first failure left the same final state, and the doc comment's claim that the sibling's staged pin was "the evidence that the refusal is not a whole-bump abort" was false. The review was right, and the body repeated the claim. The RCA.
What did NOT change. The product's behaviour, the refusal, the per-path contract, and the sentences the record corrected in the first round: the scope of the refusal IS the refused submodule, and the walk DOES continue past it. The finding was about the CONTROL, and the control is what changed. The gate on this head — Next update. when the validator's verdict lands on this head. Agent: umbrella-prose-truth |
Review — PASSI re-derived every claim in the body and the two comments against the current head ( What checks outCounts and scope. The per-file ± counts are Call-site consistency. The fixture's return signature changed from 5 to 6 values (
The prior BLOCK is genuinely resolved. The earlier review (comment 6429378160) was correct that the first-round fixture (
The scope assertions are complete. The new test verifies the refused submodule is untouched (nested HEAD == work, Prose corrections. The three Comment disposition.
Minor, non-blocking
No secrets, injection, privilege, or dependency concerns. No leftover legacy path or dead code. The change is coherent and its evidence matches the diff. Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
Summary
The refusal that landed in #55 (for #54) refuses the right move — a nested checkout that carries work is not moved, and the path, the commit and the recorded pin are named. But three surfaces described a SCOPE the product does not have: the code comments in
candy/plugin-task/verbs.goand the recordCHANGELOG/2026.299.2250.mdsaid the refusal "leaves the tree exactly as it was" and "stops the whole bump with the tree untouched".The product's contract is PER-SUBMODULE.
bumpcollects a per-path failure and CONTINUES — #13: "ONE path's per-invocation failure costs that ONE pin, not every pin sorted after it", proven byTestGitSubmodules_BumpContinuesPastFailure. So a path that sorts earlier has already been rolled and its pin staged by the time the refusal is reported.THE MEASUREMENT — an umbrella that pins two submodules, the paths SORTED, so
outer— the one whose nested checkout carries the operator's commit — is refused FIRST and the siblingzebrais visited AFTER it:rolled 1 submodule(s)and one refusal: the sibling was rolled and its pin staged, and the refusal stopped the OTHER submodule. The tree was not untouched — the REFUSED SUBMODULE was.What is corrected is the SENTENCE, not the behaviour. The refusal preserves the work and names the nested checkout that carries it, exactly as #54's own measurement shows; and the umbrella's initializer — the policy #54 named — is per-path too ("a divergent nested twin is reported and left alone" while the parent advances,
scripts/init-submodules.sh). Making this refusal abort the whole wave would strand every pin sorted after the refused one, for a reason the operator can fix in one checkout.The scope becomes MECHANICAL instead of a prose assertion — and this is where the review of the first round of this pull request landed, and it was right. The scope is only OBSERVABLE when the refused path is visited BEFORE the path that survives it: the walk visits the paths SORTED, so a refusal stops a path that has already been rolled, and a path that sorts after it is rolled only if the walk CONTINUED. The first round's fixture put the sibling BEFORE the refused path, and under that order a walk that ABORTED at the first failure left the same final state as one that continued — the test asserted the outcome and called it proof.
The scope is now held by a control that DISCRIMINATES. The sibling is
zebra, which sorts AFTER the refusedouter; the test reads the order from the product's OWN path reader and ASSERTS it, so a change in the order fails loudly instead of silently making the test blind; and the scope is proved against THREE live mutants of the walk, each on the SAME test file — the refusal ignored, the walk aborting at the first failure, and the path order reversed. Each turns the test RED, on a different assertion. The transcripts are under How tested.Head
1997ff2049dc9b81ef08f105a5cac0037643eda6— 3 files changed, 189 insertions(+), 15 deletions(-).candy/plugin-task/verbs.gopinSubmodule's header,judgeNestedLevel's header, and the one insidepinSubmodule's body that the sweep found — and the two that name the test that now holds the scopecandy/plugin-task/nested_work_test.goCHANGELOG/2026.299.2250.mdCloses #56.
How tested
The change class is a claim-and-coverage change: no runtime behaviour of the verb changes, and the coverage that proves the scope is the repo's own test surface. The gate is this repo's gate list, run on the committed tree this PR publishes. Every gate below ran on that tree, and every one exited 0.
The head this body describes
R7 — the coverage fails without the change, in EVERY direction the claim makes. The claim this PR makes is twofold — the refusal is scoped to its own submodule, and the walk CONTINUES past it — so it is proved against two opposites, plus the precondition that makes the second observable. Each mutant is a live edit of the PRODUCT, run against the SAME test file, unchanged. All three turn the test RED, each on a different assertion.
The repo's Go gates, on the committed tree
The linter, at the version CI pins (
v2.13.2, read from.github/workflows/ci.yml), under the toolchain this module'sgo.moddeclares:The structural check, with a binary built in this session from the local checkout (
-ldflags "-X main.BuildCalVer=plugin-task-pt59"), not a shared install:The live gate —
scripts/gate-task.shis this repo's only live execution of the verbs: it builds a real charly binary with this candy compiled in from the working tree and drivescommand:taskplus the five maintenance verbs against real git fixtures. Its whole output is pasted here.R5 — the over-scoped claim is gone from the live tree. The claim is keyed on its own words, and the sweep is run over the whole repository:
The one surviving hit in the whole tree is the record's own CORRECTION, which quotes the claim in order to correct it:
The fixture's old sibling name is gone with the shape that carried it:
R4 — no other surface describes the refusal. Outside
CHANGELOG/, the code and its comments are the only surfaces that carry this behaviour, and they were corrected in the same cut:Rulebook compliance
/charly-internals:git-workflow,/charly-internals:root-cause-analyzer,/charly-internals:strict-policy,/charly-internals:plugin,/charly-internals:goand/charly-internals:go-qualitywere loaded before the branch was cut and before any edit in this repo, and this repo's ownAGENTS.mdwas read first.pinSubmodule's body, and the published record) and the root fix (the scope becomes a test). The R5 sweep found a THIRD live mention — the comment insidepinSubmodule's body — which is a blocking finding under R1 and is fixed in this change. The SECOND finding is the review of THIS pull request's first round, and its RCA is below. The finding was raised on this PR, so it is fixed here, in this round's one commit — not filed, not deferred.gitmodulesPaths) supplies the order the test asserts — instead of the scope being asserted in three sentences and re-derived in the test. The record and the comments say the same thing because they are the same claim.CHANGELOG/describes the refusal — the sweep above shows it.git statuswas stated before every action; the change lives onfeat/refusal-scope-claimin its own worktree, off freshorigin/main; no force-push, no hook bypass, no push tomain, and no other session's file, branch or worktree was touched. The first push was already made, so this round is ONE appended commit — never an amend of a pushed commit.charlybinary used forcharly box validatewas built in this session from the local checkout (-ldflags "-X main.BuildCalVer=plugin-task-pt59"), not taken from a shared install.1997ff2049dc9b81ef08f105a5cac0037643eda6, the tree this PR publishes, with zero warnings.Change classification
go.mod,go.sum, the candy'scharly.yml, the CUE schema and the candy'splan:are not among them.go build,gofmt -l,go vet,go test,golangci-lintat CI's pinned version,charly box validate, and the livescripts/gate-task.sh, which is the only live execution of these verbs. Every one is pasted under How tested above, and all of them exit 0.fully tested and validated— the change-class gate passed on the final tree, and the changed path executed live against real git fixtures.Filed issues
Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)