SB-S0 (Foundation) of the second-brain arc: a new TS package scripts/brain/
(structural copy of specifics-bank) establishing the spine later slices hang on.
TDD throughout (failing test first); fold scope = P1+P2 only per operator decision.
- types.ts: one provenance vocab (human|published|ai-draft) + the six-field
ProfileFact record + two-layer ProfileDoc.
- id.ts: slugify + mintEntityId (sha256[:12], kind-namespaced, slug-keyed so the
id is stable across value edits) + normalizeProvenance (throws on unknown). [SC4]
- profile.ts: no-YAML line-grammar parse/serialize (parse∘serialize = identity over
the whole doc; values may contain ]/|/quotes) [SC2] + foldUserProfile: lossless,
idempotent, source-absent-aware fold of config/user-profile.template.md. Pinned
extraction — P1 labeled scalars (group-headers skipped, [placeholder]→empty) + P2
expertise group; stops at "### Research Tooling" so deferred explainer prose can't
leak in as fields. Checkbox-prefs (Goals/Tone/MCPs/Assets) deferred (§8). [SC3]
- dataRoot.ts: inline per-package resolver (repo idiom; no new seam → SC6).
- scaffold.ts/cli.ts: idempotent `brain init` — brain/{index,profile,operations}.md
+ journal/ + ingest/{inbox,published} under the data-dir; compare-then-skip, never
clobbers a user edit. No session-start wiring (SB-S2 owns it). [SC1]
Gate: new BRAIN floor in test-runner.sh (34 tests; trends/specifics/contract floors
unchanged) + anti-erosion floor 74→75. Full gate 90/0/0, tsc --noEmit clean. [SC5/SC6]
No new command/agent/reference → no version bump, no structure-count change.
Refs docs/second-brain/{brief,plan-sb-s0,architecture}.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
import { describe, test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { parseProfile, serializeProfile } from "../src/profile.js";
|
|
import { SCHEMA_VERSION } from "../src/types.js";
|
|
import type { ProfileDoc, ProfileFact } from "../src/types.js";
|
|
|
|
const fact = (over: Partial<ProfileFact> = {}): ProfileFact => ({
|
|
id: "abc123def456",
|
|
value: "a value",
|
|
first_seen: "2026-06-23",
|
|
last_seen: "2026-06-23",
|
|
evidence_count: 0,
|
|
provenance: "human",
|
|
status: "active",
|
|
...over,
|
|
});
|
|
|
|
describe("profile line-grammar (SC2)", () => {
|
|
test("serialized text exposes both section headers", () => {
|
|
const text = serializeProfile({ schemaVersion: SCHEMA_VERSION, static: [], dynamic: [] });
|
|
assert.match(text, /^## Static$/m);
|
|
assert.match(text, /^## Dynamic$/m);
|
|
assert.match(text, /^schemaVersion: 1$/m);
|
|
});
|
|
|
|
test("parse ∘ serialize is identity over the whole doc", () => {
|
|
const doc: ProfileDoc = {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
static: [
|
|
fact({ id: "0123456789ab", value: "Kjell Tore", provenance: "human" }),
|
|
fact({ id: "fedcba987654", value: "", evidence_count: 3, provenance: "published" }),
|
|
],
|
|
dynamic: [
|
|
fact({ id: "aaaabbbbcccc", value: "leans contrarian", status: "superseded", last_seen: "2026-06-20" }),
|
|
],
|
|
};
|
|
assert.deepEqual(parseProfile(serializeProfile(doc)), doc);
|
|
});
|
|
|
|
test("a value containing ], | and quotes round-trips intact", () => {
|
|
const tricky = 'uses "scare quotes" | pipes | and a ] bracket';
|
|
const doc: ProfileDoc = {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
static: [fact({ value: tricky })],
|
|
dynamic: [],
|
|
};
|
|
const round = parseProfile(serializeProfile(doc));
|
|
assert.equal(round.static[0].value, tricky);
|
|
assert.deepEqual(round, doc);
|
|
});
|
|
|
|
test("an empty-value fact round-trips to an empty string (the source-absent case)", () => {
|
|
const doc: ProfileDoc = {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
static: [fact({ value: "" })],
|
|
dynamic: [],
|
|
};
|
|
const round = parseProfile(serializeProfile(doc));
|
|
assert.equal(round.static[0].value, "");
|
|
assert.deepEqual(round, doc);
|
|
});
|
|
|
|
test("section attribution is preserved (static vs dynamic do not bleed)", () => {
|
|
const doc: ProfileDoc = {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
static: [fact({ id: "111111111111", value: "static one" })],
|
|
dynamic: [fact({ id: "222222222222", value: "dynamic one" })],
|
|
};
|
|
const round = parseProfile(serializeProfile(doc));
|
|
assert.equal(round.static.length, 1);
|
|
assert.equal(round.dynamic.length, 1);
|
|
assert.equal(round.static[0].value, "static one");
|
|
assert.equal(round.dynamic[0].value, "dynamic one");
|
|
});
|
|
|
|
test("an unsupported schemaVersion throws", () => {
|
|
assert.throws(() => parseProfile("# Profile\n\nschemaVersion: 99\n\n## Static\n\n## Dynamic\n"));
|
|
});
|
|
});
|