feat(linkedin-studio): brain emits OKF-compatible form — Stage 1 (bundle=brain/, ingest/ excluded) [skip-docs]
Cross-plugin OKF convergence, Stage 1 (docs/okf-convergence-brief.md): the second-brain hub now conforms to OKF-compatible form so a shared retrieval skill (and a sibling agent) can traverse it. - serializeProfile leads with a constant `type: Profile` frontmatter block (round-trip-safe: parseProfile skips it, parse-serialize identity holds). - operations.md seed -> `type: Operations`; index.md seed -> `okf_version: 0.1` marker (markdown text; index files carry no frontmatter per OKF); new brain/journal/index.md (per-level index). Premise correction: the brain is deliberately YAML-free and ingest/published has a byte-exact round-trip invariant (SC2) a frontmatter block would break, so the concept-bundle is scoped to brain/ ONLY; the ingest/ tributary is excluded and pointed to from the hub. We emit frontmatter, add no parser. Verified: new tests/okf-conform.test.ts (5/5); full brain suite 132/132 (0 regressions); cross-tool — okr/scripts/okf-check.mjs validates brain/ (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
This commit is contained in:
parent
001d76ce99
commit
9e95222d12
3 changed files with 129 additions and 2 deletions
|
|
@ -36,9 +36,18 @@ function serializeFact(f: ProfileFact): string {
|
||||||
return f.value === "" ? `- [${bracket}]` : `- [${bracket}] ${f.value}`;
|
return f.value === "" ? `- [${bracket}]` : `- [${bracket}] ${f.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deterministic full-document serialization; both sections always present. */
|
/**
|
||||||
|
* Deterministic full-document serialization; both sections always present. A
|
||||||
|
* constant OKF frontmatter block (`type: Profile`) leads the document so the brain
|
||||||
|
* bundle is OKF-compatible form (docs/okf-convergence-brief.md); it carries no
|
||||||
|
* typed-doc data, so `parseProfile` skips it and `parse ∘ serialize` stays identity.
|
||||||
|
*/
|
||||||
export function serializeProfile(doc: ProfileDoc): string {
|
export function serializeProfile(doc: ProfileDoc): string {
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
|
"---",
|
||||||
|
"type: Profile",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
"# Profile",
|
"# Profile",
|
||||||
"",
|
"",
|
||||||
`schemaVersion: ${doc.schemaVersion}`,
|
`schemaVersion: ${doc.schemaVersion}`,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ function indexSeed(): string {
|
||||||
> Map of Content — one screen pointing at every tributary, with a freshness flag.
|
> Map of Content — one screen pointing at every tributary, with a freshness flag.
|
||||||
> Generated by \`brain init\`; safe to hand-edit (a re-run never clobbers it).
|
> Generated by \`brain init\`; safe to hand-edit (a re-run never clobbers it).
|
||||||
|
|
||||||
|
okf_version: 0.1
|
||||||
|
|
||||||
| Tributary | What it holds | Freshness |
|
| Tributary | What it holds | Freshness |
|
||||||
|-----------|---------------|-----------|
|
|-----------|---------------|-----------|
|
||||||
| voice-samples | writing style | — |
|
| voice-samples | writing style | — |
|
||||||
|
|
@ -50,7 +52,10 @@ function indexSeed(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function operationsSeed(): string {
|
function operationsSeed(): string {
|
||||||
return `# Operations
|
return `---
|
||||||
|
type: Operations
|
||||||
|
---
|
||||||
|
# Operations
|
||||||
|
|
||||||
> The operations centre — where you're headed now, what you're working on, what you might
|
> The operations centre — where you're headed now, what you're working on, what you might
|
||||||
> do next. User-authored: the brain motor never writes here (a re-run of init never
|
> do next. User-authored: the brain motor never writes here (a re-run of init never
|
||||||
|
|
@ -74,6 +79,15 @@ _As of YYYY-MM-DD:_ <one or two sentences on your current direction — replace
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function journalIndexSeed(): string {
|
||||||
|
return `# Journal — episodic log
|
||||||
|
|
||||||
|
> Append-only session entries (\`YYYY-MM-session.md\`). Raw, never edited — the
|
||||||
|
> source the consolidation loop distils from. One concept per file. A reserved
|
||||||
|
> OKF index (no frontmatter); the entries carry the \`type\`.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function profileSeed(): string {
|
function profileSeed(): string {
|
||||||
const templateText = readFileSync(TEMPLATE_PATH, "utf8");
|
const templateText = readFileSync(TEMPLATE_PATH, "utf8");
|
||||||
const instancePath = dataRoot(INSTANCE_SUB);
|
const instancePath = dataRoot(INSTANCE_SUB);
|
||||||
|
|
@ -111,6 +125,7 @@ export function initBrain(): InitResult {
|
||||||
{ sub: "brain/profile.md", seed: profileSeed },
|
{ sub: "brain/profile.md", seed: profileSeed },
|
||||||
{ sub: "brain/index.md", seed: indexSeed },
|
{ sub: "brain/index.md", seed: indexSeed },
|
||||||
{ sub: "brain/operations.md", seed: operationsSeed },
|
{ sub: "brain/operations.md", seed: operationsSeed },
|
||||||
|
{ sub: "brain/journal/index.md", seed: journalIndexSeed },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { sub, seed } of files) {
|
for (const { sub, seed } of files) {
|
||||||
|
|
|
||||||
103
scripts/brain/tests/okf-conform.test.ts
Normal file
103
scripts/brain/tests/okf-conform.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
|
import { initBrain } from "../src/scaffold.js";
|
||||||
|
import { parseProfile } from "../src/profile.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OKF-compatible-form conformance for the `brain/` knowledge bundle.
|
||||||
|
*
|
||||||
|
* Stage 1 of the cross-plugin OKF convergence (docs/okf-convergence-brief.md): the
|
||||||
|
* brain is the REFERENCE design, OKF is a thin interop veneer. Contract — mirrors
|
||||||
|
* `okr/scripts/okf-check.mjs`, the reference checker:
|
||||||
|
* - every CONCEPT file (`*.md` except `index.md`) carries a non-empty `type` in a
|
||||||
|
* leading YAML frontmatter block;
|
||||||
|
* - the bundle-root `index.md` carries an `okf_version` marker as markdown TEXT
|
||||||
|
* (index files carry no frontmatter per the OKF spec);
|
||||||
|
* - each directory level has its own `index.md` (progressive disclosure).
|
||||||
|
*
|
||||||
|
* EXCLUDED by design (brief §6): the `ingest/` tributary. `ingest/published/*.md`
|
||||||
|
* is the byte-exact gold store with a hard round-trip invariant (SC2) that a YAML
|
||||||
|
* frontmatter block would break — it is a raw tributary, not a navigable concept,
|
||||||
|
* and the hub `index.md` points to it rather than folding it in. This test walks
|
||||||
|
* `brain/` ONLY; it never asserts frontmatter on the tributary.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal frontmatter `type` reader. We only ever EMIT OKF frontmatter; we never add
|
||||||
|
* a YAML-parser dependency (the brain is deliberately YAML-free), so a constrained
|
||||||
|
* regex over the leading `---` block is the right reader here.
|
||||||
|
*/
|
||||||
|
function frontmatterType(text: string): string | null {
|
||||||
|
const block = text.match(/^---\n([\s\S]*?)\n---\n/);
|
||||||
|
if (!block) return null;
|
||||||
|
const t = block[1].match(/^type:\s*(.+?)\s*$/m);
|
||||||
|
return t ? t[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All concept files (`*.md` except `index.md`) under `root`, recursive. */
|
||||||
|
function walkConceptMd(root: string): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const walk = (dir: string) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("brain/ bundle is OKF-compatible form (Stage 1)", () => {
|
||||||
|
let root: string;
|
||||||
|
const prevEnv = process.env.LINKEDIN_STUDIO_DATA;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
root = mkdtempSync(join(tmpdir(), "brain-okf-"));
|
||||||
|
process.env.LINKEDIN_STUDIO_DATA = root;
|
||||||
|
initBrain();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (prevEnv === undefined) delete process.env.LINKEDIN_STUDIO_DATA;
|
||||||
|
else process.env.LINKEDIN_STUDIO_DATA = prevEnv;
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bundle-root index.md carries an okf_version marker (markdown text, no frontmatter)", () => {
|
||||||
|
const index = readFileSync(join(root, "brain/index.md"), "utf8");
|
||||||
|
assert.match(index, /^okf_version:\s*\S+/m, "root index.md declares okf_version");
|
||||||
|
assert.equal(frontmatterType(index), null, "index.md carries NO frontmatter (OKF reserved file)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("every concept file under brain/ carries a non-empty frontmatter type", () => {
|
||||||
|
const concepts = walkConceptMd(join(root, "brain"));
|
||||||
|
assert.ok(concepts.length > 0, "the bundle has concept files to check");
|
||||||
|
for (const f of concepts) {
|
||||||
|
const type = frontmatterType(readFileSync(f, "utf8"));
|
||||||
|
assert.ok(type && type.length > 0, `concept ${f} is missing a frontmatter type:`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("profile.md is type: Profile AND still parses (rich fields preserved — round-trip intact)", () => {
|
||||||
|
const text = readFileSync(join(root, "brain/profile.md"), "utf8");
|
||||||
|
assert.equal(frontmatterType(text), "Profile");
|
||||||
|
const doc = parseProfile(text);
|
||||||
|
assert.equal(doc.schemaVersion, 1, "line-grammar still parses through the frontmatter");
|
||||||
|
assert.ok(doc.static.length > 0, "the seeded static facts survive conformance");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("operations.md is type: Operations", () => {
|
||||||
|
const text = readFileSync(join(root, "brain/operations.md"), "utf8");
|
||||||
|
assert.equal(frontmatterType(text), "Operations");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("each directory level under brain/ has its own index.md (progressive disclosure)", () => {
|
||||||
|
assert.ok(existsSync(join(root, "brain/index.md")), "brain/index.md");
|
||||||
|
assert.ok(existsSync(join(root, "brain/journal/index.md")), "brain/journal/index.md");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue