From e05d1b14b430cc5881d023c63f54216c974a537a Mon Sep 17 00:00:00 2001 From: Akeem Jenkins Date: Tue, 4 Aug 2026 19:59:20 -0600 Subject: [PATCH 1/2] feat: lint warns on unreferenced/undefined footnote definitions A footnote referenced in the body but never defined renders as a dangling marker; one defined but never referenced renders as nothing, silently dropping a source that reads as cited. Neither is a conformance requirement, so both are warnings surfaced via validate and lint, not errors. Co-Authored-By: Claude Fable 5 --- internal/validate/v02.go | 44 +++++++++++++++++++++++++- internal/validate/v02_test.go | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/internal/validate/v02.go b/internal/validate/v02.go index 4ed856f..8f7efac 100644 --- a/internal/validate/v02.go +++ b/internal/validate/v02.go @@ -106,9 +106,33 @@ var ( citationsHeading = regexp.MustCompile(`(?mi)^#{1,6}\s+Citations\s*$`) // footnoteRef matches inline footnote references [^label] (not definitions). footnoteRef = regexp.MustCompile(`\[\^([^\]\s]+)\](?::)?`) - isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) + // footnoteMark matches any footnote marker [^label], reference or + // definition; footnoteLabels classifies each occurrence by position. + footnoteMark = regexp.MustCompile(`\[\^([^\]\s]+)\]`) + isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) ) +// footnoteLabels splits a body's footnote markers into references and +// definitions. A definition is `[^label]:` at the start of a line; every +// other `[^label]` occurrence, including one that happens to be followed by +// a literal colon mid-line, is a reference (OKF §5.1). +func footnoteLabels(body string) (refs, defs map[string]bool) { + refs = make(map[string]bool) + defs = make(map[string]bool) + for _, m := range footnoteMark.FindAllStringSubmatchIndex(body, -1) { + start, end, labelStart, labelEnd := m[0], m[1], m[2], m[3] + label := body[labelStart:labelEnd] + atLineStart := start == 0 || body[start-1] == '\n' + followedByColon := end < len(body) && body[end] == ':' + if atLineStart && followedByColon { + defs[label] = true + } else { + refs[label] = true + } + } + return refs, defs +} + // validateSources checks the §5.1 provenance family: resource is REQUIRED // within an entry, usage_count needs a framing usage_window, and body // footnote labels must join into sources[].id. @@ -129,6 +153,24 @@ func validateSources(r *Report, c *concept.Concept) { } } + // §5.1/§13.1 footnote definitions: a reference with no definition renders + // as a dangling marker, and a definition with no reference renders as + // nothing, silently dropping a source that reads as cited. Both are + // warnings regardless of whether the label also joins into sources[].id. + refs, defs := footnoteLabels(c.Body) + for label := range refs { + if !defs[label] { + r.add(c.ID, SeverityWarning, fmt.Sprintf( + "body: footnote [^%s] is referenced but never defined - renders as a dangling marker (OKF §5.1)", label)) + } + } + for label := range defs { + if !refs[label] { + r.add(c.ID, SeverityWarning, fmt.Sprintf( + "body: footnote [^%s] is defined but never referenced - renders as nothing, so a source that reads as cited is absent from the output (OKF §5.1)", label)) + } + } + // Per-claim attribution: footnote labels are join keys into sources[].id. // Only meaningful when the concept declares source ids at all. if len(ids) == 0 { diff --git a/internal/validate/v02_test.go b/internal/validate/v02_test.go index db36fa6..75030d9 100644 --- a/internal/validate/v02_test.go +++ b/internal/validate/v02_test.go @@ -160,6 +160,64 @@ func TestValidate_FootnoteLabelMatchingSourceIDOK(t *testing.T) { mustNotFinding(t, Validate(b), "rev-policy") } +// A footnote referenced in the body but never defined renders as a dangling +// marker (§5.1, §13.1): warn, don't fail validate. +func TestValidate_FootnoteReferencedButNotDefinedWarns(t *testing.T) { + b := testBundle(t, map[string]string{ + "a.md": "---\ntype: T\ntitle: A\ndescription: d\ntags: [x]\n---\n\nA claim.[^orphan]\n", + }) + r := Validate(b) + mustFinding(t, r, SeverityWarning, "footnote [^orphan] is referenced but never defined") + if r.HasErrors() { + t.Fatalf("unexpected errors: %+v", r.Findings) + } +} + +// A footnote defined but never referenced renders as nothing (§5.1, §13.1): +// the source silently drops out of the rendered document. Warn, don't fail. +func TestValidate_FootnoteDefinedButNotReferencedWarns(t *testing.T) { + b := testBundle(t, map[string]string{ + "a.md": "---\ntype: T\ntitle: A\ndescription: d\ntags: [x]\n---\n\nprose with no footnote marks.\n\n[^unused]: something\n", + }) + r := Validate(b) + mustFinding(t, r, SeverityWarning, "footnote [^unused] is defined but never referenced") + if r.HasErrors() { + t.Fatalf("unexpected errors: %+v", r.Findings) + } +} + +// A label that is defined and IS present in sources[].id, but never +// referenced in the body, still warns as unreferenced: the sources[].id join +// check and the definition/reference check are independent concerns. +func TestValidate_FootnoteDefinedInSourcesButNotReferencedStillWarns(t *testing.T) { + b := testBundle(t, map[string]string{ + "a.md": "---\ntype: T\ntitle: A\ndescription: d\ntags: [x]\nsources:\n - id: rev-policy\n resource: https://example.com\n---\n\nprose with no footnote marks.\n\n[^rev-policy]: Revenue recognition policy\n", + }) + mustFinding(t, Validate(b), SeverityWarning, "footnote [^rev-policy] is defined but never referenced") +} + +// A label both defined and referenced produces neither new warning, whether +// or not it also joins into sources[].id. +func TestValidate_FootnoteDefinedAndReferencedNoWarning(t *testing.T) { + b := testBundle(t, map[string]string{ + "a.md": "---\ntype: T\ntitle: A\ndescription: d\ntags: [x]\n---\n\nA claim.[^n1]\n\n[^n1]: a note\n", + }) + r := Validate(b) + mustNotFinding(t, r, "is referenced but never defined") + mustNotFinding(t, r, "is defined but never referenced") +} + +// A body with no footnote marks at all produces no footnote definition +// findings. +func TestValidate_NoFootnotesNoDefinitionFindings(t *testing.T) { + b := testBundle(t, map[string]string{ + "a.md": okConcept, + }) + r := Validate(b) + mustNotFinding(t, r, "is referenced but never defined") + mustNotFinding(t, r, "is defined but never referenced") +} + // --- §10 attested computations --- func TestValidate_AttestedComputationRequiresRuntime(t *testing.T) { From bff0c704519437e0aea45c5531192d14f55ce720 Mon Sep 17 00:00:00 2001 From: Akeem Jenkins Date: Tue, 4 Aug 2026 20:00:45 -0600 Subject: [PATCH 2/2] fix: deterministic footnote finding order, drop misapplied spec citation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emit the new footnote warnings in first-occurrence document order instead of map iteration order, and cite only §5.1 (§13.1 covers v0.1 legacy constructs, not footnotes). Co-Authored-By: Claude Fable 5 --- internal/validate/v02.go | 45 +++++++++++++++++++++++------------ internal/validate/v02_test.go | 4 ++-- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/internal/validate/v02.go b/internal/validate/v02.go index 8f7efac..7b133ff 100644 --- a/internal/validate/v02.go +++ b/internal/validate/v02.go @@ -113,26 +113,40 @@ var ( ) // footnoteLabels splits a body's footnote markers into references and -// definitions. A definition is `[^label]:` at the start of a line; every -// other `[^label]` occurrence, including one that happens to be followed by -// a literal colon mid-line, is a reference (OKF §5.1). -func footnoteLabels(body string) (refs, defs map[string]bool) { - refs = make(map[string]bool) - defs = make(map[string]bool) +// definitions, each deduplicated in first-occurrence order so findings are +// emitted deterministically. A definition is `[^label]:` at the start of a +// line; every other `[^label]` occurrence, including one that happens to be +// followed by a literal colon mid-line, is a reference (OKF §5.1). +func footnoteLabels(body string) (refs, defs []string) { + seenRef := make(map[string]bool) + seenDef := make(map[string]bool) for _, m := range footnoteMark.FindAllStringSubmatchIndex(body, -1) { start, end, labelStart, labelEnd := m[0], m[1], m[2], m[3] label := body[labelStart:labelEnd] atLineStart := start == 0 || body[start-1] == '\n' followedByColon := end < len(body) && body[end] == ':' if atLineStart && followedByColon { - defs[label] = true - } else { - refs[label] = true + if !seenDef[label] { + seenDef[label] = true + defs = append(defs, label) + } + } else if !seenRef[label] { + seenRef[label] = true + refs = append(refs, label) } } return refs, defs } +// labelSet converts a label list to a membership set. +func labelSet(labels []string) map[string]bool { + set := make(map[string]bool, len(labels)) + for _, l := range labels { + set[l] = true + } + return set +} + // validateSources checks the §5.1 provenance family: resource is REQUIRED // within an entry, usage_count needs a framing usage_window, and body // footnote labels must join into sources[].id. @@ -153,19 +167,20 @@ func validateSources(r *Report, c *concept.Concept) { } } - // §5.1/§13.1 footnote definitions: a reference with no definition renders - // as a dangling marker, and a definition with no reference renders as + // Footnote definitions: a reference with no definition renders as a + // dangling marker, and a definition with no reference renders as // nothing, silently dropping a source that reads as cited. Both are // warnings regardless of whether the label also joins into sources[].id. refs, defs := footnoteLabels(c.Body) - for label := range refs { - if !defs[label] { + refSet, defSet := labelSet(refs), labelSet(defs) + for _, label := range refs { + if !defSet[label] { r.add(c.ID, SeverityWarning, fmt.Sprintf( "body: footnote [^%s] is referenced but never defined - renders as a dangling marker (OKF §5.1)", label)) } } - for label := range defs { - if !refs[label] { + for _, label := range defs { + if !refSet[label] { r.add(c.ID, SeverityWarning, fmt.Sprintf( "body: footnote [^%s] is defined but never referenced - renders as nothing, so a source that reads as cited is absent from the output (OKF §5.1)", label)) } diff --git a/internal/validate/v02_test.go b/internal/validate/v02_test.go index 75030d9..fea1196 100644 --- a/internal/validate/v02_test.go +++ b/internal/validate/v02_test.go @@ -161,7 +161,7 @@ func TestValidate_FootnoteLabelMatchingSourceIDOK(t *testing.T) { } // A footnote referenced in the body but never defined renders as a dangling -// marker (§5.1, §13.1): warn, don't fail validate. +// marker (§5.1): warn, don't fail validate. func TestValidate_FootnoteReferencedButNotDefinedWarns(t *testing.T) { b := testBundle(t, map[string]string{ "a.md": "---\ntype: T\ntitle: A\ndescription: d\ntags: [x]\n---\n\nA claim.[^orphan]\n", @@ -173,7 +173,7 @@ func TestValidate_FootnoteReferencedButNotDefinedWarns(t *testing.T) { } } -// A footnote defined but never referenced renders as nothing (§5.1, §13.1): +// A footnote defined but never referenced renders as nothing (§5.1): // the source silently drops out of the rendered document. Warn, don't fail. func TestValidate_FootnoteDefinedButNotReferencedWarns(t *testing.T) { b := testBundle(t, map[string]string{