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
This commit is contained in:
parent
d6acb5d36c
commit
8c927198f5
17 changed files with 1554 additions and 8 deletions
117
scripts/brain/src/scaffold.ts
Normal file
117
scripts/brain/src/scaffold.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* `initBrain` — the idempotent runtime scaffold for the second brain (SB-S0 D1/D5).
|
||||
*
|
||||
* Creates the `brain/` + `ingest/` tree under the per-user data dir (via the
|
||||
* `dataRoot` resolver, so `LINKEDIN_STUDIO_DATA` relocates it for tests + real
|
||||
* installs). Idempotent + migration-safe: a directory that exists is left alone,
|
||||
* a file that exists is NEVER rewritten (compare-then-skip, never blind-write) —
|
||||
* so a second run is a no-op and a user-edited file is preserved.
|
||||
*
|
||||
* NO session-start wiring (SB-S2 owns that) — this only ships the invokable init.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { dataRoot } from "./dataRoot.js";
|
||||
import { foldUserProfile, serializeProfile } from "./profile.js";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
/** The shipped template that defines the canonical profile field-set. */
|
||||
const TEMPLATE_PATH = join(HERE, "..", "..", "..", "config", "user-profile.template.md");
|
||||
/** The runtime instance (if the user copied + filled the template). */
|
||||
const INSTANCE_SUB = join("profile", "user-profile.md");
|
||||
|
||||
const SCAFFOLD_DIRS = ["brain/journal", "ingest/inbox", "ingest/published"];
|
||||
|
||||
function today(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function indexSeed(): string {
|
||||
return `# Brain — Index (MOC)
|
||||
|
||||
> 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).
|
||||
|
||||
| Tributary | What it holds | Freshness |
|
||||
|-----------|---------------|-----------|
|
||||
| voice-samples | writing style | — |
|
||||
| specifics-bank | lived raw material | — |
|
||||
| trends | external signal | — |
|
||||
| analytics | performance | — |
|
||||
| ingest | inbox + published (the gold signal) | — |
|
||||
|
||||
- [profile.md](profile.md) — semantic, two-layer (static + dynamic) profile
|
||||
- [operations.md](operations.md) — plans · ideas · the "who I am now" anchor
|
||||
- journal/ — episodic, append-only session log (the consolidation loop's source)
|
||||
`;
|
||||
}
|
||||
|
||||
function operationsSeed(): string {
|
||||
return `# Operations
|
||||
|
||||
## Who I am now (anchor)
|
||||
|
||||
<!-- Frozen-past-self guard: a periodic, user-authored "where I'm headed now"
|
||||
statement that deprecates older inferences. Update it when your direction shifts. -->
|
||||
|
||||
## Plans
|
||||
|
||||
## Ideas
|
||||
`;
|
||||
}
|
||||
|
||||
function profileSeed(): string {
|
||||
const templateText = readFileSync(TEMPLATE_PATH, "utf8");
|
||||
const instancePath = dataRoot(INSTANCE_SUB);
|
||||
const instanceText = existsSync(instancePath) ? readFileSync(instancePath, "utf8") : undefined;
|
||||
return serializeProfile(foldUserProfile({ templateText, instanceText, today: today() }));
|
||||
}
|
||||
|
||||
export interface InitResult {
|
||||
/** Data-relative paths created this run. */
|
||||
created: string[];
|
||||
/** Data-relative paths that already existed and were left untouched. */
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the brain scaffold idempotently. Returns the created/skipped split.
|
||||
* File content seeds are computed lazily, so an already-present file costs no
|
||||
* template read or fold.
|
||||
*/
|
||||
export function initBrain(): InitResult {
|
||||
const created: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const sub of SCAFFOLD_DIRS) {
|
||||
const abs = dataRoot(sub);
|
||||
if (existsSync(abs)) {
|
||||
skipped.push(sub);
|
||||
} else {
|
||||
mkdirSync(abs, { recursive: true });
|
||||
created.push(sub);
|
||||
}
|
||||
}
|
||||
|
||||
const files: Array<{ sub: string; seed: () => string }> = [
|
||||
{ sub: "brain/profile.md", seed: profileSeed },
|
||||
{ sub: "brain/index.md", seed: indexSeed },
|
||||
{ sub: "brain/operations.md", seed: operationsSeed },
|
||||
];
|
||||
|
||||
for (const { sub, seed } of files) {
|
||||
const abs = dataRoot(sub);
|
||||
if (existsSync(abs)) {
|
||||
skipped.push(sub);
|
||||
continue;
|
||||
}
|
||||
mkdirSync(dirname(abs), { recursive: true });
|
||||
writeFileSync(abs, seed(), "utf8");
|
||||
created.push(sub);
|
||||
}
|
||||
|
||||
return { created, skipped };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue