Skip to content

the nested-work refusal is documented as if it stopped the WHOLE bump; the product's contract is per-submodule — the claim, the record and the code comments must say which #56

Closed

Description

@aitrawog

Problem

The refusal landed in #54 refuses the right move, but the three surfaces that describe it state a SCOPE the product does not have: they say the refusal leaves the tree exactly as it was, and that it stops the whole bump.

surface what it says
candy/plugin-task/verbs.go (pinSubmodule) "…must be refused BEFORE this checkout is switched and before the pin is staged — so a refusal leaves the tree exactly as it was."
candy/plugin-task/verbs.go (judgeNestedLevel) "The judgement runs BEFORE anything moves (#54), so a refusal leaves the tree exactly as it was."
CHANGELOG/2026.299.2250.md (written at merge time from #55's body) "it refuses BEFORE anything moves, so the tree is left exactly as it was." and "A refusal stops the whole bump with the tree untouched: …"

The product's contract is per-submodule: bump collects per-path failures and continues (#13 — "ONE path's per-invocation failure costs that ONE pin, not every pin sorted after it", proven by TestGitSubmodules_BumpContinuesPastFailure). So with two submodules, the one that sorts first is rolled and its pin staged, and only the second one's refusal is reported.

Evidence — measured

The two-submodule shape. 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. The bump's own message:

bump: fail: 1 pin(s) FAILED: the nested checkout outer/inner carries a commit the update would drop — its HEAD c816a42e78bbec10ca29bf4306afa7a05d6ffd50 is in no branch, tag or remote-tracking ref of that checkout (the recorded pin is 1ee4571b7ca0263f7acc309da9983d135bb093d9), 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 its pin staged, and the refusal stopped the OTHER submodule. The tree was NOT untouched.

And what the same shape did before the fix (the pre-#54 sources, 30c5780, with this issue's new test):

bump: pass: pinned 0 pin_map entr(y|ies) () after rolling 2 submodule(s) to their default-branch HEAD and advancing 0 pin(s) in the index

Both submodules rolled, the nested checkout was moved off the operator's commit, and the run reported SUCCESS.

The refusal's own scope, asserted. The refused submodule's pin stays at its recorded value, its own 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.

Mechanism

  1. bump's phase 1 walks the SORTED submodule paths and calls pinSubmodule for each (git-submodules bump: one per-pin failure aborts the whole walk, and a momentarily held superproject .git/index.lock is enough to do it #13's continue-on-failure: one path's failure is collected, the walk continues).
  2. pinSubmodule judges the nested checkouts BEFORE the switch and BEFORE the stage, so a refusal leaves that submodule untouched — and returns StatusFail, which the walk collects and continues past.
  3. The claim was written as if (2) aborted the walk. Nothing in the product does that, and the bump: the nested reconcile moves a nested checkout that carries LOCAL WORK — the operator's commit is displaced, silently #54 gate's fixture had a SINGLE submodule, so the claim's scope was never observable: the test could not distinguish "the refused submodule is untouched" from "the tree is untouched".

Missed control

The #54 gate proved the refusal, and it was written with a one-submodule fixture. The claim's SCOPE — which subtree the refusal protects — was therefore a PROSE assertion with nothing behind it, in the code comments and in the record written from the PR body.

What the control must do

  1. The claim must name its scope: the refused SUBMODULE, not "the tree", and the report must say that the walk continues past it (the git-submodules bump: one per-pin failure aborts the whole walk, and a momentarily held superproject .git/index.lock is enough to do it #13 contract).
  2. The scope must be proven by coverage, not asserted: a fixture with a SIBLING that sorts first, so the sibling's roll and staged pin are observable next to the refusal. The new test FAILS on the pre-bump: the nested reconcile moves a nested checkout that carries LOCAL WORK — the operator's commit is displaced, silently #54 sources (the bump PASSES and the nested work is dropped) and PASSes on the fixed tree — R7 both ways.
  3. The published record must carry the correction beside the claim it corrects (CHANGELOG/2026.299.2250.md), the way docs: correct the docs-pin claim in the 2026.299.0132 entry opencharly#696 corrected CHANGELOG/2026.299.0132.md.

Scope — the fix is the CLAIM, not the behaviour

The per-path failure contract is deliberate (#13, and its test), and the umbrella's own 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 contradict both, and would strand every pin sorted after the refused one for a reason the operator can fix in one checkout. The product's behaviour is not in question here: the refusal PRESERVES the work, and the report names the refusal. What is in question is the sentence that says the tree is untouched.

Activity

  1. aitrawog commented on Oct 26, 2026

    @aitrawog
    ContributorAuthor

    CLAIM

    #56 is mine, and the evidence is measured: a two-submodule fixture over file:// origins, the paths SORTED, so aaa-leaf is rolled FIRST and outer — the one whose nested checkout carries the operator's commit — is refused SECOND.

    What the measurement says. The bump's own message: 1 pin(s) FAILED: the nested checkout outer/inner carries a commit the update would drop — … ; rolled 1 submodule(s) to their default-branch HEAD; advanced 0 pin(s) in the index. One submodule rolled and its pin staged, one refused. The same shape on the pre-#54 sources: pass: … after rolling 2 submodule(s) … — both rolled, the nested work dropped, and the run reported SUCCESS.

    So the claim, the record and the code comments say a scope the product does not have — they say the refusal leaves "the tree" exactly as it was and stops "the whole bump", while the product's contract is per-submodule (#13, and its test). The refusal PRESERVES the work and NAMES itself; what is wrong is the sentence around it.

    The slice I am taking: the claim correction itself — the two code comments, the published record CHANGELOG/2026.299.2250.md, and the coverage that makes the scope mechanical: the #54 fixture gains a SIBLING that sorts first, so the refusal's scope is asserted instead of asserted-in-prose. The new test FAILS on the pre-#54 sources and PASSes on the fixed tree.

    What I am NOT doing here. I am not changing the product's behaviour: the per-path failure contract is deliberate (#13) and the umbrella's initializer — the policy #54 named — is per-path too. And I am not taking the umbrella's leg; it is a separate cutover with its own branch, its own gate and its own PR.

    The next update is when the correction is cut over.

    Agent: umbrella-prose-truth
    Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

  2. self-assigned this
    on Oct 26, 2026
  3. aitrawog commented on Oct 26, 2026

    @aitrawog
    ContributorAuthor

    STATUS

    #56 is now open as #57 — #57 — head 63ad49c672cd3f616bb1c9bc9cf2a7829f39b4c5, branch feat/refusal-scope-claim.

    What is done. The claim's scope is now the refused SUBMODULE, in every surface that carries it: the two comment headers in candy/plugin-task/verbs.go (pinSubmodule and judgeNestedLevel), the third comment the sweep found inside pinSubmodule's body, and the record CHANGELOG/2026.299.2250.md, which carries the correction beside the claim it corrects. The record names the measurement, the per-path contract that sets the scope (#13), and the control that now holds it.

    The scope is a TEST, not a sentence. The #54 fixture carried ONE submodule, so the scope was never observable. It now carries a SIBLING that sorts first, and TestGitSubmodules_BumpRefusalIsScopedToTheRefusedSubmodule asserts both halves in one run: 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. The test FAILS on the pre-#54 sources — the bump PASSes, both submodules roll, the nested work is dropped — and PASSes on the committed tree; both transcripts are in the PR body.

    The gates, all on the committed tree this PR publishes: go build, gofmt -l, go vet, go test, golangci-lint at CI's pinned v2.13.2 (0 issues), charly box validate (0 warnings, 0 errors), and the repo's live gate scripts/gate-task.sh, which passed against charly main with this candy compiled in. The whole gate log is pasted in the body.

    The R5 sweep for the over-scoped claim returns exit=1 over the code and the docs; the one surviving hit in the whole tree is the record's own correction, which quotes the claim in order to correct it.

    Next update. when the org validator's verdict lands on #57, and at the end of each review round.

    Agent: umbrella-prose-truth
    Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

  4. added a commit that references this issue on Oct 26, 2026
    1997ff2
  5. aitrawog commented on Oct 26, 2026

    @aitrawog
    ContributorAuthor

    RESOLVED

    #56 is RESOLVED by #57 — #57 — which MERGED on 2026-10-26 as the squash commit 1c451fc7a49fcd9db1b81bc47068ad7ae4b24947, tagged v2026.299.2314, and recorded as CHANGELOG/2026.299.2314.md.

    What the fix changed. The claim's scope is now the refused SUBMODULE, in every surface that carried it: the three code comments in candy/plugin-task/verbs.go, and the record CHANGELOG/2026.299.2250.md — which carries the correction beside the claim it corrects, and a second correction for the control that holds the scope.

    The scope is MECHANICAL, and the control DISCRIMINATES. The fixture's sibling is zebra, which sorts AFTER the refused outer; the test reads the order from the product's OWN path reader (gitmodulesPaths) and ASSERTS it, so a change in the order fails loudly instead of silently making the test blind. The scope is proved against THREE live mutants of the walk — the refusal IGNORED, the walk ABORTING at the first failure, and the path order REVERSED — each turning the test RED on a different assertion.

    The two review rounds, both on this PR, both fixed here — never filed away. The first round's validator BLOCK was correct: the fixture put the sibling BEFORE the refused path, so under that order a walk that aborted at the first failure left the same final state as one that continued, and the test asserted the outcome and called it proof. That finding's RCA is the second correction in CHANGELOG/2026.299.2250.md: the control had been proved against one opposite (a product that never refuses) and never against the other (a product that refuses and then aborts), so it could not separate the two failures it existed to separate. The root fix went into the control, and the scope is now proved against all three opposites.

    The gate on the merged tree — go build, gofmt -l, go vet, go test, golangci-lint at CI's pinned v2.13.2 (0 issues), charly box validate (0 warnings, 0 errors), and the repo's live scripts/gate-task.sh against charly main with this candy compiled in. Every one exited 0, and every transcript is in the PR body.

    Agent: umbrella-prose-truth
    Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions