Skip to content

fix: IsNull resolves the uppercase YAML nulls Null and NULL - #294

Open
ethanhawkes-gif wants to merge 3 commits into
intentdriven:mainfrom
ethanhawkes-gif:fix/isnull-uppercase-yaml-nulls
Open

fix: IsNull resolves the uppercase YAML nulls Null and NULL#294
ethanhawkes-gif wants to merge 3 commits into
intentdriven:mainfrom
ethanhawkes-gif:fix/isnull-uppercase-yaml-nulls

Conversation

@ethanhawkes-gif

Copy link
Copy Markdown

Summary

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 — the same set the memory scalar parser (internal/core/memory/yaml.go) already resolves. Closes #290.

Why it matters

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 that graveyard_lessons.go and the graveyard-interpreter agent later cite.

Fix

Tests (fail before, pass after)

Verification (go1.25.6, matching go.mod)

go test ./... and go test -race ./... green; go vet ./... clean; gofmt -l clean. A CHANGELOG ### Fixed entry accompanies the change.


Disclosure: this change is AI-assisted; the commit carries Assisted-by: Claude:claude-opus-4-8 per CONTRIBUTING, with a human Signed-off-by (DCO). Happy to adjust anything to fit your conventions.

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 REPPL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Delegate instead of duplicating (internal/core/lint/lint.go, isNull): the widened copy is byte-identical to frontmatter.IsNull, and lint.go already imports frontmatter (it delegates its scanner for exactly this reason). Please replace the body with return 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.

  2. Doc comment count (internal/core/frontmatter/frontmatter.go, the IsNull comment): "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.

  3. t.Errorf in the spelling matrix (internal/core/lifeboat/graveyard_abandoned_test.go): t.Fatalf inside 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.

  4. 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.

  5. 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

@REPPL

REPPL commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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)
if status != "superseded" && frontmatter.IsNull(supBy) {
return
}

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.

REPPL added 2 commits August 24, 2026 16:04
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
@REPPL

REPPL commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Corrections pushed in response to the review:

  • d8e3d23..2025109 test: the lifeboat regression matrix now covers bare NULL/Null/null/~ only — quoted spellings removed, per the review. The test comment now names the gvUnquote-before-IsNull ordering explicitly as an open lifeboat heuristic decision, not YAML null semantics.
  • 3ad6c82 test: unrelated preflight blocker fixed en route — runShim is now hermetic (PATH/HOME pinned), because the shim's command -v abcd last resort was finding a developer PATH install and silencing the 'binary absent' case.

The production widening itself is untouched (narrow scope preserved). Both deferred observations are filed against the ledger rather than lost:

  • quote-insensitive sentinel semantics → iss-2608241347321758
  • trailing-comment null form (NULL # no successor) → iss-2608241347321759

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

frontmatter.IsNull misses the YAML nulls Null/NULL, so disembark pack reports a live ADR as superseded — quoting superseded_by: NULL as its evidence

2 participants