🐛 fix(template): accept a v0.1 flat-string sources list for required_fields - #82
Conversation
cwest
left a comment
There was a problem hiding this comment.
The change itself is right and I'd take it as-is: the sources case in hasNonEmptyField was asking a structured-parse question where a presence question belongs, and scoping the loosening to a new sourcesFieldPresent helper while leaving Node.Sources() strict is exactly the correct shape. The test matrix covers both directions — flat-string and bare-URL satisfy the field, while empty list, all-blank entries, and an absent key still drift — and the criterion-9 invariant test proves the loosening does not leak into structured provenance. I verified end to end against the real corpus (validate --templates 0 to 0, lint --strict 1 to 1, analyze byte-identical) and reproduced the fix through the CLI on a v0.1 flat-string bundle: the missing-sources warning fires on the base binary and is gone after the fix, and returns when sources is removed. §5.1/§13.1 are cited in the code, tests, and PR body.
What blocks this from being merge-ready is not in this diff. CI (build-test) is red and the merge state is UNSTABLE, but every failure is in internal/apiserver (the lexical_gate search tests), a package this PR does not touch. That breakage is already present on main on its own — the push build for the current main tip is red with the identical six failures, and they reproduce on a clean checkout of main with no PR involved. The merge ref inherits them, so this PR's CI can't go green until main is fixed and this branch is rebased onto a green tip. The apiserver allowlist rejects lexical_gate before the handler that knows how to read it ever runs.
Holding this here: the branch needs to sit behind a fix to the apiserver search-param handling on main, then rebase onto a green main so build-test passes. Nothing to change in the template code.
|
Verified this independently, control versus candidate, both built from a fresh clone with Go 1.26.5. Control is The fix holds, and the negative controls hold with itSynthetic fixture, seventeen nodes under one template declaring
Two rows move and eleven stay put. That is the shape I was hoping for: the relaxation reaches the flat-string list and stops there. A list of numbers and a list of blank strings still warn, so this is not the blunt "any non-empty list satisfies it" reading, which would have traded a false positive for a false negative. I confirmed the boundary the doc comment claims. On the candidate, One correction to my own reproduction in #79. I first wrote the body-citations control with plain Real corpusSame private v0.1-shaped corpus that surfaced #79, run read-only through
Diffing the two full outputs line by line: forty-seven lines removed, zero added. A pure subtraction, no finding relocated and no new finding anywhere in the corpus. The seventeen other required-field warnings not moving is the control that says this stayed inside the I checked the two nodes still flagged. Both genuinely carry no The red check is inherited, not yoursWorth stating clearly, because the red X on this branch is misleading. This branch is cut from What CI tests for a pull request is the merge of this head into So the sequencing is what it looks like: land #83 and this clears. I tested that directly rather than assuming it. Merging this branch on top of |
cwest
left a comment
There was a problem hiding this comment.
The template change itself is correct, well-scoped, and fully tested — nothing to change in the code. I cherry-picked this commit onto the current tip of main and ran the whole gate green there: gofmt clean, go vet clean, the conformance suite passes, the full suite passes under -race, all the new presence/negative-control tests pass, and the real-corpus run is unchanged (validate --templates OK→OK, lint --strict byte-identical, analyze byte-identical). The §5.1/§13.1 citations are in the code, the test names, and the PR body.
The one thing standing between this and ready is stale base state. This branch is still built on an older main whose CI was red for an unrelated apiserver reason, and that red result is what build-test is reporting here — not anything in this diff. That upstream apiserver issue has since been fixed on main, so main is green again now. The branch just needs to be rebased onto the current tip of main and pushed; CI will re-run green and this becomes ready.
Concretely: rebase wt/t_fecc790d onto the latest origin/main (three commits ahead of this branch's base), force-push, and let CI re-run. No code edits needed. Once the rebased CI is green I'll re-review and it passes.
Leaving this in draft until the rebased CI is green.
…fields A --templates required_fields: [sources] no longer accepted a v0.1 flat-string sources: list, so well-sourced v0.1-shaped nodes were reported as missing sources. §13.1 promises v0.1 bundles stay consumable by a v0.2 consumer under documented fallbacks; this was a case where that promise did not hold, and it inverted the curation incentive — a node with zero sources and one with five well-labelled ones produced the same finding. Root cause: hasNonEmptyField's sources case answered a structured-PARSE question (Node.SourceCitations() > 0) for what is a PRESENCE question. Node.Sources() drops any entry that is not a structured mapping (§5.1 requires a resource per entry), so a list of plain strings parsed to zero and fell through to the body Citations fallback a frontmatter-sources v0.1 node does not have. - Scope the loosening to the template presence check only: a new sourcesFieldPresent helper counts a non-empty frontmatter sources list of ANY entry shape (v0.2 structured OR v0.1 flat-string) as satisfying the required field, retaining the §13.1 legacy body citations fallback (§5.1, §13.1). - Node.Sources() / Node.SourceCitations() stay strict and unchanged — analyze, coverage and freshness keep the structured-provenance meaning. A unit test pins that a flat-string list still parses to 0 structured entries. - Empty sources list and all-blank entries still drift (non-empty required); absent still drifts; v0.2 structured and legacy body citations still pass. Closes #79
cwest
left a comment
There was a problem hiding this comment.
No changes needed.
The template presence check now answers the right question. hasNonEmptyField's sources case delegates to sourcesFieldPresent, which treats a non-empty frontmatter sources list as satisfying the required field regardless of entry shape — the v0.1 flat-string form and the v0.2 structured form both pass, and the §13.1 body # Citations fallback is retained. Node.Sources() and SourceCitations() are left strict and unchanged, so the loosening is scoped to presence and does not leak into provenance analysis (§5.1, §13.1).
The test matrix carries the change: flat-string with labels and bare-URL forms satisfy; an absent field, an empty list, and all-blank entries still drift; the structured form and legacy body-citations still pass; and a flat-string list still parses to zero structured entries through Node.Sources() — both controls present in each direction.
Verified on the rebased head against current main:
- Conformance suite (
-race): green. - Full suite (
go test ./... -race): all packages green, including the API server package that was red before the rebase. gofmt -lempty;go vetclean.- Real corpus (
~/src/knowledge-base/bundles/knowledge):validate --templates0 → 0 (the corpus is v0.2, so a count that does not move is the control that proves the change hides nothing);lint --strictbyte-identical;analyzebyte-identical. - CI
build-testpassing on the rebased base; branch merges cleanly.
Ready to go.
Why
--templateswithrequired_fields: [sources]no longer accepted a v0.1flat-string
sources:list, so well-sourced v0.1-shaped nodes were reported asmissing sources. Regression introduced by #61 (
8857f65→78ae7c3).§13.1 promises v0.1 bundles stay consumable by a v0.2 consumer under documented
fallbacks; this was a case where that promise did not hold. It also inverted the
curation incentive: a node with zero sources and a node with five well-labelled
ones produced the same finding, so the authors who did the sourcing work got the
warnings.
Root cause
hasNonEmptyField'ssourcescase answered a structured-parse question(
Node.SourceCitations() > 0) for what is a presence question.Node.Sources()drops any entry that is not a structured mapping (§5.1 requires aresourceper entry), so a list of plain strings parsed to zero and fell throughto the body
# Citationsfallback — which a frontmatter-sourcesv0.1 node doesnot have. The flat-string form fell between the two recognised paths.
What
sourcesFieldPresenthelper counts a non-empty frontmattersourceslist ofany entry shape (v0.2 structured OR v0.1 flat-string, non-blank) as
satisfying the required field, retaining the §13.1 legacy body
# Citationsfallback (§5.1, §13.1).
Node.Sources()/Node.SourceCitations()stay strict and unchanged —analyze, coverage and freshness keep the structured-provenance meaning.Verification — run output, not claims
Positive controls (the fix fires on the bad case)
sources:(url|label) → no drift (unit + binary).sources:→ no drift (unit).validate --templates:→ 1 → 0.
Negative controls (load-bearing — the fix stays silent on the good case)
sources:absent → still warns missing sources (existing test).sources: []empty list → still warns (TestTemplateDrift_EmptySourcesList_Drifts_Section5_1).sources:all-blank/whitespace entries → still warns (TestTemplateDrift_BlankSourcesEntries_Drifts_Section5_1).sources: [- resource: …]→ still passes (existing test).sources+ legacy body# Citations→ still passes (existing test, §13.1 intact).Node.Sources()unchanged — a flat-string liststill parses to 0 structured entries
(
TestNodeSources_FlatStringList_ParsesToZero_Section5_1), proving theloosening did not leak into provenance analysis.
Real corpus —
~/src/knowledge-base/bundles/knowledge(AGENTS.md layer 3)Our corpus is fully v0.2-shaped (0 flat-string
sourcesnodes), so it is thenegative control: the fix must move nothing.
validate … --templateslint --strict …analyze …diffempty) — proves criterion 9 held in practiceGates
Closes #79