fix: IsNull resolves the uppercase YAML nulls Null and NULL - #294
fix: IsNull resolves the uppercase YAML nulls Null and NULL#294ethanhawkes-gif wants to merge 3 commits into
Conversation
frontmatter.IsNull and its byte-identical lint-side copy isNull treated null as only ""/"null"/"~", missing the YAML 1.1 (!!null) / YAML 1.2 core-schema (§10.2.1.1) spellings Null and NULL that the memory scalar parser (internal/core/memory/yaml.go) already resolves. On the lifeboat pack path gvSupersededADRs unquotes superseded_by and then asks IsNull, so a live (status: accepted) ADR carrying `superseded_by: NULL` in a foreign target repo was silently emitted as a superseded-adr finding into graveyard/abandoned.json, quoting the null literal as its evidence -- a false "this decision was abandoned" claim in a machine-read artefact, later cited by graveyard_lessons.go and the graveyard-interpreter agent. Widen both predicate copies together to the four YAML nulls plus the empty scalar so the two gates capture/validate.go depends on cannot drift. Fields does not strip quotes, so a quoted "NULL" still reaches IsNull as a string in the general path and stays non-null; only the pre-existing lifeboat unquote path reads a bare NULL as null, the intended reading for a handle-or-null field. Regression tests: TestIsNull in both packages asserts the four spellings null and rejects near-misses and quoted forms; isAbsentValue inherits the widened set; and an end-to-end lifeboat test walks the full iss intentdriven#290 matrix (null | Null | NULL | ~ | "NULL" | 'Null' -> no finding) with a positive control (superseded_by: adr-9 -> still reported). Closes intentdriven#290 Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Ethan Hawkes <ethanhawkes-gif@users.noreply.github.com>
REPPL
left a comment
There was a problem hiding this comment.
Thank you for this — the widening itself is correct, the tests are green, and as our first outside contribution it also pressure-tested our conventions in useful ways. We commissioned a deep review of the diff; the full evidence trail now lives in the repo under .abcd/work/reviews/2026-08-19-pr-294-null-predicate/. The verdict is sound in scope, with a handful of changes we'd like before merge — all small, and all inside what this PR already touches:
-
Delegate instead of duplicating (
internal/core/lint/lint.go,isNull): the widened copy is byte-identical tofrontmatter.IsNull, andlint.goalready importsfrontmatter(it delegates its scanner for exactly this reason). Please replace the body withreturn frontmatter.IsNull(v)and drop the duplicated table test — that removes the very drift class this fix exists to prevent, and makes the "mirrored tests" question moot. -
Doc comment count (
internal/core/frontmatter/frontmatter.go, theIsNullcomment): "an empty value and the four YAML nulls ""/"null"/"Null"/"NULL"/"~"" lists the empty scalar twice and five items under "four". The lint-side phrasing ("four YAML nulls (null | Null | NULL | ~) plus the empty scalar") is the correct one. -
t.Errorfin the spelling matrix (internal/core/lifeboat/graveyard_abandoned_test.go):t.Fatalfinside the loop stops at the first failing spelling and the positive control never runs;t.Errorf(or a subtest per spelling) reports the whole matrix. -
Widen the capture-side agreement test (
internal/core/capture/parse_test.go,TestValidateStrictImpact): it still iterates only{"", "null", "~"}— please add the uppercase spellings so the package whose acceptance changed also pins it. -
CHANGELOG citation: our changelog cites ledger handles rather than GitHub numbers. We've retroactively captured the bug as
iss-287— please cite(iss-287)instead of(#290).
One thing we are deliberately not asking you to fix here: the review found that quoted nulls (impact: "NULL") are judged differently by capture (which unquotes before the predicate) and record-lint (which reads the raw value). That predates your change, needs a maintainer design decision, and is now tracked as iss-285 with a follow-up intent (itd-128) to consolidate the scalar decoders — your PR's evidence made that class visible, which is genuinely valuable.
Happy to merge once the five points above land.
Assisted-by: Claude:claude-fable-5
|
I would request changes on PR #294. The bare Null/NULL fix is sound, but the new regression coverage bakes in one incorrect YAML interpretation. [P2] Don't treat quoted "NULL" / 'Null' as YAML nulls — internal/core/lifeboat/graveyard_abandoned_test.go The new test puts "NULL" and 'Null' in the nulls table and requires them to produce no finding. That contradicts both YAML scalar semantics and another test added by this same PR: frontmatter.TestIsNull explicitly states that quoted null-looking values must remain strings and tests "NULL" as non-null. The repository's actual YAML parser agrees with the latter interpretation. It checks the unquoted tokens null, Null, NULL, and ~ first; quoted input then goes through the string branches, so "NULL" parses as the string NULL, not as null. The discrepancy occurs because gvSupersededADRs calls gvUnquote before frontmatter.IsNull: supBy := gvUnquote(fields["superseded_by"].Value) gvUnquote strips matching quotes indiscriminately. Therefore this PR changes superseded_by: "NULL" from a non-null string into an absent value. For a foreign repository—the exact path this fix is concerned with—that can suppress a supersession finding for a value which is not actually YAML null. I would change the new lifeboat matrix to cover bare NULL and Null, but not quoted versions. Longer-term, if lifeboat genuinely wants quote-insensitive sentinel semantics, that should be documented as a lifeboat heuristic rather than described/tested as YAML null semantics. Confidence: 0.94. One lower-priority observation: the implementation still won't recognize valid constructs such as superseded_by: NULL # no successor, because Fields only trims the captured value and IsNull uses exact string comparisons. Given that frontmatter explicitly describes itself as a line scanner rather than a YAML parser, I would treat that as a follow-up rather than block this narrowly scoped PR on it. Confidence: 0.86. Aside from the quoted-scalar issue, the actual widening from null/~ to null/Null/NULL/~ is appropriately narrow, both copies are changed together, and the added positive/negative controls are useful. Overall verdict: request changes for the quoted-null regression; approve once that expectation is corrected. |
Review follow-up on this PR: the new matrix put quoted "NULL"/'Null' in the nulls table, baking gvUnquote-before-IsNull behaviour in as YAML null semantics — contradicting TestIsNull's own negative controls (a quoted null reaches IsNull with quotes intact and stays a string) and the YAML core schema. Per the review, the matrix now covers only the four unquoted spellings; the comment names the gvUnquote ordering as an open lifeboat heuristic decision rather than tested semantics. Ledger follow-ups captured: iss-2608241347321758 (quote-insensitive sentinel heuristic), iss-2608241347321759 (trailing-comment null form). Assisted-by: ox-alpha:x-preview-f-free
…a PATH abcd TestGuardShimFailsOpenLoud/binary_absent failed on machines where abcd is installed on PATH: with no plugin-root binary the shim's documented last resort is `command -v abcd`, and the inherited environment answered with the developer's real install — a silent allow, no UNGUARDED notice. runShim now pins PATH to the system directories (keeping /bin/sh, find, printf) and moves HOME off-machine, the same lesson as iss-219's setupHermetic. Assisted-by: ox-alpha:x-preview-f-free
|
Corrections pushed in response to the review:
The production widening itself is untouched (narrow scope preserved). Both deferred observations are filed against the ledger rather than lost:
|
Summary
frontmatter.IsNull(and its byte-identical lint-side copyisNull) treated null as only""/"null"/"~", missing the YAML 1.1 (!!null) / YAML 1.2 core-schema (§10.2.1.1) spellingsNullandNULL— the same set the memory scalar parser (internal/core/memory/yaml.go) already resolves. Closes #290.Why it matters
On the lifeboat pack path
gvSupersededADRsunquotessuperseded_byand then asksIsNull, so a live (status: accepted) ADR carryingsuperseded_by: NULLin a foreign target repo was silently emitted as asuperseded-adrfinding intograveyard/abandoned.json, quoting the null literal as its evidence — a false "this decision was abandoned" claim in a machine-read artefact thatgraveyard_lessons.goand thegraveyard-interpreteragent later cite.Fix
frontmatter.IsNullandlint.isNull) to the four YAML nulls + empty scalar, so the two gatescapture/validate.godepends on cannot drift.isAbsentValueinherits it (it delegates toisNull).IsNulldoc comment, which invoked "the YAML nulls" but enumerated three of four.Fieldsdoes not strip quotes, so a quoted"NULL"still reachesIsNullas a string in the general path and stays non-null; only the pre-existing lifeboat unquote path reads a bareNULLas null — the intended reading for a handle-or-null field, matching the matrix infrontmatter.IsNullmisses the YAML nullsNull/NULL, sodisembark packreports a live ADR as superseded — quotingsuperseded_by: NULLas its evidence #290.Tests (fail before, pass after)
frontmatter.TestIsNull/lint.TestIsNull— assert the four spellings null; reject near-misses (None,nil,NUL,nullish) and quoted forms.lint.TestIsAbsentValueUppercaseNull— an uppercase null is an absence.lifeboat.TestAbandonedAcceptedADRWithUppercaseNullIsNotReported— end-to-end overgvSupersededADRs; walks the fullfrontmatter.IsNullmisses the YAML nullsNull/NULL, sodisembark packreports a live ADR as superseded — quotingsuperseded_by: NULLas its evidence #290 matrix (null | Null | NULL | ~ | "NULL" | 'Null'→ no finding) with a positive control (superseded_by: adr-9→ still reported).Verification (go1.25.6, matching go.mod)
go test ./...andgo test -race ./...green;go vet ./...clean;gofmt -lclean. A CHANGELOG### Fixedentry accompanies the change.Disclosure: this change is AI-assisted; the commit carries
Assisted-by: Claude:claude-opus-4-8per CONTRIBUTING, with a humanSigned-off-by(DCO). Happy to adjust anything to fit your conventions.