@remnic/core 9.3.708 → 9.3.709
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/dist/access-boundary.d.ts +7 -7
- package/dist/access-boundary.js +8 -8
- package/dist/access-cli.js +111 -24
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +6 -6
- package/dist/access-http.js +12 -12
- package/dist/access-mcp.d.ts +14 -6
- package/dist/access-mcp.js +11 -11
- package/dist/access-operations-batch.js +9 -9
- package/dist/access-operations.d.ts +28 -8
- package/dist/access-operations.js +14 -10
- package/dist/access-schema.d.ts +6 -6
- package/dist/{access-service-j1c1gptF.d.ts → access-service-CoIA0NrG.d.ts} +186 -3
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +7 -7
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/access-surface-catalog.js +7 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-CKxilpzS.d.ts → catalog-DQCZrBjw.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-PDJQVAGM.js → chunk-2T4TDXPC.js} +14 -14
- package/dist/{chunk-W2WQ4LE7.js → chunk-35YJ6KCV.js} +2 -2
- package/dist/{chunk-C63MK3WL.js → chunk-3TCRU4JA.js} +2 -2
- package/dist/{chunk-WRGPE6AW.js → chunk-3XHD3XGK.js} +44 -1
- package/dist/chunk-3XHD3XGK.js.map +1 -0
- package/dist/{chunk-GKI6LX5L.js → chunk-54TI5GLV.js} +2 -2
- package/dist/{chunk-IVCQW4C4.js → chunk-7IBEWQLG.js} +2 -2
- package/dist/{chunk-Y6YHAGQP.js → chunk-7NDYFAJS.js} +2 -2
- package/dist/{chunk-SPETAWFE.js → chunk-AZVHBFI3.js} +2 -2
- package/dist/{chunk-MLLTU5FX.js → chunk-BOGENF7P.js} +1 -1
- package/dist/chunk-BOGENF7P.js.map +1 -0
- package/dist/{chunk-3PTOZJOI.js → chunk-BQCFXAMN.js} +2 -2
- package/dist/{chunk-4E2QCH46.js → chunk-CNVIWMQI.js} +2 -2
- package/dist/{chunk-4DZATVK5.js → chunk-CXMXAC5R.js} +3 -3
- package/dist/{chunk-STYMKPFT.js → chunk-EVX52NCY.js} +1459 -21
- package/dist/chunk-EVX52NCY.js.map +1 -0
- package/dist/{chunk-W2S3Z5MT.js → chunk-GMRNKPWO.js} +2 -2
- package/dist/{chunk-HIV5E57C.js → chunk-HSDJCT3V.js} +2 -2
- package/dist/{chunk-NM6TSEGQ.js → chunk-JNOYYWCA.js} +2 -2
- package/dist/{chunk-IONFO7UK.js → chunk-JQ7XVM4V.js} +3 -3
- package/dist/{chunk-2VQYHHWB.js → chunk-JSDZMOT7.js} +11 -2
- package/dist/chunk-JSDZMOT7.js.map +1 -0
- package/dist/{chunk-WSWYIKXX.js → chunk-KOEKDZ6A.js} +62 -5
- package/dist/chunk-KOEKDZ6A.js.map +1 -0
- package/dist/{chunk-WQADZ3ZY.js → chunk-NK3SPJLM.js} +20 -19
- package/dist/chunk-NK3SPJLM.js.map +1 -0
- package/dist/{chunk-WDH3KUZU.js → chunk-NXL5CVE7.js} +2 -2
- package/dist/{chunk-BGAHTI4C.js → chunk-OK7FUX6R.js} +6 -6
- package/dist/{chunk-24FGNOQS.js → chunk-P6PRSI3W.js} +85 -4
- package/dist/chunk-P6PRSI3W.js.map +1 -0
- package/dist/{chunk-66TSLESZ.js → chunk-PKE7EJMX.js} +2 -2
- package/dist/chunk-PKE7EJMX.js.map +1 -0
- package/dist/{chunk-MOXFPLD6.js → chunk-QXNFQKWU.js} +2 -2
- package/dist/{chunk-MRX6ZXHZ.js → chunk-S6FQLQGH.js} +2 -2
- package/dist/{chunk-6HPJMR5I.js → chunk-SKQCFAYU.js} +2 -2
- package/dist/{chunk-EJDAXR7O.js → chunk-VWB3HDY6.js} +56 -8
- package/dist/chunk-VWB3HDY6.js.map +1 -0
- package/dist/{chunk-N75N5SNX.js → chunk-XD33EX2F.js} +3 -3
- package/dist/{chunk-ESMY4RJ4.js → chunk-XKU4YE6Z.js} +2 -2
- package/dist/{chunk-XKMDDM7P.js → chunk-Y6PIFKXX.js} +2 -2
- package/dist/{chunk-RH2OSRQY.js → chunk-Z56IHRVV.js} +2 -2
- package/dist/{cli-CbT-pyM4.d.ts → cli-qex-L3GT.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +26 -26
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +32 -32
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-D4ovYV3x.d.ts → orchestrator-DsVKLEBk.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +15 -15
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +2 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DyMUCsfN.d.ts → semantic-consolidation-_hVxkTuF.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +8 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-Couvz-L3.d.ts → types-DUK4vVnN.d.ts} +27 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -0
- package/src/access-cli.ts +107 -2
- package/src/access-http.ts +70 -1
- package/src/access-mcp.ts +65 -0
- package/src/access-operations.ts +122 -0
- package/src/access-service.ts +92 -0
- package/src/access-surface-catalog.test.ts +6 -1
- package/src/access-surface-catalog.ts +7 -0
- package/src/cli.ts +1 -0
- package/src/config.test.ts +10 -0
- package/src/config.ts +43 -0
- package/src/correction/correction-access-wiring.ts +887 -0
- package/src/correction/correction-contract.ts +416 -0
- package/src/correction/correction-executor.test.ts +742 -0
- package/src/correction/correction-executor.ts +473 -0
- package/src/correction/correction-planner.test.ts +446 -0
- package/src/correction/correction-planner.ts +546 -0
- package/src/correction/correction-service.ts +180 -0
- package/src/correction/correction-surfaces.test.ts +245 -0
- package/src/correction/index.ts +43 -0
- package/src/storage.ts +10 -0
- package/src/types.ts +27 -0
- package/dist/chunk-24FGNOQS.js.map +0 -1
- package/dist/chunk-2VQYHHWB.js.map +0 -1
- package/dist/chunk-66TSLESZ.js.map +0 -1
- package/dist/chunk-EJDAXR7O.js.map +0 -1
- package/dist/chunk-MLLTU5FX.js.map +0 -1
- package/dist/chunk-STYMKPFT.js.map +0 -1
- package/dist/chunk-WQADZ3ZY.js.map +0 -1
- package/dist/chunk-WRGPE6AW.js.map +0 -1
- package/dist/chunk-WSWYIKXX.js.map +0 -1
- /package/dist/{chunk-PDJQVAGM.js.map → chunk-2T4TDXPC.js.map} +0 -0
- /package/dist/{chunk-W2WQ4LE7.js.map → chunk-35YJ6KCV.js.map} +0 -0
- /package/dist/{chunk-C63MK3WL.js.map → chunk-3TCRU4JA.js.map} +0 -0
- /package/dist/{chunk-GKI6LX5L.js.map → chunk-54TI5GLV.js.map} +0 -0
- /package/dist/{chunk-IVCQW4C4.js.map → chunk-7IBEWQLG.js.map} +0 -0
- /package/dist/{chunk-Y6YHAGQP.js.map → chunk-7NDYFAJS.js.map} +0 -0
- /package/dist/{chunk-SPETAWFE.js.map → chunk-AZVHBFI3.js.map} +0 -0
- /package/dist/{chunk-3PTOZJOI.js.map → chunk-BQCFXAMN.js.map} +0 -0
- /package/dist/{chunk-4E2QCH46.js.map → chunk-CNVIWMQI.js.map} +0 -0
- /package/dist/{chunk-4DZATVK5.js.map → chunk-CXMXAC5R.js.map} +0 -0
- /package/dist/{chunk-W2S3Z5MT.js.map → chunk-GMRNKPWO.js.map} +0 -0
- /package/dist/{chunk-HIV5E57C.js.map → chunk-HSDJCT3V.js.map} +0 -0
- /package/dist/{chunk-NM6TSEGQ.js.map → chunk-JNOYYWCA.js.map} +0 -0
- /package/dist/{chunk-IONFO7UK.js.map → chunk-JQ7XVM4V.js.map} +0 -0
- /package/dist/{chunk-WDH3KUZU.js.map → chunk-NXL5CVE7.js.map} +0 -0
- /package/dist/{chunk-BGAHTI4C.js.map → chunk-OK7FUX6R.js.map} +0 -0
- /package/dist/{chunk-MOXFPLD6.js.map → chunk-QXNFQKWU.js.map} +0 -0
- /package/dist/{chunk-MRX6ZXHZ.js.map → chunk-S6FQLQGH.js.map} +0 -0
- /package/dist/{chunk-6HPJMR5I.js.map → chunk-SKQCFAYU.js.map} +0 -0
- /package/dist/{chunk-N75N5SNX.js.map → chunk-XD33EX2F.js.map} +0 -0
- /package/dist/{chunk-ESMY4RJ4.js.map → chunk-XKU4YE6Z.js.map} +0 -0
- /package/dist/{chunk-XKMDDM7P.js.map → chunk-Y6PIFKXX.js.map} +0 -0
- /package/dist/{chunk-RH2OSRQY.js.map → chunk-Z56IHRVV.js.map} +0 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correction planner tests — issue #1580 PR 1 acceptance.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - explicit-target plan
|
|
6
|
+
* - search-located plan
|
|
7
|
+
* - not-found target → explicit error (rule 34)
|
|
8
|
+
* - LLM classify success per classification class (stubbed adapter, rule 33)
|
|
9
|
+
* - LLM failure → deterministic fallback plan (rule 13)
|
|
10
|
+
* - plan persists atomically and expires
|
|
11
|
+
* - bulk guard: > maxAffected refuses (§39)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from "node:assert";
|
|
15
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { test } from "node:test";
|
|
19
|
+
import {
|
|
20
|
+
CorrectionContractError,
|
|
21
|
+
MEMORY_CATEGORIES,
|
|
22
|
+
validateMemoryDraft,
|
|
23
|
+
type CorrectionAction,
|
|
24
|
+
type CorrectionClassification,
|
|
25
|
+
type CorrectionPlan,
|
|
26
|
+
} from "./correction-contract.js";
|
|
27
|
+
import { CorrectionPlanner, type LlmClassificationResult, type PlannerCandidate, type PlannerDeps } from "./correction-planner.js";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Test helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
interface StubState {
|
|
34
|
+
candidatesById: Map<string, PlannerCandidate>;
|
|
35
|
+
llmResult: LlmClassificationResult | ((text: string, candidates: PlannerCandidate[]) => LlmClassificationResult);
|
|
36
|
+
writeReplacementCalls: number;
|
|
37
|
+
propagateCalls: number;
|
|
38
|
+
retireCalls: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCandidate(over: Partial<PlannerCandidate> & { memoryId: string }): PlannerCandidate {
|
|
42
|
+
return {
|
|
43
|
+
path: `facts/${over.memoryId}.md`,
|
|
44
|
+
content: `${over.memoryId} content`,
|
|
45
|
+
excerpt: `${over.memoryId} excerpt`,
|
|
46
|
+
score: 1.0,
|
|
47
|
+
...over,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeDeps(stateDir: string, state: StubState, opts: { maxAffected?: number; planTtlHours?: number } = {}): PlannerDeps {
|
|
52
|
+
const fixedNow = new Date("2026-03-15T12:00:00Z");
|
|
53
|
+
return {
|
|
54
|
+
searchCorpus: async ({ text, namespaces, limit }) => {
|
|
55
|
+
// Simple stub: return candidates whose content contains the text.
|
|
56
|
+
const all = [...state.candidatesById.values()];
|
|
57
|
+
const matched = all.filter((c) => c.content.includes(text) || text.includes(c.memoryId));
|
|
58
|
+
return matched.slice(0, limit).map((c, i) => ({ ...c, score: 1 - i * 0.1 }));
|
|
59
|
+
},
|
|
60
|
+
resolveTargets: async ({ targetIds }) => {
|
|
61
|
+
const out: PlannerCandidate[] = [];
|
|
62
|
+
const missing: string[] = [];
|
|
63
|
+
for (const id of targetIds) {
|
|
64
|
+
const c = state.candidatesById.get(id);
|
|
65
|
+
if (c) out.push(c);
|
|
66
|
+
else missing.push(id);
|
|
67
|
+
}
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
throw new CorrectionContractError(`target memory not found: ${missing[0]}`);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
},
|
|
73
|
+
expandNeighbors: async ({ seedIds }) => {
|
|
74
|
+
// No neighbors in the stub; return the seeds themselves so neighbor
|
|
75
|
+
// expansion is exercised but produces no surprises.
|
|
76
|
+
return seedIds.map((id) => state.candidatesById.get(id)).filter((c): c is PlannerCandidate => c !== undefined);
|
|
77
|
+
},
|
|
78
|
+
classifyAndDraft: async ({ text, candidates }) => {
|
|
79
|
+
return typeof state.llmResult === "function"
|
|
80
|
+
? state.llmResult(text, candidates)
|
|
81
|
+
: state.llmResult;
|
|
82
|
+
},
|
|
83
|
+
renderDiff: async ({ actions }) => `DIFF(${actions.length} actions)`,
|
|
84
|
+
storageDir: async () => stateDir,
|
|
85
|
+
maxAffected: opts.maxAffected ?? 10,
|
|
86
|
+
planTtlHours: opts.planTtlHours ?? 24,
|
|
87
|
+
now: () => fixedNow,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
92
|
+
const dir = await mkdtemp(path.join(tmpdir(), "remnic-corr-plan-"));
|
|
93
|
+
try {
|
|
94
|
+
return await fn(dir);
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(dir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Tests
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
test("explicit-target plan: targetIds resolve directly and the plan records them", async () => {
|
|
105
|
+
await withTempDir(async (dir) => {
|
|
106
|
+
const state: StubState = {
|
|
107
|
+
candidatesById: new Map([
|
|
108
|
+
["mem-postgres", makeCandidate({ memoryId: "mem-postgres", content: "User prefers PostgreSQL" })],
|
|
109
|
+
]),
|
|
110
|
+
llmResult: {
|
|
111
|
+
classification: "outdated",
|
|
112
|
+
confidence: 0.9,
|
|
113
|
+
actions: [{ kind: "supersede", loserId: "mem-postgres", replacement: { content: "User prefers MySQL" } }],
|
|
114
|
+
relevance: [{ memoryId: "mem-postgres", why: "contradicted by the correction" }],
|
|
115
|
+
warnings: [],
|
|
116
|
+
},
|
|
117
|
+
writeReplacementCalls: 0,
|
|
118
|
+
propagateCalls: 0,
|
|
119
|
+
retireCalls: 0,
|
|
120
|
+
};
|
|
121
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
122
|
+
const plan = await planner.plan(
|
|
123
|
+
{ text: "we migrated to MySQL in March", targetIds: ["mem-postgres"] },
|
|
124
|
+
["default"],
|
|
125
|
+
);
|
|
126
|
+
assert.equal(plan.classification, "outdated");
|
|
127
|
+
assert.equal(plan.actions.length, 1);
|
|
128
|
+
assert.equal(plan.actions[0].kind, "supersede");
|
|
129
|
+
assert.equal(plan.affected.length, 1);
|
|
130
|
+
assert.equal(plan.affected[0].memoryId, "mem-postgres");
|
|
131
|
+
assert.equal(plan.confidence, 0.9);
|
|
132
|
+
assert.equal(plan.namespace, "default");
|
|
133
|
+
assert.equal(plan.status, "pending");
|
|
134
|
+
assert.match(plan.diff, /DIFF\(1 actions\)/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("search-located plan: no targetIds → search runs and top hits become candidates", async () => {
|
|
139
|
+
await withTempDir(async (dir) => {
|
|
140
|
+
const state: StubState = {
|
|
141
|
+
candidatesById: new Map([
|
|
142
|
+
["mem-a", makeCandidate({ memoryId: "mem-a", content: "prefers PostgreSQL for analytics" })],
|
|
143
|
+
["mem-b", makeCandidate({ memoryId: "mem-b", content: "PostgreSQL connection string" })],
|
|
144
|
+
]),
|
|
145
|
+
llmResult: {
|
|
146
|
+
classification: "wrong",
|
|
147
|
+
confidence: 0.7,
|
|
148
|
+
actions: [{ kind: "retract", memoryId: "mem-a" }],
|
|
149
|
+
relevance: [{ memoryId: "mem-a", why: "directly contradicted" }],
|
|
150
|
+
warnings: [],
|
|
151
|
+
},
|
|
152
|
+
writeReplacementCalls: 0,
|
|
153
|
+
propagateCalls: 0,
|
|
154
|
+
retireCalls: 0,
|
|
155
|
+
};
|
|
156
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
157
|
+
const plan = await planner.plan({ text: "PostgreSQL" }, ["default"]);
|
|
158
|
+
// Both mem-a and mem-b match "PostgreSQL"; the planner offers them as
|
|
159
|
+
// candidates but only mem-a is annotated by the LLM.
|
|
160
|
+
assert.equal(plan.affected.length, 1);
|
|
161
|
+
assert.equal(plan.affected[0].memoryId, "mem-a");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("not-found target → explicit error (rule 34)", async () => {
|
|
166
|
+
await withTempDir(async (dir) => {
|
|
167
|
+
const state: StubState = {
|
|
168
|
+
candidatesById: new Map(),
|
|
169
|
+
llmResult: { classification: "outdated", confidence: 0.5, actions: [], relevance: [], warnings: [] },
|
|
170
|
+
writeReplacementCalls: 0,
|
|
171
|
+
propagateCalls: 0,
|
|
172
|
+
retireCalls: 0,
|
|
173
|
+
};
|
|
174
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
175
|
+
await assert.rejects(
|
|
176
|
+
planner.plan({ text: "fix this", targetIds: ["does-not-exist"] }, ["default"]),
|
|
177
|
+
/target memory not found: does-not-exist/,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("LLM classify success per classification class (stubbed adapter, rule 33)", async () => {
|
|
183
|
+
const classes: CorrectionClassification[] = ["wrong", "outdated", "incomplete", "wrong_scope", "never_store"];
|
|
184
|
+
for (const cls of classes) {
|
|
185
|
+
await withTempDir(async (dir) => {
|
|
186
|
+
const action: CorrectionAction =
|
|
187
|
+
cls === "wrong"
|
|
188
|
+
? { kind: "retract", memoryId: "mem-x" }
|
|
189
|
+
: cls === "outdated"
|
|
190
|
+
? { kind: "supersede", loserId: "mem-x", replacement: { content: "new" } }
|
|
191
|
+
: cls === "incomplete"
|
|
192
|
+
? { kind: "edit", memoryId: "mem-x", patch: "updated content" }
|
|
193
|
+
: cls === "wrong_scope"
|
|
194
|
+
? { kind: "rescope", memoryId: "mem-x", toNamespace: "other" }
|
|
195
|
+
: { kind: "redaction_rule", pattern: "secret-token-\\d+" };
|
|
196
|
+
const state: StubState = {
|
|
197
|
+
candidatesById: new Map([["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })]]),
|
|
198
|
+
llmResult: {
|
|
199
|
+
classification: cls,
|
|
200
|
+
confidence: 0.8,
|
|
201
|
+
actions: [action],
|
|
202
|
+
relevance: [{ memoryId: "mem-x", why: `classified as ${cls}` }],
|
|
203
|
+
warnings: [],
|
|
204
|
+
},
|
|
205
|
+
writeReplacementCalls: 0,
|
|
206
|
+
propagateCalls: 0,
|
|
207
|
+
retireCalls: 0,
|
|
208
|
+
};
|
|
209
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
210
|
+
const plan = await planner.plan({ text: "old fact", targetIds: ["mem-x"] }, ["default"]);
|
|
211
|
+
assert.equal(plan.classification, cls);
|
|
212
|
+
assert.equal(plan.actions.length, 1);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("LLM failure → deterministic fallback plan (rule 13)", async () => {
|
|
218
|
+
await withTempDir(async (dir) => {
|
|
219
|
+
const state: StubState = {
|
|
220
|
+
candidatesById: new Map([["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })]]),
|
|
221
|
+
llmResult: {
|
|
222
|
+
classification: "outdated", // ignored — fallback overrides
|
|
223
|
+
confidence: 0,
|
|
224
|
+
actions: [],
|
|
225
|
+
relevance: [{ memoryId: "mem-x", why: "located" }],
|
|
226
|
+
warnings: [],
|
|
227
|
+
fallback: true,
|
|
228
|
+
},
|
|
229
|
+
writeReplacementCalls: 0,
|
|
230
|
+
propagateCalls: 0,
|
|
231
|
+
retireCalls: 0,
|
|
232
|
+
};
|
|
233
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
234
|
+
const plan = await planner.plan({ text: "old fact", targetIds: ["mem-x"] }, ["default"]);
|
|
235
|
+
assert.equal(plan.classification, "outdated");
|
|
236
|
+
assert.equal(plan.confidence, 0);
|
|
237
|
+
assert.equal(plan.actions.length, 0);
|
|
238
|
+
assert.equal(plan.diff, "");
|
|
239
|
+
assert.ok(
|
|
240
|
+
plan.warnings.some((w) => w.includes("planner LLM unavailable")),
|
|
241
|
+
`expected fallback warning, got: ${JSON.stringify(plan.warnings)}`,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("plan persists atomically to state/corrections/pending/<planId>.json and round-trips", async () => {
|
|
247
|
+
await withTempDir(async (dir) => {
|
|
248
|
+
const state: StubState = {
|
|
249
|
+
candidatesById: new Map([["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })]]),
|
|
250
|
+
llmResult: {
|
|
251
|
+
classification: "outdated",
|
|
252
|
+
confidence: 0.9,
|
|
253
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new fact" } }],
|
|
254
|
+
relevance: [{ memoryId: "mem-x", why: "contradicted" }],
|
|
255
|
+
warnings: [],
|
|
256
|
+
},
|
|
257
|
+
writeReplacementCalls: 0,
|
|
258
|
+
propagateCalls: 0,
|
|
259
|
+
retireCalls: 0,
|
|
260
|
+
};
|
|
261
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
262
|
+
const plan = await planner.plan({ text: "old fact", targetIds: ["mem-x"] }, ["default"]);
|
|
263
|
+
// loadPlan round-trips it.
|
|
264
|
+
const reloaded = await planner.loadPlan("default", plan.planId);
|
|
265
|
+
assert.ok(reloaded, "plan should round-trip via loadPlan");
|
|
266
|
+
assert.equal(reloaded!.planId, plan.planId);
|
|
267
|
+
assert.equal(reloaded!.actions.length, 1);
|
|
268
|
+
// listPending surfaces it.
|
|
269
|
+
const pending = await planner.listPending("default");
|
|
270
|
+
assert.equal(pending.length, 1);
|
|
271
|
+
assert.equal(pending[0].planId, plan.planId);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("plan expires after TTL — apply-time rejection is tested in executor; here we verify the timestamp", async () => {
|
|
276
|
+
await withTempDir(async (dir) => {
|
|
277
|
+
const state: StubState = {
|
|
278
|
+
candidatesById: new Map([["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })]]),
|
|
279
|
+
llmResult: { classification: "outdated", confidence: 0.9, actions: [], relevance: [], warnings: [] },
|
|
280
|
+
writeReplacementCalls: 0,
|
|
281
|
+
propagateCalls: 0,
|
|
282
|
+
retireCalls: 0,
|
|
283
|
+
};
|
|
284
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state, { planTtlHours: 1 }));
|
|
285
|
+
const plan = await planner.plan({ text: "old fact", targetIds: ["mem-x"] }, ["default"]);
|
|
286
|
+
const created = new Date(plan.createdAt).getTime();
|
|
287
|
+
const expires = new Date(plan.expiresAt).getTime();
|
|
288
|
+
// TTL of 1h = 3600000ms.
|
|
289
|
+
assert.equal(expires - created, 60 * 60 * 1000);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("bulk guard: > maxAffected refuses with a clear error (§39)", async () => {
|
|
294
|
+
await withTempDir(async (dir) => {
|
|
295
|
+
const candidates = new Map<string, PlannerCandidate>();
|
|
296
|
+
for (let i = 0; i < 5; i++) {
|
|
297
|
+
candidates.set(`mem-${i}`, makeCandidate({ memoryId: `mem-${i}`, content: `fact ${i}` }));
|
|
298
|
+
}
|
|
299
|
+
const state: StubState = {
|
|
300
|
+
candidatesById: candidates,
|
|
301
|
+
llmResult: {
|
|
302
|
+
classification: "wrong",
|
|
303
|
+
confidence: 0.5,
|
|
304
|
+
actions: [...candidates.keys()].map((id) => ({ kind: "retract" as const, memoryId: id })),
|
|
305
|
+
relevance: [...candidates.keys()].map((id) => ({ memoryId: id, why: "x" })),
|
|
306
|
+
warnings: [],
|
|
307
|
+
},
|
|
308
|
+
writeReplacementCalls: 0,
|
|
309
|
+
propagateCalls: 0,
|
|
310
|
+
retireCalls: 0,
|
|
311
|
+
};
|
|
312
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state, { maxAffected: 3 }));
|
|
313
|
+
await assert.rejects(
|
|
314
|
+
planner.plan({ text: "fact" }, ["default"]),
|
|
315
|
+
/exceeding the maxAffected limit of 3/,
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("malformed action from the LLM adapter is rejected before persist (defense in depth)", async () => {
|
|
321
|
+
await withTempDir(async (dir) => {
|
|
322
|
+
const state: StubState = {
|
|
323
|
+
candidatesById: new Map([["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })]]),
|
|
324
|
+
llmResult: {
|
|
325
|
+
classification: "outdated",
|
|
326
|
+
confidence: 0.9,
|
|
327
|
+
// Missing required loserId.
|
|
328
|
+
actions: [{ kind: "supersede" } as unknown as CorrectionAction],
|
|
329
|
+
relevance: [],
|
|
330
|
+
warnings: [],
|
|
331
|
+
},
|
|
332
|
+
writeReplacementCalls: 0,
|
|
333
|
+
propagateCalls: 0,
|
|
334
|
+
retireCalls: 0,
|
|
335
|
+
};
|
|
336
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
337
|
+
await assert.rejects(
|
|
338
|
+
planner.plan({ text: "old fact", targetIds: ["mem-x"] }, ["default"]),
|
|
339
|
+
/supersede\.loserId is required/,
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("no readable namespaces → explicit error", async () => {
|
|
345
|
+
await withTempDir(async (dir) => {
|
|
346
|
+
const state: StubState = {
|
|
347
|
+
candidatesById: new Map(),
|
|
348
|
+
llmResult: { classification: "outdated", confidence: 0.5, actions: [], relevance: [], warnings: [] },
|
|
349
|
+
writeReplacementCalls: 0,
|
|
350
|
+
propagateCalls: 0,
|
|
351
|
+
retireCalls: 0,
|
|
352
|
+
};
|
|
353
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
354
|
+
await assert.rejects(
|
|
355
|
+
planner.plan({ text: "anything" }, []),
|
|
356
|
+
/at least one readable namespace/,
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("Ug8: path-traversal planId is rejected before building the plan-file path", async () => {
|
|
362
|
+
await withTempDir(async (dir) => {
|
|
363
|
+
const state: StubState = {
|
|
364
|
+
candidatesById: new Map(),
|
|
365
|
+
llmResult: { classification: "outdated", confidence: 0.5, actions: [], relevance: [], warnings: [] },
|
|
366
|
+
writeReplacementCalls: 0,
|
|
367
|
+
propagateCalls: 0,
|
|
368
|
+
retireCalls: 0,
|
|
369
|
+
};
|
|
370
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
371
|
+
// `../../meta` must NOT let path.join escape state/corrections/pending and
|
|
372
|
+
// delete arbitrary .json files under the storage root.
|
|
373
|
+
for (const bad of ["../../meta", "../sibling", "a/b", "a\\b", ".", "..", ".hidden"]) {
|
|
374
|
+
await assert.rejects(planner.loadPlan("default", bad), /Invalid plan id/);
|
|
375
|
+
await assert.rejects(planner.markConsumed("default", bad, "discarded"), /Invalid plan id/);
|
|
376
|
+
await assert.rejects(planner.deletePlan("default", bad), /Invalid plan id/);
|
|
377
|
+
}
|
|
378
|
+
// A canonical corr-... id is accepted (it returns null/not-found without throwing).
|
|
379
|
+
const ok = await planner.loadPlan("default", "corr-abc-123");
|
|
380
|
+
assert.equal(ok, null);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("Of-XJ: validateMemoryDraft rejects categories outside the MemoryCategory allow-list", () => {
|
|
385
|
+
// A path-like or unexpected category must be rejected before it reaches
|
|
386
|
+
// StorageManager.writeMemory, which incorporates the category into the
|
|
387
|
+
// generated memory id/path (review thread Of-XJ).
|
|
388
|
+
for (const cat of ["fact", "preference", "correction", "entity"]) {
|
|
389
|
+
assert.doesNotThrow(() => validateMemoryDraft({ content: "x", category: cat }));
|
|
390
|
+
}
|
|
391
|
+
for (const bad of ["../../etc/passwd", "fact/../../meta", "unknown", "FACT", ""]) {
|
|
392
|
+
assert.throws(
|
|
393
|
+
() => validateMemoryDraft({ content: "x", category: bad }),
|
|
394
|
+
/MemoryDraft.category must be one of/,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
assert.doesNotThrow(() => validateMemoryDraft({ content: "x" }));
|
|
398
|
+
assert.ok(MEMORY_CATEGORIES.includes("reasoning_trace"));
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("OgIql: untargeted search uses tokenized keyword matching (not exact-prefix substring)", async () => {
|
|
402
|
+
await withTempDir(async (dir) => {
|
|
403
|
+
// The wiring's searchMemories tokenizes the correction text and matches by
|
|
404
|
+
// keyword overlap, so "we migrated from Postgres to MySQL" finds a memory
|
|
405
|
+
// that says "Postgres is the database" — the old 32-char exact-prefix
|
|
406
|
+
// substring missed this. The planner delegates to deps.searchCorpus, so we
|
|
407
|
+
// verify the planner passes the FULL text and the tokenized search dep
|
|
408
|
+
// (mirroring the wiring logic) locates the memory.
|
|
409
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
410
|
+
["mem-db", {
|
|
411
|
+
memoryId: "mem-db",
|
|
412
|
+
path: "facts/mem-db.md",
|
|
413
|
+
content: "Postgres is the database",
|
|
414
|
+
excerpt: "Postgres is the database",
|
|
415
|
+
score: 1,
|
|
416
|
+
}],
|
|
417
|
+
]);
|
|
418
|
+
let classifyCandidates: PlannerCandidate[] = [];
|
|
419
|
+
const state: StubState = {
|
|
420
|
+
candidatesById: candidates,
|
|
421
|
+
llmResult: { classification: "outdated", confidence: 0.5, actions: [], relevance: [], warnings: [] },
|
|
422
|
+
writeReplacementCalls: 0,
|
|
423
|
+
propagateCalls: 0,
|
|
424
|
+
retireCalls: 0,
|
|
425
|
+
};
|
|
426
|
+
const deps = makeDeps(dir, state);
|
|
427
|
+
// Mirror the wiring's tokenized search: split into keywords, match overlap.
|
|
428
|
+
deps.searchCorpus = async ({ text, limit }) => {
|
|
429
|
+
const STOP = new Set(["the", "a", "an", "is", "are", "to", "of", "from", "we", "and", "or", "in", "on"]);
|
|
430
|
+
const tokens = [...new Set(text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !STOP.has(w)))];
|
|
431
|
+
const matched = [...state.candidatesById.values()].filter((c) => {
|
|
432
|
+
const hay = c.content.toLowerCase();
|
|
433
|
+
return tokens.some((t) => hay.includes(t));
|
|
434
|
+
});
|
|
435
|
+
return matched.slice(0, limit).map((c, i) => ({ ...c, score: 1 - i * 0.1 }));
|
|
436
|
+
};
|
|
437
|
+
deps.classifyAndDraft = async ({ candidates: cs }) => {
|
|
438
|
+
classifyCandidates = cs;
|
|
439
|
+
return { classification: "outdated", confidence: 0.5, actions: [], relevance: [], warnings: [] };
|
|
440
|
+
};
|
|
441
|
+
const planner = new CorrectionPlanner(deps);
|
|
442
|
+
await planner.plan({ text: "we migrated from Postgres to MySQL" }, ["default"]);
|
|
443
|
+
assert.ok(classifyCandidates.length > 0, "tokenized search must find the Postgres memory");
|
|
444
|
+
assert.equal(classifyCandidates[0].memoryId, "mem-db");
|
|
445
|
+
});
|
|
446
|
+
});
|