Bump BRAIN_TESTS_FLOOR to 82 (SB-S2 adds consolidate(12)+consolidate-cli(7)). No new test-runner section → ASSERT_BASELINE_FLOOR unchanged at 78 (the hook SC6 test runs separately via `node --test hooks/scripts/__tests__/*.test.mjs`, not the structure gate). Add docs/second-brain/consolidation-loop.md (CLI usage, engine rules, the candidate-file session↔engine contract, operator gate, honest limits incl. no-reader-until-S3). Gate 93/0/0; hook suite 136/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
4.4 KiB
Consolidation loop — the compounding mechanism (SB-S2)
How the second brain turns the published gold signal into an ever-improving, drift-resistant
brain/profile.md— operator-invoked, operator-gated, deterministic. Part of the second-brain arc (architecture.md); landed in SB-S2.
The shape
The loop is operator-invoked ("sleep-time" = when you run it), not automatic — the session-start hook is zero-dep and cannot run AI, so it only nudges when consolidation is due. The pass itself is:
brain consolidate --gather # 1. dump new published deltas + the current profile
→ (the session reads them and extracts a Candidate[] JSON)
brain consolidate --propose --candidates cand.json # 2. deterministic diff → brain/pending-diff.{md,json}
→ (you review brain/pending-diff.md — the [OPERATØR] gate)
brain consolidate --apply --diff brain/pending-diff.json --confirm # 3. the ONLY write to profile.md
--apply records brain/consolidation-state.json {last_run}; the session-start nudge reads it +
counts ingest/published/*.md to know when to nudge again. Roll back any apply via git.
The deterministic engine (scripts/brain/src/consolidate.ts)
proposeDiff({current, candidates, today, opts}) classifies each candidate (matched to existing facts
by the candidate's key):
| Rule | Condition | Effect |
|---|---|---|
| reject | provenance: ai-draft |
dropped — never learns from the engine's own drafts (model-collapse guard) |
| add | no matching fact, provenance published/human |
new dynamic fact, evidence_count: 1 |
| evidence-bump | matching fact, same value | evidence_count++, last_seen = today |
| promote | a dynamic fact reaches N = 3 observations |
dynamic → static |
| conflict | matching key, different value | keep both, timestamped, with DISTINCT ids; the old fact is untouched (no supersede in S2 — that's S3) |
| decay-flag | a dynamic fact's last_seen > 90 days |
listed in staleFlags (informational; never auto-removed) |
Id model (no duplicate ids): a concept's primary fact id is mintEntityId({kind:'observed', key});
a conflict alt fact id is mintContentId('observed-alt:'+key+'::'+value+'::'+date) — byte-distinct, so
two facts never share an id. The SB-S0 folded profile-field static seeds use a different kind, so
consolidation never mutates them (immutable in S2). applyDiff produces a ProfileDoc that round-trips
exactly through the SB-S0 grammar; re-running is idempotent (bump, not duplicate).
Defaults: promoteThreshold = 3, decayDays = 90 (operator-confirmed).
The candidate file — the session↔engine contract
--propose --candidates <file.json> takes a JSON array of candidates; each is validated
(malformed → non-zero exit, nothing written):
[
{ "key": "primary-expertise", "value": "AI governance in the public sector",
"provenance": "published", "source": "published:1a2b3c4d5e6f", "observed_date": "2026-05-26" }
]
key,value,source,observed_date— non-empty strings;provenance ∈ {human, published, ai-draft}.keyandvaluemust be single-line (no newline/CR — the profile grammar is one fact per line).- The session produces this from
--gather's output. The engine guarantees the mechanics; the quality of the candidates is the session's job (see limits).
Honest limits
- The loop's value depends on the session's extraction. The engine only guarantees threshold/conflict/ decay/provenance mechanics. Garbage candidates → a garbage diff. The operator gate + candidate-shape validation catch shape errors, not insight quality.
brain/profile.mdhas no reader yet. S2 evolves the profile; wiring content agents/commands to consume it is SB-S3. The value is deferred: the profile compounds now so S3's reader inherits rich data.- No supersede / no auto-demotion. Conflicts keep both; stale facts are flagged, never auto-removed — the operator (or S3) reconciles. Conflict alt facts persist until then.
- No AI at session-start. The nudge is a deterministic file-count + sidecar read; the consolidation pass is always operator-invoked.
- The session-start nudge is consolidation-due only — it counts published records + days since last run;
it does not parse
profile.mdfor per-fact staleness (that cost/parser is deferred).