diff --git a/docs/okf-second-brain/log.md b/docs/okf-second-brain/log.md index 22e718d..136c725 100644 --- a/docs/okf-second-brain/log.md +++ b/docs/okf-second-brain/log.md @@ -26,6 +26,9 @@ pattern): sessions. 5. **Two version markers** (spec ยง12): `okf_version` (upstream Google OKF) and this convention's own version. Either bump โ†’ log it below โ†’ each plugin re-checks. +6. **Conformance is verified, not asserted.** A plugin's status only moves to ๐ŸŸข after its bundle passes + the shared acceptance gate `catalog/scripts/okf-check.mjs` (spec ยง7) โ€” `node catalog/scripts/okf-check.mjs ` โ†’ exit 0. On a relayed conformance landing, run the gate against that + plugin's bundle and record the result here. Evidence-based flips only (operator verification-plikt). ## Per-plugin conformance status @@ -35,7 +38,7 @@ distinct from conformance (code landed), which `linkedin-studio` alone holds so | Plugin | Against minimal contract (spec ยง3) | Notes | Status | |---|---|---|---| -| **linkedin-studio** | `type` + per-level `index.md` + root `okf_version` on `brain/`; `ingest/` excluded (round-trip-critical tributary) | Reference design. Emits frontmatter, adds no parser. Brain suite 134/134; okr's `okf-check` validates `brain/` โ†’ exit 0 (also mid-propose with a pending-diff present). | ๐ŸŸข conformant @ linkedin-studio `da0a16a` (2026-06-26) | +| **linkedin-studio** | `type` + per-level `index.md` + root `okf_version` on `brain/`; `ingest/` excluded (round-trip-critical tributary) | Reference design. Emits frontmatter, adds no parser. Brain suite 134/134; passes the **shared gate** `catalog/scripts/okf-check.mjs` (scaffolded `brain/` โ†’ "OK: valid OKF bundle", exit 0) โ€” same verdict as okr's reference checker. | ๐ŸŸข conformant @ linkedin-studio `da0a16a` (2026-06-26) | | **okr** | `type` required + recommended-as-warnings + `okf_version` echo | Has the reference **writer + checker** (`okf-check.mjs`, `okf-index.mjs`, `lib/frontmatter.mjs`) + skill `okr-second-brain-search` v1.6.1. | ๐ŸŸก built ยท **spec ratified @ okr `75bfc9b` (2026-06-29)**; conformance-alignment landing = own go | | **ms-ai-architect** | designed, not built | Targets the fuller OKF package + a retrieval skill. Builds against this spec. | ๐Ÿ”ต designed ยท **spec ratified @ ms-ai-architect `72a7e2b` (2026-06-29)**; build = own go | @@ -79,3 +82,10 @@ distinct from conformance (code landed), which `linkedin-studio` alone holds so `72a7e2b` (both relayed via operator). The shared contract is now accepted by all three โ€” the interop goal of Stage 1 is met at the convention level. Conformance *landings* (okr form-alignment, ms-ai-architect build) remain each their own go. _(linkedin-studio session, operator relay.)_ +- **2026-06-29** โ€” **Shared acceptance gate landed** (`catalog/scripts/okf-check.mjs` + + `okf-frontmatter.mjs` + `okf-check.test.mjs`). Lifted faithfully from okr's reference impl; verdict + logic byte-identical, English output, zero deps, self-contained. Verified: 33/33 catalog tests green; + verdict parity with okr's checker on okr fixtures (positive + negative); a scaffolded linkedin-studio + `brain/` validates clean (exit 0). Wired as the conformance gate (protocol ยง6 + spec ยง7): a plugin + only moves to ๐ŸŸข after passing it. The only Stage-3 remainder is reconciling the TS/mjs impls โ€” not + required for the gate. _(linkedin-studio session.)_ diff --git a/docs/okf-second-brain/spec.md b/docs/okf-second-brain/spec.md index aa1fa43..7d3e997 100644 --- a/docs/okf-second-brain/spec.md +++ b/docs/okf-second-brain/spec.md @@ -84,10 +84,22 @@ only `type` required (missing โ†’ fail + names the files), recommended fields npm dependencies, only couples to a ~55-line `frontmatter.mjs`. The shared spec **generalizes okr's semantics; it does not reinvent them.** Reading okr's code is fine; **writing okr is a separate go.** -A **single shared checker** is a Stage-3 item (ยง10), deferred until measured need โ€” note that -linkedin-studio already has a parallel **TypeScript** conformance implementation (`scripts/brain/`), so -a shared checker means reconciling two language implementations of the same convention. Cross-tool -interop is already proven: okr's `okf-check.mjs` validates linkedin-studio's `brain/` โ†’ exit 0. +A **shared checker now lives here:** `catalog/scripts/okf-check.mjs` (+ vendored +`okf-frontmatter.mjs`), lifted faithfully from okr's reference impl โ€” verdict logic byte-identical, +output in English, zero deps, self-contained. It is the **canonical cross-plugin acceptance gate**: a +bundle's pass/fail is the same here as under okr's checker. Run it per bundle root: + +``` +node catalog/scripts/okf-check.mjs +``` + +Verdict parity is verified โ€” identical exit codes to okr's checker on okr's own fixtures +(`okf-minimal`, `okf-realistic`), positive and negative (injected missing-`type`) โ€” and a scaffolded +linkedin-studio `brain/` validates clean ("OK: valid OKF bundle", exit 0). Each plugin may keep its own +dev-loop check (linkedin-studio's TypeScript impl under `scripts/brain/` stays for its own suite); the +catalog `.mjs` is the shared gate all three are measured by. The only Stage-3 remainder is *reconciling* +the two language implementations into one โ€” deferred until measured need, and **not** required for the +gate to function. ## 8. Deliberately NOT mandated @@ -163,6 +175,8 @@ commands?) + **maintenance reliability** โ€” **not** against formal OKF conforma (12 June 2026, "a starting point, not a finished standard"). - **Per-plugin design notes:** linkedin-studio `docs/okf-convergence-brief.md`; okr `docs/okf-second-brain-note-2026-06.md`; ms-ai-architect `docs/okf-second-brain-brief-2026-06.md`. -- **Reference checker:** okr `scripts/okf-check.mjs` (+ `okf-index.mjs`, `lib/frontmatter.mjs`). +- **Shared checker (the cross-plugin gate):** `catalog/scripts/okf-check.mjs` (+ `okf-frontmatter.mjs`, + `okf-check.test.mjs`) โ€” lifted from okr's reference impl `okr/scripts/okf-check.mjs` + (+ `okf-index.mjs`, `lib/frontmatter.mjs`). - **Reference design:** linkedin-studio `docs/second-brain/architecture.md`; engine `scripts/brain/`. - **Coordination + rollout status:** `log.md` (this directory). diff --git a/scripts/okf-check.mjs b/scripts/okf-check.mjs new file mode 100644 index 0000000..914fbfe --- /dev/null +++ b/scripts/okf-check.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +// okf-check.mjs โ€” shared OKF-compatible second-brain conformance checker. +// +// The single cross-plugin acceptance gate for the convention in +// docs/okf-second-brain/spec.md. Validates one bundle root against the minimal +// contract (spec ยง3): +// - every concept file (.md except index.md) MUST carry `type:` in frontmatter; +// >= 1 file without type -> exit 1, count + names the files; 0 -> exit 0. +// - recommended fields (resource/title/description/timestamp) -> WARNING, not error. +// - the bundle-root index.md's `okf_version` is echoed for human comparison +// (no auto-fetch โ€” offline by design). +// +// Lifted faithfully from okr/scripts/okf-check.mjs (the de-facto reference +// implementation; spec ยง7), with English output + a vendored frontmatter reader so +// the catalog copy is self-contained. Verdict logic is byte-identical, so a bundle's +// pass/fail is the same here as under okr's checker. Run per bundle root. +// Zero npm dependencies (node: builtins). + +import { readdirSync, readFileSync, existsSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseFrontmatter } from './okf-frontmatter.mjs'; + +const RECOMMENDED = ['resource', 'title', 'description', 'timestamp']; + +// All concept files (.md except index.md) under root, recursively. +function walkConcepts(root) { + const out = []; + const walk = (dir) => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') out.push(p); + } + }; + walk(root); + return out; +} + +// Read the root's okf_version (markdown text in index.md, not frontmatter). null if absent. +function rootOkfVersion(root) { + const idx = join(root, 'index.md'); + if (!existsSync(idx)) return null; + const m = readFileSync(idx, 'utf8').match(/^okf_version:\s*(.+)$/m); + return m ? m[1].trim() : null; +} + +export function checkBundle(root) { + const concepts = walkConcepts(root); + const missingType = []; + const warnings = []; + for (const f of concepts) { + const { get } = parseFrontmatter(readFileSync(f, 'utf8')); + const rel = relative(root, f); + if (!get('type')) { + missingType.push(rel); + continue; + } + for (const field of RECOMMENDED) { + if (!get(field)) warnings.push(`${rel}: missing recommended field "${field}"`); + } + } + return { + scanned: concepts.length, + missingType, + warnings, + okfVersion: rootOkfVersion(root), + }; +} + +// --- CLI --- +const isMain = process.argv[1] + && fileURLToPath(import.meta.url) === process.argv[1]; +if (isMain) { + const root = process.argv[2]; + if (!root) { + process.stderr.write('Usage: node okf-check.mjs \n'); + process.exit(2); + } + if (!existsSync(root)) { + process.stderr.write(`Bundle root does not exist: ${root}\n`); + process.exit(2); + } + const r = checkBundle(root); + const out = []; + out.push(`OKF check: ${root}`); + out.push(` Concept files scanned: ${r.scanned}`); + out.push(` ${r.missingType.length} files without type:`); + for (const f of r.missingType) out.push(` - ${f}`); + out.push(` okf_version: ${r.okfVersion || 'MISSING (root index without okf_version)'}`); + out.push(` Warnings (recommended fields): ${r.warnings.length}`); + for (const w of r.warnings) out.push(` ! ${w}`); + out.push(r.missingType.length === 0 ? 'OK: valid OKF bundle' : `FAIL: ${r.missingType.length} file(s) missing type:`); + process.stdout.write(`${out.join('\n')}\n`); + process.exit(r.missingType.length === 0 ? 0 : 1); +} diff --git a/scripts/okf-check.test.mjs b/scripts/okf-check.test.mjs new file mode 100644 index 0000000..9808c0d --- /dev/null +++ b/scripts/okf-check.test.mjs @@ -0,0 +1,104 @@ +// Tests for the shared OKF conformance checker (the cross-plugin acceptance gate). +// Self-contained: builds tiny bundles in a temp dir, so the test has no dependency +// on any sibling repo's fixtures. okf-check is run as a subprocess to capture the +// exit code (the contract). Zero npm deps. Style mirrors check-versions.test.mjs. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { checkBundle } from './okf-check.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const CHECK = join(HERE, 'okf-check.mjs'); + +function tmpRoot() { + return mkdtempSync(join(tmpdir(), 'okf-catalog-')); +} + +// Run okf-check as a subprocess; capture non-zero exit (execFileSync throws then). +function runCheck(root) { + try { + const stdout = execFileSync('node', [CHECK, root], { encoding: 'utf8' }); + return { status: 0, stdout }; + } catch (e) { + return { status: e.status ?? 1, stdout: `${e.stdout || ''}${e.stderr || ''}` }; + } +} + +// A minimal conforming bundle: root index.md with okf_version + one fully-typed concept. +function writeValidBundle(dir) { + writeFileSync(join(dir, 'index.md'), 'okf_version: 0.1\n\n# Bundle\n'); + writeFileSync( + join(dir, 'profile.md'), + '---\ntype: Profile\ntitle: User profile\ndescription: The user.\nresource: about\ntimestamp: 2026-06-29\n---\n# Profile\n', + ); +} + +test('valid bundle (every concept has type:) -> exit 0, "0 files without type:"', () => { + const dir = tmpRoot(); + try { + writeValidBundle(dir); + const { status, stdout } = runCheck(dir); + assert.equal(status, 0, 'a valid bundle should exit 0'); + assert.match(stdout, /0 files without type:/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('concept file without type: -> exit != 0 + count > 0 + names the file', () => { + const dir = tmpRoot(); + try { + writeValidBundle(dir); + writeFileSync( + join(dir, 'no-type.md'), + '---\ntitle: Untyped\ndescription: A concept file missing the required type.\n---\n# Untyped\n', + ); + const { status, stdout } = runCheck(dir); + assert.notEqual(status, 0, 'a type-less file should exit != 0'); + assert.match(stdout, /[1-9]\d* files without type:/); + assert.match(stdout, /no-type\.md/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('echoes okf_version from the root index.md', () => { + const dir = tmpRoot(); + try { + writeValidBundle(dir); + const { stdout } = runCheck(dir); + assert.match(stdout, /okf_version:\s*0\.1/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('index.md is exempt from the type requirement (sub-levels included)', () => { + const dir = tmpRoot(); + try { + writeValidBundle(dir); + mkdirSync(join(dir, 'journal')); + writeFileSync(join(dir, 'journal', 'index.md'), '# Journal\n'); // no frontmatter, must not fail + const { status } = runCheck(dir); + assert.equal(status, 0, 'index.md must not be treated as a concept file'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('checkBundle(): recommended-field gaps are warnings, not failures', () => { + const dir = tmpRoot(); + try { + writeFileSync(join(dir, 'index.md'), 'okf_version: 0.1\n'); + writeFileSync(join(dir, 'bare.md'), '---\ntype: Note\n---\n# Bare\n'); // type only + const r = checkBundle(dir); + assert.equal(r.missingType.length, 0, 'type present -> not a failure'); + assert.ok(r.warnings.length >= 1, 'missing recommended fields -> warnings'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/scripts/okf-frontmatter.mjs b/scripts/okf-frontmatter.mjs new file mode 100644 index 0000000..49ef645 --- /dev/null +++ b/scripts/okf-frontmatter.mjs @@ -0,0 +1,32 @@ +// okf-frontmatter.mjs +// Minimal flat-frontmatter reader for the shared OKF conformance checker. +// Vendored from okr's lib/frontmatter.mjs (the reference implementation) 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. Parsing logic is kept byte-identical to okr's so verdicts stay in parity. + +const FM_RE = /^---\n([\s\S]*?)\n---/; + +export function parseFrontmatter(content) { + const match = String(content).match(FM_RE); + const raw = match ? match[1] : null; + + const get = (key) => { + if (raw === null) return null; + const m = raw.match(new RegExp(`^\\s*${key}:\\s*(.*)$`, 'm')); + if (!m) return null; + let v = m[1].trim(); + if (v === '') return null; + const q = v[0]; + if (q === '"' || q === "'") { + const end = v.indexOf(q, 1); + if (end !== -1) return v.slice(1, end); // internal '#' preserved + v = v.slice(1); // unterminated quote: fall back to the rest + } else { + v = v.replace(/\s+#.*$/, '').trim(); // unquoted: strip trailing comment + } + return v === '' ? null : v; + }; + + return { raw, get }; +}