@remnic/core 9.3.704 → 9.3.705
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 +1 -1
- package/dist/access-boundary.js +25 -25
- package/dist/access-cli.js +53 -52
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +30 -29
- package/dist/access-mcp.js +29 -28
- package/dist/access-operations-batch.d.ts +2 -0
- package/dist/access-operations-batch.js +120 -0
- package/dist/access-operations.d.ts +1 -0
- package/dist/access-operations.js +28 -27
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +24 -24
- package/dist/access-surface-catalog.js +57 -54
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/{auto-sync-3AAP25FV.js → auto-sync-24NNS3GU.js} +3 -3
- package/dist/briefing.js +4 -4
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.js +8 -8
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-PJ5MDXOF.js → chunk-2SYPOX45.js} +10 -10
- package/dist/{chunk-7G4KPGR3.js → chunk-47LPHBIP.js} +2 -2
- package/dist/{chunk-RYDT7LAI.js → chunk-5DTWZ66X.js} +2 -2
- package/dist/chunk-5DTWZ66X.js.map +1 -0
- package/dist/{chunk-OCFNG4LB.js → chunk-6GJ2WQRK.js} +2 -2
- package/dist/{chunk-4NFVPDIL.js → chunk-74MYJSKB.js} +5 -5
- package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
- package/dist/{chunk-DE2OPIQD.js → chunk-7ARKLLCV.js} +6 -6
- package/dist/{chunk-3EHCV6YV.js → chunk-7L54YZUI.js} +63 -63
- package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
- package/dist/{chunk-UNNDFYUX.js → chunk-ARV3AUOM.js} +5 -5
- package/dist/{chunk-S24J5LLQ.js → chunk-BKBSE3BU.js} +3 -3
- package/dist/{chunk-AE3C4PY6.js → chunk-BMKGG7GC.js} +2 -2
- package/dist/{chunk-NMW3T6T6.js → chunk-C2EHWNP2.js} +9 -9
- package/dist/{chunk-VKJHM6PL.js → chunk-DGTDRIUO.js} +5 -5
- package/dist/chunk-GI3MTYDU.js +1303 -0
- package/dist/chunk-GI3MTYDU.js.map +1 -0
- package/dist/{chunk-25FHSOEB.js → chunk-GT4VBKM4.js} +59 -5
- package/dist/chunk-GT4VBKM4.js.map +1 -0
- package/dist/{chunk-OOEVMJK4.js → chunk-HEHPVBWA.js} +6 -6
- package/dist/{chunk-YOI3ELXF.js → chunk-IF362TF6.js} +2 -2
- package/dist/{chunk-ABCGDW3A.js → chunk-IFR23TC5.js} +4 -4
- package/dist/{chunk-ZXFNSKPY.js → chunk-JHAYR2JY.js} +32 -32
- package/dist/{chunk-CTY6QSLZ.js → chunk-JZCKDTAS.js} +4 -4
- package/dist/{chunk-CTY6QSLZ.js.map → chunk-JZCKDTAS.js.map} +1 -1
- package/dist/{chunk-6AFVIOVZ.js → chunk-K22IJO4R.js} +2 -2
- package/dist/{chunk-QV5WFNZ2.js → chunk-KFU2SKDD.js} +2 -2
- package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
- package/dist/{chunk-TUPNAHHQ.js → chunk-KZMMAVR2.js} +2 -2
- package/dist/{chunk-6Q7JFY4P.js → chunk-LY4FJLPT.js} +2 -2
- package/dist/{chunk-XXDOJ5NL.js → chunk-M5MWSG4S.js} +7 -7
- package/dist/{chunk-YLB7NMLD.js → chunk-MNH2OODZ.js} +2 -2
- package/dist/{chunk-ORXVO6TH.js → chunk-NFGJBEPT.js} +6 -6
- package/dist/{chunk-QDIVMS25.js → chunk-NUWZFSKC.js} +2 -2
- package/dist/{chunk-6GHWF4QH.js → chunk-OM6ETLLH.js} +2 -2
- package/dist/{chunk-53EEQYE5.js → chunk-OMSTU233.js} +4 -4
- package/dist/{chunk-R64MNFPV.js → chunk-S3WYRPSA.js} +59 -59
- package/dist/{chunk-UOIRJ45Z.js → chunk-S5APSA3U.js} +2 -2
- package/dist/{chunk-WAHI7SPK.js → chunk-SY6WG6DB.js} +2 -2
- package/dist/{chunk-K7QRDDSR.js → chunk-ULVM34NP.js} +5 -5
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-QHNJVWF6.js → chunk-Y4PMATX2.js} +6 -6
- package/dist/{chunk-KF74X62T.js → chunk-YGKUAX2B.js} +4 -4
- package/dist/{chunk-YN4ZT4CW.js → chunk-YI7RZO7Y.js} +1 -1
- package/dist/cli.js +53 -52
- package/dist/compounding/engine.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/index.js +4 -4
- package/dist/entity-retrieval.js +4 -4
- package/dist/extraction.js +2 -2
- package/dist/{graph-edge-decay-D7OESCBR.js → graph-edge-decay-JLOF3YW4.js} +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/index.js +95 -94
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/maintenance.js +6 -6
- package/dist/orchestrator.js +40 -40
- package/dist/resume-bundles.js +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/storage.js +3 -3
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/access-boundary.ts +102 -1
- package/src/access-http.ts +54 -0
- package/src/access-operations-batch.ts +1468 -0
- package/src/access-operations.ts +4 -0
- package/src/access-surface-catalog.test.ts +17 -1
- package/src/access-surface-catalog.ts +57 -54
- package/src/storage-contract/atomicity.test.ts +113 -0
- package/src/storage-contract/cache-coherence.test.ts +100 -0
- package/src/storage-contract/content-hash.test.ts +102 -0
- package/src/storage-contract/failure-semantics.test.ts +108 -0
- package/src/storage-contract/harness.ts +148 -0
- package/src/storage-contract/namespace-routing.test.ts +135 -0
- package/src/storage-contract/round-trip.test.ts +226 -0
- package/src/storage-contract/surface.test.ts +90 -0
- package/src/storage-contract/versioning.test.ts +139 -0
- package/dist/chunk-25FHSOEB.js.map +0 -1
- package/dist/chunk-RYDT7LAI.js.map +0 -1
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → access-operations-batch.js.map} +0 -0
- /package/dist/{auto-sync-3AAP25FV.js.map → auto-sync-24NNS3GU.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-PJ5MDXOF.js.map → chunk-2SYPOX45.js.map} +0 -0
- /package/dist/{chunk-7G4KPGR3.js.map → chunk-47LPHBIP.js.map} +0 -0
- /package/dist/{chunk-OCFNG4LB.js.map → chunk-6GJ2WQRK.js.map} +0 -0
- /package/dist/{chunk-4NFVPDIL.js.map → chunk-74MYJSKB.js.map} +0 -0
- /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
- /package/dist/{chunk-DE2OPIQD.js.map → chunk-7ARKLLCV.js.map} +0 -0
- /package/dist/{chunk-3EHCV6YV.js.map → chunk-7L54YZUI.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
- /package/dist/{chunk-UNNDFYUX.js.map → chunk-ARV3AUOM.js.map} +0 -0
- /package/dist/{chunk-S24J5LLQ.js.map → chunk-BKBSE3BU.js.map} +0 -0
- /package/dist/{chunk-AE3C4PY6.js.map → chunk-BMKGG7GC.js.map} +0 -0
- /package/dist/{chunk-NMW3T6T6.js.map → chunk-C2EHWNP2.js.map} +0 -0
- /package/dist/{chunk-VKJHM6PL.js.map → chunk-DGTDRIUO.js.map} +0 -0
- /package/dist/{chunk-OOEVMJK4.js.map → chunk-HEHPVBWA.js.map} +0 -0
- /package/dist/{chunk-YOI3ELXF.js.map → chunk-IF362TF6.js.map} +0 -0
- /package/dist/{chunk-ABCGDW3A.js.map → chunk-IFR23TC5.js.map} +0 -0
- /package/dist/{chunk-ZXFNSKPY.js.map → chunk-JHAYR2JY.js.map} +0 -0
- /package/dist/{chunk-6AFVIOVZ.js.map → chunk-K22IJO4R.js.map} +0 -0
- /package/dist/{chunk-QV5WFNZ2.js.map → chunk-KFU2SKDD.js.map} +0 -0
- /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-TUPNAHHQ.js.map → chunk-KZMMAVR2.js.map} +0 -0
- /package/dist/{chunk-6Q7JFY4P.js.map → chunk-LY4FJLPT.js.map} +0 -0
- /package/dist/{chunk-XXDOJ5NL.js.map → chunk-M5MWSG4S.js.map} +0 -0
- /package/dist/{chunk-YLB7NMLD.js.map → chunk-MNH2OODZ.js.map} +0 -0
- /package/dist/{chunk-ORXVO6TH.js.map → chunk-NFGJBEPT.js.map} +0 -0
- /package/dist/{chunk-QDIVMS25.js.map → chunk-NUWZFSKC.js.map} +0 -0
- /package/dist/{chunk-6GHWF4QH.js.map → chunk-OM6ETLLH.js.map} +0 -0
- /package/dist/{chunk-53EEQYE5.js.map → chunk-OMSTU233.js.map} +0 -0
- /package/dist/{chunk-R64MNFPV.js.map → chunk-S3WYRPSA.js.map} +0 -0
- /package/dist/{chunk-UOIRJ45Z.js.map → chunk-S5APSA3U.js.map} +0 -0
- /package/dist/{chunk-WAHI7SPK.js.map → chunk-SY6WG6DB.js.map} +0 -0
- /package/dist/{chunk-K7QRDDSR.js.map → chunk-ULVM34NP.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-QHNJVWF6.js.map → chunk-Y4PMATX2.js.map} +0 -0
- /package/dist/{chunk-KF74X62T.js.map → chunk-YGKUAX2B.js.map} +0 -0
- /package/dist/{chunk-YN4ZT4CW.js.map → chunk-YI7RZO7Y.js.map} +0 -0
- /package/dist/{graph-edge-decay-D7OESCBR.js.map → graph-edge-decay-JLOF3YW4.js.map} +0 -0
package/src/access-operations.ts
CHANGED
|
@@ -549,6 +549,10 @@ export const codegraphIngestTracesOperation = defineOperation<
|
|
|
549
549
|
},
|
|
550
550
|
});
|
|
551
551
|
|
|
552
|
+
// Batch-migrated operations (issue #1525): registers all remaining
|
|
553
|
+
// handlers through the boundary as a side effect of importing this module.
|
|
554
|
+
import "./access-operations-batch.js";
|
|
555
|
+
|
|
552
556
|
export const REGISTERED_OPERATIONS = [
|
|
553
557
|
memoryGetOperation.spec.name,
|
|
554
558
|
memorySearchOperation.spec.name,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
// catalog-completeness correction: adding routes that were always live but
|
|
45
45
|
// omitted from the catalog (review-caught). Such a bump MUST be accompanied
|
|
46
46
|
// by the newly-cataloged entries; the higher count is the honest baseline.
|
|
47
|
-
const UNMIGRATED_HANDLER_BASELINE =
|
|
47
|
+
const UNMIGRATED_HANDLER_BASELINE = 80;
|
|
48
48
|
|
|
49
49
|
// Keep the import live — `getOperation` is the call surfaces use at dispatch
|
|
50
50
|
// time; referencing it here pins the registry's lookup contract.
|
|
@@ -187,6 +187,7 @@ function extractHttpRouteDispatchMap(): Map<string, Set<string>> {
|
|
|
187
187
|
const lines = source.split("\n");
|
|
188
188
|
const routeOps = new Map<string, Set<string>>();
|
|
189
189
|
let currentKey: string | null = null;
|
|
190
|
+
let lastSeenPathname: string | null = null;
|
|
190
191
|
|
|
191
192
|
for (let i = 0; i < lines.length; i++) {
|
|
192
193
|
const line = lines[i]!;
|
|
@@ -217,6 +218,7 @@ function extractHttpRouteDispatchMap(): Map<string, Set<string>> {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
if (pathname) {
|
|
221
|
+
lastSeenPathname = pathname;
|
|
220
222
|
// Determine the HTTP method: same line first, then backward for
|
|
221
223
|
// exact-match routes, forward for dynamic routes. Check both positive
|
|
222
224
|
// (===) and negated (!==) method patterns — `req.method !== "GET"` as
|
|
@@ -253,6 +255,20 @@ function extractHttpRouteDispatchMap(): Map<string, Set<string>> {
|
|
|
253
255
|
continue;
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
// Detect method changes within a shared route block (e.g. a single
|
|
259
|
+
// regex match with GET/PUT/DELETE branches). When a new method check
|
|
260
|
+
// appears after a route pattern was already detected, update currentKey
|
|
261
|
+
// to the new method + the same pathname so per-method getOperation
|
|
262
|
+
// calls are tracked against the right route key.
|
|
263
|
+
const methodChange = line.match(/req\.method\s*===\s*"(\w+)"/);
|
|
264
|
+
if (methodChange && lastSeenPathname) {
|
|
265
|
+
const newKey = methodChange[1]! + " " + lastSeenPathname;
|
|
266
|
+
if (!routeOps.has(newKey)) {
|
|
267
|
+
routeOps.set(newKey, new Set<string>());
|
|
268
|
+
}
|
|
269
|
+
currentKey = newKey;
|
|
270
|
+
}
|
|
271
|
+
|
|
256
272
|
// Collect getOperation calls within the current route block.
|
|
257
273
|
if (currentKey) {
|
|
258
274
|
const opMatch = line.match(/getOperation\("(\w+)"\)/);
|
|
@@ -160,65 +160,68 @@ export const MCP_TOOLS: readonly McpToolEntry[] = [
|
|
|
160
160
|
// (health, adapters, the /mcp delegate, admin console, SSE-only endpoints)
|
|
161
161
|
// carry no user request envelope and are intentionally excluded.
|
|
162
162
|
export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
|
|
163
|
-
{ method: "POST", pathname: "/engram/v1/recall", operation:
|
|
164
|
-
{ method: "POST", pathname: "/engram/v1/coding-context", operation:
|
|
165
|
-
{ method: "POST", pathname: "/engram/v1/capsules/export", operation:
|
|
166
|
-
{ method: "POST", pathname: "/engram/v1/capsules/import", operation:
|
|
167
|
-
{ method: "GET", pathname: "/engram/v1/offline-sync/snapshot", operation:
|
|
168
|
-
|
|
169
|
-
{ method: "POST", pathname: "/engram/v1/offline-sync/
|
|
170
|
-
{ method: "POST", pathname: "/engram/v1/offline-sync/
|
|
171
|
-
{ method: "POST", pathname: "/engram/v1/offline-sync/file-content", operation:
|
|
172
|
-
{ method: "POST", pathname: "/engram/v1/offline-sync/apply
|
|
173
|
-
{ method: "POST", pathname: "/engram/v1/
|
|
174
|
-
{ method: "POST", pathname: "/engram/v1/
|
|
175
|
-
{ method: "
|
|
176
|
-
{ method: "GET", pathname: "/engram/v1/recall/
|
|
177
|
-
{ method: "GET", pathname: "/engram/v1/
|
|
178
|
-
{ method: "
|
|
179
|
-
{ method: "
|
|
180
|
-
{ method: "GET", pathname: "/engram/v1/wearables/
|
|
181
|
-
{ method: "GET", pathname: "/engram/v1/wearables/
|
|
182
|
-
{ method: "
|
|
183
|
-
{ method: "POST", pathname: "/engram/v1/
|
|
184
|
-
{ method: "POST", pathname: "/engram/v1/lcm/
|
|
185
|
-
{ method: "POST", pathname: "/engram/v1/lcm/compaction/
|
|
186
|
-
{ method: "
|
|
187
|
-
{ method: "GET", pathname: "/engram/v1/lcm/status", operation: null },
|
|
163
|
+
{ method: "POST", pathname: "/engram/v1/recall", operation: "recall" },
|
|
164
|
+
{ method: "POST", pathname: "/engram/v1/coding-context", operation: "set_coding_context" },
|
|
165
|
+
{ method: "POST", pathname: "/engram/v1/capsules/export", operation: "capsule_export" },
|
|
166
|
+
{ method: "POST", pathname: "/engram/v1/capsules/import", operation: "capsule_import" },
|
|
167
|
+
{ method: "GET", pathname: "/engram/v1/offline-sync/snapshot", operation: "offline_sync_snapshot" },
|
|
168
|
+
{ method: "POST", pathname: "/engram/v1/offline-sync/snapshot", operation: "offline_sync_snapshot" },
|
|
169
|
+
{ method: "POST", pathname: "/engram/v1/offline-sync/files", operation: "offline_sync_files" },
|
|
170
|
+
{ method: "POST", pathname: "/engram/v1/offline-sync/file-content", operation: "offline_sync_file_content" },
|
|
171
|
+
{ method: "POST", pathname: "/engram/v1/offline-sync/apply-file-content", operation: "offline_sync_apply_file_content" },
|
|
172
|
+
{ method: "POST", pathname: "/engram/v1/offline-sync/apply", operation: "offline_sync_apply" },
|
|
173
|
+
{ method: "POST", pathname: "/engram/v1/recall/explain", operation: "recall_explain" },
|
|
174
|
+
{ method: "POST", pathname: "/engram/v1/action-confidence", operation: "action_confidence" },
|
|
175
|
+
{ method: "GET", pathname: "/engram/v1/recall/tier-explain", operation: "recall_tier_explain" },
|
|
176
|
+
{ method: "GET", pathname: "/engram/v1/recall/xray", operation: "recall_xray" },
|
|
177
|
+
{ method: "GET", pathname: "/engram/v1/wearables/status", operation: "wearables_status" },
|
|
178
|
+
{ method: "POST", pathname: "/engram/v1/wearables/sync", operation: "wearables_sync" },
|
|
179
|
+
{ method: "GET", pathname: "/engram/v1/wearables/transcript", operation: "transcript_day" },
|
|
180
|
+
{ method: "GET", pathname: "/engram/v1/wearables/transcripts/search", operation: "transcript_search" },
|
|
181
|
+
{ method: "GET", pathname: "/engram/v1/wearables/memories", operation: "transcript_memories" },
|
|
182
|
+
{ method: "POST", pathname: "/engram/v1/observe", operation: "observe" },
|
|
183
|
+
{ method: "POST", pathname: "/engram/v1/lcm/search", operation: "lcm_search" },
|
|
184
|
+
{ method: "POST", pathname: "/engram/v1/lcm/compaction/flush", operation: "lcm_compaction_flush" },
|
|
185
|
+
{ method: "POST", pathname: "/engram/v1/lcm/compaction/record", operation: "lcm_compaction_record" },
|
|
186
|
+
{ method: "GET", pathname: "/engram/v1/lcm/status", operation: "lcm_status" },
|
|
188
187
|
{ method: "POST", pathname: "/engram/v1/memories", operation: "memory_store" },
|
|
189
188
|
{ method: "POST", pathname: "/engram/v1/coding/decisions", operation: "coding_decision" },
|
|
190
189
|
{ method: "POST", pathname: "/engram/v1/coding/architecture", operation: "coding_architecture" },
|
|
191
190
|
{ method: "POST", pathname: "/engram/v1/coding/delta", operation: "coding_delta" },
|
|
192
|
-
{ method: "POST", pathname: "/engram/v1/suggestions", operation:
|
|
193
|
-
{ method: "GET", pathname: "/engram/v1/memories", operation:
|
|
191
|
+
{ method: "POST", pathname: "/engram/v1/suggestions", operation: "suggestion_submit" },
|
|
192
|
+
{ method: "GET", pathname: "/engram/v1/memories", operation: "memory_list" },
|
|
194
193
|
{ method: "GET", pathname: "/engram/v1/memories/:id", operation: "memory_get" },
|
|
195
|
-
{ method: "GET", pathname: "/engram/v1/memories/:id/timeline", operation:
|
|
196
|
-
{ method: "GET", pathname: "/engram/v1/entities", operation:
|
|
197
|
-
{ method: "GET", pathname: "/engram/v1/entities/:id", operation:
|
|
198
|
-
{ method: "GET", pathname: "/engram/v1/review-queue", operation:
|
|
199
|
-
{ method: "GET", pathname: "/engram/v1/maintenance", operation:
|
|
200
|
-
{ method: "GET", pathname: "/engram/v1/quality", operation:
|
|
201
|
-
{ method: "GET", pathname: "/engram/v1/trust-zones/status", operation:
|
|
202
|
-
{ method: "GET", pathname: "/engram/v1/procedural/stats", operation:
|
|
203
|
-
{ method: "GET", pathname: "/engram/v1/trust-zones/records", operation:
|
|
204
|
-
{ method: "POST", pathname: "/engram/v1/review-disposition", operation:
|
|
205
|
-
{ method: "POST", pathname: "/engram/v1/trust-zones/promote", operation:
|
|
206
|
-
{ method: "POST", pathname: "/engram/v1/trust-zones/demo-seed", operation:
|
|
207
|
-
{ method: "POST", pathname: "/v1/citations/observed", operation:
|
|
208
|
-
{ method: "GET", pathname: "/engram/v1/review/contradictions", operation:
|
|
209
|
-
{ method: "GET", pathname: "/engram/v1/review/contradictions/:id", operation:
|
|
210
|
-
{ method: "POST", pathname: "/engram/v1/review/resolve", operation:
|
|
211
|
-
{ method: "GET", pathname: "/engram/v1/graph/snapshot", operation:
|
|
212
|
-
{ method: "POST", pathname: "/engram/v1/contradiction-scan", operation:
|
|
213
|
-
|
|
214
|
-
{ method: "GET", pathname: "/engram/v1/
|
|
215
|
-
{ method: "GET", pathname: "/engram/v1/peers", operation:
|
|
216
|
-
{ method: "GET", pathname: "/engram/v1/peers/:id
|
|
217
|
-
{ method: "
|
|
218
|
-
{ method: "
|
|
219
|
-
{ method: "
|
|
220
|
-
{ method: "
|
|
221
|
-
|
|
194
|
+
{ method: "GET", pathname: "/engram/v1/memories/:id/timeline", operation: "memory_timeline" },
|
|
195
|
+
{ method: "GET", pathname: "/engram/v1/entities", operation: "entity_list" },
|
|
196
|
+
{ method: "GET", pathname: "/engram/v1/entities/:id", operation: "entity_get" },
|
|
197
|
+
{ method: "GET", pathname: "/engram/v1/review-queue", operation: "review_queue_list" },
|
|
198
|
+
{ method: "GET", pathname: "/engram/v1/maintenance", operation: "maintenance_status" },
|
|
199
|
+
{ method: "GET", pathname: "/engram/v1/quality", operation: "quality_status" },
|
|
200
|
+
{ method: "GET", pathname: "/engram/v1/trust-zones/status", operation: "trust_zones_status" },
|
|
201
|
+
{ method: "GET", pathname: "/engram/v1/procedural/stats", operation: "procedural_stats" },
|
|
202
|
+
{ method: "GET", pathname: "/engram/v1/trust-zones/records", operation: "trust_zones_records" },
|
|
203
|
+
{ method: "POST", pathname: "/engram/v1/review-disposition", operation: "review_disposition" },
|
|
204
|
+
{ method: "POST", pathname: "/engram/v1/trust-zones/promote", operation: "trust_zones_promote" },
|
|
205
|
+
{ method: "POST", pathname: "/engram/v1/trust-zones/demo-seed", operation: "trust_zones_demo_seed" },
|
|
206
|
+
{ method: "POST", pathname: "/v1/citations/observed", operation: "citations_observed" },
|
|
207
|
+
{ method: "GET", pathname: "/engram/v1/review/contradictions", operation: "review_list" },
|
|
208
|
+
{ method: "GET", pathname: "/engram/v1/review/contradictions/:id", operation: "contradiction_detail" },
|
|
209
|
+
{ method: "POST", pathname: "/engram/v1/review/resolve", operation: "review_resolve" },
|
|
210
|
+
{ method: "GET", pathname: "/engram/v1/graph/snapshot", operation: "graph_snapshot" },
|
|
211
|
+
{ method: "POST", pathname: "/engram/v1/contradiction-scan", operation: "contradiction_scan_run" },
|
|
212
|
+
{ method: "GET", pathname: "/engram/v1/console/state", operation: "console_state" },
|
|
213
|
+
{ method: "GET", pathname: "/engram/v1/peers", operation: "peer_list" },
|
|
214
|
+
{ method: "GET", pathname: "/engram/v1/peers/:id/profile", operation: "peer_profile_get" },
|
|
215
|
+
{ method: "GET", pathname: "/engram/v1/peers/:id", operation: "peer_get" },
|
|
216
|
+
{ method: "PUT", pathname: "/engram/v1/peers/:id", operation: "peer_set" },
|
|
217
|
+
{ method: "DELETE", pathname: "/engram/v1/peers/:id", operation: "peer_delete" },
|
|
218
|
+
{ method: "GET", pathname: "/engram/v1/dreams/status", operation: "dreams_status" },
|
|
219
|
+
{ method: "POST", pathname: "/engram/v1/dreams/run", operation: "dreams_run" },
|
|
220
|
+
// SSE-only endpoints: carried in the catalog for completeness tracking.
|
|
221
|
+
// The operation registration makes them visible to the boundary; the
|
|
222
|
+
// streaming lifecycle is owned by the HTTP transport (handleGraphEventsSSE).
|
|
223
|
+
{ method: "GET", pathname: "/engram/v1/offline-sync/snapshot-stream", operation: "offline_sync_snapshot_stream" },
|
|
224
|
+
{ method: "GET", pathname: "/engram/v1/graph/events", operation: "graph_events" },
|
|
222
225
|
];
|
|
223
226
|
|
|
224
227
|
/**
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: atomic write semantics.
|
|
3
|
+
*
|
|
4
|
+
* Rule 54 (CLAUDE.md): replace operations MUST write temp-then-rename, NEVER
|
|
5
|
+
* delete-before-write. This test pins that invariant by making rename fail
|
|
6
|
+
* (read-only target directory) and asserting the original file survives the
|
|
7
|
+
* failed overwrite.
|
|
8
|
+
*
|
|
9
|
+
* The atomic write primitive lives in secure-fs (`writeMaybeEncryptedFile`)
|
|
10
|
+
* and is consumed by StorageManager via `writeStorageSecureFile`. We test the
|
|
11
|
+
* primitive directly here because it is the single atomicity seam every write
|
|
12
|
+
* entry point depends on.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import test from "node:test";
|
|
20
|
+
|
|
21
|
+
import { writeMaybeEncryptedFile, readMaybeEncryptedFile } from "../secure-store/secure-fs.js";
|
|
22
|
+
|
|
23
|
+
test("atomicity: writeMaybeEncryptedFile writes temp-then-rename (no temp files left behind on success)", async () => {
|
|
24
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-atomic-"));
|
|
25
|
+
try {
|
|
26
|
+
const target = path.join(dir, "target.md");
|
|
27
|
+
await writeMaybeEncryptedFile(target, "first content", null, {}, dir);
|
|
28
|
+
|
|
29
|
+
// No temp files left behind
|
|
30
|
+
const { readdir } = await import("node:fs/promises");
|
|
31
|
+
const entries = await readdir(dir);
|
|
32
|
+
assert.deepEqual(entries.sort(), ["target.md"]);
|
|
33
|
+
|
|
34
|
+
// Overwrite — still only the target, no leftover temp
|
|
35
|
+
await writeMaybeEncryptedFile(target, "second content", null, {}, dir);
|
|
36
|
+
const entries2 = await readdir(dir);
|
|
37
|
+
assert.deepEqual(entries2.sort(), ["target.md"]);
|
|
38
|
+
|
|
39
|
+
const read = await readMaybeEncryptedFile(target, null, dir);
|
|
40
|
+
assert.equal(read, "second content");
|
|
41
|
+
} finally {
|
|
42
|
+
await rm(dir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("atomicity: a failed rename leaves the original file intact (never delete-before-write)", async (t) => {
|
|
47
|
+
// Skip on root — chmod restrictions don't apply.
|
|
48
|
+
if (process.getuid && process.getuid() === 0) {
|
|
49
|
+
t.skip("chmod-based test is unreliable when running as root");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-atomic-fail-"));
|
|
54
|
+
try {
|
|
55
|
+
const target = path.join(dir, "survivor.md");
|
|
56
|
+
await writeMaybeEncryptedFile(target, "original", null, {}, dir);
|
|
57
|
+
|
|
58
|
+
// Make the directory read-only so the rename into place fails.
|
|
59
|
+
await chmod(dir, 0o500);
|
|
60
|
+
try {
|
|
61
|
+
await assert.rejects(
|
|
62
|
+
() => writeMaybeEncryptedFile(target, "would-be-replacement", null, {}, dir),
|
|
63
|
+
// rename (or temp write) fails — the exact error varies by platform
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// THE CONTRACT: the original file must still be readable with its
|
|
67
|
+
// original content. Delete-before-write would have destroyed it before
|
|
68
|
+
// the failed rename.
|
|
69
|
+
const survived = await readMaybeEncryptedFile(target, null, dir);
|
|
70
|
+
assert.equal(survived, "original", "original file must survive a failed atomic replace");
|
|
71
|
+
} finally {
|
|
72
|
+
// Restore perms so cleanup works.
|
|
73
|
+
await chmod(dir, 0o700);
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
await rm(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("atomicity: StorageManager.updateMemory preserves the original on overwrite success", async (t) => {
|
|
81
|
+
const { StorageManager } = await import("../storage.js");
|
|
82
|
+
const { makeStorage } = await import("./harness.js");
|
|
83
|
+
const { storage, cleanup } = await makeStorage();
|
|
84
|
+
try {
|
|
85
|
+
const id = await storage.writeMemory("fact", "version one");
|
|
86
|
+
const before = await storage.getMemoryById(id);
|
|
87
|
+
assert.ok(before);
|
|
88
|
+
assert.equal(before!.content, "version one");
|
|
89
|
+
|
|
90
|
+
const updated = await storage.updateMemory(id, "version two");
|
|
91
|
+
assert.equal(updated, true);
|
|
92
|
+
|
|
93
|
+
const after = await storage.getMemoryById(id);
|
|
94
|
+
assert.ok(after);
|
|
95
|
+
assert.equal(after!.content, "version two");
|
|
96
|
+
// The id is stable across the update (same file path, not a new memory)
|
|
97
|
+
assert.equal(after!.frontmatter.id, id);
|
|
98
|
+
} finally {
|
|
99
|
+
await cleanup();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("atomicity: mkdir is implicit — writeMaybeEncryptedFile creates parent dirs", async () => {
|
|
104
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-atomic-mkdir-"));
|
|
105
|
+
try {
|
|
106
|
+
const target = path.join(dir, "nested", "deep", "file.md");
|
|
107
|
+
await writeMaybeEncryptedFile(target, "content", null, {}, dir);
|
|
108
|
+
const read = await readMaybeEncryptedFile(target, null, dir);
|
|
109
|
+
assert.equal(read, "content");
|
|
110
|
+
} finally {
|
|
111
|
+
await rm(dir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: cache coherence across instances.
|
|
3
|
+
*
|
|
4
|
+
* Rule 37 (memory-cache): write via instance A, read via a FRESH instance B
|
|
5
|
+
* over the same dir → B sees the write. The module-level caches
|
|
6
|
+
* (readAllMemories in-flight, version sentinels) are keyed by baseDir and
|
|
7
|
+
* shared/static — this test pins that a write invalidates correctly so a
|
|
8
|
+
* second instance never serves stale data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { StorageManager } from "../storage.js";
|
|
15
|
+
import { resetStaticCaches } from "./harness.js";
|
|
16
|
+
|
|
17
|
+
test("cache-coherence: write via instance A, read via fresh instance B → B sees the write", async () => {
|
|
18
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
19
|
+
const os = await import("node:os");
|
|
20
|
+
const path = await import("node:path");
|
|
21
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-cache-coherence-"));
|
|
22
|
+
try {
|
|
23
|
+
resetStaticCaches();
|
|
24
|
+
const smA = new StorageManager(dir);
|
|
25
|
+
await smA.ensureDirectories();
|
|
26
|
+
|
|
27
|
+
const id = await smA.writeMemory("fact", "written by A");
|
|
28
|
+
|
|
29
|
+
// Fresh instance over the same dir
|
|
30
|
+
const smB = new StorageManager(dir);
|
|
31
|
+
await smB.ensureDirectories();
|
|
32
|
+
const found = await smB.getMemoryById(id);
|
|
33
|
+
assert.ok(found, "instance B must see the memory written by instance A");
|
|
34
|
+
assert.equal(found!.content, "written by A");
|
|
35
|
+
} finally {
|
|
36
|
+
resetStaticCaches();
|
|
37
|
+
await rm(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("cache-coherence: delete via instance A is reflected in fresh instance B", async () => {
|
|
42
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
43
|
+
const os = await import("node:os");
|
|
44
|
+
const path = await import("node:path");
|
|
45
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-cache-del-"));
|
|
46
|
+
try {
|
|
47
|
+
resetStaticCaches();
|
|
48
|
+
const smA = new StorageManager(dir);
|
|
49
|
+
await smA.ensureDirectories();
|
|
50
|
+
const id = await smA.writeMemory("fact", "to be deleted");
|
|
51
|
+
|
|
52
|
+
// Confirm B sees it
|
|
53
|
+
const smB = new StorageManager(dir);
|
|
54
|
+
await smB.ensureDirectories();
|
|
55
|
+
assert.ok(await smB.getMemoryById(id));
|
|
56
|
+
|
|
57
|
+
// A deletes
|
|
58
|
+
await smA.invalidateMemory(id);
|
|
59
|
+
|
|
60
|
+
// Fresh instance C must NOT see it
|
|
61
|
+
const smC = new StorageManager(dir);
|
|
62
|
+
await smC.ensureDirectories();
|
|
63
|
+
const found = await smC.getMemoryById(id);
|
|
64
|
+
assert.equal(found, null, "fresh instance must not see a deleted memory");
|
|
65
|
+
} finally {
|
|
66
|
+
resetStaticCaches();
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("cache-coherence: readAllMemories reflects writes across instances after invalidation", async () => {
|
|
72
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
73
|
+
const os = await import("node:os");
|
|
74
|
+
const path = await import("node:path");
|
|
75
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-cache-list-"));
|
|
76
|
+
try {
|
|
77
|
+
resetStaticCaches();
|
|
78
|
+
const smA = new StorageManager(dir);
|
|
79
|
+
await smA.ensureDirectories();
|
|
80
|
+
await smA.writeMemory("fact", "first");
|
|
81
|
+
await smA.writeMemory("decision", "second");
|
|
82
|
+
|
|
83
|
+
const smB = new StorageManager(dir);
|
|
84
|
+
await smB.ensureDirectories();
|
|
85
|
+
const all = await smB.readAllMemories();
|
|
86
|
+
assert.ok(all.length >= 2, "fresh instance must list all written memories");
|
|
87
|
+
|
|
88
|
+
// Write more via A
|
|
89
|
+
await smA.writeMemory("principle", "third");
|
|
90
|
+
|
|
91
|
+
const smC = new StorageManager(dir);
|
|
92
|
+
await smC.ensureDirectories();
|
|
93
|
+
const allAfter = await smC.readAllMemories();
|
|
94
|
+
assert.ok(allAfter.length >= 3, "fresh instance must see the additional write");
|
|
95
|
+
assert.ok(allAfter.some((m) => m.content === "third"));
|
|
96
|
+
} finally {
|
|
97
|
+
resetStaticCaches();
|
|
98
|
+
await rm(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: content hash identity.
|
|
3
|
+
*
|
|
4
|
+
* Rule 23: the write-side and dedup-side MUST hash the same `rawContent`,
|
|
5
|
+
* never the timestamped/cited form. `writeMemory` persists a `contentHash` on
|
|
6
|
+
* the frontmatter for facts; `hasFactContentHash` queries the dedup index.
|
|
7
|
+
* This test pins that the two sides agree so subsequent extractions of the
|
|
8
|
+
* same logical fact are deduped correctly even when citation timestamps differ.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { ContentHashIndex } from "../storage.js";
|
|
15
|
+
import { makeStorage } from "./harness.js";
|
|
16
|
+
|
|
17
|
+
test("content-hash: ContentHashIndex.computeHash is deterministic for the same input", () => {
|
|
18
|
+
const a = ContentHashIndex.computeHash("the sky is blue");
|
|
19
|
+
const b = ContentHashIndex.computeHash("the sky is blue");
|
|
20
|
+
assert.equal(a, b);
|
|
21
|
+
assert.notEqual(
|
|
22
|
+
ContentHashIndex.computeHash("the sky is blue"),
|
|
23
|
+
ContentHashIndex.computeHash("the sky is green"),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("content-hash: writeMemory for fact registers the hash so hasFactContentHash returns true", async () => {
|
|
28
|
+
const { storage, cleanup } = await makeStorage();
|
|
29
|
+
try {
|
|
30
|
+
const rawFact = "PostgreSQL is our primary database";
|
|
31
|
+
await storage.writeMemory("fact", rawFact);
|
|
32
|
+
|
|
33
|
+
const has = await storage.hasFactContentHash(rawFact);
|
|
34
|
+
assert.equal(has, true, "hasFactContentHash must return true for a just-written fact");
|
|
35
|
+
} finally {
|
|
36
|
+
await cleanup();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("content-hash: contentHashSource overrides the persisted body for dedup (rule 23)", async () => {
|
|
41
|
+
const { storage, cleanup } = await makeStorage();
|
|
42
|
+
try {
|
|
43
|
+
const rawFact = "We migrated to MySQL in March";
|
|
44
|
+
// Persist a citation-annotated body, but index the RAW fact text
|
|
45
|
+
await storage.writeMemory("fact", `${rawFact} [cited 2026-01-01]`, {
|
|
46
|
+
contentHashSource: rawFact,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Querying with the RAW fact (not the cited form) must hit
|
|
50
|
+
assert.equal(
|
|
51
|
+
await storage.hasFactContentHash(rawFact),
|
|
52
|
+
true,
|
|
53
|
+
"hasFactContentHash must match on the raw fact, not the cited body",
|
|
54
|
+
);
|
|
55
|
+
// The cited form should NOT be separately indexed (it has the citation suffix)
|
|
56
|
+
assert.equal(
|
|
57
|
+
await storage.hasFactContentHash(`${rawFact} [cited 2026-01-01]`),
|
|
58
|
+
false,
|
|
59
|
+
"the cited/timestamped form must not be the indexed hash",
|
|
60
|
+
);
|
|
61
|
+
} finally {
|
|
62
|
+
await cleanup();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("content-hash: non-fact categories do not register a content hash", async () => {
|
|
67
|
+
const { storage, cleanup } = await makeStorage();
|
|
68
|
+
try {
|
|
69
|
+
await storage.writeMemory("decision", "we chose option B");
|
|
70
|
+
assert.equal(
|
|
71
|
+
await storage.hasFactContentHash("we chose option B"),
|
|
72
|
+
false,
|
|
73
|
+
"non-fact categories must not enter the fact dedup index",
|
|
74
|
+
);
|
|
75
|
+
} finally {
|
|
76
|
+
await cleanup();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("content-hash: normalizeContent produces stable text for equivalent inputs", () => {
|
|
81
|
+
const a = ContentHashIndex.normalizeContent(" Hello World ");
|
|
82
|
+
const b = ContentHashIndex.normalizeContent("Hello World");
|
|
83
|
+
assert.equal(a, b, "normalizeContent must collapse whitespace consistently");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("content-hash: the frontmatter contentHash matches computeHash of the raw content", async () => {
|
|
87
|
+
const { storage, cleanup } = await makeStorage();
|
|
88
|
+
try {
|
|
89
|
+
const rawFact = "deterministic hash check";
|
|
90
|
+
const id = await storage.writeMemory("fact", rawFact);
|
|
91
|
+
const memory = await storage.getMemoryById(id);
|
|
92
|
+
assert.ok(memory);
|
|
93
|
+
assert.ok(memory!.frontmatter.contentHash, "frontmatter must carry a contentHash for facts");
|
|
94
|
+
assert.equal(
|
|
95
|
+
memory!.frontmatter.contentHash,
|
|
96
|
+
ContentHashIndex.computeHash(rawFact),
|
|
97
|
+
"frontmatter contentHash must equal computeHash of the raw content",
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
await cleanup();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: failure semantics.
|
|
3
|
+
*
|
|
4
|
+
* Rule 34: reads distinguish empty-dir from unreadable-dir — `{ok:false}` vs
|
|
5
|
+
* `[]`. Where the current surface already does this, we pin it; where it
|
|
6
|
+
* doesn't, we record the gap (for the silent-failures issue) rather than
|
|
7
|
+
* change behavior here.
|
|
8
|
+
*
|
|
9
|
+
* Also pins containment: a FILE where a directory is expected is skipped via
|
|
10
|
+
* lstat().isDirectory(), not existsSync() (rule 24).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
|
|
19
|
+
import { StorageManager } from "../storage.js";
|
|
20
|
+
import { makeStorage } from "./harness.js";
|
|
21
|
+
|
|
22
|
+
test("failure-semantics: readAllMemories on a fresh (empty) dir returns [] not null", async () => {
|
|
23
|
+
const { storage, cleanup } = await makeStorage();
|
|
24
|
+
try {
|
|
25
|
+
const result = await storage.readAllMemories();
|
|
26
|
+
assert.ok(Array.isArray(result), "readAllMemories must return an array");
|
|
27
|
+
assert.equal(result.length, 0, "empty store must return an empty array");
|
|
28
|
+
} finally {
|
|
29
|
+
await cleanup();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("failure-semantics: getMemoryById on empty store returns null (not throw)", async () => {
|
|
34
|
+
const { storage, cleanup } = await makeStorage();
|
|
35
|
+
try {
|
|
36
|
+
const result = await storage.getMemoryById("anything");
|
|
37
|
+
assert.equal(result, null);
|
|
38
|
+
} finally {
|
|
39
|
+
await cleanup();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("failure-semantics: readMemoryByPath on a non-existent file returns null (not throw)", async () => {
|
|
44
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
45
|
+
try {
|
|
46
|
+
const result = await storage.readMemoryByPath(path.join(baseDir, "facts", "2026-01-01", "nope.md"));
|
|
47
|
+
assert.equal(result, null);
|
|
48
|
+
} finally {
|
|
49
|
+
await cleanup();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("failure-semantics: readMemoryByPath on a malformed markdown file returns null", async () => {
|
|
54
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
55
|
+
try {
|
|
56
|
+
const dir = path.join(baseDir, "facts", "2026-01-01");
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
const filePath = path.join(dir, "garbage.md");
|
|
59
|
+
await writeFile(filePath, "this has no frontmatter at all\njust random text", "utf-8");
|
|
60
|
+
|
|
61
|
+
const result = await storage.readMemoryByPath(filePath);
|
|
62
|
+
assert.equal(result, null, "malformed file (no frontmatter, not entity) must return null");
|
|
63
|
+
} finally {
|
|
64
|
+
await cleanup();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("failure-semantics: a FILE where a category directory is expected does not crash readAllMemories", async () => {
|
|
69
|
+
// Create a FILE named "facts" (not a directory) at the base dir, then scan.
|
|
70
|
+
// collectActiveMemoryPaths uses lstat().isDirectory() (not existsSync) so a
|
|
71
|
+
// file-as-directory entry is skipped without crashing (rule 24 containment).
|
|
72
|
+
// We do NOT call ensureDirectories — it would try to mkdir facts/<today> and
|
|
73
|
+
// fail on the file. The scan path must be robust independently of dir creation.
|
|
74
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-fail-file-as-dir-"));
|
|
75
|
+
try {
|
|
76
|
+
StorageManager.clearAllStaticCaches();
|
|
77
|
+
await writeFile(path.join(baseDir, "facts"), "I am a file not a directory", "utf-8");
|
|
78
|
+
const storage = new StorageManager(baseDir);
|
|
79
|
+
|
|
80
|
+
const result = await storage.readAllMemories();
|
|
81
|
+
assert.ok(Array.isArray(result));
|
|
82
|
+
assert.equal(result.length, 0, "a file-as-directory must not produce spurious memories");
|
|
83
|
+
} finally {
|
|
84
|
+
StorageManager.clearAllStaticCaches();
|
|
85
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("failure-semantics: invalidateMemory on a missing memory returns false (not throw)", async () => {
|
|
90
|
+
const { storage, cleanup } = await makeStorage();
|
|
91
|
+
try {
|
|
92
|
+
const result = await storage.invalidateMemory("totally-missing-id-99999");
|
|
93
|
+
assert.equal(result, false);
|
|
94
|
+
} finally {
|
|
95
|
+
await cleanup();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("failure-semantics: ensureDirectories is idempotent (safe to call multiple times)", async () => {
|
|
100
|
+
const { storage, cleanup } = await makeStorage();
|
|
101
|
+
try {
|
|
102
|
+
await storage.ensureDirectories();
|
|
103
|
+
await storage.ensureDirectories();
|
|
104
|
+
await storage.ensureDirectories();
|
|
105
|
+
} finally {
|
|
106
|
+
await cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|