Repository navigation
feat(retired): the org-wide sweep, and the record that classifies a hit - #289
Merged
Merged
Conversation
The contract's guard (retired.ScanTree) reads the contract's OWN surfaces —
Go sources, CUE schemas, protobuf schemas — because that is where the
contract's names live. The org's trees have more surfaces than that: an
authored yaml, a skill's prose, a script, a check bed. A withdrawn name in
those is just as live, and a scan that reads three extensions cannot see it.
So the contract grows the ORG-WIDE reach, in this package, under the rules
the contract's own scan already applies:
- retired.SweepTree reads EVERY text surface of a tree. A Go source is
read STRUCTURALLY — declarations and literals, never comments, because a
comment naming a withdrawn identifier is the RECORD of the withdrawal and
a declaration is a surface a consumer can still reach. Every other text
surface is read TEXTUALLY, comments included, because the prose IS the
published surface. A SCOPED name is never swept on a text surface: a
scope is a Go type, and a text file has no such notion. A TEST file holds
the assertions OF ABSENCE. A CHANGELOG is history. A nested repository is
a repo of its own. A SYMLINK is another path's content, not this tree's
— and a DANGLING one does not fail the run, because one broken link in
one repo would fail the arm for every repo.
- retired.Record, retired.Classify and the retired/cmd/retired-sweep
command judge the hits: a hit is a RECORD when a committed record covers
it and a CARRIER when none does. A record covers only the identifier it
names, and a record that covers no hit is ITSELF a finding, so the table
cannot rot into a hole that hides a live use.
- the token rule stays in ONE place: scanText is the shared text rule, so
the schema scan and the org-wide sweep cannot disagree about what a text
surface is.
The records TABLE lives with the ORG, not here: a record names a file in a
repository this module does not own, and this tree carries no records at all
— its guard reads its own surfaces directly. Only the RULE for what makes a
hit a carrier lives here, once.
Refs opencharly/opencharly#799
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Review — PASSI reviewed all five changed files line by line against the body, the commit ( What holds up
Non-blocking observations (do not gate)
None of these affect the correctness of the tested paths, the security posture, or the honesty of the verifiable code claims. The change ships meaningful, fail-without coverage and a single implementation of the rules. Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
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.
Refs opencharly/opencharly#799
Summary
Head
f6e3b94dd83fe80e86ec50d3943a4a23dbadf170— the commit this PR pushes: 5 files changed, 1038 insertions(+), 4 deletions(-).opencharly/opencharly#799 named the gap: the contract knows what is retired, the repos know what they say, and nothing connects the two. The contract's guard (
retired.ScanTree) reads the contract's OWN surfaces — Go sources, CUE schemas, protobuf schemas — because that is where the contract's names live. The org's trees have more surfaces than that: an authoredcharly.yml, a skill's prose, a script, a check bed. A withdrawn name in one of those is just as live, and a scan that reads three extensions cannot see it. So the contract grows the ORG-WIDE reach, in this package, under the rules its own scan already applies.One implementation of the rules, and one token matcher. The sweep shares the contract's token rule (
containsToken/tokenIndex) and its text rule (scanText) with the schema scan, so the two cannot disagree about what a text surface is, or about which occurrence matched. Nothing here re-implements a rule that already exists in this module.A hit is not automatically a carrier. The distinction is semantic — a sentence that DENIES a withdrawn name ("There is no
engine.build") and a sentence that SETS its value carry the same name and mean opposite things — so no control can guess it.retired.Recordmakes it committed data: a hit a record covers is a RECORD, a hit no record covers is a CARRIER, and a record that covers no hit is ITSELF a finding, so the table cannot rot into a hole that hides a live use. A record covers only the identifier it names, so a record written about one name can never excuse another.The records TABLE lives with the org, not here. A record names a file in a repository this module does not own, and this tree carries no records at all — its guard reads its own surfaces directly. Only the RULE for what makes a hit a carrier lives here, once.
Where the executable lives, and why it is here and not in
.github. The shape chosen for this leg was "one sweep in.github, run over every repo". The ARM lives there; the RULE and the executable live in the contract, and.githubinvokes this module's command at a pinned tag. Two reasons, both measured rather than preferred:.githubhas no Go toolchain of its own, so a Go-structural scan — the only scan that can tell a Go comment from a Go literal — would have to be re-implemented there in another language, which is a second implementation of the same rules (R2); and the org's arm already runs in every TARGET repo's context, so a command that ships with the contract is reached by one line (go run github.com/opencharly/spec/retired/cmd/retired-sweep@<tag>) instead of a per-repo matrix. The threemainpackages the module already carried (internal/wiregen,internal/schemagen,internal/cuesetup) are developer tools underinternal/; this one is deliberately PUBLIC — it is the module's SDK surface, and the org's arm must run it from another repo at a pinned tag.retired/sweep.goretired/records.goRecord,LoadRecords,ValidateRecords,Classifyretired/cmd/retired-sweep/main.goretired/sweep_test.goretired/scan.goscanText) extracted out ofscanTextFile, so the schema scan and the org-wide sweep cannot disagreeHow tested
The head this body describes
The change-class gate — this repo's own committed module CI, on the committed tree. The diff adds Go source to a contract module, so the change class is contract / library module and the gate that owns it is this repo's own
.github/workflows/ci.yml—go build ./...,go test ./...,gofmt -l .,golangci-lint run ./...— run here on the committed tree, with the CI's pinned linter version (v2.13.2) and the toolchain that linter can read (go1.26.8).The "no bed venue" premise, proved with executed output. The class's gate is the module suite BECAUSE no bed can exist here, and that premise is a negative claim, so it is proved rather than asserted: the anchored grep for a
disposable:key finds nothing, and the root manifest declares nobox:,deploy:orcheck:node.The changed path executed live, over the org's real trees. The sweep was built from this committed tree and run over 17 real repository checkouts: 3760 files read, every repo's coverage stated, every verdict PASS. Ten hits were found across 6 repos, and all ten are RECORDS — a comment that quotes a pre-rename diagnostic, prose that DENIES the name, and the plugin's own retirement registry, which answers the retired key BY NAME. The table that classifies them is the org's and lives with the arm; this PR ships the RULE that reads it.
The other eleven repos —
opencharly,sdk,spec,plugin-build,plugin-check,plugin-clean,plugin-deploy-pod,plugin-pod,plugin-substrate,plugin-vm,layer-container-nesting— carry no hit at all and PASS with no table.The control can go RED — the fail-without instrument. A gate that cannot fail is not a proof, so the carrier case was planted and the sweep run against it: a live use of a withdrawn key in an authored
charly.ymlis a CARRIER, and the command exits 1.The same tree under the contract's PRE-EXISTING scan is CLEAN —
ScanTreereads three extensions and the authored yaml is not one of them — which is why the org-wide reach cannot be built on it.The discrimination cases execute the new code, and each of them can fail. The suite builds a tree per rule so that one rule decides the verdict: a Go comment is not a surface while a Go literal is; a test file holds the assertions OF ABSENCE; a CHANGELOG is history; a nested repo is a repo of its own; a SYMLINK is another path's content, and a DANGLING one does not fail the run; a scoped name is swept only where its scope is declared; a record covers only the identifier it names, and only while its text is on the hit's line; a record that covers no hit is itself a finding. The case that keeps the suite from being vacuous is
TestTheSweepReadsEveryTextSurface: it asserts that the contract's PRE-EXISTING scan finds NOTHING in its tree, so a control that used the old scan would read the tree as clean.The two findings this change introduced, fixed in it. R1 is not a pass with warnings:
golangci-lint(errcheck):defer file.Close()ignored the error →defer func() { _ = file.Close() }().golangci-lint(prealloc):namesOfgrew a slice without its capacity →make([]string, 0, len(findings)).Both were introduced by this change, so both are fixed in it, and the lint re-run is
0 issues.One anomaly found by the live run, RCA'd and fixed before this push. The first org-wide run aborted:
retired-sweep: sweeping the tree: reading …/.dsh/skills/a11y-tools: open …: no such file or directory. RCA — mechanism:filepath.WalkDirhands a symlink entry to the callback without following it, the callback opened the link, the unreadable path ended the whole run; blast radius: one dangling link in one repo would have failed the arm for EVERY repo, and the umbrella alone carries 361 such links; missed control: the sweep had no rule for what a SYMLINK is. The root fix is the umbrella's own rule, adopted here — a symlink is another path's content, so it is not read, it is counted in the coverage, and it never fails the run. The case that proves it (TestTheSweepSkipsSymlinks) has two halves: the link pointing inside the tree proves the content is not judged, and the dangling link proves the run survives it.Rulebook compliance
git-workflow,root-cause-analyzer,strict-policy,go,go-quality,check.scanText) and adds no second matcher; the org's arm will call this one command rather than re-implement the rules in another language..github.scanTextFilenow delegates to the sharedscanText, so there is one text rule and no copy of it.git statuswas read before every action; the commit is onfeat/retired-sweepin this session's worktree; no amend, no force-push, no bypass.TestTheSweepReadsEveryTextSurfaceasserts the pre-existing scan finds nothing in its tree, and the planted carrier exits 1.Part II — the change lands in the repo that OWNS the rule (the contract), never inside a submodule (1); it is committed in this session's worktree (2); the contract's own
AGENTS.mdwas read first (4); the org's records TABLE is not here — it belongs to the org's central repo, beside the arm that reads it (5); the leg is claimed on opencharly/opencharly#799 (6, 11); no harness configuration is touched (5).Change classification
Contract / library module. The diff adds Go source to
github.com/opencharly/spec, a module with nodisposable: trueentity and therefore no bed venue — proved above with executed output, not asserted..github/workflows/ci.yml:go build ./...+go test ./...+gofmt -l .+golangci-lint run ./...) GREEN on the PR head, run above on the committed tree. The suite genuinely EXECUTES the changed paths —go test ./retired/...covers the sweep and the records rule, and every case in it is a discrimination that can fail.charly check runbed — none can exist in this module; the consumer boundary bed belongs to the consumer PR (the.githubarm), and the org's live reach is exercised by the 17-repo run above.fully tested and validated.Filed issues
None. Every finding this change surfaced — the dangling-symlink abort and the two lint findings — was introduced by this change and is fixed in it. The remaining half of #799 (the arm that reaches the org's other ~400 repos) is a change to a different repo and lands in
opencharly/.githubunder the same umbrella issue, which stays OPEN.Agent: charly
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)