feat(linkedin-studio): SB-S3b — supersede arm in the consolidation engine [skip-docs]
The engine can now RETIRE a fact via an operator-gated, explicitly-signalled
temporal-update supersede — completing the consolidation motor's one S2 TODO
("no supersede in S2 — that's S3"). A `supersedes` signal on a candidate retires
the stale fact (re-minted to an archival id, status: superseded, REPLACED IN
PLACE, retained as audit) and installs the new winner under the canonical key-id,
so mintEntityId(key) always points at the live fact.
- consolidate.ts: Candidate.supersedes? + ProfileDiff.supersedes (SupersedeOp
carries the full winner fact, so applyDiff stays a pure projector). proposeDiff
value-guarded routing fork (routes only when the target holds a DIFFERENT value —
else a re-sent signal would self-supersede every run) + intra-batch
first-supersede-wins guard. applyDiff replace-in-place + value-matched state-check
(oldId present, active, value===oldValue) → idempotent re-apply + stale-diff safe;
superseded facts never bumped/promoted (supersede wins). Decay excludes superseded.
archivalId seeded with the pre-archival id (collision-free).
- cli.ts: renderDiffMd `## Supersessions (old → new)` (rendered last, only when
present → zero-supersession diffs stay byte-identical); validateCandidates optional
single-line `supersedes`; `--gather` profileFacts filtered to active (superseded
archival facts never re-presented as live context).
- Tests: +12 brain (consolidate 10 + consolidate-cli 2). TDD: RED (6 fail) → GREEN
(94/94). BRAIN_TESTS_FLOOR 82->94; ASSERT_BASELINE_FLOOR unchanged at 80 (no new
test-runner.sh section). Gate 95/0/0.
- Docs: consolidation-loop.md rule table + honest-limit reconciled (the operator
gate is the only classification net); engine docstring updated.
All 13 success criteria deterministically tested (unlike S3a, no behavioural-only
SC). READ-only gate unchanged — brain consolidate --apply --confirm stays the sole
profile.md writer. Scope held: scripts/brain/ only; temporal-update only
(condition-dependent/distractor deferred).
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
d2ec7482be
commit
585f972a05
6 changed files with 267 additions and 19 deletions
|
|
@ -141,6 +141,10 @@ function renderDiffMd(diff: ProfileDiff): string {
|
|||
section("Promotions (dynamic→static)", diff.promotions.map((p) => p.id));
|
||||
section("⚠ Conflicts (both kept)", diff.conflicts.map((c) => `"${c.primaryValue}" (${c.primaryId}) vs new (${c.altId})`));
|
||||
section("Stale (dynamic, >decay)", diff.staleFlags.map((s) => `${s.id} — last seen ${s.last_seen} (${s.daysStale}d)`));
|
||||
// SB-S3b: rendered LAST and only when present, so a zero-supersession diff stays byte-identical.
|
||||
if ((diff.supersedes ?? []).length > 0) {
|
||||
section("Supersessions (old → new)", diff.supersedes.map((s) => `\`${s.oldValue}\` → \`${s.winner.value}\` [${s.winner.provenance}, retired ${s.oldId}]`));
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +156,10 @@ function validateCandidates(raw: unknown): Candidate[] {
|
|||
}
|
||||
if (!PROVENANCES.includes(c.provenance)) usage(`candidate ${i}: provenance must be one of ${PROVENANCES.join(", ")}`);
|
||||
if (/[\n\r]/.test(c.key) || /[\n\r]/.test(c.value)) usage(`candidate ${i}: key/value must be single-line (no newline/CR)`);
|
||||
// SB-S3b: the optional supersede signal, when present, is a non-empty single-line target key.
|
||||
if (c.supersedes !== undefined && (typeof c.supersedes !== "string" || c.supersedes === "" || /[\n\r]/.test(c.supersedes))) {
|
||||
usage(`candidate ${i}: "supersedes" must be a non-empty single-line string when present`);
|
||||
}
|
||||
});
|
||||
return raw as Candidate[];
|
||||
}
|
||||
|
|
@ -171,7 +179,8 @@ function runConsolidate(flags: Record<string, string>): void {
|
|||
const fresh = records.filter((r) => last_run === null || r.published_date > last_run);
|
||||
const profile = loadProfile();
|
||||
if (flags.json === "true") {
|
||||
console.log(JSON.stringify({ since: last_run, published: fresh.map((r) => ({ id: r.id, published_date: r.published_date, body: r.body })), profileFacts: [...profile.static, ...profile.dynamic] }, null, 2));
|
||||
// SB-S3b: only ACTIVE facts are live context — superseded archival facts must not be re-presented to the extraction session.
|
||||
console.log(JSON.stringify({ since: last_run, published: fresh.map((r) => ({ id: r.id, published_date: r.published_date, body: r.body })), profileFacts: [...profile.static, ...profile.dynamic].filter((f) => f.status === "active") }, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(`Consolidation gather — ${fresh.length} new published record(s) since ${last_run ?? "the beginning"}:`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue