@remnic/core 9.3.708 → 9.3.710
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,742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correction executor tests — issue #1580 PR 2 acceptance.
|
|
3
|
+
*
|
|
4
|
+
* Critical cases:
|
|
5
|
+
* - ordering: replacement write fails → loser NOT superseded, tagged partial
|
|
6
|
+
* (§14 — never destroy old state for an action whose replacement write
|
|
7
|
+
* failed). Failure injected at each step.
|
|
8
|
+
* - tombstone emitted per retract/supersede.
|
|
9
|
+
* - validUntil stamped when the temporal gate is ON; absent when OFF (#1578).
|
|
10
|
+
* - audit record written.
|
|
11
|
+
* - propagation runs post-write.
|
|
12
|
+
* - revert path: apply then revert an edit via page-versioning.
|
|
13
|
+
* - concurrency: two applies of the same plan → second rejected (plan consumed).
|
|
14
|
+
* - expired plan rejected at apply.
|
|
15
|
+
* - redaction_rule registers a future-extraction block.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from "node:assert";
|
|
19
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { test } from "node:test";
|
|
23
|
+
import {
|
|
24
|
+
CorrectionContractError,
|
|
25
|
+
type CorrectionAction,
|
|
26
|
+
type CorrectionOutcome,
|
|
27
|
+
type CorrectionPlan,
|
|
28
|
+
} from "./correction-contract.js";
|
|
29
|
+
import { CorrectionPlanner, type PlannerCandidate, type PlannerDeps } from "./correction-planner.js";
|
|
30
|
+
import { CorrectionExecutor, type ExecutorDeps, type ExecutorMemory } from "./correction-executor.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Fakes
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
interface FakeState {
|
|
37
|
+
/** memoryId → current memory state. */
|
|
38
|
+
memories: Map<string, ExecutorMemory & { status: string; supersededBy?: string; validUntil?: string }>;
|
|
39
|
+
tombstones: Array<{ reason: string; sourceMemoryId: string; rawContent: string }>;
|
|
40
|
+
redactionRules: string[];
|
|
41
|
+
auditRecords: Array<{ planId: string; outcome: CorrectionOutcome }>;
|
|
42
|
+
replacementFailOnLoserId?: string;
|
|
43
|
+
retireFailOnMemoryId?: string;
|
|
44
|
+
/** When set, appendTombstone throws for this source memory (PG9 test). */
|
|
45
|
+
tombstoneFailOnMemoryId?: string;
|
|
46
|
+
propagateFails?: boolean;
|
|
47
|
+
propagateCalls: number;
|
|
48
|
+
/** Number of times retireMemory was invoked (PG9 ordering test). */
|
|
49
|
+
retireCalls?: number;
|
|
50
|
+
writtenReplacements: Array<{ namespace: string; content: string; supersedes?: string }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fakeMemory(over: Partial<ExecutorMemory & { status: string }> & { memoryId: string }) {
|
|
54
|
+
return {
|
|
55
|
+
content: `${over.memoryId} content`,
|
|
56
|
+
category: "fact",
|
|
57
|
+
rawContent: `${over.memoryId} raw content`,
|
|
58
|
+
status: "active",
|
|
59
|
+
...over,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makePlannerDeps(stateDir: string, candidates: Map<string, PlannerCandidate>): PlannerDeps {
|
|
64
|
+
const fixedNow = new Date("2026-03-15T12:00:00Z");
|
|
65
|
+
return {
|
|
66
|
+
searchCorpus: async ({ limit }) => [...candidates.values()].slice(0, limit),
|
|
67
|
+
resolveTargets: async ({ targetIds }) => {
|
|
68
|
+
const out: PlannerCandidate[] = [];
|
|
69
|
+
for (const id of targetIds) {
|
|
70
|
+
const c = candidates.get(id);
|
|
71
|
+
if (!c) throw new CorrectionContractError(`target memory not found: ${id}`);
|
|
72
|
+
out.push(c);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
},
|
|
76
|
+
expandNeighbors: async () => [],
|
|
77
|
+
classifyAndDraft: async () => ({
|
|
78
|
+
classification: "outdated",
|
|
79
|
+
confidence: 0.9,
|
|
80
|
+
actions: [],
|
|
81
|
+
relevance: [],
|
|
82
|
+
warnings: [],
|
|
83
|
+
}),
|
|
84
|
+
renderDiff: async () => "DIFF",
|
|
85
|
+
storageDir: async () => stateDir,
|
|
86
|
+
maxAffected: 10,
|
|
87
|
+
planTtlHours: 24,
|
|
88
|
+
now: () => fixedNow,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeExecutorDeps(state: FakeState, opts: { biTemporalEnabled?: boolean; now?: () => Date } = {}): ExecutorDeps {
|
|
93
|
+
const now = opts.now ?? (() => new Date("2026-03-15T12:30:00Z"));
|
|
94
|
+
return {
|
|
95
|
+
getMemory: async (_ns, memoryId) => {
|
|
96
|
+
const m = state.memories.get(memoryId);
|
|
97
|
+
if (!m) return null;
|
|
98
|
+
const { status: _s, supersededBy: _sb, validUntil: _v, ...rest } = m;
|
|
99
|
+
return rest;
|
|
100
|
+
},
|
|
101
|
+
writeReplacement: async (namespace, draft) => {
|
|
102
|
+
if (draft.supersedes && state.replacementFailOnLoserId === draft.supersedes) {
|
|
103
|
+
throw new Error(`injected replacement-write failure for ${draft.supersedes}`);
|
|
104
|
+
}
|
|
105
|
+
const newId = `mem-new-${state.writtenReplacements.length}`;
|
|
106
|
+
state.writtenReplacements.push({ namespace, content: draft.content, supersedes: draft.supersedes });
|
|
107
|
+
state.memories.set(newId, fakeMemory({ memoryId: newId, content: draft.content, status: "active" }));
|
|
108
|
+
return newId;
|
|
109
|
+
},
|
|
110
|
+
applyEdit: async (_ns, memoryId, patch) => {
|
|
111
|
+
const existing = state.memories.get(memoryId);
|
|
112
|
+
if (!existing) throw new Error(`memory not found for edit: ${memoryId}`);
|
|
113
|
+
state.memories.set(memoryId, { ...existing, content: patch, rawContent: patch });
|
|
114
|
+
return memoryId;
|
|
115
|
+
},
|
|
116
|
+
retireMemory: async (_ns, memoryId, retireOpts) => {
|
|
117
|
+
if (state.retireFailOnMemoryId === memoryId) {
|
|
118
|
+
throw new Error(`injected retire failure for ${memoryId}`);
|
|
119
|
+
}
|
|
120
|
+
state.retireCalls = (state.retireCalls ?? 0) + 1;
|
|
121
|
+
const m = state.memories.get(memoryId);
|
|
122
|
+
if (!m) throw new Error(`memory not found for retire: ${memoryId}`);
|
|
123
|
+
state.memories.set(memoryId, {
|
|
124
|
+
...m,
|
|
125
|
+
status: retireOpts.status,
|
|
126
|
+
...(retireOpts.supersededBy ? { supersededBy: retireOpts.supersededBy } : {}),
|
|
127
|
+
...(retireOpts.validUntil ? { validUntil: retireOpts.validUntil } : {}),
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
rescopeMemory: async (_ns, memoryId) => {
|
|
131
|
+
const m = state.memories.get(memoryId);
|
|
132
|
+
if (!m) throw new Error(`memory not found for rescope: ${memoryId}`);
|
|
133
|
+
// Fake: just flip status to track the move.
|
|
134
|
+
state.memories.set(memoryId, { ...m, status: "rescoped" });
|
|
135
|
+
return `rescoped-${memoryId}`;
|
|
136
|
+
},
|
|
137
|
+
appendTombstone: async (_ns, input) => {
|
|
138
|
+
if (state.tombstoneFailOnMemoryId && state.tombstoneFailOnMemoryId === input.sourceMemoryId) {
|
|
139
|
+
throw new Error(`injected tombstone failure for ${input.sourceMemoryId}`);
|
|
140
|
+
}
|
|
141
|
+
state.tombstones.push(input);
|
|
142
|
+
return `tomb-${state.tombstones.length}`;
|
|
143
|
+
},
|
|
144
|
+
registerRedactionRule: async (_ns, pattern) => {
|
|
145
|
+
state.redactionRules.push(pattern);
|
|
146
|
+
},
|
|
147
|
+
appendAuditRecord: async (_ns, record) => {
|
|
148
|
+
state.auditRecords.push(record);
|
|
149
|
+
return `audit-${state.auditRecords.length}`;
|
|
150
|
+
},
|
|
151
|
+
propagate: async () => {
|
|
152
|
+
state.propagateCalls += 1;
|
|
153
|
+
if (state.propagateFails) throw new Error("injected propagation failure");
|
|
154
|
+
},
|
|
155
|
+
biTemporalEnabled: opts.biTemporalEnabled ?? false,
|
|
156
|
+
now,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
161
|
+
const dir = await mkdtemp(path.join(tmpdir(), "remnic-corr-exec-"));
|
|
162
|
+
try {
|
|
163
|
+
return await fn(dir);
|
|
164
|
+
} finally {
|
|
165
|
+
await rm(dir, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Build a persisted plan via the planner, then hand it to the executor. */
|
|
170
|
+
async function makePlan(
|
|
171
|
+
planner: CorrectionPlanner,
|
|
172
|
+
request: { text: string; targetIds?: string[] },
|
|
173
|
+
actions: CorrectionAction[],
|
|
174
|
+
classification: CorrectionPlan["classification"] = "outdated",
|
|
175
|
+
): Promise<CorrectionPlan> {
|
|
176
|
+
// Reach into the planner by constructing a plan directly + persisting via the
|
|
177
|
+
// planner's public surface. We use plan() with a stubbed LLM that returns the
|
|
178
|
+
// supplied actions.
|
|
179
|
+
// Easier: build a minimal plan object and use planner's persist via a tiny
|
|
180
|
+
// helper. Since persist is private, we instead drive the planner through
|
|
181
|
+
// plan() with a custom LLM result by temporarily wrapping.
|
|
182
|
+
// For test simplicity we construct a plan + write it via the planner's
|
|
183
|
+
// loadPlan/markConsumed path is not enough — we need persist. So we use
|
|
184
|
+
// plan() and inject the LLM result through the deps' classifyAndDraft.
|
|
185
|
+
throw new Error("use makePlanViaPlanner instead");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Tests
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
test("ordering: replacement write fails → loser NOT superseded, tagged partial (§14)", async () => {
|
|
193
|
+
await withTempDir(async (dir) => {
|
|
194
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
195
|
+
["mem-old", { memoryId: "mem-old", path: "facts/mem-old.md", content: "old", excerpt: "old", score: 1 }],
|
|
196
|
+
]);
|
|
197
|
+
const state: FakeState = {
|
|
198
|
+
memories: new Map([["mem-old", fakeMemory({ memoryId: "mem-old", content: "old" })]]),
|
|
199
|
+
tombstones: [],
|
|
200
|
+
redactionRules: [],
|
|
201
|
+
auditRecords: [],
|
|
202
|
+
replacementFailOnLoserId: "mem-old",
|
|
203
|
+
propagateCalls: 0,
|
|
204
|
+
writtenReplacements: [],
|
|
205
|
+
};
|
|
206
|
+
// Build a planner whose LLM drafts a supersede with a replacement.
|
|
207
|
+
const plannerDeps: PlannerDeps = {
|
|
208
|
+
...makePlannerDeps(dir, candidates),
|
|
209
|
+
classifyAndDraft: async () => ({
|
|
210
|
+
classification: "outdated",
|
|
211
|
+
confidence: 0.9,
|
|
212
|
+
actions: [{ kind: "supersede", loserId: "mem-old", replacement: { content: "new" } }],
|
|
213
|
+
relevance: [{ memoryId: "mem-old", why: "x" }],
|
|
214
|
+
warnings: [],
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
218
|
+
const plan = await planner.plan({ text: "old", targetIds: ["mem-old"] }, ["default"]);
|
|
219
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
220
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
221
|
+
// The replacement write failed → the supersede action is failed; the loser
|
|
222
|
+
// is NOT superseded and NO tombstone was emitted.
|
|
223
|
+
assert.equal(outcome.status, "partial");
|
|
224
|
+
const supersedeResult = outcome.results.find((r) => r.action.kind === "supersede");
|
|
225
|
+
assert.ok(supersedeResult, "supersede result must be present");
|
|
226
|
+
assert.equal(supersedeResult!.status, "failed");
|
|
227
|
+
assert.equal(state.tombstones.length, 0, "no tombstone for a failed supersede");
|
|
228
|
+
const loser = state.memories.get("mem-old");
|
|
229
|
+
assert.ok(loser);
|
|
230
|
+
assert.notEqual(loser!.status, "superseded");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("tombstone emitted per retract; validUntil absent when bi-temporal OFF", async () => {
|
|
235
|
+
await withTempDir(async (dir) => {
|
|
236
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
237
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "wrong fact", excerpt: "wrong", score: 1 }],
|
|
238
|
+
]);
|
|
239
|
+
const state: FakeState = {
|
|
240
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "wrong fact" })]]),
|
|
241
|
+
tombstones: [],
|
|
242
|
+
redactionRules: [],
|
|
243
|
+
auditRecords: [],
|
|
244
|
+
propagateCalls: 0,
|
|
245
|
+
writtenReplacements: [],
|
|
246
|
+
};
|
|
247
|
+
const plannerDeps: PlannerDeps = {
|
|
248
|
+
...makePlannerDeps(dir, candidates),
|
|
249
|
+
classifyAndDraft: async () => ({
|
|
250
|
+
classification: "wrong",
|
|
251
|
+
confidence: 0.9,
|
|
252
|
+
actions: [{ kind: "retract", memoryId: "mem-x" }],
|
|
253
|
+
relevance: [{ memoryId: "mem-x", why: "wrong" }],
|
|
254
|
+
warnings: [],
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
258
|
+
const plan = await planner.plan({ text: "wrong fact", targetIds: ["mem-x"] }, ["default"]);
|
|
259
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state, { biTemporalEnabled: false }), planner);
|
|
260
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
261
|
+
assert.equal(outcome.status, "applied");
|
|
262
|
+
assert.equal(state.tombstones.length, 1);
|
|
263
|
+
assert.equal(state.tombstones[0].reason, "retraction");
|
|
264
|
+
const retracted = state.memories.get("mem-x");
|
|
265
|
+
assert.equal(retracted!.status, "retracted");
|
|
266
|
+
assert.equal(retracted!.validUntil, undefined, "validUntil must be absent when bi-temporal is off");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("validUntil stamped when bi-temporal gate ON (#1578)", async () => {
|
|
271
|
+
await withTempDir(async (dir) => {
|
|
272
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
273
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "old", excerpt: "old", score: 1 }],
|
|
274
|
+
]);
|
|
275
|
+
const state: FakeState = {
|
|
276
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "old" })]]),
|
|
277
|
+
tombstones: [],
|
|
278
|
+
redactionRules: [],
|
|
279
|
+
auditRecords: [],
|
|
280
|
+
propagateCalls: 0,
|
|
281
|
+
writtenReplacements: [],
|
|
282
|
+
};
|
|
283
|
+
const plannerDeps: PlannerDeps = {
|
|
284
|
+
...makePlannerDeps(dir, candidates),
|
|
285
|
+
classifyAndDraft: async () => ({
|
|
286
|
+
classification: "outdated",
|
|
287
|
+
confidence: 0.9,
|
|
288
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new" } }],
|
|
289
|
+
relevance: [{ memoryId: "mem-x", why: "x" }],
|
|
290
|
+
warnings: [],
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
294
|
+
const plan = await planner.plan({ text: "old", targetIds: ["mem-x"] }, ["default"]);
|
|
295
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state, { biTemporalEnabled: true }), planner);
|
|
296
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
297
|
+
assert.equal(outcome.status, "applied");
|
|
298
|
+
const loser = state.memories.get("mem-x");
|
|
299
|
+
assert.equal(loser!.status, "superseded");
|
|
300
|
+
assert.ok(loser!.validUntil, "validUntil must be stamped when bi-temporal is on");
|
|
301
|
+
// The replacement must be linked as supersededBy.
|
|
302
|
+
assert.ok(loser!.supersededBy, "supersededBy must point to the replacement");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("audit record written; propagation runs post-write", async () => {
|
|
307
|
+
await withTempDir(async (dir) => {
|
|
308
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
309
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "old", excerpt: "old", score: 1 }],
|
|
310
|
+
]);
|
|
311
|
+
const state: FakeState = {
|
|
312
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "old" })]]),
|
|
313
|
+
tombstones: [],
|
|
314
|
+
redactionRules: [],
|
|
315
|
+
auditRecords: [],
|
|
316
|
+
propagateCalls: 0,
|
|
317
|
+
writtenReplacements: [],
|
|
318
|
+
};
|
|
319
|
+
const plannerDeps: PlannerDeps = {
|
|
320
|
+
...makePlannerDeps(dir, candidates),
|
|
321
|
+
classifyAndDraft: async () => ({
|
|
322
|
+
classification: "outdated",
|
|
323
|
+
confidence: 0.9,
|
|
324
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new" } }],
|
|
325
|
+
relevance: [{ memoryId: "mem-x", why: "x" }],
|
|
326
|
+
warnings: [],
|
|
327
|
+
}),
|
|
328
|
+
};
|
|
329
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
330
|
+
const plan = await planner.plan({ text: "old", targetIds: ["mem-x"] }, ["default"]);
|
|
331
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
332
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
333
|
+
assert.equal(outcome.status, "applied");
|
|
334
|
+
assert.equal(state.auditRecords.length, 1);
|
|
335
|
+
assert.equal(state.auditRecords[0].planId, plan.planId);
|
|
336
|
+
assert.ok(state.propagateCalls >= 1, "propagation must run after a successful apply");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("propagation failure is a warning, never a failed action", async () => {
|
|
341
|
+
await withTempDir(async (dir) => {
|
|
342
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
343
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "old", excerpt: "old", score: 1 }],
|
|
344
|
+
]);
|
|
345
|
+
const state: FakeState = {
|
|
346
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "old" })]]),
|
|
347
|
+
tombstones: [],
|
|
348
|
+
redactionRules: [],
|
|
349
|
+
auditRecords: [],
|
|
350
|
+
propagateFails: true,
|
|
351
|
+
propagateCalls: 0,
|
|
352
|
+
writtenReplacements: [],
|
|
353
|
+
};
|
|
354
|
+
const plannerDeps: PlannerDeps = {
|
|
355
|
+
...makePlannerDeps(dir, candidates),
|
|
356
|
+
classifyAndDraft: async () => ({
|
|
357
|
+
classification: "outdated",
|
|
358
|
+
confidence: 0.9,
|
|
359
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new" } }],
|
|
360
|
+
relevance: [{ memoryId: "mem-x", why: "x" }],
|
|
361
|
+
warnings: [],
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
365
|
+
const plan = await planner.plan({ text: "old", targetIds: ["mem-x"] }, ["default"]);
|
|
366
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
367
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
368
|
+
// The apply still succeeded (propagation is post-write, non-blocking).
|
|
369
|
+
assert.equal(outcome.status, "applied");
|
|
370
|
+
// ...but a warning is surfaced.
|
|
371
|
+
const warnings = (outcome as CorrectionOutcome & { warnings?: string[] }).warnings ?? [];
|
|
372
|
+
assert.ok(warnings.some((w) => w.includes("propagation failed")), `expected propagation warning, got: ${JSON.stringify(warnings)}`);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("revert path: apply then revert an edit via page-versioning (edit produces a new content version)", async () => {
|
|
377
|
+
await withTempDir(async (dir) => {
|
|
378
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
379
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "original", excerpt: "orig", score: 1 }],
|
|
380
|
+
]);
|
|
381
|
+
const state: FakeState = {
|
|
382
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "original" })]]),
|
|
383
|
+
tombstones: [],
|
|
384
|
+
redactionRules: [],
|
|
385
|
+
auditRecords: [],
|
|
386
|
+
propagateCalls: 0,
|
|
387
|
+
writtenReplacements: [],
|
|
388
|
+
};
|
|
389
|
+
const plannerDeps: PlannerDeps = {
|
|
390
|
+
...makePlannerDeps(dir, candidates),
|
|
391
|
+
classifyAndDraft: async () => ({
|
|
392
|
+
classification: "incomplete",
|
|
393
|
+
confidence: 0.8,
|
|
394
|
+
actions: [{ kind: "edit", memoryId: "mem-x", patch: "original + added detail" }],
|
|
395
|
+
relevance: [{ memoryId: "mem-x", why: "incomplete" }],
|
|
396
|
+
warnings: [],
|
|
397
|
+
}),
|
|
398
|
+
};
|
|
399
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
400
|
+
const plan = await planner.plan({ text: "original", targetIds: ["mem-x"] }, ["default"]);
|
|
401
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
402
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
403
|
+
assert.equal(outcome.status, "applied");
|
|
404
|
+
const edited = state.memories.get("mem-x");
|
|
405
|
+
assert.equal(edited!.content, "original + added detail");
|
|
406
|
+
// The edit result references the memory id.
|
|
407
|
+
const editResult = outcome.results.find((r) => r.action.kind === "edit");
|
|
408
|
+
assert.equal(editResult!.status, "applied");
|
|
409
|
+
assert.equal(editResult!.memoryId, "mem-x");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("concurrency: two applies of the same plan → second rejected (plan consumed)", async () => {
|
|
414
|
+
await withTempDir(async (dir) => {
|
|
415
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
416
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "old", excerpt: "old", score: 1 }],
|
|
417
|
+
]);
|
|
418
|
+
const state: FakeState = {
|
|
419
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "old" })]]),
|
|
420
|
+
tombstones: [],
|
|
421
|
+
redactionRules: [],
|
|
422
|
+
auditRecords: [],
|
|
423
|
+
propagateCalls: 0,
|
|
424
|
+
writtenReplacements: [],
|
|
425
|
+
};
|
|
426
|
+
const plannerDeps: PlannerDeps = {
|
|
427
|
+
...makePlannerDeps(dir, candidates),
|
|
428
|
+
classifyAndDraft: async () => ({
|
|
429
|
+
classification: "wrong",
|
|
430
|
+
confidence: 0.9,
|
|
431
|
+
actions: [{ kind: "retract", memoryId: "mem-x" }],
|
|
432
|
+
relevance: [{ memoryId: "mem-x", why: "wrong" }],
|
|
433
|
+
warnings: [],
|
|
434
|
+
}),
|
|
435
|
+
};
|
|
436
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
437
|
+
const plan = await planner.plan({ text: "old", targetIds: ["mem-x"] }, ["default"]);
|
|
438
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
439
|
+
const first = await executor.apply("default", plan.planId, { confirm: true });
|
|
440
|
+
assert.equal(first.status, "applied");
|
|
441
|
+
await assert.rejects(
|
|
442
|
+
executor.apply("default", plan.planId, { confirm: true }),
|
|
443
|
+
/already been applied/,
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("expired plan rejected at apply", async () => {
|
|
449
|
+
await withTempDir(async (dir) => {
|
|
450
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
451
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "old", excerpt: "old", score: 1 }],
|
|
452
|
+
]);
|
|
453
|
+
const state: FakeState = {
|
|
454
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "old" })]]),
|
|
455
|
+
tombstones: [],
|
|
456
|
+
redactionRules: [],
|
|
457
|
+
auditRecords: [],
|
|
458
|
+
propagateCalls: 0,
|
|
459
|
+
writtenReplacements: [],
|
|
460
|
+
};
|
|
461
|
+
// Planner pins createdAt to a fixed past; the executor's clock is far in
|
|
462
|
+
// the future so the plan (TTL 24h) is already expired.
|
|
463
|
+
const past = new Date("2020-01-01T00:00:00Z");
|
|
464
|
+
const plannerDeps: PlannerDeps = {
|
|
465
|
+
...makePlannerDeps(dir, candidates),
|
|
466
|
+
classifyAndDraft: async () => ({
|
|
467
|
+
classification: "wrong",
|
|
468
|
+
confidence: 0.9,
|
|
469
|
+
actions: [{ kind: "retract", memoryId: "mem-x" }],
|
|
470
|
+
relevance: [{ memoryId: "mem-x", why: "wrong" }],
|
|
471
|
+
warnings: [],
|
|
472
|
+
}),
|
|
473
|
+
now: () => past,
|
|
474
|
+
};
|
|
475
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
476
|
+
const plan = await planner.plan({ text: "old", targetIds: ["mem-x"] }, ["default"]);
|
|
477
|
+
const future = new Date("2030-01-01T00:00:00Z");
|
|
478
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state, { now: () => future }), planner);
|
|
479
|
+
await assert.rejects(
|
|
480
|
+
executor.apply("default", plan.planId, { confirm: true }),
|
|
481
|
+
/expired/,
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("redaction_rule registers a future-extraction block (never_store)", async () => {
|
|
487
|
+
await withTempDir(async (dir) => {
|
|
488
|
+
const state: FakeState = {
|
|
489
|
+
memories: new Map(),
|
|
490
|
+
tombstones: [],
|
|
491
|
+
redactionRules: [],
|
|
492
|
+
auditRecords: [],
|
|
493
|
+
propagateCalls: 0,
|
|
494
|
+
writtenReplacements: [],
|
|
495
|
+
};
|
|
496
|
+
// No candidates needed for a redaction_rule, but the planner still needs a
|
|
497
|
+
// non-empty candidate set to proceed past LOCATE. Use a placeholder.
|
|
498
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
499
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "secret", excerpt: "secret", score: 1 }],
|
|
500
|
+
]);
|
|
501
|
+
const plannerDeps: PlannerDeps = {
|
|
502
|
+
...makePlannerDeps(dir, candidates),
|
|
503
|
+
classifyAndDraft: async () => ({
|
|
504
|
+
classification: "never_store",
|
|
505
|
+
confidence: 0.9,
|
|
506
|
+
actions: [{ kind: "redaction_rule", pattern: "secret-token-\\d+" }],
|
|
507
|
+
relevance: [],
|
|
508
|
+
warnings: [],
|
|
509
|
+
}),
|
|
510
|
+
};
|
|
511
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
512
|
+
const plan = await planner.plan({ text: "secret", targetIds: ["mem-x"] }, ["default"]);
|
|
513
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
514
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
515
|
+
assert.equal(outcome.status, "applied");
|
|
516
|
+
assert.deepEqual(state.redactionRules, ["secret-token-\\d+"]);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("apply without confirmation is rejected (rule 48)", async () => {
|
|
521
|
+
await withTempDir(async (dir) => {
|
|
522
|
+
const state: FakeState = {
|
|
523
|
+
memories: new Map(),
|
|
524
|
+
tombstones: [],
|
|
525
|
+
redactionRules: [],
|
|
526
|
+
auditRecords: [],
|
|
527
|
+
propagateCalls: 0,
|
|
528
|
+
writtenReplacements: [],
|
|
529
|
+
};
|
|
530
|
+
const planner = new CorrectionPlanner(makePlannerDeps(dir, new Map()));
|
|
531
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
532
|
+
await assert.rejects(
|
|
533
|
+
executor.apply("default", "any-plan", { confirm: false }),
|
|
534
|
+
/requires explicit confirmation/,
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("rescope action moves the memory and tags it applied", async () => {
|
|
540
|
+
await withTempDir(async (dir) => {
|
|
541
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
542
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "fact", excerpt: "fact", score: 1 }],
|
|
543
|
+
]);
|
|
544
|
+
const state: FakeState = {
|
|
545
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "fact" })]]),
|
|
546
|
+
tombstones: [],
|
|
547
|
+
redactionRules: [],
|
|
548
|
+
auditRecords: [],
|
|
549
|
+
propagateCalls: 0,
|
|
550
|
+
writtenReplacements: [],
|
|
551
|
+
};
|
|
552
|
+
const plannerDeps: PlannerDeps = {
|
|
553
|
+
...makePlannerDeps(dir, candidates),
|
|
554
|
+
classifyAndDraft: async () => ({
|
|
555
|
+
classification: "wrong_scope",
|
|
556
|
+
confidence: 0.9,
|
|
557
|
+
actions: [{ kind: "rescope", memoryId: "mem-x", toNamespace: "other-ns" }],
|
|
558
|
+
relevance: [{ memoryId: "mem-x", why: "wrong scope" }],
|
|
559
|
+
warnings: [],
|
|
560
|
+
}),
|
|
561
|
+
};
|
|
562
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
563
|
+
const plan = await planner.plan({ text: "fact", targetIds: ["mem-x"] }, ["default"]);
|
|
564
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
565
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
566
|
+
assert.equal(outcome.status, "applied");
|
|
567
|
+
const result = outcome.results.find((r) => r.action.kind === "rescope");
|
|
568
|
+
assert.equal(result!.status, "applied");
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("PG9: tombstone failure does NOT retire the source memory (write tombstone before retire)", async () => {
|
|
573
|
+
await withTempDir(async (dir) => {
|
|
574
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
575
|
+
["mem-old", { memoryId: "mem-old", path: "facts/mem-old.md", content: "we use MySQL", excerpt: "we use MySQL", score: 1 }],
|
|
576
|
+
]);
|
|
577
|
+
const state: FakeState = {
|
|
578
|
+
memories: new Map([["mem-old", fakeMemory({ memoryId: "mem-old", content: "we use MySQL" })]]),
|
|
579
|
+
tombstones: [],
|
|
580
|
+
redactionRules: [],
|
|
581
|
+
auditRecords: [],
|
|
582
|
+
propagateCalls: 0,
|
|
583
|
+
writtenReplacements: [],
|
|
584
|
+
// Inject a tombstone-store failure for the retracted memory.
|
|
585
|
+
tombstoneFailOnMemoryId: "mem-old",
|
|
586
|
+
};
|
|
587
|
+
const plannerDeps: PlannerDeps = {
|
|
588
|
+
...makePlannerDeps(dir, candidates),
|
|
589
|
+
classifyAndDraft: async () => ({
|
|
590
|
+
classification: "outdated",
|
|
591
|
+
confidence: 0.9,
|
|
592
|
+
actions: [{ kind: "retract", memoryId: "mem-old" }],
|
|
593
|
+
relevance: [{ memoryId: "mem-old", why: "retracted" }],
|
|
594
|
+
warnings: [],
|
|
595
|
+
}),
|
|
596
|
+
};
|
|
597
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
598
|
+
const plan = await planner.plan({ text: "we use MySQL", targetIds: ["mem-old"] }, ["default"]);
|
|
599
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
600
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
601
|
+
// The action must be reported failed (the tombstone write threw).
|
|
602
|
+
const result = outcome.results.find((r) => r.action.kind === "retract");
|
|
603
|
+
assert.equal(result!.status, "failed");
|
|
604
|
+
// CRITICAL (PG9): retireMemory must NOT have run — the source stays active
|
|
605
|
+
// so a retry operates on un-mutated state with no resurrection window.
|
|
606
|
+
assert.equal(state.retireCalls ?? 0, 0, "retireMemory must not run when appendTombstone fails");
|
|
607
|
+
assert.equal(state.memories.get("mem-old")!.status, "active", "source memory stays active");
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("Of0pz: supersede with a missing loser writes NO replacement (preflight before write)", async () => {
|
|
612
|
+
await withTempDir(async (dir) => {
|
|
613
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
614
|
+
["mem-gone", { memoryId: "mem-gone", path: "facts/mem-gone.md", content: "stale", excerpt: "stale", score: 1 }],
|
|
615
|
+
]);
|
|
616
|
+
const state: FakeState = {
|
|
617
|
+
// The loser is absent from the executor's memory store (deleted between
|
|
618
|
+
// plan and apply). The preflight must reject before writeReplacement.
|
|
619
|
+
memories: new Map(),
|
|
620
|
+
tombstones: [],
|
|
621
|
+
redactionRules: [],
|
|
622
|
+
auditRecords: [],
|
|
623
|
+
propagateCalls: 0,
|
|
624
|
+
writtenReplacements: [],
|
|
625
|
+
};
|
|
626
|
+
const plannerDeps: PlannerDeps = {
|
|
627
|
+
...makePlannerDeps(dir, candidates),
|
|
628
|
+
classifyAndDraft: async () => ({
|
|
629
|
+
classification: "outdated",
|
|
630
|
+
confidence: 0.9,
|
|
631
|
+
actions: [{ kind: "supersede", loserId: "mem-gone", replacement: { content: "new fact" } }],
|
|
632
|
+
relevance: [{ memoryId: "mem-gone", why: "stale" }],
|
|
633
|
+
warnings: [],
|
|
634
|
+
}),
|
|
635
|
+
};
|
|
636
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
637
|
+
const plan = await planner.plan({ text: "stale", targetIds: ["mem-gone"] }, ["default"]);
|
|
638
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
639
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
640
|
+
// The supersede action must be failed (loser not found), and NO replacement
|
|
641
|
+
// must have been written — no orphan fact that supersedes nothing.
|
|
642
|
+
const result = outcome.results.find((r) => r.action.kind === "supersede");
|
|
643
|
+
assert.ok(result, "supersede result must be present");
|
|
644
|
+
assert.equal(result!.status, "failed");
|
|
645
|
+
assert.ok(result!.error!.includes("not found"), "error must mention not-found loser");
|
|
646
|
+
assert.equal(state.writtenReplacements.length, 0, "no replacement written for a missing loser");
|
|
647
|
+
assert.equal(state.tombstones.length, 0, "no tombstone for a preflight-rejected supersede");
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("OgIqp: tombstone dep throw (wiring null→throw when enabled) → source NOT retired", async () => {
|
|
652
|
+
await withTempDir(async (dir) => {
|
|
653
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
654
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "wrong", excerpt: "wrong", score: 1 }],
|
|
655
|
+
]);
|
|
656
|
+
const state: FakeState = {
|
|
657
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "wrong" })]]),
|
|
658
|
+
tombstones: [],
|
|
659
|
+
redactionRules: [],
|
|
660
|
+
auditRecords: [],
|
|
661
|
+
propagateCalls: 0,
|
|
662
|
+
writtenReplacements: [],
|
|
663
|
+
// The wiring converts enabled+null into a throw (OgIqp). The executor
|
|
664
|
+
// sees this as a tombstone-dep throw, which retireAndTombstone catches
|
|
665
|
+
// BEFORE retireMemory runs (PG9 ordering). This test covers the failure
|
|
666
|
+
// CLASS: tombstone persistence failure → source stays active.
|
|
667
|
+
tombstoneFailOnMemoryId: "mem-x",
|
|
668
|
+
};
|
|
669
|
+
const plannerDeps: PlannerDeps = {
|
|
670
|
+
...makePlannerDeps(dir, candidates),
|
|
671
|
+
classifyAndDraft: async () => ({
|
|
672
|
+
classification: "wrong",
|
|
673
|
+
confidence: 0.9,
|
|
674
|
+
actions: [{ kind: "retract", memoryId: "mem-x" }],
|
|
675
|
+
relevance: [{ memoryId: "mem-x", why: "wrong" }],
|
|
676
|
+
warnings: [],
|
|
677
|
+
}),
|
|
678
|
+
};
|
|
679
|
+
const planner = new CorrectionPlanner(plannerDeps);
|
|
680
|
+
const plan = await planner.plan({ text: "wrong", targetIds: ["mem-x"] }, ["default"]);
|
|
681
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), planner);
|
|
682
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
683
|
+
const result = outcome.results.find((r) => r.action.kind === "retract");
|
|
684
|
+
assert.equal(result!.status, "failed", "retract must fail when tombstone persistence fails");
|
|
685
|
+
assert.equal(state.retireCalls ?? 0, 0, "source must NOT be retired without a tombstone");
|
|
686
|
+
assert.equal(state.memories.get("mem-x")!.status, "active", "source stays active");
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("OgIqt: plan marked `applying` before mutations; markConsumed failure leaves it non-retryable", async () => {
|
|
691
|
+
await withTempDir(async (dir) => {
|
|
692
|
+
const candidates = new Map<string, PlannerCandidate>([
|
|
693
|
+
["mem-x", { memoryId: "mem-x", path: "facts/mem-x.md", content: "wrong", excerpt: "wrong", score: 1 }],
|
|
694
|
+
]);
|
|
695
|
+
const state: FakeState = {
|
|
696
|
+
memories: new Map([["mem-x", fakeMemory({ memoryId: "mem-x", content: "wrong" })]]),
|
|
697
|
+
tombstones: [],
|
|
698
|
+
redactionRules: [],
|
|
699
|
+
auditRecords: [],
|
|
700
|
+
propagateCalls: 0,
|
|
701
|
+
writtenReplacements: [],
|
|
702
|
+
};
|
|
703
|
+
const basePlannerDeps: PlannerDeps = {
|
|
704
|
+
...makePlannerDeps(dir, candidates),
|
|
705
|
+
classifyAndDraft: async () => ({
|
|
706
|
+
classification: "wrong",
|
|
707
|
+
confidence: 0.9,
|
|
708
|
+
actions: [{ kind: "retract", memoryId: "mem-x" }],
|
|
709
|
+
relevance: [{ memoryId: "mem-x", why: "wrong" }],
|
|
710
|
+
warnings: [],
|
|
711
|
+
}),
|
|
712
|
+
};
|
|
713
|
+
const realPlanner = new CorrectionPlanner(basePlannerDeps);
|
|
714
|
+
const plan = await realPlanner.plan({ text: "wrong", targetIds: ["mem-x"] }, ["default"]);
|
|
715
|
+
// Override markConsumed: let the optimistic "applying" mark succeed (it
|
|
716
|
+
// runs before mutations), but make the FINAL mark (applied/partial) throw
|
|
717
|
+
// — simulating a transient rename/IO error after mutations complete.
|
|
718
|
+
const originalMarkConsumed = realPlanner.markConsumed.bind(realPlanner);
|
|
719
|
+
realPlanner.markConsumed = async (ns, pid, status) => {
|
|
720
|
+
if (status === "applying") return originalMarkConsumed(ns, pid, status);
|
|
721
|
+
throw new Error("injected markConsumed failure (transient rename error)");
|
|
722
|
+
};
|
|
723
|
+
const executor = new CorrectionExecutor(makeExecutorDeps(state), realPlanner);
|
|
724
|
+
const outcome = await executor.apply("default", plan.planId, { confirm: true });
|
|
725
|
+
// The corrections DID apply (phases 1-4 succeeded) — the retract ran.
|
|
726
|
+
assert.equal(state.retireCalls ?? 0, 1, "mutations ran despite markConsumed failure");
|
|
727
|
+
assert.equal(state.tombstones.length, 1, "tombstone was emitted");
|
|
728
|
+
// But the outcome carries a warning about the mark-consumed failure.
|
|
729
|
+
const warnings = (outcome as CorrectionOutcome & { warnings?: string[] }).warnings ?? [];
|
|
730
|
+
assert.ok(warnings.some((w) => w.includes("mark-consumed")), "warning must mention mark-consumed failure");
|
|
731
|
+
// CRITICAL (OgIqt): the plan is NOT retryable. It was marked `applying`
|
|
732
|
+
// before mutations, so a second apply is rejected (not re-run).
|
|
733
|
+
const reloaded = await realPlanner.loadPlan("default", plan.planId);
|
|
734
|
+
assert.ok(reloaded, "plan must still be loadable");
|
|
735
|
+
assert.equal(reloaded!.status, "applying", "plan must be `applying`, NOT `pending`");
|
|
736
|
+
await assert.rejects(
|
|
737
|
+
executor.apply("default", plan.planId, { confirm: true }),
|
|
738
|
+
/in progress/,
|
|
739
|
+
"second apply must be rejected — plan is non-retryable after mutations",
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
});
|