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 1 commit 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 Partnermedia#290 matrix (null | Null | NULL | ~ | "NULL" | 'Null' -> no finding) with a positive control (superseded_by: adr-9 -> still reported). Closes Partnermedia#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
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.