@gamaze/hicortex 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/consolidate.d.ts +4 -3
- package/dist/consolidate.js +37 -13
- package/dist/db.js +83 -3
- package/dist/distiller.js +19 -12
- package/dist/eval/recall-sweep.d.ts +23 -0
- package/dist/eval/recall-sweep.js +288 -2
- package/dist/eval/relevance-eval.d.ts +64 -0
- package/dist/eval/relevance-eval.js +1954 -0
- package/dist/index.js +16 -6
- package/dist/init.d.ts +1 -1
- package/dist/init.js +53 -100
- package/dist/lessons-context.js +3 -2
- package/dist/llm.d.ts +3 -1
- package/dist/llm.js +18 -4
- package/dist/mcp-server.js +24 -19
- package/dist/memory-instructions.js +1 -1
- package/dist/prompts.js +22 -6
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +7 -2
- package/dist/recall-index.d.ts +76 -6
- package/dist/recall-index.js +101 -22
- package/dist/retrieval.d.ts +52 -1
- package/dist/retrieval.js +144 -23
- package/dist/seed-lesson.d.ts +1 -1
- package/dist/seed-lesson.js +1 -1
- package/dist/storage.d.ts +56 -3
- package/dist/storage.js +93 -17
- package/dist/types.d.ts +4 -0
- package/dist/uninstall.js +18 -4
- package/hermes-plugin/hicortex/client.py +8 -4
- package/hermes-plugin/hicortex/config.py +11 -0
- package/hermes-plugin/hicortex/provider.py +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/skills/hicortex-activate/SKILL.md +0 -53
- package/skills/hicortex-learn/SKILL.md +0 -40
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ The run is resumable — interrupt it any time and it continues where it stopped
|
|
|
122
122
|
- **hicortex_update** — Fix incorrect memories (re-embeds on content change)
|
|
123
123
|
- **hicortex_delete** — Remove memories with cascade cleanup
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Explicit learnings: call `hicortex_ingest` directly (capture is otherwise automatic, nightly).
|
|
126
126
|
|
|
127
127
|
## Context Layer
|
|
128
128
|
|
|
@@ -202,6 +202,7 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
202
202
|
| `models` | Optional nested per-stage model overrides (`score`/`distill`/`reflect`/`classify`) — see [Advanced: per-stage models](#advanced-per-stage-models) |
|
|
203
203
|
| `distillFallback` | `"strict"` (default) — abort on remote distill failure, retry next run; `"local"` — fall back to base model (lower quality, 0.9.0 behaviour) |
|
|
204
204
|
| `authToken` | Bearer token for endpoint auth. Generated on first `init` in server mode. Find the active token with `hicortex status` or in `~/.hicortex/config.json`. |
|
|
205
|
+
| `corsAllowedOrigins` | Browser origins allowed to read cross-origin responses, e.g. `["https://ui.example.com"]`. **Empty by default** — the server sends no `Access-Control-Allow-Origin` and never `Allow-Credentials`, so no external web page can read its data. The bundled `/viz` and `/context/ui` pages are same-origin and need no entry. |
|
|
205
206
|
| `licenseKey` | Commercial license key (optional; for display in `hicortex status`) |
|
|
206
207
|
| `domains` | Your memory domain list (`[{name, description}]`). Scaffolded by `init`; edit freely — see [Memory Domains & Tags](#memory-domains--tags) |
|
|
207
208
|
| `weakPrimaryFloor` | Minimum similarity for a no-fit memory to keep a weak domain association (default: 0.45) |
|
|
@@ -225,6 +226,7 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
225
226
|
| `recallMinSimilarity` | Relevance floor for index entries (default: 0.55; text-search matches always pass) |
|
|
226
227
|
| `recallReshowTurns` | Turns before an already-shown memory may reappear in the same session (default: 30) |
|
|
227
228
|
| `recallMinPromptChars` | Prompts shorter than this skip the recall index (default: 20) |
|
|
229
|
+
| `recallTitleChars` | Chars of each memory's first line shown in an index entry (default: 150, range 40–400). Raised from 100 on 2026-08-02: with topic-first memory titles, 150 chars carries the subject *and* its claim, where 100 cut the claim mid-sentence. Costs roughly +74 tokens per 6-line block |
|
|
228
230
|
| `sessionIntentWeight` | Blend weight of the session-intent rolling centroid in the recall search vector: `query = (1-w)·prompt + w·centroid` (default: 0.33; set 0 to disable — pure-prompt recall, the kill-switch). The first turn of a session searches with pure prompt and seeds the centroid; subsequent turns blend so recall follows the session's intent instead of being query-literal. The EMA rate (0.4) is a shipped constant, not configurable |
|
|
229
231
|
| `dedupMergeThreshold` | Minimum cosine similarity for `hicortex dedup` to cluster memories as near-duplicates (default: 0.92) |
|
|
230
232
|
| `supersessionMinSimilarity` | Minimum cosine similarity for a nightly supersession candidate pair (default: 0.80) |
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -162,9 +162,10 @@ export declare function buildSupersessionPrompt(oldContent: string, newContent:
|
|
|
162
162
|
export declare function parseSupersessionReply(reply: string): boolean | null;
|
|
163
163
|
/**
|
|
164
164
|
* Nightly supersession-detection stage. Scans memories/rowid > cursor whose
|
|
165
|
-
* shape
|
|
166
|
-
* same-shape neighbors, and links confirmed
|
|
167
|
-
* discovery + the free idempotency check only —
|
|
165
|
+
* shape is supersedable (decision/correction/fact/state — isSupersedableShape),
|
|
166
|
+
* checks each against its older same-shape neighbors, and links confirmed
|
|
167
|
+
* supersessions. Dry-run performs discovery + the free idempotency check only —
|
|
168
|
+
* no LLM calls, no writes, no
|
|
168
169
|
* cursor persistence (mirrors stageImportance/stageContentDomains's dry-run
|
|
169
170
|
* convention of never spending budget on a preview).
|
|
170
171
|
*
|
package/dist/consolidate.js
CHANGED
|
@@ -307,7 +307,10 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
307
307
|
const severity = String(lo.severity ?? "important");
|
|
308
308
|
const confidence = String(lo.confidence ?? "medium");
|
|
309
309
|
const sourcePattern = String(lo.source_pattern ?? "");
|
|
310
|
-
|
|
310
|
+
// No `## Lesson:` prefix: memory_type='lesson' carries the type, and the
|
|
311
|
+
// text is the topic-first first line (display reads the first line, not a
|
|
312
|
+
// header parse — see lessons-context.ts / index.ts).
|
|
313
|
+
let content = `${lessonText}\n\n`;
|
|
311
314
|
content += `**Type:** ${lessonType}\n`;
|
|
312
315
|
content += `**Severity:** ${severity}\n`;
|
|
313
316
|
content += `**Confidence:** ${confidence}\n`;
|
|
@@ -857,11 +860,21 @@ const SUPERSESSION_NEIGHBOR_POOL = 15;
|
|
|
857
860
|
const SUPERSESSION_NEIGHBOR_TOP_K = 5;
|
|
858
861
|
/** Candidate rows read per SQL page (call budget stops the loop well before this in practice). */
|
|
859
862
|
const SUPERSESSION_BATCH_SIZE = 500;
|
|
860
|
-
/**
|
|
861
|
-
|
|
863
|
+
/**
|
|
864
|
+
* A memory whose content/type marks it as a SUPERSEDABLE claim — one a newer
|
|
865
|
+
* memory about the same subject can replace. Decisions and corrections were the
|
|
866
|
+
* original scope; plain facts and project-state updates were added because an
|
|
867
|
+
* updated fact ("distillModel is X" → later "is Y") otherwise never gets a
|
|
868
|
+
* superseded_by link and both versions compete in recall forever. Ordinary
|
|
869
|
+
* episodic chatter and problem/solution history stay excluded: they record
|
|
870
|
+
* events, not mutable state, so there is nothing to supersede.
|
|
871
|
+
*/
|
|
872
|
+
function isSupersedableShape(mem) {
|
|
862
873
|
return (mem.memory_type === "decision" ||
|
|
863
874
|
mem.content.includes("[Decisions Made]") ||
|
|
864
|
-
mem.content.includes("[Corrections & Rejections]")
|
|
875
|
+
mem.content.includes("[Corrections & Rejections]") ||
|
|
876
|
+
mem.content.includes("[Facts Learned]") ||
|
|
877
|
+
mem.content.includes("[Project State Changes]"));
|
|
865
878
|
}
|
|
866
879
|
/** True when a `superseded_by` link already exists between the pair, either direction. */
|
|
867
880
|
function alreadySupersedeLinked(db, oldId, newId) {
|
|
@@ -881,9 +894,12 @@ function buildSupersessionPrompt(oldContent, newContent) {
|
|
|
881
894
|
return (`You are checking whether a NEWER memory supersedes an OLDER one in an AI agent's long-term memory.\n\n` +
|
|
882
895
|
`OLDER MEMORY:\n${trunc(oldContent)}\n\n` +
|
|
883
896
|
`NEWER MEMORY:\n${trunc(newContent)}\n\n` +
|
|
884
|
-
`Does the NEWER memory reverse, replace, or invalidate the OLDER one — e.g. a later decision
|
|
885
|
-
`earlier one,
|
|
886
|
-
`
|
|
897
|
+
`Does the NEWER memory reverse, replace, update, or invalidate the OLDER one — e.g. a later decision ` +
|
|
898
|
+
`overturns an earlier one, a correction retracts a prior claim, or a later fact updates the SAME subject's ` +
|
|
899
|
+
`value/status that has since changed (e.g. "model is X" → "model is Y")? Reply true ONLY for a genuine ` +
|
|
900
|
+
`replacement of the same fact/decision. Two memories that are merely related, or that can both still be ` +
|
|
901
|
+
`true — even about the same project or entity (different facts, an addition, an elaboration) — are NOT a ` +
|
|
902
|
+
`supersession.\n` +
|
|
887
903
|
`Reply with ONLY a JSON object, no prose: {"superseded": true} or {"superseded": false}.`);
|
|
888
904
|
}
|
|
889
905
|
/**
|
|
@@ -932,16 +948,17 @@ async function findOlderNeighbors(db, candidate, embedFn, minSimilarity) {
|
|
|
932
948
|
return storage
|
|
933
949
|
.vectorSearch(db, embedding, SUPERSESSION_NEIGHBOR_POOL, [candidate.id])
|
|
934
950
|
.filter((n) => n.created_at < candidate.created_at &&
|
|
935
|
-
|
|
951
|
+
isSupersedableShape(n) &&
|
|
936
952
|
(0, retrieval_js_1.l2ToCosine)(n.distance) >= minSimilarity)
|
|
937
953
|
.sort((a, b) => (0, retrieval_js_1.l2ToCosine)(b.distance) - (0, retrieval_js_1.l2ToCosine)(a.distance))
|
|
938
954
|
.slice(0, SUPERSESSION_NEIGHBOR_TOP_K);
|
|
939
955
|
}
|
|
940
956
|
/**
|
|
941
957
|
* Nightly supersession-detection stage. Scans memories/rowid > cursor whose
|
|
942
|
-
* shape
|
|
943
|
-
* same-shape neighbors, and links confirmed
|
|
944
|
-
* discovery + the free idempotency check only —
|
|
958
|
+
* shape is supersedable (decision/correction/fact/state — isSupersedableShape),
|
|
959
|
+
* checks each against its older same-shape neighbors, and links confirmed
|
|
960
|
+
* supersessions. Dry-run performs discovery + the free idempotency check only —
|
|
961
|
+
* no LLM calls, no writes, no
|
|
945
962
|
* cursor persistence (mirrors stageImportance/stageContentDomains's dry-run
|
|
946
963
|
* convention of never spending budget on a preview).
|
|
947
964
|
*
|
|
@@ -965,9 +982,16 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
965
982
|
const maxCalls = validNumber(options.maxCalls, exports.DEFAULT_SUPERSESSION_MAX_CALLS, (n) => n >= 0);
|
|
966
983
|
const startCursor = (0, state_js_1.loadState)(stateDir).supersessionCursor ?? 0;
|
|
967
984
|
const rows = db
|
|
968
|
-
.prepare(
|
|
985
|
+
.prepare(
|
|
986
|
+
// Candidate shape must mirror isSupersedableShape() exactly — keep the two
|
|
987
|
+
// in lockstep (an inline SQL copy, so drift here silently narrows scope).
|
|
988
|
+
`SELECT rowid AS __rowid, * FROM memories
|
|
969
989
|
WHERE rowid > ?
|
|
970
|
-
AND (memory_type = 'decision'
|
|
990
|
+
AND (memory_type = 'decision'
|
|
991
|
+
OR content LIKE '%[Decisions Made]%'
|
|
992
|
+
OR content LIKE '%[Corrections & Rejections]%'
|
|
993
|
+
OR content LIKE '%[Facts Learned]%'
|
|
994
|
+
OR content LIKE '%[Project State Changes]%')
|
|
971
995
|
ORDER BY rowid ASC LIMIT ?`)
|
|
972
996
|
.all(startCursor, SUPERSESSION_BATCH_SIZE);
|
|
973
997
|
let scanned = 0;
|
package/dist/db.js
CHANGED
|
@@ -126,20 +126,33 @@ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
|
126
126
|
CREATE INDEX IF NOT EXISTS idx_links_source ON memory_links(source_id);
|
|
127
127
|
CREATE INDEX IF NOT EXISTS idx_links_target ON memory_links(target_id);
|
|
128
128
|
`;
|
|
129
|
+
// Fielded FTS5 (#205, migration v10 "fts_fielded"): three columns so bm25()
|
|
130
|
+
// can weight matches per field (body / project / domain). Column order matters
|
|
131
|
+
// — the weights passed to `bm25(memories_fts, w_body, w_project, w_domain)` in
|
|
132
|
+
// storage.searchFts are positional on this declaration. `domain` reads the
|
|
133
|
+
// derived PRIMARY `memories.domain` (the argmax-weight tag from the classifier,
|
|
134
|
+
// set nightly), NOT a memory_tags join — that was judged too fiddly for this
|
|
135
|
+
// phase (a multi-table FTS trigger is fragile and rebuilds on every tag edit).
|
|
136
|
+
// `content_rowid='rowid'` is preserved for lockstep with the legacy schema.
|
|
129
137
|
const FTS_SCHEMA = `
|
|
130
138
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
131
139
|
content,
|
|
140
|
+
project,
|
|
141
|
+
domain,
|
|
132
142
|
content_rowid='rowid'
|
|
133
143
|
);
|
|
134
144
|
|
|
135
145
|
CREATE TRIGGER IF NOT EXISTS memories_fts_insert AFTER INSERT ON memories
|
|
136
146
|
BEGIN
|
|
137
|
-
INSERT INTO memories_fts (rowid, content
|
|
147
|
+
INSERT INTO memories_fts (rowid, content, project, domain)
|
|
148
|
+
VALUES (NEW.rowid, NEW.content, NEW.project, NEW.domain);
|
|
138
149
|
END;
|
|
139
150
|
|
|
140
|
-
CREATE TRIGGER IF NOT EXISTS memories_fts_update AFTER UPDATE OF content ON memories
|
|
151
|
+
CREATE TRIGGER IF NOT EXISTS memories_fts_update AFTER UPDATE OF content, project, domain ON memories
|
|
141
152
|
BEGIN
|
|
142
|
-
UPDATE memories_fts
|
|
153
|
+
UPDATE memories_fts
|
|
154
|
+
SET content = NEW.content, project = NEW.project, domain = NEW.domain
|
|
155
|
+
WHERE rowid = NEW.rowid;
|
|
143
156
|
END;
|
|
144
157
|
|
|
145
158
|
CREATE TRIGGER IF NOT EXISTS memories_fts_delete AFTER DELETE ON memories
|
|
@@ -361,6 +374,73 @@ const MIGRATIONS = [
|
|
|
361
374
|
db.exec("CREATE INDEX IF NOT EXISTS idx_dedup_log_source_session ON dedup_log(source_session)");
|
|
362
375
|
},
|
|
363
376
|
},
|
|
377
|
+
{
|
|
378
|
+
version: 10,
|
|
379
|
+
name: "fts_fielded",
|
|
380
|
+
up: (db) => {
|
|
381
|
+
// #205 fielded BM25F. FTS5 cannot be ALTERed (see the virtual-table rule
|
|
382
|
+
// above), so the single-column `memories_fts(content)` from pre-v10 must
|
|
383
|
+
// be dropped + recreated multi-column (`content, project, domain`) and
|
|
384
|
+
// rebuilt from the canonical `memories` rows. The runner wraps up() in a
|
|
385
|
+
// single transaction (db.ts:216) — that transaction IS the crash window:
|
|
386
|
+
// a power loss mid-migration rolls back, leaving the OLD single-column
|
|
387
|
+
// FTS intact (recall falls back to vector-only until the next open;
|
|
388
|
+
// never silently empty). The nightly capture lock does NOT cover
|
|
389
|
+
// initDb migrations, so the tx guard is the only safety net.
|
|
390
|
+
//
|
|
391
|
+
// Idempotent by construction: DROP IF EXISTS + CREATE + rebuild. On a
|
|
392
|
+
// fresh DB (where FTS_SCHEMA already created the multi-column form) this
|
|
393
|
+
// is a 0-row rebuild — wasted but harmless. On a legacy single-column DB
|
|
394
|
+
// it converts in place. Re-running against an already-migrated DB is a
|
|
395
|
+
// no-op shape + a refill of the same rows.
|
|
396
|
+
//
|
|
397
|
+
// Triggers are dropped + recreated to pick up the new column list (the
|
|
398
|
+
// pre-v10 update trigger fired only on `UPDATE OF content`; the new one
|
|
399
|
+
// also fires on project/domain updates so a tag reclassification lands
|
|
400
|
+
// in FTS without a content edit).
|
|
401
|
+
db.exec("DROP TRIGGER IF EXISTS memories_fts_insert");
|
|
402
|
+
db.exec("DROP TRIGGER IF EXISTS memories_fts_update");
|
|
403
|
+
db.exec("DROP TRIGGER IF EXISTS memories_fts_delete");
|
|
404
|
+
db.exec("DROP TABLE IF EXISTS memories_fts");
|
|
405
|
+
db.exec(`
|
|
406
|
+
CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
407
|
+
content,
|
|
408
|
+
project,
|
|
409
|
+
domain,
|
|
410
|
+
content_rowid='rowid'
|
|
411
|
+
)
|
|
412
|
+
`);
|
|
413
|
+
// COALESCE on project/domain because FTS5 stores NULL as no-tokens,
|
|
414
|
+
// which is what we want for unscoped memories (NULL domain = not yet
|
|
415
|
+
// classified; NULL project = no cwd-derived label). content is NOT NULL
|
|
416
|
+
// by the insert contract.
|
|
417
|
+
db.exec(`
|
|
418
|
+
INSERT INTO memories_fts (rowid, content, project, domain)
|
|
419
|
+
SELECT rowid, content, COALESCE(project, ''), COALESCE(domain, '') FROM memories
|
|
420
|
+
`);
|
|
421
|
+
db.exec(`
|
|
422
|
+
CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories
|
|
423
|
+
BEGIN
|
|
424
|
+
INSERT INTO memories_fts (rowid, content, project, domain)
|
|
425
|
+
VALUES (NEW.rowid, NEW.content, NEW.project, NEW.domain);
|
|
426
|
+
END
|
|
427
|
+
`);
|
|
428
|
+
db.exec(`
|
|
429
|
+
CREATE TRIGGER memories_fts_update AFTER UPDATE OF content, project, domain ON memories
|
|
430
|
+
BEGIN
|
|
431
|
+
UPDATE memories_fts
|
|
432
|
+
SET content = NEW.content, project = NEW.project, domain = NEW.domain
|
|
433
|
+
WHERE rowid = NEW.rowid;
|
|
434
|
+
END
|
|
435
|
+
`);
|
|
436
|
+
db.exec(`
|
|
437
|
+
CREATE TRIGGER memories_fts_delete AFTER DELETE ON memories
|
|
438
|
+
BEGIN
|
|
439
|
+
DELETE FROM memories_fts WHERE rowid = OLD.rowid;
|
|
440
|
+
END
|
|
441
|
+
`);
|
|
442
|
+
},
|
|
443
|
+
},
|
|
364
444
|
];
|
|
365
445
|
/**
|
|
366
446
|
* Run all pending migrations against the database.
|
package/dist/distiller.js
CHANGED
|
@@ -314,6 +314,14 @@ async function distillChunk(llm, transcript, projectName, date) {
|
|
|
314
314
|
return { entries: [], dropped: [] };
|
|
315
315
|
}
|
|
316
316
|
const parsed = parseDistilledEntries(result);
|
|
317
|
+
// Smoke alarm (PR #218 review): the prompt enforces topic-first, but models
|
|
318
|
+
// sometimes ignore constraints (cf. the prior max-15-bullet failure). Count
|
|
319
|
+
// entries that still look actor-led or bracket-led so a format regression
|
|
320
|
+
// shows in nightly logs, not months later in the next eval. Non-blocking.
|
|
321
|
+
const offTopic = parsed.filter((e) => /^\s*(user|ai|the user|assistant)\b/i.test(e) || /^\s*\[/.test(e)).length;
|
|
322
|
+
if (parsed.length > 0 && offTopic > 0) {
|
|
323
|
+
console.log(`[hicortex] topic-first check: ${offTopic}/${parsed.length} entries look actor/bracket-led (prompt may be ignored)`);
|
|
324
|
+
}
|
|
317
325
|
const entries = [];
|
|
318
326
|
const dropped = [];
|
|
319
327
|
for (const entry of parsed) {
|
|
@@ -409,23 +417,22 @@ function hasMinimalSubstance(entry) {
|
|
|
409
417
|
function parseDistilledEntries(markdown) {
|
|
410
418
|
const entries = [];
|
|
411
419
|
const lines = markdown.split("\n");
|
|
412
|
-
let currentSection = "";
|
|
413
420
|
for (const line of lines) {
|
|
414
421
|
const trimmed = line.trim();
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
422
|
+
// Skip all markdown headers (session title, classification, section
|
|
423
|
+
// headings). Sections are NOT prefixed onto entries: each bullet already
|
|
424
|
+
// starts with its [SUBJECT] (topic-first, enforced by prompts.ts), and
|
|
425
|
+
// prepending "[Section]" re-introduced the category-first prefix the
|
|
426
|
+
// 2026-08-02 corpus rewrite removed. The section label is unused
|
|
427
|
+
// downstream (distilled memories all store memory_type='episode').
|
|
428
|
+
if (trimmed.startsWith("# ") ||
|
|
429
|
+
trimmed.startsWith("## ") ||
|
|
430
|
+
trimmed.startsWith("### ")) {
|
|
418
431
|
continue;
|
|
419
432
|
}
|
|
420
|
-
//
|
|
421
|
-
if (trimmed.startsWith("# ") || trimmed.startsWith("## "))
|
|
422
|
-
continue;
|
|
423
|
-
// Bullet items are individual memories
|
|
433
|
+
// Bullet items are individual, already topic-first memories.
|
|
424
434
|
if (trimmed.startsWith("- ") && trimmed.length > 5) {
|
|
425
|
-
|
|
426
|
-
? `[${currentSection}] ${trimmed.slice(2)}`
|
|
427
|
-
: trimmed.slice(2);
|
|
428
|
-
entries.push(entry);
|
|
435
|
+
entries.push(trimmed.slice(2));
|
|
429
436
|
}
|
|
430
437
|
}
|
|
431
438
|
return entries;
|
|
@@ -54,6 +54,29 @@
|
|
|
54
54
|
* badly post-shift. Report written to data/eval-recall-sweep/report.md and
|
|
55
55
|
* printed to stdout.
|
|
56
56
|
*
|
|
57
|
+
* ## Section 4 — SCOPE dimension (#203 soft project affinity)
|
|
58
|
+
*
|
|
59
|
+
* A THIRD family on a SEPARATE project-labeled corpus (the blend-sweep corpus
|
|
60
|
+
* above has no project labels). Goal: prove #203's soft project affinity drops
|
|
61
|
+
* cross-scope noise (the boat/battery case — a "hardware" query surfacing
|
|
62
|
+
* "marine" memories on a shared token like "battery") below the cap WITHOUT a
|
|
63
|
+
* hard filter, and WITHOUT losing same-scope recall.
|
|
64
|
+
*
|
|
65
|
+
* Corpus: 6 hardware + 6 marine memories. 3 marine memories are
|
|
66
|
+
* CONTAMINATION SEEDS (share a token with the hardware queries); 3 are
|
|
67
|
+
* marine-only fillers (control).
|
|
68
|
+
*
|
|
69
|
+
* Each hardware query runs TWICE on the same static DB:
|
|
70
|
+
* scope OFF — no `project` sent (byte-identical to pre-#203).
|
|
71
|
+
* scope ON — `project: "hardware"` (computeScore adds +projectAffinity 0.15
|
|
72
|
+
* to hardware memories; marine gets 0; no hard filter).
|
|
73
|
+
*
|
|
74
|
+
* Metrics: contamination@5 (marine in top-5 / 5 — LOWER is better),
|
|
75
|
+
* recall@5 (gold hardware memory surfaced), sameScope@5 (hardware in top-5).
|
|
76
|
+
* Scope is orthogonal to the blend, so the blend weight is held at 0 here —
|
|
77
|
+
* `project` is the ONLY variable. Same invariants (noStrengthen, real
|
|
78
|
+
* embedder, uniform metadata, neverCalledEmbed self-check).
|
|
79
|
+
*
|
|
57
80
|
* Run: npm run eval:recall-sweep (== node dist/eval/recall-sweep.js)
|
|
58
81
|
*/
|
|
59
82
|
export {};
|