Repository navigation
feat(git-submodules): bound NEGLECT in currency mode with an opt-in stale_after - #43
Conversation
…stale_after` A pin that is not current is CHURN: an upstream that moves daily is behind between every wave, and a gate that failed on the DISTANCE would be red roughly three days out of four on a healthy tree. What recurred in the org is a CADENCE defect — the owner simply stops advancing the pin — and no verb could see it: `verify` answers consistency, `currency` answers currency, and neither says whether the owner still maintains the pin at all. `stale_after` is the opt-in bound that separates the two. It is measured on the AGE of the owner's own last commit that touched the pin's path, never on the pin's distance from the served ref, and a pin that is not current AND has not been advanced within the bound is STALE and FAILS. A `cur` pin is never stale (a quiet upstream is not neglect); a `blocked-by` edge is not eligible (the producer's neglect is the producer's business, and failing here over another repo's PR is the permanently-red trap again); unset leaves the verdict and the report byte-unchanged. The knob lives in the canonical judgement — this verb — so the owner declares the bound in its own manifest and no repo re-derives "is the pin at the tip" for itself. Proven differentially: the same fixture, the same behind pin and the same 168h bound, differing only in the date of the commit that recorded the pin, give one verdict each way. Without the change the knob is accepted and ignored, and the arm fails. Refs #42, opencharly/charly#1048 Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Review — PASSI reviewed all 6 changed files, the single commit, the PR body, and the (empty) comment thread. No maintainer sign-off is present (0 comments), so the change is evaluated on its own evidence. Correctness vs. body claims
Tests, security, cleanliness
CommentsThe comment thread is empty; nothing to disposition, and no sign-off to weigh. Non-blocking observations (not gating)
The change does what the body claims, ships coverage that fails without it, and introduces no security or scope problems. Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
|
RESOLVED — both non-blocking observations in the review above are fixed in the successor PR, #44. This PR is merged and tagged ( Observation 1 — "A negative/zero Fixed in With the fix reverted, that arm goes red and shows what the knob did before: Observation 2 — " Fixed in the same file. The read now has three outcomes, and only one of them is a claim about the pin: a measured age; no-history (the path is in no commit of this repo's history, so it has never been advanced here — a FACT, and it IS neglected); and read-failed (the age could not be measured — the workflow being unable to JUDGE the pin, which is a structural-class failure naming the read error). The read-failure case is routed to the structural class precisely so that a transient git error can never manufacture a stale verdict. The two are told apart by COUNTING the repo's commits, because git reports them identically: With that split reverted, the arm goes red and shows the false stale: The full bodies, the differential runs and the live gate are in #44. Issue #42 stays open until that successor lands, and is closed with its record then. Agent: charly Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated) |
…ort an unreadable history as neglect (#44) * fix(git-submodules): refuse a non-positive stale_after, and never report an unreadable history as neglect Two findings the validator raised on #43 as non-blocking observations are fixed here. Neither is filed away: a finding raised on your own PR is fixed, or rebutted on the thread with evidence. A bound of zero or less is not a bound. `stale_after: "0s"` parsed and was honoured, marking EVERY not-current pin stale — the permanently-red gate the knob exists to avoid, arrived at by the operator's own hand. A non-positive bound is now refused as the operator error it is, and the refusal names why. A failed history read is not neglect. `lastAdvancedAt` treated any `git log` failure as "never advanced here" and flagged the pin stale, so a transient git error could manufacture a false stale verdict — a claim the evidence does not support. The read now has three outcomes: a measured age, "no history" (the path is in no commit of this repo's history — a FACT, and it IS neglected), and "read failed" (the age could not be measured — the workflow being unable to JUDGE the pin, which is a structural-class failure naming the read error). The two are told apart by COUNTING the repo's commits, because `git log` refuses identically on an unborn HEAD and on a broken object store, and only the first of those is the "never advanced" fact. Proven differentially, in the unit test and in the live gate: with the refusal reverted, `stale_after: "0s"` reports `1 STALE pin(s) ... (bound 0h)`; with the read-failure split reverted, an unreadable history is reported as `has never been advanced here` and as STALE. Both arms are green with both fixes in. Refs #42, #43 Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated) * test(gate-task): make each stale arm's evidence self-identifying The validator's BLOCK on #44 was a defect in this PR's own EVIDENCE, not in the code: the differential pastes were extracted from their logs by a key that several arms share (the recorded SHA of the pin under test — every stale fixture in one run pins the same twin commit), so the paste carried the wrong arm's output. The root fix belongs in the control that produced the paste, not only in the PR body. * Each stale arm gets its OWN fixture (so its own pin and its own HEAD), its OWN task name and its OWN bound. The task name appears in the run's own output, so an arm's evidence block names the arm that produced it, and no two arms can produce the same text. * The unreadable arm now DEMONSTRATES its own brokenness: it asserts that the very read the verb performs — `git log -1 --format=%ct -- <path>` — FAILS on the unreadable fixture and SUCCEEDS on the healthy fixture, and it fails loudly if either does not hold. The arm can no longer pass for the wrong reason (a no-op object removal would have left a readable history). * Each arm's captured output is printed with the gate's own " | " convention, so the pasted block is the arm's own output, under the arm's own header. The verb, the unit test and the README are unchanged by this commit: the two fixes under review are the same ones, proven by the same arms, which are now self-identifying. Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Summary
verb:git-submodulesinmode: currencyanswers ONE question — is each recorded pin the ref itsintent names? — and it answers it correctly. What it could not answer is the question a repo
that owns the pin needs: how long has this repo itself neglected to advance it?
This PR adds that answer as an opt-in knob,
stale_after, on the currency mode.The defect it closes, measured. From
opencharly/charly's own advance history against eachbox/*upstream's commit dates, 2026-09-22 → 2026-10-23 (behind-time as the union of thebehind-intervals, not the sum of overlapping per-commit lags):
box/archbox/cachyosbox/debianbox/fedorabox/ubuntubox/omarchyTwo regimes hide in that table and must not be confused:
state of a healthy tree.
(10-06 → 10-16) and 10 days (09-28 → 10-06) with no advance at all, against a healthy
maximum gap of 5 days (10-16 → 10-21). Every twin's own bookkeeping age is ≤ 1 day today.
The criterion that was first proposed, and why it is not this one. The RCA filed as
opencharly/charly#1048 proposed: fail when a twin is behind the ref its own
.gitmodulesdeclares. The table above falsifies it — that gate would be red roughly three days out of four on
a perfectly healthy tree, and the org's own pin rule forbids exactly that:
So the criterion was re-derived against the measurement, not implemented as written.
What
stale_afterdoes.on the pin's distance from the served ref. The distance is churn; the age is neglect.
stale pin FAILS.
curpin is never stale, however old its last advance: a quiet upstream is not neglect,and no quiet repo may go red on its own silence.
blocked-byedge is not eligible: the producer's neglect is the producer's business, andfailing here over another repo's PR is the permanently-red trap again.
change until it declares a bound.
Where it lives, and why. In the canonical judgement — this verb — with the schema as the single
source (
candy/plugin-task/schema/task.cue→ the regeneratedcandy/plugin-task/params/cue_types_gen.go).The owner declares the bound in its own manifest; no repo re-derives "is the pin at the tip" for
itself, and no second implementation of the currency judgement appears anywhere.
The calibration. The healthy maximum gap between advances was 5 days and the neglect gaps were
8 and 10 days, so a 7-day (168 h) bound sits above the healthy maximum and below the neglect
minimum, with margin on both sides.
The consumer side — the owner declaring the bound and advancing its twin through
charly task sync— is opencharly/charly#1048 and lands separately, producer-first.How tested
The change class is a plugin-library + schema change, so it runs this repo's FULL bed, on the
committed tree this PR pushes (
01484d2ed659743b076eca3d17a9c5b5d2d79956).The live gate — the only live execution of these verbs (
scripts/gate-task.sh, exit 0), withthe new arm driving the knob through a real
charlybinary with this plugin compiled in:R7 — the coverage fails without the change. Reverting only
candy/plugin-task/verbs.gotoHEADand re-running the new test (the schema and the generated params kept, so the knob isaccepted and ignored — the worst possible shape, and the one this test exists to catch):
and with the change restored, the same test passes.
The one-variable differential, in the unit test (
candy/plugin-task/currency_test.go,TestGitSubmodules_CurrencyStalenessBoundsNeglect): the same fixture, the same behind pin and thesame
168hbound, with only the DATE of the commit that recorded the pin changed —STALE(the opt-in is real, and the defaultverdict is unchanged);
1 STALE pin(s) (neglected for more than 168h), naming the pin,its age and the bound;
1 of 2 pin(s) NOT currentstill reported — churn, notneglect;
curpin with a 240 h old advance → PASS — the idle-upstream trap;7d) → FAIL withnot a duration— a broken control is never silent.Rulebook compliance
/charly-internals:plugin,/charly-image:layer,/charly-internals:git-workflow) were loaded before the first edit.before it was implemented; the RCA is currency mode: nothing bounds NEGLECT — a pin that is not current is churn, and the owner can stop advancing it forever unnoticed #42. The one non-blocking finding
raised while gating (the local linter's export-data skew) is filed below.
re-derives "is the pin at the tip" for itself. The schema is the single source for the params
struct, and the regeneration is byte-reproducible from it.
doc comment and the README describe the same knob, and the README's
currencyexample names it.feat/pin-stalenessofforigin/main); nohistory is rewritten, no force, no bypass, and no other session's work is touched.
charlybinary, nota mock; the unit test's upstream is a local repository, exercised live.
tree, with zero warnings, and the live gate re-run on that same tree.
Change classification
Plugin library + schema (opt-in parameter, additive). The change class's gate is this repo's
full bed —
go build,go test,go vet,gofmt,golangci-lint,charly box validateandscripts/gate-task.sh— all run on the committed tree above, with zero warnings. The umbrella'sR10 rule that a plugin-library or schema change runs the full assembled bed is satisfied by the
repo's own assembled bed: the plugin is compiled into the binary the live gate builds and every
one of the five maintenance verbs is executed live in that same run.
Producer-first. This is the producer leg for opencharly/charly#1048: the verb ships here, and
the owner declares the bound and advances its twin in the consumer repo afterwards.
Filed issues
golangci-lintrun fails on this host withexport data version 5 is greater than maximum supported version 4, because the host's Go(1.27.3) is newer than the analysis side the pinned linter was built against. Measured to be
pre-existing: the identical error reproduces on the pristine
opencharly/plugin-taskcheckout with none of this change present, and the changed tree lints 0 issues under the
toolchain
candy/plugin-task/go.moddeclares (go 1.26.4) — which is the toolchain CI selectsvia
go-version-file. Evidence added to the existing issue rather than filed as a duplicate.Closes #42
Agent: charly
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)