@remnic/core 9.51.1 → 9.52.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/dist/access-admin-ops-surface.d.ts +6 -5
- package/dist/access-admin-ops-surface.js +16 -16
- package/dist/access-authorization-probe.d.ts +6 -5
- package/dist/access-authorization-probe.js +17 -17
- package/dist/access-boundary.d.ts +6 -5
- package/dist/access-boundary.js +16 -16
- package/dist/access-cli.js +39 -39
- package/dist/access-extraction-force-flush.d.ts +6 -5
- package/dist/access-extraction-force-flush.js +16 -16
- package/dist/access-http-lcm-compaction.d.ts +6 -5
- package/dist/access-http-lifecycle-flush.d.ts +6 -5
- package/dist/access-http-offline-stream.d.ts +6 -5
- package/dist/access-http-query.js +16 -16
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +22 -22
- package/dist/access-identity-continuity-surface.d.ts +5 -4
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.d.ts +6 -5
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +20 -20
- package/dist/access-namespace-preflight.js +16 -16
- package/dist/access-observe-write-surface.d.ts +6 -5
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-offline-manifest.d.ts +5 -4
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.d.ts +9 -8
- package/dist/access-operations.js +19 -19
- package/dist/access-recall-concurrency.d.ts +6 -5
- package/dist/access-recall-concurrency.js +16 -16
- package/dist/access-recall-response.d.ts +6 -5
- package/dist/access-recall-response.js +16 -16
- package/dist/access-recall-surface.d.ts +6 -5
- package/dist/access-recall-surface.js +16 -16
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-BY0KJ44c.d.ts → access-service-Cbk3bNbD.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +6 -5
- package/dist/access-service.d.ts +6 -5
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/bootstrap.d.ts +5 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- package/dist/buffer.d.ts +1 -1
- package/dist/{capsule-merge-XMFBXVDP.js → capsule-merge-HCJJMLPS.js} +4 -4
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-PWN32TAG.js → chunk-2ASAS3CQ.js} +2 -2
- package/dist/{chunk-WQM326GS.js → chunk-2LY6GIIE.js} +2 -2
- package/dist/{chunk-JL43BVDE.js → chunk-2OIQDTDC.js} +2 -2
- package/dist/{chunk-33SV3E43.js → chunk-3OY7KXHM.js} +3 -3
- package/dist/{chunk-AIUBBHSF.js → chunk-5UZ6UCOZ.js} +3 -3
- package/dist/chunk-5WITETWU.js +591 -0
- package/dist/chunk-5WITETWU.js.map +1 -0
- package/dist/{chunk-UT5BV5OG.js → chunk-7HXZJAID.js} +2 -2
- package/dist/{chunk-M7BXDKPR.js → chunk-7M6NYQWB.js} +3 -3
- package/dist/{chunk-TPTECFWR.js → chunk-B6LZ4IBH.js} +3 -3
- package/dist/{chunk-J5LLX6IL.js → chunk-BF6SXNQJ.js} +10 -10
- package/dist/{chunk-4PC4PRJE.js → chunk-BKIFTXHR.js} +2 -2
- package/dist/{chunk-VVG2BOKA.js → chunk-BMEYMKWX.js} +2 -2
- package/dist/{chunk-DCZBSE4F.js → chunk-D5YOZ66N.js} +18 -18
- package/dist/{chunk-BHXYAVUM.js → chunk-DLG5V6ND.js} +2 -2
- package/dist/{chunk-52UKFTUK.js → chunk-F4EU6VUC.js} +2 -2
- package/dist/{chunk-M4A44LUA.js → chunk-GSTIBXBW.js} +2 -2
- package/dist/{chunk-W5XVQFLM.js → chunk-GXFSQ6SD.js} +2 -2
- package/dist/{chunk-XADV5IZ3.js → chunk-HDIUYMSB.js} +5 -5
- package/dist/{chunk-FXTYIA46.js → chunk-J272WKX2.js} +2 -2
- package/dist/{chunk-ENTCVTQR.js → chunk-JJRVPWFY.js} +11 -1
- package/dist/chunk-JJRVPWFY.js.map +1 -0
- package/dist/{chunk-JYL3BVVZ.js → chunk-JSEJH2YQ.js} +2 -2
- package/dist/{chunk-5UDMFE3Q.js → chunk-MLXZD4WM.js} +2 -2
- package/dist/{chunk-PUSLMZBQ.js → chunk-NOEMGE56.js} +2 -2
- package/dist/{chunk-C5QLI33R.js → chunk-QINJITDS.js} +2 -2
- package/dist/{chunk-45BRYON6.js → chunk-QPOHMO6M.js} +2 -2
- package/dist/{chunk-ZQOCRIGA.js → chunk-QYN5PXDZ.js} +3 -3
- package/dist/{chunk-PA45XCUD.js → chunk-RANKJI5T.js} +2 -2
- package/dist/{chunk-ZHY5TMJG.js → chunk-RMHMUUBW.js} +2392 -628
- package/dist/chunk-RMHMUUBW.js.map +1 -0
- package/dist/{chunk-37ZO6LLJ.js → chunk-RYLEDKQ2.js} +2 -2
- package/dist/{chunk-CV6BBVYV.js → chunk-TEMSWHQD.js} +2 -2
- package/dist/{chunk-QGR6OVGT.js → chunk-VFSAWRMZ.js} +2 -2
- package/dist/{chunk-NLK2VZYZ.js → chunk-VXCNQYTC.js} +2 -2
- package/dist/{chunk-AZ2QBGYB.js → chunk-W7Y5JDVB.js} +1 -1
- package/dist/{chunk-VBHPOWBJ.js → chunk-WD5B5AIE.js} +1051 -591
- package/dist/chunk-WD5B5AIE.js.map +1 -0
- package/dist/{chunk-OYESWJ43.js → chunk-WLRD2TXF.js} +15 -10
- package/dist/chunk-WLRD2TXF.js.map +1 -0
- package/dist/{chunk-4F6X3RXW.js → chunk-X57M2Q4I.js} +2 -2
- package/dist/{chunk-6MTSFX7A.js → chunk-X7XEDOSL.js} +8 -8
- package/dist/{chunk-UEVLYVNW.js → chunk-YXKVLYXU.js} +2 -2
- package/dist/{chunk-OQAL6SIR.js → chunk-ZMSZVISA.js} +2 -2
- package/dist/{cli-CnOkH9hA.d.ts → cli-DMAFI0qt.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +33 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -7
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/consolidation-undo.js +3 -3
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +1 -1
- package/dist/convergence-refresh.js +9 -9
- package/dist/corpus-watermark.js +9 -9
- package/dist/dependency-propagation-delivery-D63r24pH.d.ts +41 -0
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +7 -7
- package/dist/explicit-capture.d.ts +5 -4
- package/dist/explicit-capture.js +4 -4
- package/dist/external-wiki-access.d.ts +6 -5
- package/dist/external-wiki-access.js +17 -17
- package/dist/external-wiki-mcp-tools.d.ts +6 -5
- package/dist/extraction.js +7 -7
- package/dist/{forget-UJDJHG6R.js → forget-HDS4N6CG.js} +3 -3
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +687 -686
- package/dist/index.js +39 -39
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/{maintenance-CKwxpdRx.d.ts → maintenance-kBII2qUI.d.ts} +3 -2
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-doctor-corpus.js +10 -10
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +12 -12
- package/dist/{orchestrator-BshGam-o.d.ts → orchestrator-DHy5LtTy.d.ts} +20 -35
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +39 -39
- package/dist/page-versioning.js +3 -3
- package/dist/schemas.d.ts +138 -138
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/{service-BIgVM7fL.d.ts → service-PT2_z7xm.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +7 -7
- package/dist/{storage-CVfGWqCI.d.ts → storage-BmVudhk4.d.ts} +46 -27
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +6 -6
- package/dist/temporal-supersession.d.ts +51 -26
- package/dist/temporal-supersession.js +3 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +7 -7
- package/package.json +2 -2
- package/src/lifecycle/tombstones.ts +30 -0
- package/src/orchestration/consolidation-run.ts +324 -106
- package/src/orchestration/contradiction-linking-coordinator.ts +237 -64
- package/src/orchestration/dependency-propagation-delivery.test.ts +2353 -0
- package/src/orchestration/dependency-propagation-delivery.ts +1143 -0
- package/src/orchestration/dependency-propagation-queue-state.ts +220 -0
- package/src/orchestration/dependency-propagation-recovery-replay.ts +237 -0
- package/src/orchestration/dependency-propagation-replay-reconciliation.ts +73 -0
- package/src/orchestration/dependency-propagation-triggers.test.ts +1069 -11
- package/src/orchestration/dependency-propagation.test.ts +82 -31
- package/src/orchestration/dependency-propagation.ts +55 -17
- package/src/orchestration/extraction-persist-deps.ts +2 -2
- package/src/orchestration/extraction-persist.ts +14 -15
- package/src/orchestration/extraction-queue-coordinator.ts +23 -1
- package/src/orchestration/extraction-shutdown.ts +29 -0
- package/src/orchestration/maintenance.ts +13 -7
- package/src/orchestration/turn-ingestion.ts +5 -1
- package/src/orchestrator-background-writes.test.ts +265 -1
- package/src/orchestrator-extraction-queue.test.ts +39 -0
- package/src/orchestrator.ts +59 -58
- package/src/storage/committed-invalidation.ts +34 -0
- package/src/storage/deletion-revision-store.ts +529 -0
- package/src/storage/supersession-audit.ts +41 -0
- package/src/storage/supersession-side-effects.ts +52 -25
- package/src/storage-contract/failure-semantics.test.ts +12 -0
- package/src/storage-contract/round-trip.test.ts +184 -2
- package/src/storage-deletion-revisions.test.ts +213 -1
- package/src/storage-frontmatter-cas.test.ts +141 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +372 -0
- package/src/storage-replay.test.ts +149 -0
- package/src/storage.ts +345 -358
- package/src/temporal-supersession.test.ts +431 -2
- package/src/temporal-supersession.ts +567 -107
- package/src/testing/subjects/dependency-propagation.test.ts +179 -38
- package/dist/chunk-ENTCVTQR.js.map +0 -1
- package/dist/chunk-EWMTOVMY.js +0 -484
- package/dist/chunk-EWMTOVMY.js.map +0 -1
- package/dist/chunk-OYESWJ43.js.map +0 -1
- package/dist/chunk-VBHPOWBJ.js.map +0 -1
- package/dist/chunk-ZHY5TMJG.js.map +0 -1
- /package/dist/{capsule-merge-XMFBXVDP.js.map → capsule-merge-HCJJMLPS.js.map} +0 -0
- /package/dist/{chunk-PWN32TAG.js.map → chunk-2ASAS3CQ.js.map} +0 -0
- /package/dist/{chunk-WQM326GS.js.map → chunk-2LY6GIIE.js.map} +0 -0
- /package/dist/{chunk-JL43BVDE.js.map → chunk-2OIQDTDC.js.map} +0 -0
- /package/dist/{chunk-33SV3E43.js.map → chunk-3OY7KXHM.js.map} +0 -0
- /package/dist/{chunk-AIUBBHSF.js.map → chunk-5UZ6UCOZ.js.map} +0 -0
- /package/dist/{chunk-UT5BV5OG.js.map → chunk-7HXZJAID.js.map} +0 -0
- /package/dist/{chunk-M7BXDKPR.js.map → chunk-7M6NYQWB.js.map} +0 -0
- /package/dist/{chunk-TPTECFWR.js.map → chunk-B6LZ4IBH.js.map} +0 -0
- /package/dist/{chunk-J5LLX6IL.js.map → chunk-BF6SXNQJ.js.map} +0 -0
- /package/dist/{chunk-4PC4PRJE.js.map → chunk-BKIFTXHR.js.map} +0 -0
- /package/dist/{chunk-VVG2BOKA.js.map → chunk-BMEYMKWX.js.map} +0 -0
- /package/dist/{chunk-DCZBSE4F.js.map → chunk-D5YOZ66N.js.map} +0 -0
- /package/dist/{chunk-BHXYAVUM.js.map → chunk-DLG5V6ND.js.map} +0 -0
- /package/dist/{chunk-52UKFTUK.js.map → chunk-F4EU6VUC.js.map} +0 -0
- /package/dist/{chunk-M4A44LUA.js.map → chunk-GSTIBXBW.js.map} +0 -0
- /package/dist/{chunk-W5XVQFLM.js.map → chunk-GXFSQ6SD.js.map} +0 -0
- /package/dist/{chunk-XADV5IZ3.js.map → chunk-HDIUYMSB.js.map} +0 -0
- /package/dist/{chunk-FXTYIA46.js.map → chunk-J272WKX2.js.map} +0 -0
- /package/dist/{chunk-JYL3BVVZ.js.map → chunk-JSEJH2YQ.js.map} +0 -0
- /package/dist/{chunk-5UDMFE3Q.js.map → chunk-MLXZD4WM.js.map} +0 -0
- /package/dist/{chunk-PUSLMZBQ.js.map → chunk-NOEMGE56.js.map} +0 -0
- /package/dist/{chunk-C5QLI33R.js.map → chunk-QINJITDS.js.map} +0 -0
- /package/dist/{chunk-45BRYON6.js.map → chunk-QPOHMO6M.js.map} +0 -0
- /package/dist/{chunk-ZQOCRIGA.js.map → chunk-QYN5PXDZ.js.map} +0 -0
- /package/dist/{chunk-PA45XCUD.js.map → chunk-RANKJI5T.js.map} +0 -0
- /package/dist/{chunk-37ZO6LLJ.js.map → chunk-RYLEDKQ2.js.map} +0 -0
- /package/dist/{chunk-CV6BBVYV.js.map → chunk-TEMSWHQD.js.map} +0 -0
- /package/dist/{chunk-QGR6OVGT.js.map → chunk-VFSAWRMZ.js.map} +0 -0
- /package/dist/{chunk-NLK2VZYZ.js.map → chunk-VXCNQYTC.js.map} +0 -0
- /package/dist/{chunk-AZ2QBGYB.js.map → chunk-W7Y5JDVB.js.map} +0 -0
- /package/dist/{chunk-4F6X3RXW.js.map → chunk-X57M2Q4I.js.map} +0 -0
- /package/dist/{chunk-6MTSFX7A.js.map → chunk-X7XEDOSL.js.map} +0 -0
- /package/dist/{chunk-UEVLYVNW.js.map → chunk-YXKVLYXU.js.map} +0 -0
- /package/dist/{chunk-OQAL6SIR.js.map → chunk-ZMSZVISA.js.map} +0 -0
- /package/dist/{forget-UJDJHG6R.js.map → forget-HDS4N6CG.js.map} +0 -0
|
@@ -3,11 +3,12 @@ import {
|
|
|
3
3
|
type TombstoneCreatedBy,
|
|
4
4
|
type TombstoneReason,
|
|
5
5
|
} from "../lifecycle/tombstones.js";
|
|
6
|
-
import {
|
|
6
|
+
import { log } from "../logger.js";
|
|
7
7
|
import { stripCitationForTemplate } from "../source-attribution.js";
|
|
8
|
+
import { supersessionKeysForFact } from "../temporal-supersession.js";
|
|
8
9
|
import type { MemoryFile, MemoryFrontmatter } from "../types.js";
|
|
9
10
|
import { composeMemoryEnvelope, type SealedMemoryEnvelope } from "../write-envelope.js";
|
|
10
|
-
import {
|
|
11
|
+
import { hasSupersessionAudit } from "./supersession-audit.js";
|
|
11
12
|
|
|
12
13
|
type TombstoneInput = {
|
|
13
14
|
reason: TombstoneReason;
|
|
@@ -16,6 +17,7 @@ type TombstoneInput = {
|
|
|
16
17
|
rawContent: string;
|
|
17
18
|
entityRef?: string;
|
|
18
19
|
supersessionKey?: string;
|
|
20
|
+
operationKey?: string;
|
|
19
21
|
createdAt?: string;
|
|
20
22
|
contentHash?: string;
|
|
21
23
|
};
|
|
@@ -25,40 +27,45 @@ export interface SupersessionSideEffectOptions {
|
|
|
25
27
|
newMemoryId: string;
|
|
26
28
|
reason: string;
|
|
27
29
|
now: string;
|
|
30
|
+
operationId: string;
|
|
31
|
+
exactReplay: boolean;
|
|
28
32
|
currentBefore: MemoryFile;
|
|
29
33
|
updatedFm: MemoryFrontmatter;
|
|
30
34
|
citationTemplate: string;
|
|
35
|
+
correctionsDir: string;
|
|
36
|
+
readMemoryByPath: (filePath: string) => Promise<MemoryFile | null>;
|
|
31
37
|
isColdOrArchiveTierPath: (memoryPath: string) => boolean;
|
|
32
38
|
invalidateColdMemoriesCache: () => void;
|
|
39
|
+
invalidateAllMemoriesCache: () => void;
|
|
33
40
|
bumpMemoryCorpusVersion: () => void;
|
|
34
41
|
appendLifecycleEvent: () => Promise<void>;
|
|
35
42
|
bumpMemoryStatusVersion: () => void;
|
|
43
|
+
hasExactTombstone: (input: TombstoneInput) => Promise<boolean>;
|
|
36
44
|
appendTombstone: (input: TombstoneInput) => Promise<string | null>;
|
|
37
|
-
writeSealedMemory: (
|
|
45
|
+
writeSealedMemory: (
|
|
46
|
+
envelope: SealedMemoryEnvelope,
|
|
47
|
+
extras: { lineage: [string, string]; sourceMemoryId: string }
|
|
48
|
+
) => Promise<unknown>;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
export async function runSupersessionSideEffects(options: SupersessionSideEffectOptions): Promise<
|
|
51
|
+
export async function runSupersessionSideEffects(options: SupersessionSideEffectOptions): Promise<boolean> {
|
|
41
52
|
const {
|
|
42
53
|
oldMemoryId,
|
|
43
54
|
newMemoryId,
|
|
44
55
|
reason,
|
|
45
56
|
now,
|
|
57
|
+
operationId,
|
|
58
|
+
exactReplay,
|
|
46
59
|
currentBefore,
|
|
47
60
|
updatedFm,
|
|
48
61
|
citationTemplate,
|
|
49
|
-
isColdOrArchiveTierPath,
|
|
50
|
-
invalidateColdMemoriesCache,
|
|
51
|
-
bumpMemoryCorpusVersion,
|
|
52
|
-
appendLifecycleEvent,
|
|
53
|
-
bumpMemoryStatusVersion,
|
|
54
|
-
appendTombstone,
|
|
55
|
-
writeSealedMemory,
|
|
56
62
|
} = options;
|
|
57
63
|
try {
|
|
58
|
-
if (isColdOrArchiveTierPath(currentBefore.path)) invalidateColdMemoriesCache();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
if (options.isColdOrArchiveTierPath(currentBefore.path)) options.invalidateColdMemoriesCache();
|
|
65
|
+
if (exactReplay) options.invalidateAllMemoriesCache();
|
|
66
|
+
else options.bumpMemoryCorpusVersion();
|
|
67
|
+
await options.appendLifecycleEvent();
|
|
68
|
+
options.bumpMemoryStatusVersion();
|
|
62
69
|
log.debug(`superseded memory ${oldMemoryId} by ${newMemoryId}: ${reason}`);
|
|
63
70
|
|
|
64
71
|
if (currentBefore.frontmatter.category === "fact") {
|
|
@@ -75,23 +82,43 @@ export async function runSupersessionSideEffects(options: SupersessionSideEffect
|
|
|
75
82
|
createdBy: "contradiction_resolution",
|
|
76
83
|
createdAt: now,
|
|
77
84
|
supersessionKeysForFact,
|
|
78
|
-
}
|
|
85
|
+
}
|
|
79
86
|
)) {
|
|
80
|
-
|
|
87
|
+
const exactInput = { ...input, operationKey: operationId };
|
|
88
|
+
const exists = (await options.hasExactTombstone(exactInput)) || (await options.hasExactTombstone(input));
|
|
89
|
+
if (!exists) await options.appendTombstone(exactInput);
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
const
|
|
93
|
+
const auditBody = `Superseded: ${currentBefore.content}\n\nReason: ${reason}`;
|
|
94
|
+
const auditExists = await hasSupersessionAudit(
|
|
85
95
|
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
confidence: 1.0,
|
|
89
|
-
tags: ["supersession", "auto-resolved"],
|
|
96
|
+
correctionsDir: options.correctionsDir,
|
|
97
|
+
readMemoryByPath: options.readMemoryByPath,
|
|
90
98
|
},
|
|
91
|
-
|
|
99
|
+
oldMemoryId,
|
|
100
|
+
newMemoryId,
|
|
101
|
+
auditBody
|
|
92
102
|
);
|
|
93
|
-
|
|
103
|
+
if (!auditExists) {
|
|
104
|
+
const auditEnvelope = composeMemoryEnvelope(
|
|
105
|
+
{
|
|
106
|
+
content: auditBody,
|
|
107
|
+
category: "correction",
|
|
108
|
+
confidence: 1,
|
|
109
|
+
tags: ["supersession", "auto-resolved"],
|
|
110
|
+
},
|
|
111
|
+
{ source: "contradiction-detection" }
|
|
112
|
+
);
|
|
113
|
+
await options.writeSealedMemory(auditEnvelope, {
|
|
114
|
+
lineage: [oldMemoryId, newMemoryId],
|
|
115
|
+
sourceMemoryId: oldMemoryId,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
options.invalidateAllMemoriesCache();
|
|
119
|
+
return true;
|
|
94
120
|
} catch (err) {
|
|
95
|
-
log.error(`
|
|
121
|
+
log.error(`supersession side effects failed for ${oldMemoryId}:`, err);
|
|
122
|
+
return false;
|
|
96
123
|
}
|
|
97
124
|
}
|
|
@@ -29,6 +29,18 @@ test("failure-semantics: readAllMemories on a fresh (empty) dir returns [] not n
|
|
|
29
29
|
await cleanup();
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
+
test("failure-semantics: readArchivedMemories surfaces archive readdir errors", async () => {
|
|
33
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
34
|
+
try {
|
|
35
|
+
await writeFile(path.join(baseDir, "archive"), "archive is not a directory", "utf-8");
|
|
36
|
+
await assert.rejects(
|
|
37
|
+
storage.readArchivedMemories(),
|
|
38
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOTDIR",
|
|
39
|
+
);
|
|
40
|
+
} finally {
|
|
41
|
+
await cleanup();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
32
44
|
|
|
33
45
|
test("failure-semantics: getMemoryById on empty store returns null (not throw)", async () => {
|
|
34
46
|
const { storage, cleanup } = await makeStorage();
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
|
-
import { utimes } from "node:fs/promises";
|
|
15
|
+
import { mkdir, readFile, rm, symlink, utimes, writeFile } from "node:fs/promises";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import type { MemoryCategory } from "../types.js";
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
|
|
20
20
|
import { StorageManager } from "../storage.js";
|
|
21
|
+
import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
|
|
21
22
|
import {
|
|
22
23
|
ALL_CATEGORY_DIRS,
|
|
23
24
|
ALL_CATEGORY_KEYS,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
RECALL_NON_MEMORY_DIRS,
|
|
26
27
|
categoryDirName,
|
|
27
28
|
} from "../utils/category-dir.js";
|
|
28
|
-
import { makeStorage } from "./harness.js";
|
|
29
|
+
import { makeStorage, rawMemoryMarkdown } from "./harness.js";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The singular category keys writeMemory accepts, minus "entity" (entities use
|
|
@@ -141,6 +142,165 @@ test("round-trip: invalidateMemory returns false for a non-existent id (not a th
|
|
|
141
142
|
}
|
|
142
143
|
});
|
|
143
144
|
|
|
145
|
+
test("round-trip: invalidation does not record commit proof by default", async () => {
|
|
146
|
+
const { storage, cleanup } = await makeStorage();
|
|
147
|
+
try {
|
|
148
|
+
const { id } = await storage.writeMemory("fact", "proof source");
|
|
149
|
+
const snapshot = await storage.getMemoryById(id);
|
|
150
|
+
assert.ok(snapshot);
|
|
151
|
+
|
|
152
|
+
assert.equal(await storage.invalidateMemory(id), true);
|
|
153
|
+
assert.equal(await storage.hasCommittedInvalidation(snapshot), false);
|
|
154
|
+
const explicitOff = await storage.writeMemory("fact", "proof source with explicit off");
|
|
155
|
+
const explicitSnapshot = await storage.getMemoryById(explicitOff.id);
|
|
156
|
+
assert.ok(explicitSnapshot);
|
|
157
|
+
assert.equal(
|
|
158
|
+
await storage.invalidateMemory(explicitOff.id, undefined, { recordCommitProof: false }),
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
assert.equal(await storage.hasCommittedInvalidation(explicitSnapshot), false);
|
|
162
|
+
} finally {
|
|
163
|
+
await cleanup();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("round-trip: invalidation commit proof survives restart and rejects a changed snapshot", async () => {
|
|
168
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
169
|
+
try {
|
|
170
|
+
const { id } = await storage.writeMemory("fact", "proof source", {
|
|
171
|
+
tags: ["proof"],
|
|
172
|
+
});
|
|
173
|
+
const snapshot = await storage.getMemoryById(id);
|
|
174
|
+
assert.ok(snapshot);
|
|
175
|
+
|
|
176
|
+
assert.equal(await storage.invalidateMemory(id, undefined, { recordCommitProof: true }), true);
|
|
177
|
+
assert.equal(await storage.hasCommittedInvalidation(snapshot), true);
|
|
178
|
+
|
|
179
|
+
const restarted = new StorageManager(baseDir);
|
|
180
|
+
assert.equal(await restarted.hasCommittedInvalidation(snapshot), true);
|
|
181
|
+
assert.equal(
|
|
182
|
+
await restarted.hasCommittedInvalidation({
|
|
183
|
+
...snapshot,
|
|
184
|
+
frontmatter: {
|
|
185
|
+
...snapshot.frontmatter,
|
|
186
|
+
accessCount: 99,
|
|
187
|
+
lastAccessed: "2026-08-09T12:00:00.000Z",
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
true,
|
|
191
|
+
);
|
|
192
|
+
assert.equal(
|
|
193
|
+
await restarted.hasCommittedInvalidation({
|
|
194
|
+
...snapshot,
|
|
195
|
+
content: "changed source",
|
|
196
|
+
}),
|
|
197
|
+
false,
|
|
198
|
+
);
|
|
199
|
+
} finally {
|
|
200
|
+
await cleanup();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
test("round-trip: a post-delete failure retains the committed invalidation proof", async () => {
|
|
204
|
+
const { storage, cleanup } = await makeStorage();
|
|
205
|
+
try {
|
|
206
|
+
const { id } = await storage.writeMemory("fact", "proof survives post-delete failure");
|
|
207
|
+
const snapshot = await storage.getMemoryById(id);
|
|
208
|
+
assert.ok(snapshot);
|
|
209
|
+
const managedStorage = storage as unknown as {
|
|
210
|
+
deleteManagedStorageFile: (filePath: string) => Promise<boolean>;
|
|
211
|
+
rebuildTombstoneBlockedCaptureAfterInvalidation: (ownedMarker?: string) => Promise<void>;
|
|
212
|
+
};
|
|
213
|
+
const originalDelete = managedStorage.deleteManagedStorageFile;
|
|
214
|
+
managedStorage.deleteManagedStorageFile = async (filePath) => originalDelete.call(storage, filePath);
|
|
215
|
+
managedStorage.rebuildTombstoneBlockedCaptureAfterInvalidation = async () => {
|
|
216
|
+
throw new Error("synthetic post-delete failure");
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
assert.equal(
|
|
220
|
+
await storage.invalidateMemory(id, snapshot, { recordCommitProof: true }),
|
|
221
|
+
false,
|
|
222
|
+
);
|
|
223
|
+
assert.equal(await storage.readMemoryByPath(snapshot.path), null);
|
|
224
|
+
assert.equal(await storage.hasCommittedInvalidation(snapshot), true);
|
|
225
|
+
} finally {
|
|
226
|
+
await cleanup();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("round-trip: dependency propagation queue rejects symlinks in root, parent, and target paths", async () => {
|
|
231
|
+
const { storage, baseDir, cleanup } = await makeStorage("remnic-dependency-queue-symlink-");
|
|
232
|
+
const rootLink = path.join(path.dirname(baseDir), `${path.basename(baseDir)}-root-link`);
|
|
233
|
+
const parentTarget = path.join(path.dirname(baseDir), `${path.basename(baseDir)}-parent-target`);
|
|
234
|
+
const parentLink = path.join(baseDir, "queue-parent-link");
|
|
235
|
+
const targetFile = path.join(path.dirname(baseDir), `${path.basename(baseDir)}-target.json`);
|
|
236
|
+
const targetLink = path.join(baseDir, "queue-target-link.json");
|
|
237
|
+
const payload = JSON.stringify({ value: "synthetic" });
|
|
238
|
+
try {
|
|
239
|
+
await symlink(baseDir, rootLink, "dir");
|
|
240
|
+
const storageWithSymlinkRoot = storage as unknown as { baseDir: string };
|
|
241
|
+
const originalBaseDir = storageWithSymlinkRoot.baseDir;
|
|
242
|
+
try {
|
|
243
|
+
storageWithSymlinkRoot.baseDir = rootLink;
|
|
244
|
+
const rootQueuePath = path.join(rootLink, "state", "dependency-propagation", "ready", "job.json");
|
|
245
|
+
await assert.rejects(() => storage.readDependencyPropagationQueueFile(rootQueuePath));
|
|
246
|
+
await assert.rejects(() => storage.writeDependencyPropagationQueueFile(rootQueuePath, payload));
|
|
247
|
+
} finally {
|
|
248
|
+
storageWithSymlinkRoot.baseDir = originalBaseDir;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await mkdir(parentTarget, { recursive: true });
|
|
252
|
+
await symlink(parentTarget, parentLink, "dir");
|
|
253
|
+
const parentQueuePath = path.join(parentLink, "job.json");
|
|
254
|
+
await assert.rejects(() => storage.readDependencyPropagationQueueFile(parentQueuePath));
|
|
255
|
+
await assert.rejects(() => storage.writeDependencyPropagationQueueFile(parentQueuePath, payload));
|
|
256
|
+
|
|
257
|
+
await writeFile(targetFile, payload, "utf8");
|
|
258
|
+
await symlink(targetFile, targetLink);
|
|
259
|
+
await assert.rejects(() => storage.readDependencyPropagationQueueFile(targetLink));
|
|
260
|
+
await assert.rejects(() => storage.writeDependencyPropagationQueueFile(targetLink, payload));
|
|
261
|
+
} finally {
|
|
262
|
+
await rm(rootLink, { recursive: true, force: true });
|
|
263
|
+
await rm(parentLink, { recursive: true, force: true });
|
|
264
|
+
await rm(parentTarget, { recursive: true, force: true });
|
|
265
|
+
await rm(targetLink, { force: true });
|
|
266
|
+
await rm(targetFile, { force: true });
|
|
267
|
+
await cleanup();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("dependency propagation queue storage encrypts payload and fails closed while locked", async () => {
|
|
272
|
+
const { storage, baseDir, cleanup } = await makeStorage("remnic-dependency-queue-storage-");
|
|
273
|
+
const queuePath = path.join(baseDir, "state", "dependency-propagation", "ready", "job.json");
|
|
274
|
+
const payload = JSON.stringify({ secret: "synthetic queue payload" });
|
|
275
|
+
const key = Buffer.alloc(32, 37);
|
|
276
|
+
try {
|
|
277
|
+
storage.setSecureStoreRequired(true);
|
|
278
|
+
storage.setSecureStoreKey(key);
|
|
279
|
+
await storage.writeDependencyPropagationQueueFile(queuePath, payload);
|
|
280
|
+
|
|
281
|
+
const raw = await readFile(queuePath, "utf8");
|
|
282
|
+
assert.doesNotMatch(raw, /synthetic queue payload/);
|
|
283
|
+
|
|
284
|
+
const unlocked = new StorageManager(baseDir);
|
|
285
|
+
unlocked.setSecureStoreRequired(true);
|
|
286
|
+
unlocked.setSecureStoreKey(key);
|
|
287
|
+
assert.equal(await unlocked.readDependencyPropagationQueueFile(queuePath), payload);
|
|
288
|
+
|
|
289
|
+
const locked = new StorageManager(baseDir);
|
|
290
|
+
locked.setSecureStoreRequired(true);
|
|
291
|
+
await assert.rejects(
|
|
292
|
+
() => locked.readDependencyPropagationQueueFile(queuePath),
|
|
293
|
+
SecureStoreLockedError,
|
|
294
|
+
);
|
|
295
|
+
await assert.rejects(
|
|
296
|
+
() => locked.writeDependencyPropagationQueueFile(queuePath, payload),
|
|
297
|
+
SecureStoreLockedError,
|
|
298
|
+
);
|
|
299
|
+
} finally {
|
|
300
|
+
await cleanup();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
144
304
|
test("round-trip: getMemoryById returns null for a non-existent id", async () => {
|
|
145
305
|
const { storage, cleanup } = await makeStorage();
|
|
146
306
|
try {
|
|
@@ -163,6 +323,28 @@ test("round-trip: readMemoryByPath returns null for a non-existent path", async
|
|
|
163
323
|
}
|
|
164
324
|
});
|
|
165
325
|
|
|
326
|
+
test("round-trip: archive fallback skips a bad file and returns readable siblings", async () => {
|
|
327
|
+
const { storage, baseDir, cleanup } = await makeStorage("remnic-archive-fallback-");
|
|
328
|
+
try {
|
|
329
|
+
const archiveDir = path.join(baseDir, "archive", "2026-08-10");
|
|
330
|
+
await mkdir(archiveDir, { recursive: true });
|
|
331
|
+
await writeFile(
|
|
332
|
+
path.join(archiveDir, "readable.md"),
|
|
333
|
+
rawMemoryMarkdown("archived-readable", "fact", "readable archive content"),
|
|
334
|
+
"utf-8",
|
|
335
|
+
);
|
|
336
|
+
await writeFile(path.join(archiveDir, "broken.md"), "not markdown", "utf-8");
|
|
337
|
+
|
|
338
|
+
const archived = await storage.readArchivedMemories();
|
|
339
|
+
assert.deepEqual(
|
|
340
|
+
archived.map((memory) => memory.frontmatter.id),
|
|
341
|
+
["archived-readable"],
|
|
342
|
+
);
|
|
343
|
+
} finally {
|
|
344
|
+
await cleanup();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
166
348
|
test("round-trip: ALL_CATEGORY_DIRS is exactly facts + every CATEGORY_DIR_MAP value", () => {
|
|
167
349
|
// ALL_CATEGORY_DIRS must contain directory NAMES (facts, decisions, ...) not
|
|
168
350
|
// singular keys (fact, decision, ...). This guards the parity between
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import * as assert from "node:assert/strict";
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { test } from "node:test";
|
|
6
|
+
import { DeletionRevisionStore } from "./storage/deletion-revision-store.js";
|
|
7
|
+
import type { MemoryFile } from "./types.js";
|
|
6
8
|
import { StorageManager } from "./storage.js";
|
|
7
9
|
|
|
10
|
+
function createDeletionRevisionStore(baseDir: string): DeletionRevisionStore {
|
|
11
|
+
const metadataDir = path.join(baseDir, ".offline-sync");
|
|
12
|
+
return new DeletionRevisionStore({
|
|
13
|
+
baseDir,
|
|
14
|
+
deletionRevisionMetadataPath: path.join(metadataDir, "deletion-revisions.v1.json"),
|
|
15
|
+
invalidationCommitMetadataPath: path.join(metadataDir, "invalidation-commits.v1.json"),
|
|
16
|
+
deletionRevisionLockPath: path.join(metadataDir, "deletion-revisions.v1.json.lock"),
|
|
17
|
+
assertManagedStoragePath: (filePath) => path.resolve(filePath),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function proofMemory(id = "memory-1"): MemoryFile {
|
|
22
|
+
return {
|
|
23
|
+
path: `facts/${id}.md`,
|
|
24
|
+
content: "proof source",
|
|
25
|
+
frontmatter: { id },
|
|
26
|
+
} as MemoryFile;
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
test("storage persists the actual revision time for a successful path deletion", async () => {
|
|
9
30
|
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-deletion-revision-"));
|
|
10
31
|
try {
|
|
@@ -89,3 +110,194 @@ test("storage merges a known replicated deletion revision for an already-absent
|
|
|
89
110
|
await rm(root, { recursive: true, force: true });
|
|
90
111
|
}
|
|
91
112
|
});
|
|
113
|
+
|
|
114
|
+
test("deletion revision and proof stores reject a symlinked storage root", async () => {
|
|
115
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-root-outside-"));
|
|
116
|
+
const rootLink = path.join(path.dirname(outside), `${path.basename(outside)}-link`);
|
|
117
|
+
try {
|
|
118
|
+
await symlink(outside, rootLink, "dir");
|
|
119
|
+
const store = createDeletionRevisionStore(rootLink);
|
|
120
|
+
|
|
121
|
+
await assert.rejects(store.readDeletionRevisions(), /must not pass through a symlink/);
|
|
122
|
+
await assert.rejects(store.recordCommittedInvalidation(proofMemory()), /must not pass through a symlink/);
|
|
123
|
+
await assert.rejects(
|
|
124
|
+
lstat(path.join(outside, ".offline-sync")),
|
|
125
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT",
|
|
126
|
+
);
|
|
127
|
+
} finally {
|
|
128
|
+
await rm(rootLink, { force: true });
|
|
129
|
+
await rm(outside, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("deletion revision and proof stores reject a symlinked metadata parent before outside writes", async () => {
|
|
134
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-parent-root-"));
|
|
135
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-parent-outside-"));
|
|
136
|
+
try {
|
|
137
|
+
await symlink(outside, path.join(root, ".offline-sync"), "dir");
|
|
138
|
+
const store = createDeletionRevisionStore(root);
|
|
139
|
+
|
|
140
|
+
await assert.rejects(
|
|
141
|
+
store.recordReplicatedDeletionRevision(path.join(root, "facts", "deleted.md"), 2000),
|
|
142
|
+
/must not pass through a symlink/,
|
|
143
|
+
);
|
|
144
|
+
await assert.rejects(store.recordCommittedInvalidation(proofMemory()), /must not pass through a symlink/);
|
|
145
|
+
await assert.rejects(
|
|
146
|
+
lstat(path.join(outside, "deletion-revisions.v1.json")),
|
|
147
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT",
|
|
148
|
+
);
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
lstat(path.join(outside, "invalidation-commits.v1.json")),
|
|
151
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT",
|
|
152
|
+
);
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(root, { recursive: true, force: true });
|
|
155
|
+
await rm(outside, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("deletion revision and proof stores reject symlinked metadata targets before outside reads or writes", async () => {
|
|
160
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-target-root-"));
|
|
161
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-target-outside-"));
|
|
162
|
+
const deletionSentinel = path.join(outside, "deletion-sentinel");
|
|
163
|
+
const proofSentinel = path.join(outside, "proof-sentinel");
|
|
164
|
+
try {
|
|
165
|
+
const metadataDir = path.join(root, ".offline-sync");
|
|
166
|
+
await mkdir(metadataDir, { recursive: true });
|
|
167
|
+
await writeFile(deletionSentinel, "deletion sentinel");
|
|
168
|
+
await writeFile(proofSentinel, "proof sentinel");
|
|
169
|
+
await symlink(deletionSentinel, path.join(metadataDir, "deletion-revisions.v1.json"));
|
|
170
|
+
await symlink(proofSentinel, path.join(metadataDir, "invalidation-commits.v1.json"));
|
|
171
|
+
const store = createDeletionRevisionStore(root);
|
|
172
|
+
|
|
173
|
+
await assert.rejects(store.readDeletionRevisions(), /must not pass through a symlink/);
|
|
174
|
+
await assert.rejects(
|
|
175
|
+
store.recordCommittedInvalidation(proofMemory()),
|
|
176
|
+
/must not pass through a symlink/,
|
|
177
|
+
);
|
|
178
|
+
assert.equal(await readFile(deletionSentinel, "utf8"), "deletion sentinel");
|
|
179
|
+
assert.equal(await readFile(proofSentinel, "utf8"), "proof sentinel");
|
|
180
|
+
} finally {
|
|
181
|
+
await rm(root, { recursive: true, force: true });
|
|
182
|
+
await rm(outside, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("managed deletion and write reject a symlinked storage-file parent before outside access", async () => {
|
|
187
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-file-root-"));
|
|
188
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-symlink-file-outside-"));
|
|
189
|
+
const outsideDeleted = path.join(outside, "deleted.md");
|
|
190
|
+
const outsideWritten = path.join(outside, "written.md");
|
|
191
|
+
try {
|
|
192
|
+
await writeFile(outsideDeleted, "must remain");
|
|
193
|
+
await symlink(outside, path.join(root, "facts"), "dir");
|
|
194
|
+
const store = createDeletionRevisionStore(root);
|
|
195
|
+
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
store.deleteManagedStorageFile(path.join(root, "facts", "deleted.md")),
|
|
198
|
+
/must not pass through a symlink/,
|
|
199
|
+
);
|
|
200
|
+
await assert.rejects(
|
|
201
|
+
store.writeManagedStorageFile(path.join(root, "facts", "written.md"), async () => {
|
|
202
|
+
await writeFile(outsideWritten, "must not write");
|
|
203
|
+
}),
|
|
204
|
+
/must not pass through a symlink/,
|
|
205
|
+
);
|
|
206
|
+
assert.equal(await readFile(outsideDeleted, "utf8"), "must remain");
|
|
207
|
+
await assert.rejects(
|
|
208
|
+
lstat(outsideWritten),
|
|
209
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT",
|
|
210
|
+
);
|
|
211
|
+
} finally {
|
|
212
|
+
await rm(root, { recursive: true, force: true });
|
|
213
|
+
await rm(outside, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
test("failed proof rollback stays quarantined and fails closed", async () => {
|
|
217
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-invalidation-proof-quarantine-"));
|
|
218
|
+
try {
|
|
219
|
+
const store = createDeletionRevisionStore(root);
|
|
220
|
+
const memory = proofMemory("quarantine-memory");
|
|
221
|
+
await store.recordCommittedInvalidation(memory);
|
|
222
|
+
const internal = store as unknown as {
|
|
223
|
+
writeInvalidationCommitMetadata: (...args: unknown[]) => Promise<void>;
|
|
224
|
+
};
|
|
225
|
+
internal.writeInvalidationCommitMetadata = async () => {
|
|
226
|
+
throw new Error("synthetic proof rollback failure");
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
await assert.rejects(
|
|
230
|
+
store.clearCommittedInvalidation(memory),
|
|
231
|
+
/synthetic proof rollback failure/,
|
|
232
|
+
);
|
|
233
|
+
assert.equal(await store.hasCommittedInvalidation(memory), false);
|
|
234
|
+
} finally {
|
|
235
|
+
await rm(root, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
test("proof quarantine is instance-scoped and does not mask durable proof in another store", async () => {
|
|
239
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-invalidation-proof-quarantine-instance-"));
|
|
240
|
+
try {
|
|
241
|
+
const writer = createDeletionRevisionStore(root);
|
|
242
|
+
const reader = createDeletionRevisionStore(root);
|
|
243
|
+
const memory = proofMemory("instance-scoped-quarantine");
|
|
244
|
+
await writer.recordCommittedInvalidation(memory);
|
|
245
|
+
|
|
246
|
+
const internal = writer as unknown as {
|
|
247
|
+
writeInvalidationCommitMetadata: (...args: unknown[]) => Promise<void>;
|
|
248
|
+
};
|
|
249
|
+
internal.writeInvalidationCommitMetadata = async () => {
|
|
250
|
+
throw new Error("synthetic proof rollback failure");
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await assert.rejects(
|
|
254
|
+
writer.clearCommittedInvalidation(memory),
|
|
255
|
+
/synthetic proof rollback failure/,
|
|
256
|
+
);
|
|
257
|
+
assert.equal(await writer.hasCommittedInvalidation(memory), false);
|
|
258
|
+
assert.equal(await reader.hasCommittedInvalidation(memory), true);
|
|
259
|
+
} finally {
|
|
260
|
+
await rm(root, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
test("failed proof rollback quarantine stays bounded and fails closed after the cap", async () => {
|
|
264
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-invalidation-proof-quarantine-cap-"));
|
|
265
|
+
try {
|
|
266
|
+
const store = createDeletionRevisionStore(root);
|
|
267
|
+
const internal = store as unknown as {
|
|
268
|
+
invalidationProofQuarantine: Set<string>;
|
|
269
|
+
writeInvalidationCommitMetadata: (...args: unknown[]) => Promise<void>;
|
|
270
|
+
};
|
|
271
|
+
const originalWrite = internal.writeInvalidationCommitMetadata;
|
|
272
|
+
for (let index = 0; index <= 1024; index += 1) {
|
|
273
|
+
const memory = proofMemory(`bounded-quarantine-${index}`);
|
|
274
|
+
await store.recordCommittedInvalidation(memory);
|
|
275
|
+
internal.writeInvalidationCommitMetadata = async () => {
|
|
276
|
+
throw new Error("synthetic proof rollback failure");
|
|
277
|
+
};
|
|
278
|
+
await assert.rejects(store.clearCommittedInvalidation(memory), /synthetic proof rollback failure/);
|
|
279
|
+
internal.writeInvalidationCommitMetadata = originalWrite;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const quarantinedBeforeOverflow = proofMemory("bounded-quarantine-0");
|
|
283
|
+
assert.equal(await store.hasCommittedInvalidation(quarantinedBeforeOverflow), false);
|
|
284
|
+
|
|
285
|
+
const recordedAfterOverflow = proofMemory("recorded-after-quarantine-cap");
|
|
286
|
+
await store.recordCommittedInvalidation(recordedAfterOverflow);
|
|
287
|
+
assert.equal(await store.hasCommittedInvalidation(recordedAfterOverflow), true);
|
|
288
|
+
internal.writeInvalidationCommitMetadata = async () => {
|
|
289
|
+
throw new Error("synthetic post-overflow rollback failure");
|
|
290
|
+
};
|
|
291
|
+
await assert.rejects(
|
|
292
|
+
store.clearCommittedInvalidation(recordedAfterOverflow),
|
|
293
|
+
/synthetic post-overflow rollback failure/,
|
|
294
|
+
);
|
|
295
|
+
assert.equal(await store.hasCommittedInvalidation(recordedAfterOverflow), false);
|
|
296
|
+
internal.writeInvalidationCommitMetadata = originalWrite;
|
|
297
|
+
await store.recordCommittedInvalidation(recordedAfterOverflow);
|
|
298
|
+
assert.equal(await store.hasCommittedInvalidation(recordedAfterOverflow), true);
|
|
299
|
+
assert.equal(internal.invalidationProofQuarantine.size, 1024);
|
|
300
|
+
} finally {
|
|
301
|
+
await rm(root, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
});
|