linkedin-studio/scripts/brain/tests/profile.test.ts
Kjell Tore Guttormsen 8c927198f5 feat(linkedin-studio): second-brain SB-S0 foundation — brain/ scaffold + profile fold + id/provenance spine [skip-docs]
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
2026-06-23 14:05:24 +02:00

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"));
});
});