From f91ffddc8c524cb555751ab5c37e88a9333965c7 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Tue, 23 Jun 2026 17:09:48 +0200 Subject: [PATCH] =?UTF-8?q?chore(linkedin-studio):=20SB-S2=20gate=20brain?= =?UTF-8?q?=20floor=2063=E2=86=9282=20+=20consolidation-loop=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4 --- docs/second-brain/consolidation-loop.md | 74 +++++++++++++++++++++++++ scripts/test-runner.sh | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 docs/second-brain/consolidation-loop.md diff --git a/docs/second-brain/consolidation-loop.md b/docs/second-brain/consolidation-loop.md new file mode 100644 index 0000000..f88b852 --- /dev/null +++ b/docs/second-brain/consolidation-loop.md @@ -0,0 +1,74 @@ +# 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 ` takes a JSON **array** of candidates; each is validated +(malformed → non-zero exit, nothing written): + +```json +[ + { "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}`. +- `key` and `value` must 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.md` has 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.md` for per-fact staleness (that cost/parser is deferred). diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 76b128b..50ba718 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -711,7 +711,7 @@ if [ -x "$BR_DIR/node_modules/.bin/tsx" ]; then BR_OUT=$( set +e; (cd "$BR_DIR" && npm test) 2>&1; echo "BR_EXIT:$?" ) BR_EXIT=$(echo "$BR_OUT" | grep -oE 'BR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1) BR_TESTS=$(echo "$BR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1) - BRAIN_TESTS_FLOOR=63 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + BRAIN_TESTS_FLOOR=82 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + SB-S2 19 [consolidate(12)+consolidate-cli(7)] if [ "$BR_EXIT" = "0" ] && [ -n "$BR_TESTS" ] && [ "$BR_TESTS" -ge "$BRAIN_TESTS_FLOOR" ]; then pass "brain suite green: $BR_TESTS tests pass (floor $BRAIN_TESTS_FLOOR)" else