Make a history record required, once it is one record per change (#325) - #357
Conversation
DECISION: enforce it, after fixing the granularity that made enforcing it unreasonable. The issue asked two things: whether a bulk migration warrants a record at all, and whether it should be enforced. Answering the second first was the mistake available here - as written the convention could not be enforced without doing damage. WHY NOT DROP IT. I had leaned toward folding everything into the per-file curation_history: block as duplication. That is wrong, and the README's own justification (merge-conflict avoidance) is not what distinguishes the two - curation_history is per-file, so it does not conflict either. What distinguishes it is that HistoryOutcomeEnum has `no_change`. A session that audited a trait and correctly concluded nothing needed changing produces no file edit, so curation_history has nothing to attach to and the work is invisible. Same for `blocked` and `needs_followup`. Add model, agent_tool, agent_version and structured issue/PR links, none of which CurationEvent has a slot for, and it is a real capability rather than a second copy. WHY THE GRANULARITY HAD TO CHANGE FIRST. "One record per session per target" is right for hand curation, where session and target coincide - the sulfur_globule record is a paragraph of real reasoning about one graph. Read literally it also makes a 128-file migration owe 128 near-identical stubs, burying the three substantive records the directory exists for. That is destroying the signal in the name of provenance, and blocking on it would have been a fair thing to route around. So: one record per CHANGE, targeting the migration script, which IS the artifact that says what drove it and is reviewable in a way that 128 copies of one sentence are not. No schema change - `infrastructure` and `other` kinds already exist. WHY BLOCKING. The old comment said a hard gate "trains people to route around it". The measurement disagrees: of 134 commits that modified trait records, 2 added a history record. Nobody routed around the gate because there was no gate. Meanwhile 275 trait records carry an issue number hand-typed into a `changes` string - the same provenance in a form nothing can query, and my own 334 commits did it, which is how I know it is substitution rather than forgetting. The rule lives in scripts/audit_history_records.py, not in workflow YAML. Shell embedded in a workflow cannot be unit-tested or run locally before pushing, which is how this repo keeps ending up with gates nobody can exercise. Five tests, including one through the CLI, since the workflow calls the CLI and a wiring error would leave the rule correct and unused. Two bugs found while wiring it, both mine: - `set -euo pipefail` plus a `just ... | tee` pipeline aborts the step the moment the audit fails, so the job summary would never be written - losing the remediation text on exactly the runs that need it. Now `|| status=$?`. - the job never installed `just`. Added setup-just, same pinned action the sibling pr-checks-present.yaml uses. CANARY, against real diffs rather than fixtures: A fix/334-enables-range vs main 15 traits, 0 records -> exit 1 B this branch vs main 0 traits, 0 records -> exit 0 C A plus one scaffolded record 15 traits, 1 record -> exit 0 C used `just new-history` for real (the no-claw fallback) and the record passed `just validate-history`. That record is now on PR #355, so main does not get a grandfathered gap on the day the gate lands. Deliberately NOT checked: that the record is ABOUT the change. The cheap proxy - does target.path name a changed file? - is wrong for exactly the migration case, where the honest target is the script rather than any record it edited. The guard against an empty record is validate-history, which fails while the --details TODO placeholder is unfilled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 4m 14s —— View job ReviewTwo things need changing. The design call — blocking, one record per change, rule in a testable script rather than in workflow YAML — is right, and the 🟡 The rule this PR establishes is still contradicted in three canonical places, one of which states the opposite policy outright.
The PR argues that an unenforced convention drifts. Three surviving statements of the superseded rule are that drift, on day one, in the artifacts a reader hits first. The schema is vendored from claw, so if editing it here is off the table say so — but 🟡
Nothing bypasses it today — all 477 trait files are Fix is to delete the 🔵 Over-claim worth softening. The docstring, the workflow comment and Not blocking, for the record: the schema-level anti-gaming pattern is real, I could not execute the test suite here — |
There was a problem hiding this comment.
Two 🟡 items — see the detailed review comment.
-
The rule this PR establishes is still contradicted in three canonical places:
src/traitmech/schema/history.yaml:20-23still states presence is advisory and CI does not block;history/README.md:3,justfile:345andhistory.yaml:9still state 'one record per session per target' unqualified. -
scripts/audit_history_records.py:47-48— git pathspec**is not shell**.data/traits/**/*.yamlrequires an intervening directory, so a trait atdata/traits/foo.yamlstarts the job (the workflow'spaths:filter uses Actions glob semantics, which match it) and then clears the presence gate. Drop the**/;data/traits/*.yamlmatches all 477 today.collect()has no test coverage, which is why the unit tests can't see it.
The design call — blocking, one record per change, rule in a testable script — is right, and the || status=$? and missing-just fixes are real.
…357 review) Both findings correct, and the second is a live bug rather than prose. THE PATHSPEC UNDER-MATCHED. A git pathspec is not a shell glob: git's `*` already crosses `/`, so `data/traits/**/*.yaml` must STILL consume the literal slash in `**/` and therefore requires at least one intervening directory. Verified in this checkout: git ls-files -- 'scripts/**/*.py' -> 0 git ls-files -- 'scripts/*.py' -> 41 Nothing bypasses it today because all 477 trait files sit under a category directory. But the workflow's own trigger is `paths: data/traits/**`, which is GitHub Actions semantics and DOES match a top-level file - so a trait added at data/traits/x.yaml would START the job and then clear the gate reporting "0 trait records changed". Silently permissive, which is the failure the test file's docstring claims to pin against. Proved it with a probe file: the `**` form saw 0, the `*` form saw 1. Same shape on the other side: a record at history/foo.yaml would not have counted toward presence. Both globs lose the `**/`. `data/traits/*.yaml` is strictly more inclusive and matches the same 477 files today. The reviewer's sharper point was WHY the unit tests could not see this: the glob lives in collect(), which had no tests at all, while missing_record() was thoroughly covered. Two tests now build a real git repo with a top-level and a nested trait and assert collect() returns both. Confirmed they FAIL against the old globs and pass against these, so they pin the fix rather than restate it. THE SUPERSEDED RULE SURVIVED IN THREE PLACES, which is this PR's own argument turned on itself - an unenforced convention drifts, and here the drift was on day one in the artifacts a reader hits first: - history/README.md's lede, 73 lines above the section that re-scopes it. - the justfile comment sitting directly above `new-history`, the command the failure message tells people to run. - src/traitmech/schema/history.yaml, which states the OPPOSITE policy verbatim: "Presence of a record is *advisory* - CI warns, it does not block, because a hard gate ... trains people to route around it." The first two are TraitMech's own and are fixed. The schema is NOT edited here on purpose: it is vendored byte-identical from claw, which is private and unreachable from this repo's CI, and `src/traitmech/schema/history.yaml` is not in check_vendored_sync.sh's checked set - so a one-copy edit would create drift nothing detects, which is the gap #209 tracks. Filed as #358 and documented in the README, so the divergence is stale by design rather than by neglect. SOFTENED THE OVER-CLAIM. I said three times that scaffolding an empty record "does not work". The guard is a schema pattern rejecting the literal "TODO: replace this placeholder" prefix and nothing else - `--details 'see PR'` passes. That is the design (this gate asks whether provenance was recorded, not whether it was recorded well), but stating it as closed invites someone to rely on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both correct, and the pathspec one is a live bug rather than prose. 🟡
|
|
Claude finished @realmarcin's task in 3m 26s —— View job ReviewBoth 🟡 items are fixed, and the fixes are right rather than merely responsive. Pathspec — That last one is the load-bearing check: the depth-3 records still match, so dropping Prose — The workflow's 🔵 One factual correction to the response, not a request. The over-claim is fixed in the docstring ( Approving. I could not execute |
There was a problem hiding this comment.
Both 🟡 items fixed and verified.
Pathspec: data/traits/*.yaml / history/*.yaml are correct — re-confirmed history/*.yaml still matches the depth-3 records, so dropping **/ widened coverage without losing any. The two new collect() tests exercise the pathspec itself, closing the coverage gap that hid the bug.
Prose: README lede and justfile:344 now state one-record-per-change. Leaving src/traitmech/schema/history.yaml alone is the right call — check_vendored_sync.sh genuinely does not cover that path, so a one-copy edit would be undetected drift; #358 plus the new README section is the correct disposition.
🔵 Non-blocking: the 'scaffolding an empty record does not work' conclusion still stands in history/README.md and the workflow comment; conditioning it on the unfilled placeholder makes it true as written, so fine to leave.
Could not run pytest in this job, so the 519-test claim is unverified from my side. Merge #355 first.
Closes #325. Decision: enforce it — after fixing the granularity that made enforcing it unreasonable.
The issue asked two things: whether a bulk migration warrants a record at all, and whether it should be enforced. Answering the second first was the mistake available here — as written, the convention could not be enforced without doing damage.
Why not drop it
I had leaned toward folding everything into the per-file
curation_history:block as duplication. That was wrong, and the README's own justification (merge-conflict avoidance) isn't what distinguishes the two —curation_historyis per-file, so it doesn't conflict either.What distinguishes it:
HistoryOutcomeEnumhasno_change. A session that audited a trait and correctly concluded nothing needed changing produces no file edit, socuration_historyhas nothing to attach to and the work is invisible. Same forblockedandneeds_followup. Addmodel,agent_tool,agent_versionand structured issue/PR links — none of whichCurationEventhas a slot for — and it's a real capability, not a second copy.Why the granularity had to change first
"One record per session per target" is right for hand curation, where session and target coincide; the
sulfur_globulerecord is a paragraph of real reasoning about one graph. Read literally it also makes a 128-file migration owe 128 near-identical stubs, burying the three substantive records the directory exists for. That's destroying the signal in the name of provenance, and blocking on it would have been a fair thing to route around.So: one record per change, targeting the migration script — which is the artifact that says what drove it, and is reviewable in a way that 128 copies of one sentence are not. No schema change needed;
infrastructureandotherkinds already exist.Why blocking
The old comment said a hard gate "trains people to route around it." The measurement disagrees:
data/traits/*.yamlchangesstringNobody routed around the gate, because there was no gate. And the 275 are the tell: that's the same provenance in a form nothing can query. My own #334 commits did it, which is how I know it's substitution rather than forgetting.
The rule is a script, not workflow YAML
scripts/audit_history_records.py, with 5 tests including one through the CLI — the workflow calls the CLI, so a wiring error would leave the rule correct and unused. Shell embedded in a workflow can't be unit-tested or run locally before pushing, which is how this repo keeps ending up with gates nobody can exercise.Two bugs found while wiring it, both mine:
set -euo pipefailplus ajust … | teepipeline aborts the step the moment the audit fails, so the job summary would never be written — losing the remediation text on exactly the runs that need it. Now|| status=$?.just. Addedsetup-just, the same pinned action the siblingpr-checks-present.yamluses.🐤 Canary — against real diffs, not fixtures
fix/334-enables-rangevs main — 15 traits, 0 recordsC used
just new-historyfor real (exercising the no-claw fallback) and the record passedjust validate-history. That record is now on PR #355, so main doesn't get a grandfathered gap on the day the gate lands.Deliberately not checked
That the record is about the change. The cheap proxy — does
target.pathname a changed file? — is wrong for exactly the migration case, where the honest target is the script rather than any record it edited. The guard against an empty record isvalidate-history, which fails while the--detailsTODO placeholder is unfilled.Merge order
#355 first. It carries the retrospective record for #334; if this lands first, #355's already-completed checks won't re-run and main would briefly contain a 15-file trait change with no record.
One self-inflicted mess worth flagging: I switched branches with this PR's working-tree changes present and
git add history/swepthistory/README.mdinto a commit on #355's branch. Fixed forward-only with a follow-up commit rather than by rewriting the pushed branch — the two-commit trail shows the mistake, which is better than hiding it.just qcgreen · 517 tests pass · ruff clean.