From 74d6e936fdbd846e94c85f456e1ffa2c0362a72c Mon Sep 17 00:00:00 2001 From: opendpp-node Date: Mon, 27 Jul 2026 00:26:15 +0300 Subject: [PATCH] fix(okf): accept the v0.2 `generated` mapping, retire the v0.1 `timestamp` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundle emitter moved to OKF v0.2, where SPEC §5.2 replaced the bare `timestamp` frontmatter key with a `generated: { by, at }` mapping. This validator still required `timestamp`, so the first v0.2 sync failed CI on all 213 concepts. Mirror the upstream contract (opendpp-node:src/okf/validate.ts): `by` is a non-empty actor, `at` is an ISO 8601 instant, and a leftover top-level `timestamp` is now itself an error — a bundle carrying both shapes is half-migrated, and accepting it here would hide exactly that drift. `generated` is a mapping, so its indented children are out of reach of the scalar `key: value` probe and get their own block check. That probe also moves from `\s*\S` to `[ \t]*\S`: `\s` matches newlines, so it stepped onto the following line and read an empty `title:` as populated. --- validate.mjs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/validate.mjs b/validate.mjs index a1e91ba..4ba918c 100644 --- a/validate.mjs +++ b/validate.mjs @@ -2,14 +2,18 @@ // Standalone OKF conformance check for this published bundle — zero dependencies (Node built-ins only), // so it runs in CI without an install step. Mirrors the source-repo validator // (opendpp-node:src/okf/validate.ts): every non-reserved .md has a frontmatter block with a non-empty -// `type` (+ title/description/timestamp); reserved index.md/log.md carry no frontmatter (except the +// `type` (+ title/description/generated); reserved index.md/log.md carry no frontmatter (except the // root index.md, which may declare only okf_version); and every internal Markdown link resolves. import { readFileSync, readdirSync } from "node:fs"; import { join, relative, sep, dirname } from "node:path"; const ROOT = process.cwd(); const SEGMENT_RE = /^[A-Za-z0-9_][A-Za-z0-9_.\-]*$/; -const REQUIRED = ["type", "title", "description", "timestamp"]; +// SCALAR required keys. `generated` is NOT here: OKF v0.2 made it a MAPPING (SPEC §5.2), so it needs +// the block check below rather than a `key: value` probe. The probe is `[ \t]*` and not `\s*` on +// purpose — `\s` matches newlines, so `\s*\S` would step onto the NEXT line and read an empty +// `title:` as populated. +const REQUIRED_SCALAR = ["type", "title", "description"]; // Files this repo OWNS — everything at the root that is not part of the synced bundle. The upstream // mirror (opendpp-node's okf-publish.yml) rsyncs the bundle in with `--delete`, so it must --exclude // every one of these; its pre-sync gate refuses to delete a path the bundle doesn't own. Asserting @@ -75,6 +79,28 @@ function frontmatter(content) { return content.slice(4, end + 1); } +// OKF v0.2 §5.2 moved provenance into a `generated` MAPPING and RETIRED v0.1's bare `timestamp`: +// +// generated: +// by: process:emit-okf +// at: 2026-07-26T00:00:00Z +// +// Its children are indented, so the scalar probe above cannot reach them — capture the block, then +// read `by`/`at` out of it. Mirrors the upstream contract (opendpp-node:src/okf/validate.ts): `by` is +// a non-empty actor, `at` is an ISO 8601 instant. +function generatedErrors(p, fm) { + const block = /^generated:[ \t]*\r?\n((?:[ \t]+\S[^\n]*\r?\n?)*)/m.exec(fm); + if (!block) return [`${p}: missing "generated" mapping (SPEC §5.2)`]; + const out = []; + const by = /^[ \t]+by:[ \t]*(\S[^\n]*?)[ \t]*$/m.exec(block[1]); + const at = /^[ \t]+at:[ \t]*(\S[^\n]*?)[ \t]*$/m.exec(block[1]); + if (!by) out.push(`${p}: "generated.by" is required and must be a non-empty actor (SPEC §5.2/§7)`); + if (!at || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(at[1])) { + out.push(`${p}: "generated.at" must be an ISO 8601 datetime (SPEC §5.2)`); + } + return out; +} + function resolveLink(from, href) { let t = href.split("#")[0].trim(); if (t === "") return null; @@ -106,7 +132,11 @@ for (const p of all) { errors.push(`${p}: missing/invalid frontmatter`); continue; } - for (const key of REQUIRED) if (!new RegExp(`^${key}:\\s*\\S`, "m").test(fm)) errors.push(`${p}: missing non-empty "${key}"`); + for (const key of REQUIRED_SCALAR) if (!new RegExp(`^${key}:[ \\t]*\\S`, "m").test(fm)) errors.push(`${p}: missing non-empty "${key}"`); + errors.push(...generatedErrors(p, fm)); + // A bundle carrying BOTH shapes is half-migrated: consumers would read a field the producer no + // longer maintains. Upstream rejects it too, so accepting it here would hide the drift. + if (/^timestamp:/m.test(fm)) errors.push(`${p}: legacy v0.1 "timestamp" key — use "generated.at" (SPEC §5.2)`); } // internal links resolve (concept files only; repo-meta uses GitHub-relative links)