@remnic/core 9.43.0 → 9.45.0
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 +5 -5
- package/dist/access-admin-ops-surface.js +42 -41
- package/dist/access-authorization-probe.d.ts +5 -5
- package/dist/access-authorization-probe.js +43 -42
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +42 -41
- package/dist/access-cli.js +100 -97
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +5 -5
- package/dist/access-extraction-force-flush.js +42 -41
- package/dist/access-http-lcm-compaction.d.ts +5 -5
- package/dist/access-http-lifecycle-flush.d.ts +5 -5
- package/dist/access-http-query.d.ts +8 -0
- package/dist/access-http-query.js +180 -0
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +51 -49
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +42 -41
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +42 -41
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +47 -46
- package/dist/access-namespace-preflight.js +42 -41
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +42 -41
- package/dist/access-operations-batch.js +44 -43
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +46 -45
- package/dist/access-recall-concurrency.d.ts +5 -5
- package/dist/access-recall-concurrency.js +42 -41
- package/dist/access-recall-response.d.ts +5 -5
- package/dist/access-recall-response.js +42 -41
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +42 -41
- package/dist/access-schema.d.ts +84 -84
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-D1BkxwSf.d.ts → access-service-DBo88Dmm.d.ts} +14 -4
- package/dist/access-service-helpers.d.ts +5 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +42 -41
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-surface-catalog.js +5 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/{auto-sync-UPTUVE4X.js → auto-sync-5OE5QOZB.js} +3 -3
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +9 -8
- package/dist/buffer.d.ts +1 -1
- package/dist/buffer.js +6 -6
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +13 -12
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-LEPOP46W.js → chunk-227UFSEQ.js} +4 -4
- package/dist/{chunk-CMENJQ6I.js → chunk-2QR2RRH6.js} +2 -2
- package/dist/{chunk-ZERSIPFC.js → chunk-3HKQOFOV.js} +22 -20
- package/dist/{chunk-ZERSIPFC.js.map → chunk-3HKQOFOV.js.map} +1 -1
- package/dist/{chunk-4DYBLQZC.js → chunk-3HLCRYBV.js} +3 -3
- package/dist/{chunk-L2DPF4DF.js → chunk-3SHB4YAW.js} +4 -4
- package/dist/{chunk-UV72DEZD.js → chunk-4B77XSG5.js} +5 -5
- package/dist/{chunk-GUQYMOPU.js → chunk-4LMA77JH.js} +5 -5
- package/dist/{chunk-Q53RLEW7.js → chunk-5SM35XZR.js} +2 -2
- package/dist/{chunk-YJHU5B3M.js → chunk-5YGO5VEH.js} +2 -2
- package/dist/{chunk-AX2J2LC4.js → chunk-5ZE7QHTK.js} +2 -2
- package/dist/{chunk-5QZEVMLY.js → chunk-65V4K4MT.js} +56 -38
- package/dist/chunk-65V4K4MT.js.map +1 -0
- package/dist/{chunk-YFHMW37V.js → chunk-6CZPDBJS.js} +5 -5
- package/dist/{chunk-BI6RE2ZS.js → chunk-6OLQJJYE.js} +2 -2
- package/dist/chunk-6UWB5PTR.js +413 -0
- package/dist/chunk-6UWB5PTR.js.map +1 -0
- package/dist/chunk-6ZYQ36IB.js +30 -0
- package/dist/chunk-6ZYQ36IB.js.map +1 -0
- package/dist/{chunk-GS5UVGQK.js → chunk-7JO6RVBI.js} +6 -6
- package/dist/{chunk-MBTPUVHZ.js → chunk-7PVFOFPJ.js} +2 -2
- package/dist/{chunk-KPJQTSRJ.js → chunk-C7EI7CA6.js} +2 -2
- package/dist/{chunk-NRV4Y3PM.js → chunk-CAGPP5NB.js} +1 -1
- package/dist/{chunk-NF3U3MPG.js → chunk-ECK75GDJ.js} +2 -2
- package/dist/{chunk-KDXD2AM6.js → chunk-FLBPATMZ.js} +44 -44
- package/dist/{chunk-OEXKYYVH.js → chunk-FVGQ5LIS.js} +6 -6
- package/dist/{chunk-AR2FSZBP.js → chunk-G24XIK6H.js} +2 -2
- package/dist/{chunk-5TOV3D6U.js → chunk-GBJJUPNV.js} +6 -6
- package/dist/{chunk-UP67AFRN.js → chunk-GOPWZYZH.js} +29 -29
- package/dist/chunk-GOPWZYZH.js.map +1 -0
- package/dist/{chunk-EPQ76W5T.js → chunk-GQBZ7EVF.js} +2 -2
- package/dist/{chunk-BGXHVOBI.js → chunk-HLPD7B7P.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-JDXDA5OO.js → chunk-J4OKD6QX.js} +2 -2
- package/dist/{chunk-LZ5JKQCS.js → chunk-JZ7SSAQ5.js} +3 -3
- package/dist/chunk-K6OTW3TP.js +24 -0
- package/dist/chunk-K6OTW3TP.js.map +1 -0
- package/dist/{chunk-EHKDORQN.js → chunk-NVRYW264.js} +9 -9
- package/dist/{chunk-MU7ZHSZE.js → chunk-PO5LSKQO.js} +2 -2
- package/dist/{chunk-ONHRRJZM.js → chunk-QJAQNXWI.js} +6 -6
- package/dist/{chunk-WGCTFGLW.js → chunk-QQ3NDJOM.js} +3 -3
- package/dist/{chunk-BEQOZOYZ.js → chunk-RVLJEII5.js} +30 -511
- package/dist/chunk-RVLJEII5.js.map +1 -0
- package/dist/chunk-S5SLVSVY.js +517 -0
- package/dist/chunk-S5SLVSVY.js.map +1 -0
- package/dist/{chunk-QY66HJ4X.js → chunk-SS7YWVPF.js} +5 -5
- package/dist/{chunk-ZVVASRUT.js → chunk-U5G66ORD.js} +90 -88
- package/dist/chunk-U5G66ORD.js.map +1 -0
- package/dist/{chunk-JYIFGGEM.js → chunk-UZGJFIIG.js} +2 -2
- package/dist/{chunk-NHRNYJPJ.js → chunk-VCGPO2VD.js} +7 -7
- package/dist/{chunk-F2KMAFEB.js → chunk-VRYJSKOM.js} +2 -2
- package/dist/{chunk-HNZKVLAN.js → chunk-W6ROK2KU.js} +2 -2
- package/dist/{chunk-SEUSFKNS.js → chunk-WC4SPYHX.js} +14 -14
- package/dist/{chunk-MSXWUXFE.js → chunk-WG2PI44B.js} +2 -2
- package/dist/{chunk-4VXGRIH6.js → chunk-WUNIWKY4.js} +5 -5
- package/dist/{chunk-3C2TGBDB.js → chunk-XFQRFWVS.js} +2 -2
- package/dist/{chunk-5N26WBPQ.js → chunk-XMZV27S5.js} +5 -5
- package/dist/{chunk-JGXXPDM2.js → chunk-YQQK7W7T.js} +3 -3
- package/dist/{chunk-3F2LKUMU.js → chunk-ZAB5PG65.js} +2 -2
- package/dist/{chunk-MVW234LO.js → chunk-ZPJJE7MF.js} +2 -2
- package/dist/{chunk-6GIMMNVK.js → chunk-ZS5UZU5M.js} +4 -4
- package/dist/{cli-IUhV-lmk.d.ts → cli-DQ4VhjqX.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +69 -67
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +9 -8
- package/dist/connectors/codex-materialize-runner.js +9 -8
- package/dist/connectors/index.js +9 -8
- 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/convergence-refresh.d.ts +24 -0
- package/dist/convergence-refresh.js +75 -0
- package/dist/convergence-refresh.js.map +1 -0
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +20 -19
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +9 -8
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +4 -3
- package/dist/extraction.js +15 -14
- package/dist/{graph-edge-decay-Z6ENALU2.js → graph-edge-decay-GF3ZSGSP.js} +4 -4
- package/dist/graph-edge-decay-GF3ZSGSP.js.map +1 -0
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/index.d.ts +398 -398
- package/dist/index.js +161 -158
- package/dist/lcm/index.js +3 -3
- package/dist/maintenance/memory-governance.js +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -8
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-DVBLXkFw.d.ts → maintenance-_RV38GlE.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +19 -18
- package/dist/namespaces/search.js +9 -9
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +9 -8
- package/dist/offline-sync-impression-drain.js +4 -4
- package/dist/operator-doctor-corpus.js +21 -20
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +30 -29
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +16 -15
- package/dist/{orchestrator-fYTncnLy.d.ts → orchestrator-B2ecFpRO.d.ts} +4 -4
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +101 -98
- package/dist/qmd.js +4 -4
- package/dist/reconcile/manifest.d.ts +33 -0
- package/dist/reconcile/manifest.js +248 -0
- package/dist/reconcile/manifest.js.map +1 -0
- package/dist/reconcile/plan.d.ts +1 -1
- package/dist/reconcile/plan.js +8 -404
- package/dist/reconcile/plan.js.map +1 -1
- package/dist/resume-bundles.js +1 -1
- package/dist/schemas.d.ts +76 -76
- package/dist/search/factory.js +8 -8
- package/dist/search/index.js +14 -14
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +9 -8
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-CLuMO-pa.d.ts → service-DW06sY3P.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +14 -13
- package/dist/{storage-Blr71GpS.d.ts → storage-qRIQPA6F.d.ts} +12 -0
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +8 -7
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +10 -9
- package/package.json +12 -2
- package/src/access-http-query.ts +25 -0
- package/src/access-http.test.ts +136 -0
- package/src/access-http.ts +26 -26
- package/src/access-service-offline-file-content.test.ts +40 -0
- package/src/access-service.ts +34 -0
- package/src/access-surface-catalog.ts +5 -0
- package/src/convergence-refresh.ts +28 -0
- package/src/orchestrator-convergence-refresh.test.ts +114 -0
- package/src/orchestrator.ts +5 -4
- package/src/reconcile/manifest.test.ts +261 -0
- package/src/reconcile/manifest.ts +278 -0
- package/src/reconcile/plan.ts +1 -0
- package/src/storage/content-hash-index.ts +26 -0
- package/dist/chunk-5QZEVMLY.js.map +0 -1
- package/dist/chunk-BEQOZOYZ.js.map +0 -1
- package/dist/chunk-UP67AFRN.js.map +0 -1
- package/dist/chunk-ZVVASRUT.js.map +0 -1
- /package/dist/{graph-edge-decay-Z6ENALU2.js.map → access-http-query.js.map} +0 -0
- /package/dist/{auto-sync-UPTUVE4X.js.map → auto-sync-5OE5QOZB.js.map} +0 -0
- /package/dist/{chunk-LEPOP46W.js.map → chunk-227UFSEQ.js.map} +0 -0
- /package/dist/{chunk-CMENJQ6I.js.map → chunk-2QR2RRH6.js.map} +0 -0
- /package/dist/{chunk-4DYBLQZC.js.map → chunk-3HLCRYBV.js.map} +0 -0
- /package/dist/{chunk-L2DPF4DF.js.map → chunk-3SHB4YAW.js.map} +0 -0
- /package/dist/{chunk-UV72DEZD.js.map → chunk-4B77XSG5.js.map} +0 -0
- /package/dist/{chunk-GUQYMOPU.js.map → chunk-4LMA77JH.js.map} +0 -0
- /package/dist/{chunk-Q53RLEW7.js.map → chunk-5SM35XZR.js.map} +0 -0
- /package/dist/{chunk-YJHU5B3M.js.map → chunk-5YGO5VEH.js.map} +0 -0
- /package/dist/{chunk-AX2J2LC4.js.map → chunk-5ZE7QHTK.js.map} +0 -0
- /package/dist/{chunk-YFHMW37V.js.map → chunk-6CZPDBJS.js.map} +0 -0
- /package/dist/{chunk-BI6RE2ZS.js.map → chunk-6OLQJJYE.js.map} +0 -0
- /package/dist/{chunk-GS5UVGQK.js.map → chunk-7JO6RVBI.js.map} +0 -0
- /package/dist/{chunk-MBTPUVHZ.js.map → chunk-7PVFOFPJ.js.map} +0 -0
- /package/dist/{chunk-KPJQTSRJ.js.map → chunk-C7EI7CA6.js.map} +0 -0
- /package/dist/{chunk-NRV4Y3PM.js.map → chunk-CAGPP5NB.js.map} +0 -0
- /package/dist/{chunk-NF3U3MPG.js.map → chunk-ECK75GDJ.js.map} +0 -0
- /package/dist/{chunk-KDXD2AM6.js.map → chunk-FLBPATMZ.js.map} +0 -0
- /package/dist/{chunk-OEXKYYVH.js.map → chunk-FVGQ5LIS.js.map} +0 -0
- /package/dist/{chunk-AR2FSZBP.js.map → chunk-G24XIK6H.js.map} +0 -0
- /package/dist/{chunk-5TOV3D6U.js.map → chunk-GBJJUPNV.js.map} +0 -0
- /package/dist/{chunk-EPQ76W5T.js.map → chunk-GQBZ7EVF.js.map} +0 -0
- /package/dist/{chunk-BGXHVOBI.js.map → chunk-HLPD7B7P.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-JDXDA5OO.js.map → chunk-J4OKD6QX.js.map} +0 -0
- /package/dist/{chunk-LZ5JKQCS.js.map → chunk-JZ7SSAQ5.js.map} +0 -0
- /package/dist/{chunk-EHKDORQN.js.map → chunk-NVRYW264.js.map} +0 -0
- /package/dist/{chunk-MU7ZHSZE.js.map → chunk-PO5LSKQO.js.map} +0 -0
- /package/dist/{chunk-ONHRRJZM.js.map → chunk-QJAQNXWI.js.map} +0 -0
- /package/dist/{chunk-WGCTFGLW.js.map → chunk-QQ3NDJOM.js.map} +0 -0
- /package/dist/{chunk-QY66HJ4X.js.map → chunk-SS7YWVPF.js.map} +0 -0
- /package/dist/{chunk-JYIFGGEM.js.map → chunk-UZGJFIIG.js.map} +0 -0
- /package/dist/{chunk-NHRNYJPJ.js.map → chunk-VCGPO2VD.js.map} +0 -0
- /package/dist/{chunk-F2KMAFEB.js.map → chunk-VRYJSKOM.js.map} +0 -0
- /package/dist/{chunk-HNZKVLAN.js.map → chunk-W6ROK2KU.js.map} +0 -0
- /package/dist/{chunk-SEUSFKNS.js.map → chunk-WC4SPYHX.js.map} +0 -0
- /package/dist/{chunk-MSXWUXFE.js.map → chunk-WG2PI44B.js.map} +0 -0
- /package/dist/{chunk-4VXGRIH6.js.map → chunk-WUNIWKY4.js.map} +0 -0
- /package/dist/{chunk-3C2TGBDB.js.map → chunk-XFQRFWVS.js.map} +0 -0
- /package/dist/{chunk-5N26WBPQ.js.map → chunk-XMZV27S5.js.map} +0 -0
- /package/dist/{chunk-JGXXPDM2.js.map → chunk-YQQK7W7T.js.map} +0 -0
- /package/dist/{chunk-3F2LKUMU.js.map → chunk-ZAB5PG65.js.map} +0 -0
- /package/dist/{chunk-MVW234LO.js.map → chunk-ZPJJE7MF.js.map} +0 -0
- /package/dist/{chunk-6GIMMNVK.js.map → chunk-ZS5UZU5M.js.map} +0 -0
package/src/access-http.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type EngramAccessWriteResponse,
|
|
19
19
|
} from "./access-service.js";
|
|
20
20
|
import { maybeHandleLifecycleFlush, type LifecycleFlushHttpDeps } from "./access-http-lifecycle-flush.js";
|
|
21
|
+
import { nonEmptyQueryParam, optionalQueryString, positiveIntQueryParam } from "./access-http-query.js";
|
|
21
22
|
import { CorrectionContractError } from "./correction/correction-contract.js";
|
|
22
23
|
import { WearablesInputError } from "./wearables/errors.js"; import { respondMeetingsList, respondMeetingsGet, respondMeetingsBuild } from "./meetings/http-glue.js";
|
|
23
24
|
import { EngramMcpServer, MCP_SUPPORTED_PROTOCOL_VERSIONS } from "./access-mcp.js";
|
|
@@ -1322,6 +1323,31 @@ export class EngramAccessHttpServer {
|
|
|
1322
1323
|
return;
|
|
1323
1324
|
}
|
|
1324
1325
|
|
|
1326
|
+
if (
|
|
1327
|
+
req.method === "POST" &&
|
|
1328
|
+
(
|
|
1329
|
+
pathname === "/engram/v1/offline-sync/convergence-complete" ||
|
|
1330
|
+
pathname === "/remnic/v1/offline-sync/convergence-complete"
|
|
1331
|
+
)
|
|
1332
|
+
) {
|
|
1333
|
+
this.enforceTokenOp("offline_sync_apply_file_content");
|
|
1334
|
+
const namespaceParams = parsed.searchParams.getAll("namespace").filter(Boolean);
|
|
1335
|
+
const requestedNamespaces: Array<string | undefined> =
|
|
1336
|
+
namespaceParams.length > 0 ? namespaceParams : [undefined];
|
|
1337
|
+
const namespaces = requestedNamespaces
|
|
1338
|
+
.map((namespace) => this.resolveNamespace(req, namespace))
|
|
1339
|
+
.filter((namespace): namespace is string => namespace !== undefined);
|
|
1340
|
+
this.ensureWriteRateLimitAvailable(req);
|
|
1341
|
+
const result = await this.service.offlineSyncFinalizeConvergence({
|
|
1342
|
+
...(namespaces.length > 0 ? { namespaces } : {}),
|
|
1343
|
+
principal: this.resolveRequestPrincipal(req),
|
|
1344
|
+
sourceId: this.readRequiredDecodedHeader(req, "x-remnic-source-id"),
|
|
1345
|
+
});
|
|
1346
|
+
this.recordWriteRateLimitHit(req);
|
|
1347
|
+
this.respondJson(res, 200, result);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1325
1351
|
if (
|
|
1326
1352
|
req.method === "POST" &&
|
|
1327
1353
|
(pathname === "/engram/v1/offline-sync/apply" || pathname === "/remnic/v1/offline-sync/apply")
|
|
@@ -3925,29 +3951,3 @@ export class EngramAccessHttpServer {
|
|
|
3925
3951
|
return false;
|
|
3926
3952
|
}
|
|
3927
3953
|
}
|
|
3928
|
-
|
|
3929
|
-
/** Optional string field from a JSON body: absent/null/"" → undefined. */
|
|
3930
|
-
function optionalQueryString(value: unknown, label: string): string | undefined {
|
|
3931
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
3932
|
-
if (typeof value !== "string") {
|
|
3933
|
-
throw new EngramAccessInputError(
|
|
3934
|
-
`${label} must be a string (got ${JSON.stringify(value)})`,
|
|
3935
|
-
);
|
|
3936
|
-
}
|
|
3937
|
-
return value;
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
|
-
/** Optional non-empty query param: null/"" → undefined. */
|
|
3941
|
-
function nonEmptyQueryParam(value: string | null): string | undefined {
|
|
3942
|
-
return value !== null && value.length > 0 ? value : undefined;
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
/** Optional positive-integer query param; rejects invalid values. */
|
|
3946
|
-
function positiveIntQueryParam(value: string | null, label: string): number | undefined {
|
|
3947
|
-
if (value === null || value.length === 0) return undefined;
|
|
3948
|
-
const parsed = Number(value);
|
|
3949
|
-
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
3950
|
-
throw new EngramAccessInputError(`${label} expects a positive integer`);
|
|
3951
|
-
}
|
|
3952
|
-
return parsed;
|
|
3953
|
-
}
|
|
@@ -52,3 +52,43 @@ test("offline apply-file-content reports invalid metadata as input errors", asyn
|
|
|
52
52
|
/mtimeMs must be within JavaScript Date range/.test(error.message),
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
test("offline convergence completion refreshes authorized namespaces as one batch and rejects other sources", async () => {
|
|
57
|
+
const refreshed: string[][] = [];
|
|
58
|
+
const orchestratorStub = {
|
|
59
|
+
config: {
|
|
60
|
+
memoryDir: "/tmp/remnic-access-service-convergence-test",
|
|
61
|
+
namespacesEnabled: false,
|
|
62
|
+
defaultNamespace: "global",
|
|
63
|
+
sharedNamespace: "shared",
|
|
64
|
+
},
|
|
65
|
+
refreshNamespacesAfterConvergence: async (namespaces: readonly string[]) => {
|
|
66
|
+
refreshed.push([...namespaces]);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const service = new EngramAccessService(
|
|
70
|
+
orchestratorStub as unknown as ConstructorParameters<typeof EngramAccessService>[0],
|
|
71
|
+
);
|
|
72
|
+
const convergenceService = service as unknown as {
|
|
73
|
+
offlineSyncFinalizeConvergence(options: {
|
|
74
|
+
namespaces?: string[];
|
|
75
|
+
principal?: string;
|
|
76
|
+
sourceId: string;
|
|
77
|
+
}): Promise<{ namespaces: string[]; refreshed: true }>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await assert.rejects(
|
|
81
|
+
() => convergenceService.offlineSyncFinalizeConvergence({ sourceId: "laptop" }),
|
|
82
|
+
(error) =>
|
|
83
|
+
error instanceof EngramAccessInputError
|
|
84
|
+
&& /sourceId must be remnic-converge/.test(error.message),
|
|
85
|
+
);
|
|
86
|
+
assert.deepEqual(refreshed, []);
|
|
87
|
+
|
|
88
|
+
const result = await convergenceService.offlineSyncFinalizeConvergence({
|
|
89
|
+
sourceId: "remnic-converge",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assert.deepEqual(result, { namespaces: ["global"], refreshed: true });
|
|
93
|
+
assert.deepEqual(refreshed, [["global"]]);
|
|
94
|
+
});
|
package/src/access-service.ts
CHANGED
|
@@ -818,6 +818,12 @@ export interface EngramAccessOfflineSyncApplyRequest {
|
|
|
818
818
|
returnCurrentFiles?: boolean;
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
+
export interface EngramAccessOfflineSyncFinalizeConvergenceRequest {
|
|
822
|
+
namespaces?: string[];
|
|
823
|
+
principal?: string;
|
|
824
|
+
sourceId: string;
|
|
825
|
+
}
|
|
826
|
+
|
|
821
827
|
export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnapshot {
|
|
822
828
|
namespace: string;
|
|
823
829
|
}
|
|
@@ -843,6 +849,11 @@ export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyCh
|
|
|
843
849
|
namespace: string;
|
|
844
850
|
}
|
|
845
851
|
|
|
852
|
+
export interface EngramAccessOfflineSyncFinalizeConvergenceResponse {
|
|
853
|
+
namespaces: string[];
|
|
854
|
+
refreshed: true;
|
|
855
|
+
}
|
|
856
|
+
|
|
846
857
|
export type EngramAccessActionConfidenceRequest = ActionConfidenceInput;
|
|
847
858
|
export type EngramAccessActionConfidenceResponse = ActionConfidenceResult;
|
|
848
859
|
|
|
@@ -5621,6 +5632,29 @@ export class EngramAccessService {
|
|
|
5621
5632
|
}
|
|
5622
5633
|
}
|
|
5623
5634
|
|
|
5635
|
+
async offlineSyncFinalizeConvergence(
|
|
5636
|
+
options: EngramAccessOfflineSyncFinalizeConvergenceRequest,
|
|
5637
|
+
): Promise<EngramAccessOfflineSyncFinalizeConvergenceResponse> {
|
|
5638
|
+
if (options.sourceId !== "remnic-converge") {
|
|
5639
|
+
throw new EngramAccessInputError("sourceId must be remnic-converge");
|
|
5640
|
+
}
|
|
5641
|
+
const requestedNamespaces = options.namespaces?.length
|
|
5642
|
+
? options.namespaces
|
|
5643
|
+
: [undefined];
|
|
5644
|
+
const resolvedNamespaces = Array.from(new Set(
|
|
5645
|
+
requestedNamespaces.map((namespace) => this.writableNamespaceFor(
|
|
5646
|
+
namespace,
|
|
5647
|
+
undefined,
|
|
5648
|
+
options.principal,
|
|
5649
|
+
)),
|
|
5650
|
+
));
|
|
5651
|
+
await this.orchestrator.refreshNamespacesAfterConvergence(resolvedNamespaces);
|
|
5652
|
+
return {
|
|
5653
|
+
namespaces: resolvedNamespaces,
|
|
5654
|
+
refreshed: true,
|
|
5655
|
+
};
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5624
5658
|
async offlineSyncApply(
|
|
5625
5659
|
options: EngramAccessOfflineSyncApplyRequest,
|
|
5626
5660
|
): Promise<EngramAccessOfflineSyncApplyResponse> {
|
|
@@ -183,6 +183,11 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
|
|
|
183
183
|
{ method: "POST", pathname: "/engram/v1/offline-sync/files", operation: "offline_sync_files" },
|
|
184
184
|
{ method: "POST", pathname: "/engram/v1/offline-sync/file-content", operation: "offline_sync_file_content" },
|
|
185
185
|
{ method: "POST", pathname: "/engram/v1/offline-sync/apply-file-content", operation: "offline_sync_apply_file_content" },
|
|
186
|
+
{
|
|
187
|
+
method: "POST",
|
|
188
|
+
pathname: "/engram/v1/offline-sync/convergence-complete",
|
|
189
|
+
operation: "offline_sync_apply_file_content",
|
|
190
|
+
},
|
|
186
191
|
{ method: "POST", pathname: "/engram/v1/offline-sync/apply", operation: "offline_sync_apply" },
|
|
187
192
|
{ method: "POST", pathname: "/engram/v1/recall/explain", operation: "recall_explain" },
|
|
188
193
|
{ method: "POST", pathname: "/engram/v1/action-confidence", operation: "action_confidence" },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type RebuildMemoryProjectionOptions,
|
|
3
|
+
rebuildMemoryProjection,
|
|
4
|
+
} from "./maintenance/rebuild-memory-projection.js";
|
|
5
|
+
import type { NamespaceSearchRouter } from "./namespaces/search.js";
|
|
6
|
+
|
|
7
|
+
type ConvergenceStorage = NonNullable<RebuildMemoryProjectionOptions["storage"]>;
|
|
8
|
+
|
|
9
|
+
export async function refreshConvergedNamespaces(
|
|
10
|
+
namespaces: readonly string[],
|
|
11
|
+
namespaceSearchRouter: Pick<NamespaceSearchRouter, "updateNamespacesDetailed">,
|
|
12
|
+
getStorage: (namespace: string) => Promise<ConvergenceStorage>
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const targetNamespaces = Array.from(new Set(namespaces.map((namespace) => namespace.trim()).filter(Boolean)));
|
|
15
|
+
if (targetNamespaces.length === 0) return;
|
|
16
|
+
|
|
17
|
+
await namespaceSearchRouter.updateNamespacesDetailed(targetNamespaces, undefined, { strict: true });
|
|
18
|
+
|
|
19
|
+
for (const namespace of targetNamespaces) {
|
|
20
|
+
const storage = await getStorage(namespace);
|
|
21
|
+
await rebuildMemoryProjection({
|
|
22
|
+
memoryDir: storage.dir,
|
|
23
|
+
storage,
|
|
24
|
+
defaultNamespace: namespace,
|
|
25
|
+
dryRun: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { readProjectedMemoryBrowse } from "./memory-projection-store.js";
|
|
8
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
9
|
+
import { StorageManager } from "./storage.js";
|
|
10
|
+
|
|
11
|
+
test("refreshNamespacesAfterConvergence runs one strict QMD batch and rebuilds each namespace projection once", async () => {
|
|
12
|
+
const teamDir = await mkdtemp(path.join(os.tmpdir(), "remnic-convergence-team-"));
|
|
13
|
+
const sharedDir = await mkdtemp(path.join(os.tmpdir(), "remnic-convergence-shared-"));
|
|
14
|
+
try {
|
|
15
|
+
const teamStorage = new StorageManager(teamDir);
|
|
16
|
+
const sharedStorage = new StorageManager(sharedDir);
|
|
17
|
+
const teamMemory = await teamStorage.writeMemory("fact", "A received team fact.", {
|
|
18
|
+
source: "test",
|
|
19
|
+
});
|
|
20
|
+
const sharedMemory = await sharedStorage.writeMemory("fact", "A received shared fact.", {
|
|
21
|
+
source: "test",
|
|
22
|
+
});
|
|
23
|
+
const qmdCalls: Array<{
|
|
24
|
+
namespaces: string[];
|
|
25
|
+
options: { strict?: boolean } | undefined;
|
|
26
|
+
}> = [];
|
|
27
|
+
const storageCalls: string[] = [];
|
|
28
|
+
const orchestrator = Object.create(Orchestrator.prototype) as Orchestrator;
|
|
29
|
+
const internals = orchestrator as unknown as {
|
|
30
|
+
config: { defaultNamespace: string };
|
|
31
|
+
namespaceSearchRouter: {
|
|
32
|
+
updateNamespacesDetailed(
|
|
33
|
+
namespaces: string[],
|
|
34
|
+
execution?: unknown,
|
|
35
|
+
options?: { strict?: boolean }
|
|
36
|
+
): Promise<{ backendCount: number; eligibleNamespaces: string[] }>;
|
|
37
|
+
};
|
|
38
|
+
getStorage(namespace: string): Promise<StorageManager>;
|
|
39
|
+
};
|
|
40
|
+
internals.config = { defaultNamespace: "default" };
|
|
41
|
+
internals.namespaceSearchRouter = {
|
|
42
|
+
async updateNamespacesDetailed(namespaces, _execution, options) {
|
|
43
|
+
qmdCalls.push({ namespaces, options });
|
|
44
|
+
return { backendCount: 1, eligibleNamespaces: namespaces };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const storageByNamespace = new Map([
|
|
48
|
+
["team", teamStorage],
|
|
49
|
+
["shared", sharedStorage],
|
|
50
|
+
]);
|
|
51
|
+
internals.getStorage = async (namespace) => {
|
|
52
|
+
storageCalls.push(namespace);
|
|
53
|
+
const storage = storageByNamespace.get(namespace);
|
|
54
|
+
assert.ok(storage);
|
|
55
|
+
return storage;
|
|
56
|
+
};
|
|
57
|
+
const refreshSurface = orchestrator as unknown as {
|
|
58
|
+
refreshNamespacesAfterConvergence(namespaces: readonly string[]): Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await refreshSurface.refreshNamespacesAfterConvergence(["team", "shared", "team"]);
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(qmdCalls, [
|
|
64
|
+
{
|
|
65
|
+
namespaces: ["team", "shared"],
|
|
66
|
+
options: { strict: true },
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
assert.deepEqual(storageCalls, ["team", "shared"]);
|
|
70
|
+
const teamProjection = readProjectedMemoryBrowse(teamDir, { limit: 10, offset: 0 });
|
|
71
|
+
const sharedProjection = readProjectedMemoryBrowse(sharedDir, { limit: 10, offset: 0 });
|
|
72
|
+
assert.equal(teamProjection?.memories[0]?.id, teamMemory.id);
|
|
73
|
+
assert.equal(sharedProjection?.memories[0]?.id, sharedMemory.id);
|
|
74
|
+
} finally {
|
|
75
|
+
await Promise.all([rm(teamDir, { recursive: true, force: true }), rm(sharedDir, { recursive: true, force: true })]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("refreshNamespacesAfterConvergence rebuilds projection when search is disabled", async () => {
|
|
80
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-convergence-no-search-"));
|
|
81
|
+
try {
|
|
82
|
+
const storage = new StorageManager(memoryDir);
|
|
83
|
+
const memory = await storage.writeMemory("fact", "A received fact without search enabled.", {
|
|
84
|
+
source: "test",
|
|
85
|
+
});
|
|
86
|
+
const orchestrator = Object.create(Orchestrator.prototype) as Orchestrator;
|
|
87
|
+
const internals = orchestrator as unknown as {
|
|
88
|
+
namespaceSearchRouter: {
|
|
89
|
+
updateNamespacesDetailed(
|
|
90
|
+
namespaces: string[],
|
|
91
|
+
execution?: unknown,
|
|
92
|
+
options?: { strict?: boolean }
|
|
93
|
+
): Promise<{ backendCount: number; eligibleNamespaces: string[] }>;
|
|
94
|
+
};
|
|
95
|
+
getStorage(namespace: string): Promise<StorageManager>;
|
|
96
|
+
};
|
|
97
|
+
internals.namespaceSearchRouter = {
|
|
98
|
+
async updateNamespacesDetailed() {
|
|
99
|
+
return { backendCount: 0, eligibleNamespaces: [] };
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
internals.getStorage = async () => storage;
|
|
103
|
+
const refreshSurface = orchestrator as unknown as {
|
|
104
|
+
refreshNamespacesAfterConvergence(namespaces: readonly string[]): Promise<void>;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
await refreshSurface.refreshNamespacesAfterConvergence(["default"]);
|
|
108
|
+
|
|
109
|
+
const projection = readProjectedMemoryBrowse(memoryDir, { limit: 10, offset: 0 });
|
|
110
|
+
assert.equal(projection?.memories[0]?.id, memory.id);
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
package/src/orchestrator.ts
CHANGED
|
@@ -218,6 +218,7 @@ import {
|
|
|
218
218
|
runPatternReinforcement,
|
|
219
219
|
type PatternReinforcementResult,
|
|
220
220
|
} from "./maintenance/pattern-reinforcement.js";
|
|
221
|
+
import { refreshConvergedNamespaces } from "./convergence-refresh.js";
|
|
221
222
|
import { ModelRegistry } from "./model-registry.js";
|
|
222
223
|
import { applyRuntimeRetrievalPolicy, expandQuery } from "./retrieval.js";
|
|
223
224
|
import {
|
|
@@ -1278,11 +1279,11 @@ export class Orchestrator {
|
|
|
1278
1279
|
return await this.namespaceSearchRouter.healthForNamespace(namespace, execution);
|
|
1279
1280
|
}
|
|
1280
1281
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
return this.qmd.isAvailable();
|
|
1282
|
+
async refreshNamespacesAfterConvergence(namespaces: readonly string[]): Promise<void> {
|
|
1283
|
+
return refreshConvergedNamespaces(namespaces, this.namespaceSearchRouter, this.getStorage.bind(this));
|
|
1284
1284
|
}
|
|
1285
1285
|
|
|
1286
|
+
|
|
1286
1287
|
invalidateLiveContentHashIndex(): void {
|
|
1287
1288
|
this.contentHashIndex = null;
|
|
1288
1289
|
this.contentHashIndexesByStorageDir.clear();
|
|
@@ -1530,7 +1531,7 @@ export class Orchestrator {
|
|
|
1530
1531
|
});
|
|
1531
1532
|
this.contradictionLinkingCoordinator = new ContradictionLinkingCoordinator({
|
|
1532
1533
|
getConfig: () => this.config,
|
|
1533
|
-
isSearchAvailable: () => this.
|
|
1534
|
+
isSearchAvailable: () => resolveNamespaceCapabilities(this.config).namespaces || this.qmd.isAvailable(),
|
|
1534
1535
|
searchAcrossNamespaces: (options) => this.searchAcrossNamespaces(options),
|
|
1535
1536
|
extractMemoryIdsFromResults: (results) => this.extractMemoryIdsFromResults(results),
|
|
1536
1537
|
namespaceFromPath: (p) => this.namespaceFromPath(p),
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import * as assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { ContentHashIndex } from "../storage/content-hash-index.js";
|
|
5
|
+
import { type ReconcileManifest, buildReconcileManifest, collapseActiveFactDuplicates } from "./manifest.js";
|
|
6
|
+
import { planReconciliation } from "./plan.js";
|
|
7
|
+
|
|
8
|
+
const fileHash = (content: string): string => createHash("sha256").update(content).digest("hex");
|
|
9
|
+
|
|
10
|
+
function memoryFile(options: {
|
|
11
|
+
id: string;
|
|
12
|
+
content: string;
|
|
13
|
+
contentHash?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
}): string {
|
|
16
|
+
return [
|
|
17
|
+
"---",
|
|
18
|
+
`id: ${options.id}`,
|
|
19
|
+
"category: fact",
|
|
20
|
+
"created: 2026-01-01T00:00:00.000Z",
|
|
21
|
+
"updated: 2026-01-01T00:00:00.000Z",
|
|
22
|
+
...(options.contentHash ? [`contentHash: ${options.contentHash}`] : []),
|
|
23
|
+
...(options.status ? [`status: ${options.status}`] : []),
|
|
24
|
+
"---",
|
|
25
|
+
options.content,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("reconcile manifest keeps file identity separate from canonical semantic identity", async () => {
|
|
30
|
+
const content = "The deployment uses a durable queue with an enrichment suffix.";
|
|
31
|
+
const semanticHash = ContentHashIndex.computeHash("The deployment uses a durable queue.");
|
|
32
|
+
const serialized = memoryFile({ id: "fact-a", content, contentHash: semanticHash, status: "active" });
|
|
33
|
+
const serializedHash = fileHash(serialized);
|
|
34
|
+
|
|
35
|
+
const manifest = await buildReconcileManifest({
|
|
36
|
+
files: [{ path: "facts/fact-a.md", sha256: serializedHash, bytes: Buffer.byteLength(serialized) }],
|
|
37
|
+
readFile: async () => Buffer.from(serialized),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(manifest.format, "remnic-reconcile-manifest");
|
|
41
|
+
assert.equal(manifest.schemaVersion, 1);
|
|
42
|
+
assert.equal(manifest.files[0]?.sha256, serializedHash);
|
|
43
|
+
assert.notEqual(serializedHash, semanticHash);
|
|
44
|
+
assert.deepEqual(manifest.files[0]?.memory, {
|
|
45
|
+
id: "fact-a",
|
|
46
|
+
category: "fact",
|
|
47
|
+
contentHash: semanticHash,
|
|
48
|
+
status: "active",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("reconcile manifest hashes parsed legacy content and uses effective lifecycle status", async () => {
|
|
53
|
+
const active = memoryFile({ id: "legacy", content: "Legacy fact body" });
|
|
54
|
+
const archived = memoryFile({ id: "archived", content: "Archived fact body", status: "active" });
|
|
55
|
+
const rawByPath = new Map([
|
|
56
|
+
["facts/legacy.md", Buffer.from(active)],
|
|
57
|
+
["archive/facts/archived.md", Buffer.from(archived)],
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const manifest = await buildReconcileManifest({
|
|
61
|
+
files: [...rawByPath].map(([path, raw]) => ({ path, sha256: fileHash(raw.toString()) })),
|
|
62
|
+
readFile: async (file) => rawByPath.get(file.path) ?? null,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.equal(manifest.files[0]?.memory?.contentHash, ContentHashIndex.computeHash("Archived fact body"));
|
|
66
|
+
assert.equal(manifest.files[0]?.memory?.status, "archived");
|
|
67
|
+
assert.equal(manifest.files[1]?.memory?.contentHash, ContentHashIndex.computeHash("Legacy fact body"));
|
|
68
|
+
assert.equal(manifest.files[1]?.memory?.status, "active");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("reconcile manifest keeps file identity when memory bytes cannot be trusted", async () => {
|
|
72
|
+
const readable = memoryFile({ id: "readable", content: "Readable fact" });
|
|
73
|
+
const changed = memoryFile({ id: "changed", content: "Changed after census" });
|
|
74
|
+
const files = [
|
|
75
|
+
{ path: "facts/readable.md", sha256: fileHash(readable) },
|
|
76
|
+
{ path: "facts/locked.md", sha256: "f".repeat(64) },
|
|
77
|
+
{ path: "facts/changed.md", sha256: fileHash("bytes from census") },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const manifest = await buildReconcileManifest({
|
|
81
|
+
files,
|
|
82
|
+
readFile: async (file) => {
|
|
83
|
+
if (file.path === "facts/locked.md") throw new Error("locked");
|
|
84
|
+
return file.path === "facts/changed.md" ? changed : readable;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(manifest.files.length, 3);
|
|
89
|
+
assert.equal(manifest.files.find((file) => file.path === "facts/readable.md")?.memory?.id, "readable");
|
|
90
|
+
assert.equal(manifest.files.find((file) => file.path === "facts/locked.md")?.memory, undefined);
|
|
91
|
+
assert.equal(manifest.files.find((file) => file.path === "facts/changed.md")?.memory, undefined);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("ContentHashIndex resolves a semantic hash to a stable canonical path without changing membership", () => {
|
|
95
|
+
const semanticHash = ContentHashIndex.computeHash("same fact");
|
|
96
|
+
const entries = [
|
|
97
|
+
{ path: "facts/z.md", contentHash: semanticHash },
|
|
98
|
+
{ path: "facts/a.md", contentHash: semanticHash },
|
|
99
|
+
{ path: "facts/other.md", contentHash: ContentHashIndex.computeHash("other fact") },
|
|
100
|
+
];
|
|
101
|
+
const index = new ContentHashIndex("unused");
|
|
102
|
+
|
|
103
|
+
assert.equal(ContentHashIndex.resolvePathByHash(semanticHash, entries), "facts/a.md");
|
|
104
|
+
assert.equal(ContentHashIndex.resolvePathByHash("f".repeat(64), entries), undefined);
|
|
105
|
+
assert.equal(index.has("same fact"), false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("semantic collapse prevents different active fact paths from transferring both directions", () => {
|
|
109
|
+
const semanticHash = ContentHashIndex.computeHash("same active fact");
|
|
110
|
+
const localFile = { path: "facts/local-id.md", sha256: "a".repeat(64) };
|
|
111
|
+
const peerFile = { path: "facts/peer-id.md", sha256: "b".repeat(64) };
|
|
112
|
+
const plan = planReconciliation([{ namespace: "default", local: [localFile], peer: [peerFile] }]);
|
|
113
|
+
const local: ReconcileManifest = {
|
|
114
|
+
format: "remnic-reconcile-manifest",
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
files: [
|
|
117
|
+
{ ...localFile, memory: { id: "local-id", category: "fact", contentHash: semanticHash, status: "active" } },
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
const peer: ReconcileManifest = {
|
|
121
|
+
format: "remnic-reconcile-manifest",
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
files: [{ ...peerFile, memory: { id: "peer-id", category: "fact", contentHash: semanticHash, status: "active" } }],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const collapsed = collapseActiveFactDuplicates(plan, new Map([["default", local]]), new Map([["default", peer]]));
|
|
127
|
+
|
|
128
|
+
assert.equal(collapsed.converged, true);
|
|
129
|
+
assert.deepEqual(collapsed.entries, [
|
|
130
|
+
{
|
|
131
|
+
path: "facts/local-id.md",
|
|
132
|
+
namespace: "default",
|
|
133
|
+
action: "identical",
|
|
134
|
+
reason: "semantic_duplicate",
|
|
135
|
+
localSha256: localFile.sha256,
|
|
136
|
+
peerSha256: peerFile.sha256,
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
assert.equal(collapsed.byNamespace[0]?.identical, 1);
|
|
140
|
+
assert.equal(collapsed.byNamespace[0]?.pull, 0);
|
|
141
|
+
assert.equal(collapsed.byNamespace[0]?.push, 0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("semantic collapse does not fold non-active facts or unrelated path conflicts", () => {
|
|
145
|
+
const semanticHash = ContentHashIndex.computeHash("same inactive fact");
|
|
146
|
+
const localFile = { path: "facts/local.md", sha256: "a".repeat(64) };
|
|
147
|
+
const peerFile = { path: "facts/peer.md", sha256: "b".repeat(64) };
|
|
148
|
+
const plan = planReconciliation([{ namespace: "default", local: [localFile], peer: [peerFile] }]);
|
|
149
|
+
const manifest = (file: typeof localFile, id: string, status: "active" | "archived"): ReconcileManifest => ({
|
|
150
|
+
format: "remnic-reconcile-manifest",
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
files: [{ ...file, memory: { id, category: "fact", contentHash: semanticHash, status } }],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const collapsed = collapseActiveFactDuplicates(
|
|
156
|
+
plan,
|
|
157
|
+
new Map([["default", manifest(localFile, "local", "active")]]),
|
|
158
|
+
new Map([["default", manifest(peerFile, "peer", "archived")]])
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(collapsed, plan);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("semantic collapse leaves tombstone suppression authoritative", () => {
|
|
165
|
+
const semanticHash = ContentHashIndex.computeHash("retracted fact");
|
|
166
|
+
const localFile = { path: "facts/local.md", sha256: "a".repeat(64) };
|
|
167
|
+
const peerFile = { path: "facts/peer.md", sha256: "b".repeat(64) };
|
|
168
|
+
const plan = planReconciliation([
|
|
169
|
+
{
|
|
170
|
+
namespace: "default",
|
|
171
|
+
local: [localFile],
|
|
172
|
+
peer: [peerFile],
|
|
173
|
+
tombstonedFileSha256: [localFile.sha256],
|
|
174
|
+
},
|
|
175
|
+
]);
|
|
176
|
+
const manifest = (file: typeof localFile, id: string): ReconcileManifest => ({
|
|
177
|
+
format: "remnic-reconcile-manifest",
|
|
178
|
+
schemaVersion: 1,
|
|
179
|
+
files: [{ ...file, memory: { id, category: "fact", contentHash: semanticHash, status: "active" } }],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const collapsed = collapseActiveFactDuplicates(
|
|
183
|
+
plan,
|
|
184
|
+
new Map([["default", manifest(localFile, "local")]]),
|
|
185
|
+
new Map([["default", manifest(peerFile, "peer")]])
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
assert.deepEqual(collapsed, plan);
|
|
189
|
+
assert.equal(
|
|
190
|
+
collapsed.entries.some((entry) => entry.action === "suppress"),
|
|
191
|
+
true
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("semantic collapse preserves same-path metadata updates", () => {
|
|
196
|
+
const path = "facts/shared.md";
|
|
197
|
+
const localFile = { path, sha256: "a".repeat(64) };
|
|
198
|
+
const peerFile = { path, sha256: "b".repeat(64) };
|
|
199
|
+
const baseFile = { path, sha256: peerFile.sha256 };
|
|
200
|
+
const semanticHash = ContentHashIndex.computeHash("same raw fact");
|
|
201
|
+
const plan = planReconciliation([{ namespace: "default", local: [localFile], peer: [peerFile], base: [baseFile] }]);
|
|
202
|
+
const manifest = (file: typeof localFile, id: string): ReconcileManifest => ({
|
|
203
|
+
format: "remnic-reconcile-manifest",
|
|
204
|
+
schemaVersion: 1,
|
|
205
|
+
files: [{ ...file, memory: { id, category: "fact", contentHash: semanticHash, status: "active" } }],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const collapsed = collapseActiveFactDuplicates(
|
|
209
|
+
plan,
|
|
210
|
+
new Map([["default", manifest(localFile, "local")]]),
|
|
211
|
+
new Map([["default", manifest(peerFile, "peer")]])
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
assert.deepEqual(collapsed, plan);
|
|
215
|
+
assert.equal(collapsed.entries[0]?.action, "push");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("semantic collapse remains converged after transferring the canonical one-sided duplicate", () => {
|
|
219
|
+
const semanticHash = ContentHashIndex.computeHash("same one-sided fact");
|
|
220
|
+
const canonical = { path: "facts/a.md", sha256: "a".repeat(64) };
|
|
221
|
+
const duplicate = { path: "facts/b.md", sha256: "b".repeat(64) };
|
|
222
|
+
const manifest = (files: Array<{ path: string; sha256: string }>): ReconcileManifest => ({
|
|
223
|
+
format: "remnic-reconcile-manifest",
|
|
224
|
+
schemaVersion: 1,
|
|
225
|
+
files: files.map((file) => ({
|
|
226
|
+
...file,
|
|
227
|
+
memory: {
|
|
228
|
+
id: file.path,
|
|
229
|
+
category: "fact",
|
|
230
|
+
contentHash: semanticHash,
|
|
231
|
+
status: "active",
|
|
232
|
+
},
|
|
233
|
+
})),
|
|
234
|
+
});
|
|
235
|
+
const localManifest = manifest([canonical, duplicate]);
|
|
236
|
+
const firstPlan = planReconciliation([{ namespace: "default", local: [canonical, duplicate], peer: [] }]);
|
|
237
|
+
|
|
238
|
+
const firstCollapsed = collapseActiveFactDuplicates(
|
|
239
|
+
firstPlan,
|
|
240
|
+
new Map([["default", localManifest]]),
|
|
241
|
+
new Map([["default", manifest([])]])
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
assert.deepEqual(
|
|
245
|
+
firstCollapsed.entries.map((entry) => [entry.path, entry.action]),
|
|
246
|
+
[[canonical.path, "push"]]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const rerunPlan = planReconciliation([{ namespace: "default", local: [canonical, duplicate], peer: [canonical] }]);
|
|
250
|
+
const rerunCollapsed = collapseActiveFactDuplicates(
|
|
251
|
+
rerunPlan,
|
|
252
|
+
new Map([["default", localManifest]]),
|
|
253
|
+
new Map([["default", manifest([canonical])]])
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
assert.equal(rerunCollapsed.converged, true, JSON.stringify(rerunCollapsed));
|
|
257
|
+
assert.deepEqual(
|
|
258
|
+
rerunCollapsed.entries.map((entry) => [entry.path, entry.action]),
|
|
259
|
+
[[canonical.path, "identical"]]
|
|
260
|
+
);
|
|
261
|
+
});
|