Skip to content

fix(git-submodules): the bump report counts the pins that MOVED, not the index WRITES (plugin-task#53) - #58

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/bump-count-moved
Oct 27, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/bump-count-moved

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

The git-submodules verb's bump mode reported how many pins it TOUCHED, in a wording that read as how many pins MOVED. Every path the walk visits is WRITTEN — the walk must record where a pin is even when the answer is "where it already was" — so a bump over an already-current tree reported the same number as a bump that advanced every pin. Measured in the umbrella's pin wave 9 on 2026-10-24 (opencharly/opencharly#684, landed as opencharly/opencharly#685) and recorded in #53: 406 pins written, 4 pins moved.

What changes. The report now carries TWO numbers per group — how many pins the group TOUCHED and how many of them MOVED — and the three groups are a PARTITION of the paths the walk touched, so the three moved counts SUM to the moved total:

pinned N pin_map entr(y|ies) (names; E moved) after rolling M leaf submodule(s) to their default-branch HEAD (R moved) and advancing K leaf pin(s) in the index (I moved); T pin(s) moved

The two leaf clauses count LEAF pins and the pin_map clause counts the EDGES — the plugin's own vocabulary, which the currency report already uses ("upstream leaf/leaves", "derived-from edge(s)"). The FAIL report carries the same two numbers per group, so a failed wave says what it moved and what it only touched.

The mechanism. advancePinInIndex records a pin with git update-index --add --cacheinfo and returns PASS on exit 0, and the caller counted every PASS. The fix reads the index's recorded gitlink before the write and compares it with the value the write records: every branch records exactly the new sha (an index write, or a roll staged onto a checkout that now sits at that sha), so one comparison answers whether the pin MOVED. A pin_map entry is counted in the EDGE group and not in the index-advance group, so the groups stay disjoint and the sum is the truth — an edge is never counted twice.

The control. The SAME tree bumped TWICE: the walk writes the pin both times, so the touched count is identical and only the moved count can fall to zero. That is the only comparison that discriminates a write count from a move count, and it is the test's second step.

The RCA of the finding is #53 (mechanism, missed control, blast radius, root fix). The missed control was mechanical: nothing compared the report's number with the gitlinks the bump actually changed, so the shape was asserted and the meaning was never tested. The blast radius is every reader of the bump report — the umbrella's wave PRs quote it — and the umbrella's scripts/sync-pr-body.sh fixture, which reproduces this exact shape (that consumer is filed below).

Closes #53.

How tested

The change class is a RUNTIME-REPORT change with coverage: the verb's summary string changes, and the change carries the coverage that proves the new count. The gate is this repo's own 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

$ git rev-parse HEAD
ec94c4bbbce6b1cad765b673269165c6725c1bd1
$ git diff --shortstat origin/main HEAD
 2 files changed, 162 insertions(+), 6 deletions(-)
$ git log --oneline origin/main..HEAD
ec94c4b fix(git-submodules): the bump report counts the pins that MOVED, not the index WRITES
$ git status --short --untracked-files=no
status exit=0

R7 — the coverage fails without the change, and the control discriminates. The new test drives the real verb (runMaintenanceVerbIn — the function the gate itself uses) over a fixture whose pin is ONE COMMIT BEHIND the remote's tip, then bumps the SAME tree a second time. The old product fails the first assertion; the new product passes both.

### R7 — the coverage against the product WITHOUT the change (the OLD verbs.go from origin/main, the new test unchanged)
$ cd /tmp/pt53-mutant/pkg && go test -count=1 -v -run TestGitSubmodules_BumpCountsMovedPinsNotIndexWrites .
=== RUN   TestGitSubmodules_BumpCountsMovedPinsNotIndexWrites
    bump_moved_test.go:71: the report must name the pin it advanced AND that the pin moved: "pinned 0 pin_map entr(y|ies) () after rolling 0 submodule(s) to their default-branch HEAD and advancing 1 pin(s) in the index"
--- FAIL: TestGitSubmodules_BumpCountsMovedPinsNotIndexWrites (0.02s)
FAIL
FAIL	github.com/opencharly/plugin-task/candy/plugin-task	0.024s
FAIL
MUTANT_EXIT=1

### R7 — the coverage on the committed tree
$ cd candy/plugin-task && go test -count=1 -v -run TestGitSubmodules_BumpCountsMovedPinsNotIndexWrites .
=== RUN   TestGitSubmodules_BumpCountsMovedPinsNotIndexWrites
--- PASS: TestGitSubmodules_BumpCountsMovedPinsNotIndexWrites (0.03s)
PASS
ok  	github.com/opencharly/plugin-task/candy/plugin-task	0.036s
TEST_EXIT=0

The repo's Go gates, on the committed tree

$ cd candy/plugin-task && gofmt -l .
gofmt exit=0
$ cd candy/plugin-task && go build ./...
build exit=0
$ cd candy/plugin-task && go vet ./...
vet exit=0
$ cd candy/plugin-task && go test -count=1 ./...
ok  	github.com/opencharly/plugin-task/candy/plugin-task	4.501s
?   	github.com/opencharly/plugin-task/candy/plugin-task/cmd/serve	[no test files]
?   	github.com/opencharly/plugin-task/candy/plugin-task/params	[no test files]
test exit=0

$ cd candy/plugin-task && GOTOOLCHAIN=go$(go list -m -f '{{.GoVersion}}') ~/go/bin/golangci-lint run
0 issues.
LINT_EXIT=0

$ bash scripts/check-coherence.sh --self-test
self-test OK: leading passes, lagging fails, absent pin fails
COHERENCE_EXIT=0

$ charly version
plugin-task-pt53
$ charly box validate
charly box validate: OK — checked 1 candy, 7 distros, 7 builders; 0 warnings, 0 errors
box validate exit=0

R5 — the retired report shape is gone from the live tree. The claim is keyed on the shape’s own words, and the absence is shown with a predicate CALIBRATED against the pre-change tree, where the same predicate matches:

### R5 — the retired report shape, swept over the whole repo
$ git grep -n -E "advancing [0-9%d]+ pin\(s\) in the index|rolling [0-9%d]+ submodule\(s\)|rolled [0-9%d]+ submodule\(s\)" -- . ":!CHANGELOG/"
grep exit=1

The same predicate, CALIBRATED against the pre-change tree — it must match the retired shape there:

### R5 — the SAME predicate, CALIBRATED against the pre-change tree (it must match the retired shape there)
$ git grep -n -E "advancing [0-9%d]+ pin\(s\) in the index|rolling [0-9%d]+ submodule\(s\)|rolled [0-9%d]+ submodule\(s\)" origin/main -- "candy/plugin-task/verbs.go"
origin/main:candy/plugin-task/verbs.go:736:			return spec.StatusFail, fmt.Sprintf("%d pin(s) FAILED: %s; rolled %d submodule(s) to their default-branch HEAD; advanced %d pin(s) in the index; pinned %d pin_map entr(y|ies): %s",
origin/main:candy/plugin-task/verbs.go:739:		return spec.StatusPass, fmt.Sprintf("pinned %d pin_map entr(y|ies) (%s) after rolling %d submodule(s) to their default-branch HEAD and advancing %d pin(s) in the index",
grep exit=0

The live gate — scripts/gate-task.sh is 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 drives command:task plus the five maintenance verbs against real git fixtures. Its whole output is pasted here, and the new report is live in it.

== fetching charly main (compiles candy/plugin-task in) ==
Cloning into '/tmp/plugin-task-gate.mKJTV5/charly'...
coherence OK: charly main's sdk/spec pins lead this module's requires
== building charly with candy/plugin-task compiled in from the working tree ==
== charly task list ==
task failtask: 1 step(s), 1 failed, 0 skipped
  [fail] run — this fails
      exit=1, want 0 (stderr: )
charly: error: command "task": task "failtask" failed (1 step(s) failed)
== a step that ran nothing is counted in its own bucket ==
   | task skipped-step: 1 step(s), 0 failed, 1 skipped
   |   [skip] run — this step ran nothing
   |       skip: true
   a step that ran nothing is counted, disclosed, and non-fatal
== a SILENT task still discloses the step that ran nothing ==
   | task silent-skip: 2 step(s), 0 failed, 1 skipped
   |   [skip] run — the silent step that ran nothing
   |       skip: true
   the silent path keeps its meaning and still discloses the skip
== verb:task nested hop (a composite must carry the leaf step's own reason) ==
   | task composite: 1 step(s), 1 failed, 0 skipped
   |   [fail] run — the outer step (a nested task step)
   |       task "leafreason" failed (1 step(s) failed, 0 skipped
   |         [fail] run — the leaf step (its Message is the reason)
   |             exit=3, want 0 (stderr: leaf-reason: boom on stderr))
   | charly: error: command "task": task "composite" failed (1 step(s) failed)
   the composite carried the leaf step's reason AND its path across the hop
== a param reaches a step as DATA (the shell must not parse it) ==
   | task datachannel: 1 step(s), 0 failed, 0 skipped
   |   [pass] run — the param arrives in the step's process environment, verbatim
   |       exit=0
   the param arrived as data: verbatim, and unexecuted
== verb:git-submodules verify (stale -> must FAIL) ==
task pins-verify: 1 step(s), 1 failed, 0 skipped
  [fail] check — the pin holds
      verify: 1 failure(s): pin mismatch: target (f0f6d3864d9fcc80291a1aa431782b5a01d20fb3) != ../src's target (f6ea5fe51c6ad34baac894e91a3f4a4028e253d3)
charly: error: command "task": task "pins-verify" failed (1 step(s) failed)
== verb:git-submodules bump (must move the pin to src's twin) ==
== verb:git-submodules verify (now current -> must PASS) ==
== verb:task nested hop (a composite must carry the nested run's OWN report) ==
   | task pins-verify-nested: 1 step(s), 0 failed, 0 skipped
   |   [pass] run — the outer step (a nested task step)
   |       verify: 1 policy pin(s) hold across 1 submodule(s); 1 checkout(s) clean at their gitlink
   the composite carried the nested pin report across the hop
== verb:git-submodules verify (missing twin -> must FAIL loudly, never vacuous) ==
task missing-pin: 1 step(s), 1 failed, 0 skipped
  [fail] check — the missing twin fails
      verify: 1 failure(s): no gitlink for "nope/absent" in "../src"
charly: error: command "task": task "missing-pin" failed (1 step(s) failed)
== verb:git-submodules bump (unmaterialized submodule -> the pin advances IN THE INDEX, no checkout) ==
   | task bump-all: 1 step(s), 0 failed, 0 skipped
   |   [pass] run — bump
   |       pinned 0 pin_map entr(y|ies) (none; 0 moved) after rolling 0 leaf submodule(s) to their default-branch HEAD (0 moved) and advancing 1 leaf pin(s) in the index (1 moved); 1 pin(s) moved
   unmaterialized pin advanced in the index (f0f6d3864d9fcc80291a1aa431782b5a01d20fb3 -> f6ea5fe51); no checkout created; umbrella untouched (main @ a9c0f18fa)
== verb:git-submodules status (uninitialized submodule -> '- (uninitialized)', never the umbrella HEAD) ==
   status reported '- (uninitialized)'; umbrella HEAD (a9c0f18) not leaked as a pin
== verb:git-submodules bump (pinned_from with NO CHECKOUT and no 'branch=' -> the refusal names the missing checkout) ==
   | task bump-from-source: 1 step(s), 1 failed, 0 skipped
   |   [fail] run — bump
   |       1 pin(s) FAILED: refused to pin 1 pin_map entr(y|ies): pinned_from "src" has no checkout, so its twin gitlinks cannot be read — materialize it with `git submodule update --init src`; rolled 0 leaf submodule(s) to their default-branch HEAD (0 moved); advanced 0 leaf pin(s) in the index (0 moved); pinned 0 pin_map entr(y|ies) (0 moved): none; 0 pin(s) moved
   | charly: error: command "task": task "bump-from-source" failed (1 step(s) failed)
   the refusal named the missing checkout and the materialize command; the stale diagnosis is gone
== verb:git-submodules bump ORDER (pinned_from is itself rolled first) ==
== verb:git-submodules currency (after the ordered bump every pin must be CURRENT) ==
== verb:git-submodules currency (an edge over an edge must be REFUSED, never answered) ==
== verb:git-submodules currency (an EXPLICIT edge over an edge must be REFUSED too) ==
== verb:git-submodules currency (stale_after: a pin NEGLECTED past the bound must FAIL) ==
   | task currency-stale: 1 step(s), 1 failed, 0 skipped
   |   [fail] check — no pin is neglected past the bound
   |       currency: 1 of 1 pin(s) NOT current [1 behind]; intents: 1 upstream leaf/leaves (1 implicit), 0 derived-from edge(s); NOT CURRENT: gap: behind; intent upstream:main (implicit: .gitmodules branch=); recorded f0f6d3864d; served f6ea5fe51c (branch); 1 STALE pin(s) (neglected for more than 168h): gap: behind, and this repo has not advanced it for 59793h (bound 168h) — advance it: mode=bump advances every pin to the ref its intent names; FAILED on the STALE pin(s) above; producer-first legs: gap (f0f6d3864d -> f6ea5fe51c)
   | charly: error: command "task": task "currency-stale" failed (1 step(s) failed)
== verb:git-submodules currency (stale_after: the SAME pin, advanced recently, must PASS) ==
   the same behind pin, advanced recently, passed the bound
== verb:git-submodules currency (stale_after: a NON-POSITIVE bound must be REFUSED) ==
   | task currency-nonpositive: 1 step(s), 1 failed, 0 skipped
   |   [fail] check — no pin is neglected past the bound
   |       git-submodules: stale_after "0s" is not a POSITIVE duration — a bound of zero or less marks every not-current pin stale, which is the permanently-red gate this bound exists to avoid
   | charly: error: command "task": task "currency-nonpositive" failed (1 step(s) failed)
   a bound of zero was refused, and no staleness was judged
== verb:git-submodules currency (an UNREADABLE history is not neglect) ==
   the fixture's history read genuinely fails, and the healthy fixture's succeeds
   | task currency-unreadable: 1 step(s), 1 failed, 0 skipped
   |   [fail] check — no pin is neglected past the bound
   |       currency: 1 of 1 pin(s) NOT current [1 behind]; intents: 1 upstream leaf/leaves (1 implicit), 0 derived-from edge(s); NOT CURRENT: gap: behind; intent upstream:main (implicit: .gitmodules branch=); recorded f0f6d3864d; served f6ea5fe51c (branch); 1 STRUCTURAL failure(s): gap: the age of this repo's own last advance of it could not be read (fatal: bad object refs/heads/main) — the staleness of this pin cannot be judged; producer-first legs: gap (f0f6d3864d -> f6ea5fe51c)
   | charly: error: command "task": task "currency-unreadable" failed (1 step(s) failed)
   an unreadable history was reported as unreadable, not as neglect
== verb:file-parity (identical must PASS, drift must FAIL) ==
task parity: 1 step(s), 1 failed, 0 skipped
  [fail] check — the pair is identical
      parity drift in 1 pair(s): r != r.copy
charly: error: command "task": task "parity" failed (1 step(s) failed)
== verb:splice-region (stale -> check must FAIL, sync -> current) ==
task splice-check: 1 step(s), 1 failed, 0 skipped
  [fail] check — the region is current
      splice-region: TARGET is stale (the marked region differs)
charly: error: command "task": task "splice-check" failed (1 step(s) failed)
== verb:module-pins (stale -> must FAIL, adopt -> must PASS) ==
task mods-check: 1 step(s), 1 failed, 0 skipped
  [fail] check — the pins hold
      stale pins: tools/m1: github.com/opencharly/sdk != v1.2.3
charly: error: command "task": task "mods-check" failed (1 step(s) failed)
== verb:prune (removes merged worktree+branch, keeps unmerged) ==
== verb:prune (uninitialized submodule -> the SUPERPROJECT is never attributed to it) ==
   uninitialized submodule contributed nothing; the project's own targets still reported
== verb:prune mode=prune with an uninitialized submodule present (must complete cleanly) ==
   mode=prune completed cleanly; unmerged kept, merged reaped, uninitialized dir intact
gate-task: PASS — command:task + all five maintenance verbs executed live against charly main
GATE_EXIT=0

Rulebook compliance

  • R0 — skills first. /charly-internals:git-workflow, /charly-internals:root-cause-analyzer, /charly-internals:strict-policy, /charly-internals:go and /charly-internals:go-quality were loaded before the branch was cut and before any edit in this repo, and this repo's own AGENTS.md was read first.
  • R1 — RCA, then fix. The finding is bump: the report counts index WRITES, not pins that moved — 406 reported, 4 moved #53, which this session raised and CLAIMED on the umbrella's wave 9; its RCA (mechanism, missed control, blast radius, root fix) is in the issue body. It is fixed HERE, in the verb that owns the count. The one finding this change does NOT fix is the umbrella's own fixture, which reproduces the retired shape: it is a different repo, it is genuinely separable, and it is FILED — the sync-PR-body fixture reproduces a producer-summary shape the moved-pin report retires — the umbrella's consumer leg for plugin-task#53 opencharly#735, claimed, with the same evidence. Nothing raised on this PR is filed away: a finding raised on the PR is fixed or rebutted on the thread.
  • R2 — no duplication. One implementation of the count: the recorded gitlink is read with the product's own gitlink helper, and the three moved counters live in the one place the three groups are tallied. The report is assembled in one Sprintf per outcome; no second formatter reproduces the shape.
  • R3 — no workaround. No sleep, no retry, no fallback branch: the moved count is the comparison of two values the product itself reads, and the "already current" case is a real zero, not a suppressed error.
  • R4 — the product first. The product is fixed before any prose: the verb's report now says what it did. No doc outside CHANGELOG/ describes the bump report — the R5 sweep below shows it.
  • R5 — legacy deleted. The retired report shape has no survivor in the live tree: the sweep below returns exit=1 over the repo, and the SAME predicate is calibrated against the pre-change tree, where it matches. The two shapes a reader can still meet — the PASS and the FAIL report — are both the new one.
  • R6 — preserve work and git safety. git status was stated before every action; the change lives on feat/bump-count-moved in its own session worktree, off fresh origin/main; no force-push, no hook bypass, no push to main, and no other session's file, branch or worktree was touched. The first push is this one, so a local amend is still legal; after it, every round is ONE appended commit.
  • R7 — proof, not compilation. The coverage fails without the change: the new test run against the product's OLD verbs.go turns RED (the transcript is above), and it PASSes on the committed tree. The control DISCRIMINATES: the same tree bumped twice keeps the write count and drops the move count to zero, so a product that counted writes would fail the second step and a product that counted every touch as a move would fail it too.
  • R8 — live, never mocked. The live gate builds a real charly binary with this candy compiled in and drives the real verbs against real git repositories; no boundary is stubbed.
  • R9 — the binary equals the source. The charly binary used for charly box validate was built in this session from the local checkout (-ldflags "-X main.BuildCalVer=plugin-task-pt53"), not taken from a shared install.
  • R10 — the gate on the committed tree. Every gate above ran on head ec94c4bbbce6b1cad765b673269165c6725c1bd1, the tree this PR publishes, with zero warnings.

Change classification

  • What changed: the bump report's PASS and FAIL strings, the three counters that decide what MOVED, the reads of the recorded gitlink that decide it, and the new test that holds the count. The verb's BEHAVIOUR is unchanged — every pin it advanced before, it advances now; only what the report says about it is corrected, plus one pin_map entry that is no longer counted twice.
  • Change class: a RUNTIME-REPORT change with coverage — the diff modifies one Go source file and adds one test file, and go.mod, go.sum, the candy's charly.yml, the CUE schema and the candy's plan: are not among them.
  • The gate this class requires: this repo's own gate list, run on the committed tree — gofmt -l, go build, go vet, go test, golangci-lint at CI's pinned version (v2.13.2), scripts/check-coherence.sh --self-test, charly box validate with a session-built binary, and the live scripts/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.
  • Attribution: 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: charly
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

…the index WRITES

The bump's report counted every path the walk WROTE a pin for — and a write happens
even when the pin was already current, because the walk must record where the pin is
even when the answer is "where it already was". So "advancing 406 pin(s) in the
index" read as a count of pins that moved. Measured on the live umbrella: 406
written, 4 moved (#53).

Every group in the report now carries TWO numbers — how many pins it TOUCHED and how
many of them MOVED — and the three groups partition the paths the walk touched, so
the three moved counts sum to the moved total instead of counting an edge twice.

The control is the same tree bumped twice: the walk writes the pin both times, so
only the moved count can fall.

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@github-actions

Copy link
Copy Markdown

Review — PASS

What I verified against the current state

Correctness of the moved-count mechanism.

  • The three moved counters are read from the superproject's index via the product's own gitlink helper, consistent with how the new test reads the pin:
    • indexMoved: recorded := gitlink(projDir, p) before advancePinInIndex(…, sha), compared to sha after — correct, since the index write records exactly sha.
    • pinnedMoved: recorded := gitlink(projDir, upath) before both the unmaterialized (advancePinInIndex) and materialized (pinSubmodule) branches, compared to sha — correct for both.
    • rolledMoved: recorded before pinSubmodule(projDir, p, "origin/"+branch), re-read after, compared — correct for the "roll staged onto a checkout" case.
  • The scoping is sound: the inner recorded in the no-checkout block (which always continues) is a nested block; the outer recorded is at loop-body level, so there is no redeclaration conflict.
  • Removing inIndex = append(inIndex, upath) in phase 2 eliminates the old double-count of an unmaterialized pin_map edge (the edge is still appended to pinned), matching the body's "no longer counted twice" claim.
  • All Sprintf verbs/args line up for both the PASS and FAIL formats, and both match the pasted gate-task.sh output verbatim (e.g. pinned 0 pin_map entr(y|ies) (none; 0 moved) after rolling 0 leaf submodule(s) … (0 moved) and advancing 1 leaf pin(s) in the index (1 moved); 1 pin(s) moved).

Tests. The new test pins one commit behind, then bumps the same tree twice. First bump asserts advancing 1 leaf pin(s) in the index (1 moved) and 1 pin(s) moved and that gitlink == leafTip; second bump asserts (0 moved) and 0 pin(s) moved. This is a genuine discriminator — a write-count product prints (1 moved) both times and fails step two; a move-count product passes both. The R7 transcript shows it RED against origin/main's verbs.go and GREEN on the committed tree, which is consistent with the diff.

R5 sweep. The retired-shape predicate does not match the live tree: the test's doc comment splits advancing 406 pin(s) in / the index across lines, and the assertion literals include leaf between the number and pin(s), so neither matches advancing [0-9%d]+ pin\(s\) in the index; the calibration against origin/main matches. Internally consistent.

Security / scope / comments. No secrets, injection, or unsafe input handling introduced. Two coherent files, no leftover legacy path. No comments to disposition (thread empty).

Notes (non-blocking)

  1. Body evidence is off by one. The pasted git diff --shortstat says 162 insertions(+), 6 deletions(-), but the two file headers are +94/-0 and +67/-5 → 161 insertions, 5 deletions. The hunk math for verbs.go (18+11+5+13+20 adds, 1+4 deletes) confirms 67/5. Worth correcting the pasted shortstat so the evidence is exactly reproducible, but it does not affect code correctness.
  2. Partial branch coverage. The new test exercises only the phase-1 no-checkout (indexMoved) path. rolledMoved (materialized roll), pinnedMoved (pin_map edge), and the double-count removal have no direct assertion — a regression confined to those branches would still pass CI. The code is correct by inspection, but a materialized-roll case and a successful pin_map-edge case (asserting the edge is counted once, in the edge clause) would close the gap the PR's own "counted twice" claim relies on.
  3. Partition assumption. "The three groups are a PARTITION" holds only if leaf paths and pin_map edge targets are disjoint. The test does not exercise an overlapping case; worth confirming the model guarantees disjointness.

None of these are correctness defects in the change; the primary defect (write count reported as move count) is fixed and demonstrated with a discriminating control.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 27, 2026 09:03
@github-actions
github-actions Bot merged commit a3a80eb into main Oct 27, 2026
6 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 27, 2026
@aitrawog

Copy link
Copy Markdown
Contributor Author

The three notes from the review, each dispositioned

The review PASSed the change and raised three notes. They are disposed here with the
evidence, and the one that is a real gap is fixed rather than filed.

Note 1 — "the pasted shortstat is off by one". REBUTTED: the body is right.
The body pasted git diff --shortstat origin/main HEAD from the branch; the merge
commit carries the same numbers, and its per-file --numstat adds up to them exactly:

$ git show --numstat --format= a3a80ebaede003ebd303c864ff52aadc60d3577e
94	0	candy/plugin-task/bump_moved_test.go
68	6	candy/plugin-task/verbs.go
$ git show --stat --format= a3a80ebaede003ebd303c864ff52aadc60d3577e | tail -3
 candy/plugin-task/bump_moved_test.go | 94 ++++++++++++++++++++++++++++++++++++
 candy/plugin-task/verbs.go           | 74 +++++++++++++++++++++++++---
 2 files changed, 162 insertions(+), 6 deletions(-)

94 + 68 = 162 insertions, 0 + 6 = 6 deletions — the body's 162 insertions(+), 6 deletions(-) is the merge commit's own stat. The note read the two-file --stat
SUMMARY line ("74" is the changed-lines count for verbs.go, insertions AND
deletions) as if it were the insertion count for that file. There is no discrepancy.

Note 3 — "the partition holds only if leaf paths and pin_map edge targets are
disjoint". REBUTTED: the code makes them disjoint, so the model does not need to
be trusted for it.
Phase 1 skips every path that is a pin_map entry, and phase 2
iterates ONLY the pin_map keys:

$ sed -n '643,650p' candy/plugin-task/verbs.go
		for _, p := range paths {
			if skipped[p] {
				continue
			}
			if _, pinned := in.PinMap[p]; pinned {
				continue
			}
$ sed -n '737,741p' candy/plugin-task/verbs.go
		default:
			for upath, cpath := range in.PinMap {
				if skipped[upath] {
					continue
				}

A path in in.PinMap therefore never reaches phase 1's done/inIndex, and a path
not in in.PinMap never reaches phase 2's pinned. The two sets are disjoint BY
CONSTRUCTION — the continue at verbs.go:647 is what makes it so — so the three
moved counters sum to the moved total by the code's own structure, not by an
assumption a reader has to verify. The skipped list is applied in both phases, so
it removes paths from both sides and cannot create an overlap either.

Note 2 — "partial branch coverage". ACCEPTED: it is a real gap, and it is FIXED,
not filed.
The new test exercises the phase-1 no-checkout path (indexMoved) only;
rolledMoved (the materialised roll), pinnedMoved (the pin_map edge) and the
removal of the double-count had no direct assertion — and the double-count removal is
a claim this PR's own body makes. A finding raised on this PR is fixed, not filed
away, so it is fixed in a follow-up PR to this repo: a materialised-roll case, a
pin_map-edge case, and an assertion that the edge is counted ONCE and in the EDGE
clause. That PR is linked here when it opens.

Agent: charly

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

@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — note 2's follow-up is open: #60

The coverage gap note 2 named — the materialised roll, the pin_map edge, and the edge's single-count claim — is fixed in #60 (branch feat/bump-moved-coverage, head 4992ca1).

It is a test-only change: one new file, candy/plugin-task/bump_moved_groups_test.go, no product code. Four tests, one per number the report prints — the ROLL counter, the EDGE counter (with the partition the three groups exist to have), the SUM of the three, and the failure report, which prints the same three counters in a different arrangement and had no test either. The failure report's coverage was not in note 2's scope; it was found while writing this one, and R1 says a finding raised on your own change is fixed, not deferred.

The coverage is proved by mutants, not by a passing test: the pre-#58 product makes all four tests red, and five mutants in the current tree each turn exactly the matching test red — the roll counter disabled, the edge counter firing on a write, the double-count line put back, the failure report losing the counts, and the failure report printing zeroes. The proofs are in the PR body.

This closes #59, which is CLAIMED here.

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

github-actions Bot pushed a commit that referenced this pull request Oct 27, 2026
…er (#60)

* test(git-submodules): the moved counters are HELD, one test per counter

#53 added a MOVED count beside every TOUCHED count, and PR #58
landed it. Its coverage reached only ONE of the three counters: the fixture had a
path with NO checkout, so the walk advanced it in the index and `indexMoved` was the
only counter that could rise. `rolledMoved` (a path WITH a checkout, rolled onto its
branch tip) and `pinnedMoved` (a pin_map EDGE) were never exercised, so a regression
in either reached the product silently — and the umbrella's wave PRs quote the report
verbatim.

Four tests: the ROLL counter, the EDGE counter (with the partition property the
three groups exist to have), the SUM of the three, and the FAILURE report, which
prints the same three counters in a different arrangement.

Coverage fails without the change, and the proofs are in the PR body: the pre-#58
product's report carries no moved count at all (all four tests red), and five mutants
in the CURRENT tree each turn exactly the matching test red — the roll counter
disabled, the edge counter firing on a write, the double-count line put back, the
failure report losing the counts, and the failure report printing zeroes.

No product change: the report is unchanged, and this is the coverage the report's
numbers were never held by.

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

* test(git-submodules): the sum assertion gets a fixture where a write is not a move

The validator's round-1 finding on #60: the body's R7 claim that each mutant
turns "exactly the matching test" red was MEASURED with a targeted -run filter,
so the isolation was asserted and never observed — and it is false for the roll
mutant, which zeroes the report's TOTAL as well and so reds the sum test too.

The sum test now runs on a fixture where the walk TOUCHED two paths and MOVED
one: a materialised submodule rolled onto its origin's tip, and a declared path
with NO checkout whose recorded pin is already the remote's current tip. The sum
is observable there — a report that counted writes instead of moves prints 2 in
the index clause and the four printed numbers stop adding up. The assertion is
on the RELATION alone, so that defect is caught by the sum and not by a clause
assertion one clause earlier.

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@aitrawog

Copy link
Copy Markdown
Contributor Author

RESOLVED — note 2's follow-up is FIXED and CLOSED: #60 (merged 2026-10-27, squash 669a5ee5, tag v2026.300.0951), and the issue it was tracked in — #59 — is closed.

Done. The three counters note 2 named are held, one test each, and the failure report's own arrangement of the same three is held as well:

  • the ROLL counter — a materialised submodule pinned one commit behind its origin, rolled onto it, asserted on the (1 moved) clause and held at (0 moved) when the same tree is bumped twice;
  • the EDGE counter — a pin_map edge whose twin is recorded in the source's tree, asserted in the edge clause exactly once, and not double-counted in the index clause;
  • the INDEX counter — a path with no checkout whose pin moves in the index only;
  • the FAILURE report — the same three counters in the report's other arrangement, with the sum asserted there too;
  • and the relation itself — the SUM test's fixture TOUCHES two paths and MOVES one (a rolled submodule, plus a declared path with no checkout whose recorded pin is already the remote's current tip), so the four printed numbers are observable as a relation rather than as a tautology over zeroes.

The R7 proof is measured, not asserted. Six mutants, each run against the whole package's suite with the complete failing set pasted in the PR body: the pre-#58 product reds all four new tests and #58's own moved-count test; mutant A (the roll counter not incremented) reds all four; B and C red the edge test; D and E red the failure test; F (the index clause reporting the pins the group TOUCHED instead of the ones it MOVED — the exact defect this report exists to prevent, #53) reds the sum test and #58's own test, and no other test in the file catches it.

The round-1 validator BLOCK was real and is fixed, not filed. Its finding — the body claimed each mutant turned "exactly the matching test" red and "left the other three green", but the evidence ran a targeted -run filter, so the isolation claim had never been observed, and it was false — was fixed in the round-2 commit 48b4852: the whole suite now runs per mutant, the complete failing sets are pasted, the claim is corrected to what the output shows, and the sum test gained an independent failure mode. The RCA (mechanism, missed control, blast radius, root fix) is in the PR thread.

The validator's final verdict on the round-2 head is PASS; the change merged through it, unchanged.

What lands next, in the producer-first order. plugin-task is tagged v0.2026300.951 — the first published module version whose bump report carries the three moved counters. The consumer legs follow: charly adopts that module version so its binary prints the three-bucket summary, and the umbrella then advances its charly gitlink and re-keys its scripts/sync-pr-body.sh fixture to the shape the running binary actually prints (opencharly/opencharly#735).

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

github-actions Bot pushed a commit to opencharly/charly that referenced this pull request Oct 27, 2026
… the pins that MOVED) (#1076)

`charly task sync`'s bump prints a summary line, and the org's pin waves quote it.
The counts in that line were INDEX WRITES — opencharly/plugin-task#53 measured 406
written against 4 moved on the live umbrella — so a reader could not tell a pin that
MOVED from a pin that was merely rewritten. opencharly/plugin-task#58 replaces the
report with one that counts the pins that MOVED, in three clauses: the materialised
roll, the index advance, and the pin_map edge, plus the total they sum to.

The producer's work is merged and tagged: v0.2026300.951 (tag v2026.300.0951,
commit 669a5ee5) carries #58 and #60, the coverage that holds each of the three
counters. This repo's compiled-in pin is the consumer that has not moved, so the
binary still prints the two-bucket shape.

The delta between the two versions is that report change and its tests. `charly/go.mod`
requires the new version; `charly/go.sum` carries its hash pair. The tree's other
module locks are untouched.

Fixes #1075

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bump: the report counts index WRITES, not pins that moved — 406 reported, 4 moved

1 participant