fix(okf-check): required and recommended fields are read at top level only
The frontmatter reader matched `^\s*<key>:` with the m flag — indentation-
agnostic, so a block-form nested entry satisfied a top-level lookup. Measured
against okf/SPEC.md at frozen 3fcbb9f, this is a field confusion, not a near
miss: :467-469 names `resource`, `sources[].resource`, `executor.resource` and
`attester.resource` as DISTINCT fields. Top-level `resource` is the URI of the
asset a concept describes (:196); `sources[].resource` is the material it
derives from (:302). `sources` entries carry their own `title` and `type` too.
The consequence was not confined to warnings. Measured before the fix, a
concept with NO top-level `type:` and a `sources[].type` reported "0 files
without type: / OK: valid OKF bundle" — a false negative on §4.1's only
always-required field. `untyped` IS in the parity signature
(check-okf-parity.mjs:73-76), but okr vendors the same regex, so both impls
were blind identically and the gate stayed green while both were wrong.
Anchoring the key at column 0 fixes it. Flow-form never had the bug: in
`sources: [{ id: s1, resource: fixture }]` the nested key is mid-line, so `^`
cannot match it — measured against llm-ingestion-okf's v0.2 golden bundle
(6e0a7c0, read-only), which warns about `resource` and `description` both
before and after.
The divergence from okr is deliberate and is NOT okr lagging. Their reader is
SHARED, and the nested match is documented as load-bearing for their injector
(lib/frontmatter.mjs:7-9 -> inject:69) — while the same module backs their
scripts/okf-check.mjs:101, which needs the opposite. Pinned as parity fixture
`red-nested-key` (catalog FAILs on the nested type, okr passes it), so the
split is a running red/green signal instead of a note. It flips to `agree`
only if okr scopes the checker's reader without touching inject.
Correcting two premises carried in from the previous session, both measured:
- The reader was NOT flat/top-level-only. It read nested keys, so the suspected
false POSITIVE on `resource` was actually a false NEGATIVE, opposite sign.
- "No v0.2 bundle exists" held for our own corpora and emitters only.
llm-ingestion-okf ships a v0.2 golden bundle, where the previous commit's
version-conditional list has real effect — and behaves correctly there.
docs/okf-second-brain/spec.md is untouched deliberately: it makes no claim
about key scope, so gate and convention do not disagree here.
Tests 103 -> 106 (okf-check 22 -> 25), parity 9/9 -> 10/10. All six suites
green; check-versions 11 OK / 0 WARN / 0 ERROR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RGZGiDPYcHUMSDCVJavRhp
This commit is contained in:
parent
0b1063e911
commit
c4b776e4c6
5 changed files with 94 additions and 2 deletions
|
|
@ -383,3 +383,63 @@ test('§13.1: 0.10 is NEWER than 0.2 — the version compare is not parseFloat',
|
|||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// --- §4.1 vs §5: a nested key is not the top-level key of the same name ---
|
||||
// okf/SPEC.md @ 3fcbb9f, measured 2026-08-01. The spec names `resource`, `sources[].resource`,
|
||||
// `executor.resource`, and `attester.resource` as DISTINCT fields (:467-469): top-level
|
||||
// `resource` is the URI of the asset a concept DESCRIBES (:196), `sources[].resource` is the
|
||||
// material it DERIVES FROM (:302). `sources` entries carry their own `title`/`type` too.
|
||||
//
|
||||
// The reader's key regex was `^\s*<key>:` with the m flag — indentation-agnostic, so a
|
||||
// block-form nested entry satisfied the top-level lookup. Measured before the fix: a concept
|
||||
// with NO top-level `type` and a `sources[].type` reported "0 files without type: / OK",
|
||||
// i.e. a false negative on §4.1's ONLY always-required field. `untyped` is in the parity
|
||||
// signature (check-okf-parity.mjs:73-76) and okr vendors the same regex, so both impls were
|
||||
// blind identically and the parity gate stayed green while both were wrong.
|
||||
//
|
||||
// Flow-form (`sources: [{ id: s1, resource: fixture }]`) never had the bug — the nested key is
|
||||
// mid-line, so `^` cannot match it. Only block form did.
|
||||
|
||||
const NESTED_SOURCES = 'sources:\n - id: s1\n resource: https://example.com/schema\n title: GA4 schema\n description: derived-from, not described-by\n type: Reference\n';
|
||||
|
||||
test('§4.1: a nested sources[].type does NOT satisfy the required top-level type', () => {
|
||||
const dir = tmpRoot();
|
||||
try {
|
||||
writeFileSync(join(dir, 'index.md'), 'okf_version: 0.1\n\n# Bundle\n');
|
||||
writeFileSync(join(dir, 'c.md'), `---\ntitle: T\n${NESTED_SOURCES}---\n# Concept\n`);
|
||||
assert.deepEqual(checkBundle(dir).missingType, ['c.md'], 'type is required AT TOP LEVEL');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('§4.1: nested sources[] keys do NOT satisfy the recommended top-level fields', () => {
|
||||
const dir = tmpRoot();
|
||||
try {
|
||||
writeFileSync(join(dir, 'index.md'), 'okf_version: 0.1\n\n# Bundle\n');
|
||||
writeFileSync(join(dir, 'c.md'), `---\ntype: Note\ntimestamp: 2026-06-29\n${NESTED_SOURCES}---\n# Concept\n`);
|
||||
const warned = checkBundle(dir).warnings.join('\n');
|
||||
for (const field of ['resource', 'title', 'description']) {
|
||||
assert.ok(warned.includes(`"${field}"`), `${field} is absent at top level, so it must warn`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// The fix anchors the key to column 0. Guard the documented flow-form behaviour it must not
|
||||
// disturb: a top-level `generated: { by, at }` is one line starting at column 0 -> present.
|
||||
test('§4.1: anchoring does not break the top-level flow-form value', () => {
|
||||
const dir = tmpRoot();
|
||||
try {
|
||||
writeFileSync(join(dir, 'index.md'), '---\nokf_version: "0.2"\n---\n\n# Bundle\n');
|
||||
writeFileSync(
|
||||
join(dir, 'c.md'),
|
||||
'---\ntype: Note\ntitle: T\ndescription: D\nresource: about\n' +
|
||||
'generated: { by: human:ktg, at: 2026-06-29T00:00:00Z }\n---\n# Concept\n',
|
||||
);
|
||||
assert.deepEqual(checkBundle(dir).warnings, [], 'every recommended field is present at top level');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
// okf-frontmatter.mjs
|
||||
// Minimal flat-frontmatter reader for the shared OKF conformance checker.
|
||||
// Minimal TOP-LEVEL frontmatter reader for the shared OKF conformance checker.
|
||||
// Provenance: vendored from okr's lib/frontmatter.mjs at c06e4d7 so the catalog-hosted
|
||||
// checker is self-contained — zero npm dependencies, no cross-repo import. The checker
|
||||
// only reads (never writes), so only parseFrontmatter().get is vendored. okr has since
|
||||
// added BOM/CRLF normalization here (lib/frontmatter.mjs:23); this copy has not, so the
|
||||
// two have diverged and are no longer byte-identical (parity is the parity-gate's job,
|
||||
// not this file's).
|
||||
//
|
||||
// SECOND, DELIBERATE DIVERGENCE (2026-08-01): the key is anchored at column 0 here, while
|
||||
// okr keeps `^\s*key:`. That is not okr lagging — their reader is SHARED, and the nested
|
||||
// match is load-bearing for their injector (lib/frontmatter.mjs:7-9 -> inject:69). A
|
||||
// conformance checker needs the opposite: §4.1's keys are top-level, and `sources[]`
|
||||
// entries carry their own `resource`/`title`/`type`. Pinned as parity fixture
|
||||
// `red-nested-key`.
|
||||
|
||||
const FM_RE = /^---\n([\s\S]*?)\n---/;
|
||||
|
||||
|
|
@ -15,7 +22,11 @@ export function parseFrontmatter(content) {
|
|||
|
||||
const get = (key) => {
|
||||
if (raw === null) return null;
|
||||
const m = raw.match(new RegExp(`^\\s*${key}:\\s*(.*)$`, 'm'));
|
||||
// Anchored at column 0: OKF's §4.1 keys are TOP-LEVEL keys. `sources[]` entries carry their
|
||||
// own `resource`/`title`/`type` (§5:302), and §? :467-469 names them as distinct fields from
|
||||
// the same-named top-level ones. A leading \s* matched those nested entries and reported the
|
||||
// top-level key as present — a false negative, on `type` too (§4.1's only required field).
|
||||
const m = raw.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
|
||||
if (!m) return null;
|
||||
let v = m[1].trim();
|
||||
if (v === '') return null;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@
|
|||
"note": "Root index.md carries a layout snapshot (kb-layout-2026-06) in okf_version instead of the upstream version -> catalog enforces the §3 shape rule and FAILs, okr still pure-echoes the value and passes (their CHANGELOG 1.8.1: 'verdiene valideres fortsatt ikke'). Otherwise clean, so the divergence isolates the marker axis. EXPECTED TO FLIP TO 'agree' once okr mirrors the enforcement — this expectation encodes okr's lag, not a permanent design split.",
|
||||
"note2": "This axis was invisible until the parity signature carried the shape VERDICT: comparing the okf_version value alone reads 'agree' precisely when the impls disagree about whether that value is acceptable."
|
||||
},
|
||||
"red-nested-key": {
|
||||
"axis": "key-scope",
|
||||
"expected": "diverge",
|
||||
"note": "A concept with NO top-level `type:` whose only `type:` sits in a block-form `sources[]` entry -> catalog anchors the key at column 0 (okf-frontmatter.mjs, 2026-08-01) and reports it untyped + FAILs; okr's `^\\s*key:` reads the nested entry and passes it as typed. §4.1 makes `type` the only always-required field, and SPEC.md:467-469 names top-level `resource` and `sources[].resource` as DISTINCT fields, so the nested match is a false negative on the required axis.",
|
||||
"note2": "NOT symmetrical with the other red fixtures: okr's nested lookup is DELIBERATE and load-bearing for their injector (lib/frontmatter.mjs:7-9 -> inject:69). The shared module has one caller that needs nested and one (scripts/okf-check.mjs:101) that must not — so this expectation encodes a real design split, not merely okr's lag. Flips to 'agree' only if okr scopes the checker's reader without touching inject."
|
||||
},
|
||||
"green-hierarchical": {
|
||||
"axis": "tree-hierarchical",
|
||||
"expected": "agree",
|
||||
|
|
|
|||
12
test/okf-parity-corpus/red-nested-key/derived.md
Normal file
12
test/okf-parity-corpus/red-nested-key/derived.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: A concept with no top-level type
|
||||
description: Its only `type:` sits inside a sources[] entry.
|
||||
resource: about
|
||||
timestamp: 2026-08-01
|
||||
sources:
|
||||
- id: ga4-schema
|
||||
resource: https://developers.google.com/analytics/bigquery/export-schema
|
||||
title: GA4 BigQuery Export schema
|
||||
type: Reference
|
||||
---
|
||||
# Derived
|
||||
3
test/okf-parity-corpus/red-nested-key/index.md
Normal file
3
test/okf-parity-corpus/red-nested-key/index.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
okf_version: 0.1
|
||||
|
||||
# Bundle root
|
||||
Loading…
Add table
Add a link
Reference in a new issue