@remnic/core 9.57.4 → 9.57.6
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 +1 -1
- package/dist/access-admin-ops-surface.js +23 -23
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +24 -24
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +23 -23
- package/dist/access-cli.js +60 -60
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +23 -23
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +1 -1
- package/dist/access-http-query.js +23 -23
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +34 -34
- package/dist/access-identity-continuity-surface.js +23 -23
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +23 -23
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +30 -30
- package/dist/access-memory-search-fanout.js +4 -3
- package/dist/access-namespace-preflight.js +23 -23
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +23 -23
- package/dist/access-offline-manifest.js +10 -10
- package/dist/access-operations-batch.js +26 -26
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +29 -29
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +23 -23
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +23 -23
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +23 -23
- package/dist/access-schema.d.ts +88 -88
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +23 -23
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +25 -25
- package/dist/active-memory-bridge.js +5 -5
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise.js +3 -3
- package/dist/buffer.js +6 -5
- package/dist/causal-consolidation.js +7 -7
- package/dist/{chunk-O54B5YNM.js → chunk-24PNX2GF.js} +3 -3
- package/dist/{chunk-ONCWJBWV.js → chunk-3AJOJOFP.js} +2 -2
- package/dist/{chunk-QSDPVZPD.js → chunk-4O6R3GPK.js} +8 -8
- package/dist/{chunk-IXGQP4RZ.js → chunk-4SGDYYWV.js} +4 -4
- package/dist/{chunk-UTDMY476.js → chunk-4UUF2EX3.js} +3 -3
- package/dist/{chunk-T6EQPZ6E.js → chunk-4YZCIK5E.js} +16 -14
- package/dist/chunk-4YZCIK5E.js.map +1 -0
- package/dist/{chunk-S6AH7WSF.js → chunk-5PGYCWA7.js} +5 -5
- package/dist/{chunk-7NSC4IVA.js → chunk-6RK2X7BR.js} +3 -3
- package/dist/{chunk-VWP5DEHU.js → chunk-6SX4MY5Q.js} +2 -2
- package/dist/{chunk-GUNIXT36.js → chunk-7BY7V4YX.js} +2 -2
- package/dist/{chunk-R7UHWPYI.js → chunk-7PJARJS4.js} +3 -3
- package/dist/{chunk-V3IT3XNG.js → chunk-7VSHDLJQ.js} +6 -6
- package/dist/{chunk-SFCJCMZQ.js → chunk-A7266TRJ.js} +2 -2
- package/dist/{chunk-TF3LMM5E.js → chunk-CSV34HIW.js} +3 -3
- package/dist/{chunk-FK7T55BA.js → chunk-CZUHNJHX.js} +19 -19
- package/dist/{chunk-NFQB6RJT.js → chunk-D4OCJSLV.js} +3 -3
- package/dist/{chunk-JKL53MOY.js → chunk-DLQQU4EG.js} +3 -3
- package/dist/{chunk-CME347TM.js → chunk-DNC4A7RS.js} +2 -2
- package/dist/{chunk-4THNU7OF.js → chunk-DUHMYBVE.js} +23 -23
- package/dist/{chunk-F2JRDPON.js → chunk-EGBDFJ36.js} +2 -2
- package/dist/{chunk-TFXYQNBH.js → chunk-FI3PIOOI.js} +9 -9
- package/dist/{chunk-CKSEJD7K.js → chunk-FKTD7IVS.js} +2 -2
- package/dist/{chunk-FONW766A.js → chunk-GXIY65UH.js} +2 -2
- package/dist/{chunk-2YREVL4T.js → chunk-HZC3OECF.js} +2 -2
- package/dist/{chunk-X5OIPQME.js → chunk-HZNYJN77.js} +6 -6
- package/dist/{chunk-LYPG7GOO.js → chunk-I2GIYTRU.js} +2 -2
- package/dist/{chunk-FR3FNPCJ.js → chunk-IT57AOMC.js} +4 -4
- package/dist/{chunk-CMRUIKVA.js → chunk-KCT36JAW.js} +25 -9
- package/dist/chunk-KCT36JAW.js.map +1 -0
- package/dist/{chunk-KJH6W2I7.js → chunk-L3CHRBHE.js} +4 -4
- package/dist/{chunk-PSH3VFZ7.js → chunk-L3LRWDJW.js} +7 -7
- package/dist/{chunk-JFPZEWY3.js → chunk-LIF6DO6U.js} +37 -13
- package/dist/chunk-LIF6DO6U.js.map +1 -0
- package/dist/{chunk-QZR3WT3L.js → chunk-LJJWFV2Q.js} +2 -2
- package/dist/chunk-LMXUWAXW.js +200 -0
- package/dist/chunk-LMXUWAXW.js.map +1 -0
- package/dist/{chunk-TLVLJSWS.js → chunk-LRDP2AC2.js} +2 -2
- package/dist/{chunk-QV7OEHFN.js → chunk-LTBKKSCX.js} +2 -2
- package/dist/{chunk-IGWVMA4Y.js → chunk-MRV7VSQK.js} +20 -10
- package/dist/chunk-MRV7VSQK.js.map +1 -0
- package/dist/{chunk-6P2NUVM5.js → chunk-NEJQ65NV.js} +2 -2
- package/dist/{chunk-7PKNOXYB.js → chunk-NTJ6TY44.js} +2 -2
- package/dist/{chunk-HYVBJLEK.js → chunk-O5AFC5KM.js} +2 -2
- package/dist/{chunk-P4BC54KI.js → chunk-P6WBAR27.js} +8 -32
- package/dist/chunk-P6WBAR27.js.map +1 -0
- package/dist/{chunk-U3PN77QT.js → chunk-PCXDUHQH.js} +11 -4
- package/dist/chunk-PCXDUHQH.js.map +1 -0
- package/dist/{chunk-N655QIA2.js → chunk-PEDYKCJI.js} +3 -3
- package/dist/{chunk-TVKWT2CY.js → chunk-Q57GOHLF.js} +11 -9
- package/dist/{chunk-TVKWT2CY.js.map → chunk-Q57GOHLF.js.map} +1 -1
- package/dist/{chunk-IFFW25NG.js → chunk-REVMY7RA.js} +36 -36
- package/dist/{chunk-4QFCAKNM.js → chunk-RUZM6Z52.js} +1 -1
- package/dist/{chunk-O4TTIMOP.js → chunk-SFR7J2G6.js} +2 -2
- package/dist/{chunk-ION47U3Z.js → chunk-SNW3YRO6.js} +2 -2
- package/dist/{chunk-XL24QQ63.js → chunk-TBUEE7XF.js} +2 -2
- package/dist/{chunk-66SLUXKM.js → chunk-TEHPIMXD.js} +2 -2
- package/dist/{chunk-TAPFJ3ET.js → chunk-U7XLOSG2.js} +3 -3
- package/dist/{chunk-T3HC7RJ7.js → chunk-USAUBGBC.js} +2 -2
- package/dist/{chunk-TAMY4NKO.js → chunk-USGKHF36.js} +2 -2
- package/dist/{chunk-XRPPHIRI.js → chunk-VUDALPI5.js} +2 -2
- package/dist/{chunk-EGGE52UU.js → chunk-VUFL7X3H.js} +13 -6
- package/dist/chunk-VUFL7X3H.js.map +1 -0
- package/dist/{chunk-HC55YCNK.js → chunk-VYIXCMUP.js} +4 -4
- package/dist/{chunk-HQ2VGCGP.js → chunk-XBGLEOID.js} +8 -8
- package/dist/{chunk-XX4HPFFM.js → chunk-XDL5CYZN.js} +4 -4
- package/dist/{chunk-THACANBI.js → chunk-YOKSRIFI.js} +3 -3
- package/dist/{chunk-BWJMBJCY.js → chunk-YTZA7U6Q.js} +6 -6
- package/dist/{chunk-4KM3V6VS.js → chunk-ZN7SMKAK.js} +2 -2
- package/dist/chunking.d.ts +3 -1
- package/dist/chunking.js +7 -3
- package/dist/{cli-DTOkYhKI.d.ts → cli-CQdYilcx.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +46 -46
- package/dist/compounding/engine.js +5 -5
- package/dist/connectors/codex-materialize-runner.js +6 -6
- package/dist/connectors/index.js +6 -6
- package/dist/contradiction/index.js +3 -3
- package/dist/{contradiction-scan-522AYLQC.js → contradiction-scan-VOTQRRSX.js} +4 -4
- package/dist/convergence-refresh.js +7 -7
- package/dist/corpus-watermark.js +8 -8
- package/dist/entity-retrieval.js +5 -5
- package/dist/explicit-capture.js +4 -4
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +24 -24
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/extraction.js +11 -11
- package/dist/{first-start-migration-HZ4VPRFQ.js → first-start-migration-4NETATFF.js} +4 -4
- package/dist/{graph-edge-decay-ZXGBXNPW.js → graph-edge-decay-A4SDFMMS.js} +4 -3
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +680 -680
- package/dist/index.js +60 -60
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +7 -7
- package/dist/namespaces/principal.js +3 -2
- package/dist/namespaces/search.js +2 -2
- package/dist/namespaces/storage.js +6 -6
- package/dist/offline-sync-impression-drain.js +6 -6
- package/dist/operator-doctor-corpus.js +9 -9
- package/dist/operator-toolkit.js +16 -16
- package/dist/orchestration/maintenance.js +14 -14
- package/dist/orchestrator.js +60 -60
- package/dist/{public-http-C01DLhAp.d.ts → public-http-Dw0bJhcC.d.ts} +1 -1
- package/dist/reconcile/manifest.js +3 -3
- package/dist/reconcile/plan.js +2 -2
- package/dist/routing/engine.js +2 -1
- package/dist/routing/store.js +3 -2
- package/dist/schemas.d.ts +138 -138
- package/dist/semantic-chunking.js +2 -2
- package/dist/semantic-consolidation.js +7 -7
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +10 -10
- package/dist/storage.js +4 -4
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +26 -26
- package/dist/tier-routing.js +3 -3
- package/dist/{tier-stats-7E6OBUSD.js → tier-stats-4B5IJ3NE.js} +4 -4
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/chunking.ts +119 -34
- package/src/namespaces/catalog.ts +9 -9
- package/src/namespaces/identity.ts +15 -6
- package/src/namespaces/principal.test.ts +10 -1
- package/src/namespaces/principal.ts +17 -9
- package/src/namespaces/scope-profiles.ts +35 -13
- package/src/namespaces/storage.ts +63 -13
- package/src/namespaces/unicode-routing.test.ts +71 -0
- package/src/routing/engine.test.ts +30 -0
- package/src/routing/engine.ts +12 -3
- package/src/scopes/scope-plan.ts +8 -2
- package/src/semantic-chunking.ts +10 -48
- package/src/storage-contract/namespace-routing.test.ts +18 -1
- package/dist/chunk-CMRUIKVA.js.map +0 -1
- package/dist/chunk-EGGE52UU.js.map +0 -1
- package/dist/chunk-IGWVMA4Y.js.map +0 -1
- package/dist/chunk-JFPZEWY3.js.map +0 -1
- package/dist/chunk-P4BC54KI.js.map +0 -1
- package/dist/chunk-T6EQPZ6E.js.map +0 -1
- package/dist/chunk-U3PN77QT.js.map +0 -1
- package/dist/chunk-UQ7RN5HK.js +0 -126
- package/dist/chunk-UQ7RN5HK.js.map +0 -1
- package/dist/{access-service-D9xXmKvd.d.ts → access-service-4DO8XMq6.d.ts} +64 -64
- /package/dist/{chunk-O54B5YNM.js.map → chunk-24PNX2GF.js.map} +0 -0
- /package/dist/{chunk-ONCWJBWV.js.map → chunk-3AJOJOFP.js.map} +0 -0
- /package/dist/{chunk-QSDPVZPD.js.map → chunk-4O6R3GPK.js.map} +0 -0
- /package/dist/{chunk-IXGQP4RZ.js.map → chunk-4SGDYYWV.js.map} +0 -0
- /package/dist/{chunk-UTDMY476.js.map → chunk-4UUF2EX3.js.map} +0 -0
- /package/dist/{chunk-S6AH7WSF.js.map → chunk-5PGYCWA7.js.map} +0 -0
- /package/dist/{chunk-7NSC4IVA.js.map → chunk-6RK2X7BR.js.map} +0 -0
- /package/dist/{chunk-VWP5DEHU.js.map → chunk-6SX4MY5Q.js.map} +0 -0
- /package/dist/{chunk-GUNIXT36.js.map → chunk-7BY7V4YX.js.map} +0 -0
- /package/dist/{chunk-R7UHWPYI.js.map → chunk-7PJARJS4.js.map} +0 -0
- /package/dist/{chunk-V3IT3XNG.js.map → chunk-7VSHDLJQ.js.map} +0 -0
- /package/dist/{chunk-SFCJCMZQ.js.map → chunk-A7266TRJ.js.map} +0 -0
- /package/dist/{chunk-TF3LMM5E.js.map → chunk-CSV34HIW.js.map} +0 -0
- /package/dist/{chunk-FK7T55BA.js.map → chunk-CZUHNJHX.js.map} +0 -0
- /package/dist/{chunk-NFQB6RJT.js.map → chunk-D4OCJSLV.js.map} +0 -0
- /package/dist/{chunk-JKL53MOY.js.map → chunk-DLQQU4EG.js.map} +0 -0
- /package/dist/{chunk-CME347TM.js.map → chunk-DNC4A7RS.js.map} +0 -0
- /package/dist/{chunk-4THNU7OF.js.map → chunk-DUHMYBVE.js.map} +0 -0
- /package/dist/{chunk-F2JRDPON.js.map → chunk-EGBDFJ36.js.map} +0 -0
- /package/dist/{chunk-TFXYQNBH.js.map → chunk-FI3PIOOI.js.map} +0 -0
- /package/dist/{chunk-CKSEJD7K.js.map → chunk-FKTD7IVS.js.map} +0 -0
- /package/dist/{chunk-FONW766A.js.map → chunk-GXIY65UH.js.map} +0 -0
- /package/dist/{chunk-2YREVL4T.js.map → chunk-HZC3OECF.js.map} +0 -0
- /package/dist/{chunk-X5OIPQME.js.map → chunk-HZNYJN77.js.map} +0 -0
- /package/dist/{chunk-LYPG7GOO.js.map → chunk-I2GIYTRU.js.map} +0 -0
- /package/dist/{chunk-FR3FNPCJ.js.map → chunk-IT57AOMC.js.map} +0 -0
- /package/dist/{chunk-KJH6W2I7.js.map → chunk-L3CHRBHE.js.map} +0 -0
- /package/dist/{chunk-PSH3VFZ7.js.map → chunk-L3LRWDJW.js.map} +0 -0
- /package/dist/{chunk-QZR3WT3L.js.map → chunk-LJJWFV2Q.js.map} +0 -0
- /package/dist/{chunk-TLVLJSWS.js.map → chunk-LRDP2AC2.js.map} +0 -0
- /package/dist/{chunk-QV7OEHFN.js.map → chunk-LTBKKSCX.js.map} +0 -0
- /package/dist/{chunk-6P2NUVM5.js.map → chunk-NEJQ65NV.js.map} +0 -0
- /package/dist/{chunk-7PKNOXYB.js.map → chunk-NTJ6TY44.js.map} +0 -0
- /package/dist/{chunk-HYVBJLEK.js.map → chunk-O5AFC5KM.js.map} +0 -0
- /package/dist/{chunk-N655QIA2.js.map → chunk-PEDYKCJI.js.map} +0 -0
- /package/dist/{chunk-IFFW25NG.js.map → chunk-REVMY7RA.js.map} +0 -0
- /package/dist/{chunk-4QFCAKNM.js.map → chunk-RUZM6Z52.js.map} +0 -0
- /package/dist/{chunk-O4TTIMOP.js.map → chunk-SFR7J2G6.js.map} +0 -0
- /package/dist/{chunk-ION47U3Z.js.map → chunk-SNW3YRO6.js.map} +0 -0
- /package/dist/{chunk-XL24QQ63.js.map → chunk-TBUEE7XF.js.map} +0 -0
- /package/dist/{chunk-66SLUXKM.js.map → chunk-TEHPIMXD.js.map} +0 -0
- /package/dist/{chunk-TAPFJ3ET.js.map → chunk-U7XLOSG2.js.map} +0 -0
- /package/dist/{chunk-T3HC7RJ7.js.map → chunk-USAUBGBC.js.map} +0 -0
- /package/dist/{chunk-TAMY4NKO.js.map → chunk-USGKHF36.js.map} +0 -0
- /package/dist/{chunk-XRPPHIRI.js.map → chunk-VUDALPI5.js.map} +0 -0
- /package/dist/{chunk-HC55YCNK.js.map → chunk-VYIXCMUP.js.map} +0 -0
- /package/dist/{chunk-HQ2VGCGP.js.map → chunk-XBGLEOID.js.map} +0 -0
- /package/dist/{chunk-XX4HPFFM.js.map → chunk-XDL5CYZN.js.map} +0 -0
- /package/dist/{chunk-THACANBI.js.map → chunk-YOKSRIFI.js.map} +0 -0
- /package/dist/{chunk-BWJMBJCY.js.map → chunk-YTZA7U6Q.js.map} +0 -0
- /package/dist/{chunk-4KM3V6VS.js.map → chunk-ZN7SMKAK.js.map} +0 -0
- /package/dist/{contradiction-scan-522AYLQC.js.map → contradiction-scan-VOTQRRSX.js.map} +0 -0
- /package/dist/{first-start-migration-HZ4VPRFQ.js.map → first-start-migration-4NETATFF.js.map} +0 -0
- /package/dist/{graph-edge-decay-ZXGBXNPW.js.map → graph-edge-decay-A4SDFMMS.js.map} +0 -0
- /package/dist/{tier-stats-7E6OBUSD.js.map → tier-stats-4B5IJ3NE.js.map} +0 -0
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./chunk-2LSZVONP.js";
|
|
14
14
|
import {
|
|
15
15
|
isSafeRouteNamespace
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PCXDUHQH.js";
|
|
17
17
|
|
|
18
18
|
// src/maintenance/graph-edge-decay.ts
|
|
19
19
|
import {
|
|
@@ -203,4 +203,4 @@ export {
|
|
|
203
203
|
discoverGraphNamespaceRoots,
|
|
204
204
|
runGraphEdgeDecayMaintenanceAcrossNamespaces
|
|
205
205
|
};
|
|
206
|
-
//# sourceMappingURL=chunk-
|
|
206
|
+
//# sourceMappingURL=chunk-LTBKKSCX.js.map
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isLikelyUnsafeRegex
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PCXDUHQH.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeNamespaceIdentity
|
|
6
|
+
} from "./chunk-VUFL7X3H.js";
|
|
4
7
|
import {
|
|
5
8
|
resolveNamespaceCapabilities
|
|
6
9
|
} from "./chunk-JUOZ62Y7.js";
|
|
@@ -49,28 +52,35 @@ function resolvePrincipal(sessionKey, config) {
|
|
|
49
52
|
function canReadNamespace(principal, namespace, config) {
|
|
50
53
|
if (!resolveNamespaceCapabilities(config).namespaces) return true;
|
|
51
54
|
if (!principal) return false;
|
|
52
|
-
const
|
|
53
|
-
|
|
55
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
56
|
+
const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);
|
|
57
|
+
if (!policy) {
|
|
58
|
+
return identity === normalizeNamespaceIdentity(config.defaultNamespace) || identity === normalizeNamespaceIdentity(config.sharedNamespace);
|
|
59
|
+
}
|
|
54
60
|
return policy.readPrincipals.includes(principal) || policy.readPrincipals.includes("*");
|
|
55
61
|
}
|
|
56
62
|
function canWriteNamespace(principal, namespace, config) {
|
|
57
63
|
if (!resolveNamespaceCapabilities(config).namespaces) return true;
|
|
58
64
|
if (!principal) return false;
|
|
59
|
-
const
|
|
60
|
-
|
|
65
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
66
|
+
const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);
|
|
67
|
+
if (!policy) return identity === normalizeNamespaceIdentity(config.defaultNamespace);
|
|
61
68
|
return policy.writePrincipals.includes(principal) || policy.writePrincipals.includes("*");
|
|
62
69
|
}
|
|
63
70
|
function isNamespacePolicyCovered(namespace, config) {
|
|
64
71
|
if (!resolveNamespaceCapabilities(config).namespaces) return true;
|
|
65
|
-
const
|
|
66
|
-
|
|
72
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
73
|
+
const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);
|
|
74
|
+
if (!policy) return identity === normalizeNamespaceIdentity(config.defaultNamespace);
|
|
67
75
|
return policy.writePrincipals.some((w) => w.trim().length > 0);
|
|
68
76
|
}
|
|
69
77
|
function defaultNamespaceForPrincipal(principal, config) {
|
|
70
78
|
if (!resolveNamespaceCapabilities(config).namespaces) return config.defaultNamespace;
|
|
71
79
|
if (!principal) return config.defaultNamespace;
|
|
72
|
-
const
|
|
73
|
-
|
|
80
|
+
const policy = config.namespacePolicies.find(
|
|
81
|
+
(p) => normalizeNamespaceIdentity(p.name) === normalizeNamespaceIdentity(principal)
|
|
82
|
+
);
|
|
83
|
+
return policy ? normalizeNamespaceIdentity(policy.name) : config.defaultNamespace;
|
|
74
84
|
}
|
|
75
85
|
function recallNamespacesForPrincipal(principal, config) {
|
|
76
86
|
const out = [];
|
|
@@ -111,4 +121,4 @@ export {
|
|
|
111
121
|
recallNamespacesForPrincipal,
|
|
112
122
|
citationAuthorizedNamespaces
|
|
113
123
|
};
|
|
114
|
-
//# sourceMappingURL=chunk-
|
|
124
|
+
//# sourceMappingURL=chunk-MRV7VSQK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/namespaces/principal.ts"],"sourcesContent":["import { resolveNamespaceCapabilities } from \"../capabilities.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { isLikelyUnsafeRegex } from \"../routing/engine.js\";\nimport { normalizeNamespaceIdentity } from \"./identity.js\";\nconst MAX_REGEX_SESSION_KEY_LENGTH = 512;\n\nfunction compileSafePrincipalRegex(pattern: string): RegExp | null {\n if (isLikelyUnsafeRegex(pattern)) return null;\n try {\n return new RegExp(pattern);\n } catch {\n return null;\n }\n}\n\nexport function resolvePrincipal(sessionKey: string | undefined, config: PluginConfig): string | undefined {\n if (!resolveNamespaceCapabilities(config).namespaces) return \"default\";\n const sk = typeof sessionKey === \"string\" && sessionKey.trim().length > 0\n ? sessionKey\n : \"\";\n const mode = config.principalFromSessionKeyMode;\n const rules = config.principalFromSessionKeyRules ?? [];\n\n if (!sk) return undefined;\n\n if (mode === \"prefix\") {\n for (const r of rules) {\n if (sk.startsWith(r.match)) return r.principal;\n }\n } else if (mode === \"map\") {\n for (const r of rules) {\n if (sk === r.match) return r.principal;\n }\n } else if (mode === \"regex\") {\n if (sk.length <= MAX_REGEX_SESSION_KEY_LENGTH) {\n for (const r of rules) {\n const re = compileSafePrincipalRegex(r.match);\n if (re?.test(sk)) {\n return r.principal;\n }\n }\n }\n }\n\n // Fallback heuristic: \"agent:<agentId>:<channelType>:...\"\n const parts = sk.split(\":\");\n if (parts.length >= 2 && parts[0] === \"agent\") {\n const agentId = parts[1];\n if (agentId && agentId.length > 0) return agentId;\n }\n return \"default\";\n}\n\nexport function canReadNamespace(principal: string | undefined, namespace: string, config: PluginConfig): boolean {\n if (!resolveNamespaceCapabilities(config).namespaces) return true;\n if (!principal) return false;\n const identity = normalizeNamespaceIdentity(namespace);\n const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);\n if (!policy) {\n return identity === normalizeNamespaceIdentity(config.defaultNamespace) ||\n identity === normalizeNamespaceIdentity(config.sharedNamespace);\n }\n return policy.readPrincipals.includes(principal) || policy.readPrincipals.includes(\"*\");\n}\n\nexport function canWriteNamespace(principal: string | undefined, namespace: string, config: PluginConfig): boolean {\n if (!resolveNamespaceCapabilities(config).namespaces) return true;\n if (!principal) return false;\n const identity = normalizeNamespaceIdentity(namespace);\n const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);\n if (!policy) return identity === normalizeNamespaceIdentity(config.defaultNamespace);\n return policy.writePrincipals.includes(principal) || policy.writePrincipals.includes(\"*\");\n}\n\n/**\n * Config-time namespace-policy lint (issue #1888 improvement 3). Principal-\n * agnostic mirror of {@link canWriteNamespace}: true when the namespace is\n * writable by AT LEAST ONE principal. When namespace enforcement is off every\n * write is allowed. Otherwise an explicit `namespacePolicies` entry takes\n * precedence (exactly as `canWriteNamespace`): with a matching policy the\n * namespace is covered only if that policy grants at least one non-blank\n * writer — even when the name equals `defaultNamespace`; without a policy it\n * is covered only if it IS the `defaultNamespace`. The `sharedNamespace` is\n * NOT implicitly writable (matching\n * `canWriteNamespace`), and a policy entry with no `writePrincipals` grants\n * nobody. A configured namespace that is NOT covered is writable for no one,\n * so every write to it is rejected by the ACL and dead-lettered — `remnic\n * doctor` warns on that before a session silently loses memory.\n */\nexport function isNamespacePolicyCovered(namespace: string, config: PluginConfig): boolean {\n if (!resolveNamespaceCapabilities(config).namespaces) return true;\n // An explicit policy wins over the default fallback (exactly as\n // canWriteNamespace), so a policy naming the default with no writers is NOT\n // covered. Only fall back to defaultNamespace when no policy matches.\n const identity = normalizeNamespaceIdentity(namespace);\n const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);\n if (!policy) return identity === normalizeNamespaceIdentity(config.defaultNamespace);\n return policy.writePrincipals.some((w) => w.trim().length > 0);\n}\n\n/**\n * Default \"self\" namespace for a principal.\n *\n * Heuristic:\n * - If there's a namespace policy with the same name as the principal, use it.\n * - Otherwise use config.defaultNamespace.\n */\nexport function defaultNamespaceForPrincipal(principal: string | undefined, config: PluginConfig): string {\n if (!resolveNamespaceCapabilities(config).namespaces) return config.defaultNamespace;\n if (!principal) return config.defaultNamespace;\n const policy = config.namespacePolicies.find(\n (p) => normalizeNamespaceIdentity(p.name) === normalizeNamespaceIdentity(principal),\n );\n return policy ? normalizeNamespaceIdentity(policy.name) : config.defaultNamespace;\n}\n\nexport function recallNamespacesForPrincipal(principal: string | undefined, config: PluginConfig): string[] {\n const out: string[] = [];\n if (!resolveNamespaceCapabilities(config).namespaces) return [config.defaultNamespace];\n if (!principal) return out;\n\n const selfNs = defaultNamespaceForPrincipal(principal, config);\n if (config.defaultRecallNamespaces.includes(\"self\") && canReadNamespace(principal, selfNs, config)) {\n out.push(selfNs);\n }\n if (config.defaultRecallNamespaces.includes(\"shared\") && canReadNamespace(principal, config.sharedNamespace, config)) {\n out.push(config.sharedNamespace);\n }\n\n for (const p of config.namespacePolicies) {\n if (p.includeInRecallByDefault && canReadNamespace(principal, p.name, config)) {\n if (!out.includes(p.name)) out.push(p.name);\n }\n }\n\n return out;\n}\n\n/**\n * Namespaces a principal may attribute a citation to: the default-recall\n * namespaces, every readable policy namespace, and the readable built-in\n * default/shared namespaces — which `defaultRecallNamespaces` can omit even\n * though `canReadNamespace` grants access (#2020).\n */\nexport function citationAuthorizedNamespaces(\n principal: string | undefined,\n config: PluginConfig,\n): string[] {\n return Array.from(\n new Set([\n ...recallNamespacesForPrincipal(principal, config),\n ...config.namespacePolicies\n .filter((p) => canReadNamespace(principal, p.name, config))\n .map((p) => p.name),\n ...[config.defaultNamespace, config.sharedNamespace].filter((ns) =>\n canReadNamespace(principal, ns, config),\n ),\n ]),\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./chunk-7ZNJN5WZ.js";
|
|
16
16
|
import {
|
|
17
17
|
compareEntityTimestamps
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-YTZA7U6Q.js";
|
|
19
19
|
import {
|
|
20
20
|
normalizeEntityName
|
|
21
21
|
} from "./chunk-YXZG5TVN.js";
|
|
@@ -889,4 +889,4 @@ export {
|
|
|
889
889
|
entityIndexVersion,
|
|
890
890
|
entityRecentTranscriptLookbackHours
|
|
891
891
|
};
|
|
892
|
-
//# sourceMappingURL=chunk-
|
|
892
|
+
//# sourceMappingURL=chunk-NEJQ65NV.js.map
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
headerConnector,
|
|
7
7
|
renderExtractionConversation,
|
|
8
8
|
resolveSourceConnector
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-A7266TRJ.js";
|
|
10
10
|
import {
|
|
11
11
|
ConsolidationResultSchema,
|
|
12
12
|
DaySummaryResultSchema,
|
|
@@ -2442,4 +2442,4 @@ export {
|
|
|
2442
2442
|
shouldEnableLocalExtractionThinking,
|
|
2443
2443
|
ExtractionEngine
|
|
2444
2444
|
};
|
|
2445
|
-
//# sourceMappingURL=chunk-
|
|
2445
|
+
//# sourceMappingURL=chunk-NTJ6TY44.js.map
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./chunk-UYCIS6V5.js";
|
|
8
8
|
import {
|
|
9
9
|
StorageManager
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YTZA7U6Q.js";
|
|
11
11
|
import {
|
|
12
12
|
parseContinuityImprovementLoops
|
|
13
13
|
} from "./chunk-LDXUBPMO.js";
|
|
@@ -1283,4 +1283,4 @@ export {
|
|
|
1283
1283
|
defaultTierMigrationCycleBudget,
|
|
1284
1284
|
CompoundingEngine
|
|
1285
1285
|
};
|
|
1286
|
-
//# sourceMappingURL=chunk-
|
|
1286
|
+
//# sourceMappingURL=chunk-O5AFC5KM.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
chunkContent
|
|
3
|
-
|
|
2
|
+
chunkContent,
|
|
3
|
+
joinSentences,
|
|
4
|
+
splitSentences
|
|
5
|
+
} from "./chunk-LMXUWAXW.js";
|
|
4
6
|
|
|
5
7
|
// src/semantic-chunking.ts
|
|
6
8
|
var DEFAULT_SEMANTIC_CHUNKING_CONFIG = {
|
|
@@ -72,32 +74,6 @@ function findLocalMinima(series, threshold) {
|
|
|
72
74
|
}
|
|
73
75
|
return minima;
|
|
74
76
|
}
|
|
75
|
-
function splitSentences(text) {
|
|
76
|
-
const sentences = [];
|
|
77
|
-
let start = 0;
|
|
78
|
-
for (let i = 0; i < text.length; i++) {
|
|
79
|
-
const ch = text[i];
|
|
80
|
-
if (ch !== "." && ch !== "!" && ch !== "?") continue;
|
|
81
|
-
let end = i;
|
|
82
|
-
while (end + 1 < text.length) {
|
|
83
|
-
const n = text[end + 1];
|
|
84
|
-
if (n !== "." && n !== "!" && n !== "?") break;
|
|
85
|
-
end++;
|
|
86
|
-
}
|
|
87
|
-
const after = text[end + 1];
|
|
88
|
-
if (after === void 0 || /\s/.test(after)) {
|
|
89
|
-
const sentence = text.slice(start, end + 1).trim();
|
|
90
|
-
if (sentence.length > 0) sentences.push(sentence);
|
|
91
|
-
start = end + 1;
|
|
92
|
-
}
|
|
93
|
-
i = end;
|
|
94
|
-
}
|
|
95
|
-
if (start < text.length) {
|
|
96
|
-
const remaining = text.slice(start).trim();
|
|
97
|
-
if (remaining.length > 0) sentences.push(remaining);
|
|
98
|
-
}
|
|
99
|
-
return sentences;
|
|
100
|
-
}
|
|
101
77
|
function estimateTokens(text) {
|
|
102
78
|
return Math.ceil(text.length / 4);
|
|
103
79
|
}
|
|
@@ -145,7 +121,7 @@ function mergeShortSegments(segments, minTokens) {
|
|
|
145
121
|
let buffer = [];
|
|
146
122
|
for (let i = 0; i < segments.length; i++) {
|
|
147
123
|
buffer = [...buffer, ...segments[i]];
|
|
148
|
-
const tokenCount = estimateTokens(buffer
|
|
124
|
+
const tokenCount = estimateTokens(joinSentences(buffer));
|
|
149
125
|
if (tokenCount >= minTokens || i === segments.length - 1) {
|
|
150
126
|
merged.push(buffer);
|
|
151
127
|
buffer = [];
|
|
@@ -161,7 +137,7 @@ function mergeShortSegments(segments, minTokens) {
|
|
|
161
137
|
return merged;
|
|
162
138
|
}
|
|
163
139
|
function splitLongSegment(segment, maxTokens, targetTokens) {
|
|
164
|
-
const text = segment
|
|
140
|
+
const text = joinSentences(segment);
|
|
165
141
|
const cappedTarget = Math.min(targetTokens, maxTokens);
|
|
166
142
|
const result = chunkContent(text, {
|
|
167
143
|
targetTokens: cappedTarget,
|
|
@@ -284,7 +260,7 @@ async function semanticChunkContent(content, embedFn, config) {
|
|
|
284
260
|
let sentenceOffset = 0;
|
|
285
261
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
286
262
|
const segment = segments[segIdx];
|
|
287
|
-
const segText = segment
|
|
263
|
+
const segText = joinSentences(segment);
|
|
288
264
|
const segTokens = estimateTokens(segText);
|
|
289
265
|
if (segTokens > cfg.maxTokens) {
|
|
290
266
|
const subChunks = splitLongSegment(segment, cfg.maxTokens, cfg.targetTokens);
|
|
@@ -346,4 +322,4 @@ export {
|
|
|
346
322
|
findLocalMinima,
|
|
347
323
|
semanticChunkContent
|
|
348
324
|
};
|
|
349
|
-
//# sourceMappingURL=chunk-
|
|
325
|
+
//# sourceMappingURL=chunk-P6WBAR27.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/semantic-chunking.ts"],"sourcesContent":["/**\n * Semantic Chunking with Smoothing-Based Topic Boundaries (Issue #368)\n *\n * An optional alternative to the recursive chunker in chunking.ts.\n * Uses sentence embeddings + cosine similarity + smoothing to detect\n * natural topic boundaries, producing more coherent chunks.\n */\n\nimport {\n chunkContent,\n joinSentences,\n splitSentences,\n type Chunk,\n type ChunkResult,\n} from \"./chunking.js\";\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface SemanticChunkingConfig {\n /** Target tokens per chunk. Default: 200. */\n targetTokens: number;\n /** Minimum tokens for a segment before merging with neighbor. Default: 100. */\n minTokens: number;\n /** Maximum tokens for a segment before recursive splitting. Default: 400. */\n maxTokens: number;\n /** Window size for the moving-average smoothing filter. Default: 3. */\n smoothingWindowSize: number;\n /** How many standard deviations below the mean constitutes a boundary. Default: 1.0. */\n boundaryThresholdStdDevs: number;\n /** Batch size for embedding requests. Default: 32. */\n embeddingBatchSize: number;\n /** Fall back to recursive chunking when embeddings are unavailable. Default: true. */\n fallbackToRecursive: boolean;\n}\n\nexport const DEFAULT_SEMANTIC_CHUNKING_CONFIG: SemanticChunkingConfig = {\n targetTokens: 200,\n minTokens: 100,\n maxTokens: 400,\n smoothingWindowSize: 3,\n boundaryThresholdStdDevs: 1.0,\n embeddingBatchSize: 32,\n fallbackToRecursive: true,\n};\n\n// ---------------------------------------------------------------------------\n// Result types\n// ---------------------------------------------------------------------------\n\nexport interface SemanticChunk extends Chunk {\n /** Optional topic hint derived from position. */\n topicLabel?: string;\n /** Cosine similarity score at the trailing boundary of this chunk. */\n boundaryScore: number;\n}\n\nexport interface SemanticChunkResult {\n /** Whether content was split into multiple chunks. */\n chunked: boolean;\n /** The chunks produced. */\n chunks: SemanticChunk[];\n /** Sentence indices where topic splits occurred. */\n boundaries: number[];\n /** Which algorithm produced the result. */\n method: \"semantic\" | \"recursive-fallback\";\n}\n\n// ---------------------------------------------------------------------------\n// Embedding function signature\n// ---------------------------------------------------------------------------\n\n/** Caller-provided function that embeds an array of texts, returning vectors. */\nexport type EmbedFn = (texts: string[]) => Promise<number[][]>;\n\n// ---------------------------------------------------------------------------\n// Math utilities (exported for testing)\n// ---------------------------------------------------------------------------\n\n/**\n * Cosine similarity between two vectors.\n * Returns a value in [-1, 1]. Identical direction = 1, orthogonal = 0.\n *\n * NOTE: This duplicates cosineSimilarity in recall-mmr.ts and embedding-fallback.ts.\n * Consider extracting to a shared math utility in a future refactor.\n */\nexport function cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length) {\n throw new Error(\n `cosineSimilarity: vector length mismatch (${a.length} vs ${b.length})`,\n );\n }\n if (a.length === 0) return 0;\n\n let dot = 0;\n let magA = 0;\n let magB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n magA += a[i] * a[i];\n magB += b[i] * b[i];\n }\n\n const denom = Math.sqrt(magA) * Math.sqrt(magB);\n if (denom === 0) return 0;\n return dot / denom;\n}\n\n/**\n * Arithmetic mean of a numeric series.\n */\nexport function mean(series: number[]): number {\n if (series.length === 0) return 0;\n let sum = 0;\n for (const v of series) sum += v;\n return sum / series.length;\n}\n\n/**\n * Population standard deviation of a numeric series.\n */\nexport function stddev(series: number[]): number {\n if (series.length === 0) return 0;\n const m = mean(series);\n let sumSq = 0;\n for (const v of series) {\n const d = v - m;\n sumSq += d * d;\n }\n return Math.sqrt(sumSq / series.length);\n}\n\n/**\n * Simple moving average over a 1D series.\n * The window is centered: for window size W, each output[i] averages\n * series[i - floor(W/2) .. i + floor(W/2)], clamped to bounds.\n *\n * Even window sizes are rounded up to the next odd value so the window\n * is symmetric around the center point (Finding 4, PR #420).\n */\nexport function movingAverage(series: number[], windowSize: number): number[] {\n if (series.length === 0) return [];\n if (windowSize < 1) windowSize = 1;\n // Round even values up to the next odd so the window is symmetric.\n if (windowSize % 2 === 0) windowSize = windowSize + 1;\n\n const halfW = Math.floor(windowSize / 2);\n const result: number[] = new Array(series.length);\n\n for (let i = 0; i < series.length; i++) {\n const lo = Math.max(0, i - halfW);\n const hi = Math.min(series.length - 1, i + halfW);\n let sum = 0;\n for (let j = lo; j <= hi; j++) sum += series[j];\n result[i] = sum / (hi - lo + 1);\n }\n return result;\n}\n\n/**\n * Find indices in the series that are local minima AND below the threshold.\n * A local minimum is a point lower than both its immediate neighbors\n * (or lower-or-equal at series boundaries).\n */\nexport function findLocalMinima(\n series: number[],\n threshold: number,\n): number[] {\n if (series.length <= 2) return [];\n\n const minima: number[] = [];\n for (let i = 1; i < series.length - 1; i++) {\n if (\n series[i] < series[i - 1] &&\n series[i] < series[i + 1] &&\n series[i] < threshold\n ) {\n minima.push(i);\n }\n }\n return minima;\n}\n\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Token estimation\n// ---------------------------------------------------------------------------\n\n/** Rough token estimate: ~4 chars per token for English. */\nfunction estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\n// ---------------------------------------------------------------------------\n// Core semantic chunking\n// ---------------------------------------------------------------------------\n\n/**\n * Batch-embed sentences using the provided embed function.\n * Respects the configured batch size.\n */\nasync function batchEmbed(\n sentences: string[],\n embedFn: EmbedFn,\n batchSize: number,\n): Promise<number[][]> {\n const allEmbeddings: number[][] = [];\n\n for (let i = 0; i < sentences.length; i += batchSize) {\n const batch = sentences.slice(i, i + batchSize);\n const batchResult = await embedFn(batch);\n for (const vec of batchResult) {\n allEmbeddings.push(vec);\n }\n }\n\n return allEmbeddings;\n}\n\nfunction findEmbeddingDimensionMismatch(\n embeddings: number[][],\n): { expected: number; actual: number; index: number } | null {\n if (embeddings.length <= 1) return null;\n const expected = embeddings[0].length;\n for (let i = 1; i < embeddings.length; i++) {\n const actual = embeddings[i].length;\n if (actual !== expected) {\n return { expected, actual, index: i };\n }\n }\n return null;\n}\n\n/**\n * Build segments from boundary indices.\n * boundaries are sentence indices at which splits occur (i.e., the split\n * happens AFTER the boundary index sentence).\n */\nfunction buildSegments(\n sentences: string[],\n boundaries: number[],\n): string[][] {\n const sorted = [...boundaries].sort((a, b) => a - b);\n const segments: string[][] = [];\n let start = 0;\n\n for (const b of sorted) {\n // Split after sentence at index b: segment is [start .. b]\n const splitPoint = b + 1;\n if (splitPoint > start && splitPoint <= sentences.length) {\n segments.push(sentences.slice(start, splitPoint));\n start = splitPoint;\n }\n }\n\n // Remaining sentences\n if (start < sentences.length) {\n segments.push(sentences.slice(start));\n }\n\n return segments;\n}\n\n/**\n * Merge short segments (below minTokens) with their neighbor.\n * Prefers merging forward; falls back to merging backward.\n */\nfunction mergeShortSegments(\n segments: string[][],\n minTokens: number,\n): string[][] {\n if (segments.length <= 1) return segments;\n\n const merged: string[][] = [];\n let buffer: string[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n buffer = [...buffer, ...segments[i]];\n const tokenCount = estimateTokens(joinSentences(buffer));\n\n if (tokenCount >= minTokens || i === segments.length - 1) {\n merged.push(buffer);\n buffer = [];\n }\n }\n\n // If the last merge left a dangling buffer, attach it to the last segment\n if (buffer.length > 0) {\n if (merged.length > 0) {\n merged[merged.length - 1] = [...merged[merged.length - 1], ...buffer];\n } else {\n merged.push(buffer);\n }\n }\n\n return merged;\n}\n\n/**\n * Split an oversized segment using recursive chunking.\n */\nfunction splitLongSegment(\n segment: string[],\n maxTokens: number,\n targetTokens: number,\n): SemanticChunk[] {\n const text = joinSentences(segment);\n // Cap targetTokens to maxTokens so recursive splitting never produces\n // segments larger than the configured maximum (Finding 2, PR #420).\n const cappedTarget = Math.min(targetTokens, maxTokens);\n const result: ChunkResult = chunkContent(text, {\n targetTokens: cappedTarget,\n minTokens: Math.min(cappedTarget, maxTokens),\n overlapSentences: 0,\n });\n\n return result.chunks.map((c) => ({\n content: c.content,\n index: c.index,\n tokenCount: c.tokenCount,\n boundaryScore: 0,\n }));\n}\n\n/**\n * Semantic chunking with smoothing-based topic boundary detection.\n *\n * @param content - Full text to chunk.\n * @param embedFn - Async function that embeds an array of texts.\n * @param config - Optional partial config overrides.\n * @returns SemanticChunkResult\n */\nexport async function semanticChunkContent(\n content: string,\n embedFn: EmbedFn,\n config?: Partial<SemanticChunkingConfig>,\n): Promise<SemanticChunkResult> {\n const cfg: SemanticChunkingConfig = {\n ...DEFAULT_SEMANTIC_CHUNKING_CONFIG,\n ...config,\n };\n\n // Guard against non-positive batch size which would cause an infinite loop\n const batchSize = Math.max(1, cfg.embeddingBatchSize);\n\n // --- Empty / trivially short input ---\n if (!content || content.trim().length === 0) {\n return {\n chunked: false,\n chunks: [],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n const sentences = splitSentences(content);\n\n if (sentences.length <= 1) {\n const tokenCount = estimateTokens(content);\n return {\n chunked: false,\n chunks: [\n {\n content: content.trim(),\n index: 0,\n tokenCount,\n boundaryScore: 1,\n },\n ],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n // If total tokens is short enough, return as single chunk\n const totalTokens = estimateTokens(content);\n if (totalTokens <= cfg.minTokens) {\n return {\n chunked: false,\n chunks: [\n {\n content: content.trim(),\n index: 0,\n tokenCount: totalTokens,\n boundaryScore: 1,\n },\n ],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n // --- Attempt embedding ---\n let embeddings: number[][];\n try {\n embeddings = await batchEmbed(sentences, embedFn, batchSize);\n } catch {\n // Embedding failed — fall back if configured\n if (cfg.fallbackToRecursive) {\n return buildRecursiveFallback(content, cfg);\n }\n throw new Error(\n \"Semantic chunking failed: embedding function threw and fallbackToRecursive is disabled\",\n );\n }\n\n if (embeddings.length !== sentences.length) {\n if (cfg.fallbackToRecursive) {\n return buildRecursiveFallback(content, cfg);\n }\n throw new Error(\n `Semantic chunking failed: expected ${sentences.length} embeddings but received ${embeddings.length}`,\n );\n }\n\n const dimensionMismatch = findEmbeddingDimensionMismatch(embeddings);\n if (dimensionMismatch) {\n if (cfg.fallbackToRecursive) {\n return buildRecursiveFallback(content, cfg);\n }\n throw new Error(\n `Semantic chunking failed: embedding vectors have mismatched dimensions ` +\n `(${dimensionMismatch.expected} vs ${dimensionMismatch.actual} at index ${dimensionMismatch.index})`,\n );\n }\n\n // --- Compute pairwise cosine similarity ---\n const similarities: number[] = [];\n for (let i = 0; i < sentences.length - 1; i++) {\n similarities.push(cosineSimilarity(embeddings[i], embeddings[i + 1]));\n }\n\n // If only one pair (2 sentences), nothing to smooth or split meaningfully.\n // However, if the combined content exceeds maxTokens, apply recursive splitting.\n if (similarities.length <= 1) {\n if (totalTokens > cfg.maxTokens) {\n return buildRecursiveFallback(content, cfg);\n }\n return {\n chunked: false,\n chunks: [\n {\n content: content.trim(),\n index: 0,\n tokenCount: totalTokens,\n boundaryScore: similarities.length === 1 ? similarities[0] : 1,\n },\n ],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n // --- Smooth the similarity series ---\n const smoothed = movingAverage(similarities, cfg.smoothingWindowSize);\n\n // --- Detect boundaries: local minima below (mean - k * stddev) ---\n const m = mean(smoothed);\n const s = stddev(smoothed);\n const threshold = m - cfg.boundaryThresholdStdDevs * s;\n const rawBoundaries = findLocalMinima(smoothed, threshold);\n\n // --- Build segments, merge short, split long ---\n let segments = buildSegments(sentences, rawBoundaries);\n segments = mergeShortSegments(segments, cfg.minTokens);\n\n // --- Convert segments to chunks, splitting oversized ones ---\n const chunks: SemanticChunk[] = [];\n const finalBoundaries: number[] = [];\n let sentenceOffset = 0;\n\n for (let segIdx = 0; segIdx < segments.length; segIdx++) {\n const segment = segments[segIdx];\n const segText = joinSentences(segment);\n const segTokens = estimateTokens(segText);\n\n if (segTokens > cfg.maxTokens) {\n // Recursive split for oversized segment\n const subChunks = splitLongSegment(segment, cfg.maxTokens, cfg.targetTokens);\n for (const sc of subChunks) {\n chunks.push({\n ...sc,\n index: chunks.length,\n });\n }\n } else {\n // Compute boundary score: the similarity at the trailing edge\n const trailingSentenceIdx = sentenceOffset + segment.length - 1;\n let bScore = 1;\n if (\n trailingSentenceIdx < similarities.length &&\n segIdx < segments.length - 1\n ) {\n bScore = smoothed[trailingSentenceIdx] ?? similarities[trailingSentenceIdx] ?? 1;\n }\n\n chunks.push({\n content: segText,\n index: chunks.length,\n tokenCount: segTokens,\n boundaryScore: bScore,\n });\n }\n\n // Record boundaries (all but the last segment produce a boundary)\n if (segIdx < segments.length - 1) {\n finalBoundaries.push(sentenceOffset + segment.length - 1);\n }\n sentenceOffset += segment.length;\n }\n\n return {\n chunked: chunks.length > 1,\n chunks,\n boundaries: finalBoundaries,\n method: \"semantic\",\n };\n}\n\n// ---------------------------------------------------------------------------\n// Recursive fallback helper\n// ---------------------------------------------------------------------------\n\nfunction buildRecursiveFallback(\n content: string,\n cfg: SemanticChunkingConfig,\n): SemanticChunkResult {\n // Cap targetTokens to maxTokens so the recursive fallback path honours the\n // same constraint as splitLongSegment (PR #439 post-merge cursor[bot] finding).\n const cappedTarget = Math.min(cfg.targetTokens, cfg.maxTokens);\n const result: ChunkResult = chunkContent(content, {\n targetTokens: cappedTarget,\n minTokens: Math.min(cfg.minTokens, cappedTarget),\n overlapSentences: 0,\n });\n\n return {\n chunked: result.chunked,\n chunks: result.chunks.map((c) => ({\n ...c,\n boundaryScore: 0,\n })),\n boundaries: [],\n method: \"recursive-fallback\",\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
namespaceIdentityToken
|
|
3
|
+
} from "./chunk-VUFL7X3H.js";
|
|
4
|
+
|
|
1
5
|
// src/routing/engine.ts
|
|
2
6
|
var DEFAULT_CATEGORIES = [
|
|
3
7
|
"fact",
|
|
@@ -15,8 +19,9 @@ var DEFAULT_CATEGORIES = [
|
|
|
15
19
|
"reasoning_trace"
|
|
16
20
|
];
|
|
17
21
|
function normalizeNamespace(namespace) {
|
|
18
|
-
return namespace.
|
|
22
|
+
return namespace.normalize("NFC");
|
|
19
23
|
}
|
|
24
|
+
var SAFE_ROUTE_NAMESPACE_CHARS = /^[\p{L}\p{M}\p{N}._-]+$/u;
|
|
20
25
|
function isLikelyUnsafeRegex(pattern) {
|
|
21
26
|
const value = pattern.trim();
|
|
22
27
|
if (value.length === 0) return true;
|
|
@@ -32,17 +37,19 @@ function isLikelyUnsafeRegex(pattern) {
|
|
|
32
37
|
function isSafeRouteNamespace(namespace) {
|
|
33
38
|
const value = normalizeNamespace(namespace);
|
|
34
39
|
if (value.length === 0) return false;
|
|
40
|
+
if (Array.from(value).length > 64) return false;
|
|
41
|
+
if (namespaceIdentityToken(value).length > 255) return false;
|
|
35
42
|
if (value === ".") return false;
|
|
36
43
|
if (value.includes("/") || value.includes("\\")) return false;
|
|
37
44
|
if (value.includes("..")) return false;
|
|
38
|
-
return
|
|
45
|
+
return SAFE_ROUTE_NAMESPACE_CHARS.test(value);
|
|
39
46
|
}
|
|
40
47
|
function validateRouteTarget(target, options) {
|
|
41
48
|
if (!target || typeof target !== "object") {
|
|
42
49
|
return { ok: false, error: "target must be an object" };
|
|
43
50
|
}
|
|
44
51
|
const allowedCategories = new Set(options?.allowedCategories ?? DEFAULT_CATEGORIES);
|
|
45
|
-
const allowedNamespaces = options?.allowedNamespaces ? new Set(options.allowedNamespaces.map((v) => v.trim()).filter((v) => v.length > 0)) : null;
|
|
52
|
+
const allowedNamespaces = options?.allowedNamespaces ? new Set(options.allowedNamespaces.map((v) => normalizeNamespace(v.trim())).filter((v) => v.length > 0)) : null;
|
|
46
53
|
const normalized = {};
|
|
47
54
|
if (typeof target.category === "string") {
|
|
48
55
|
if (!allowedCategories.has(target.category)) {
|
|
@@ -110,4 +117,4 @@ export {
|
|
|
110
117
|
doesRuleMatch,
|
|
111
118
|
selectRouteRule
|
|
112
119
|
};
|
|
113
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-PCXDUHQH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/routing/engine.ts"],"sourcesContent":["import { namespaceIdentityToken } from \"../namespaces/identity.js\";\nimport type { MemoryCategory } from \"../types.js\";\n\nexport type RoutePatternType = \"regex\" | \"keyword\";\n\nexport interface RouteTarget {\n category?: MemoryCategory;\n namespace?: string;\n}\n\nexport interface RouteRule {\n id: string;\n patternType: RoutePatternType;\n pattern: string;\n priority: number;\n target: RouteTarget;\n enabled?: boolean;\n}\n\nexport interface RoutingEngineOptions {\n allowedNamespaces?: string[];\n allowedCategories?: MemoryCategory[];\n}\n\nexport interface RouteSelection {\n rule: RouteRule;\n target: RouteTarget;\n}\n\nconst DEFAULT_CATEGORIES: readonly MemoryCategory[] = [\n \"fact\",\n \"preference\",\n \"correction\",\n \"entity\",\n \"decision\",\n \"relationship\",\n \"principle\",\n \"commitment\",\n \"moment\",\n \"skill\",\n \"rule\",\n \"procedure\",\n \"reasoning_trace\",\n] as const;\n\nfunction normalizeNamespace(namespace: string): string {\n return namespace.normalize(\"NFC\");\n}\n\nconst SAFE_ROUTE_NAMESPACE_CHARS = /^[\\p{L}\\p{M}\\p{N}._-]+$/u;\n\nexport function isLikelyUnsafeRegex(pattern: string): boolean {\n const value = pattern.trim();\n if (value.length === 0) return true;\n if (value.length > 120) return true;\n if (/\\\\[1-9]/.test(value)) return true; // backreferences\n if (/\\(\\?<?[=!]/.test(value)) return true; // lookaround assertions\n if (/\\((?:[^()\\\\]|\\\\.)*[+*](?:[^()\\\\]|\\\\.)*\\)[+*{]/.test(value)) return true; // nested quantifiers\n // Conservative fail-closed guardrail: grouped/alternation regexes are user-configurable and can be expensive.\n if (/(^|[^\\\\])[()|]/.test(value)) return true;\n // Multiple quantifiers in one user pattern are high risk for catastrophic backtracking on non-matches.\n const quantifierCount =\n (value.match(/(^|[^\\\\])[*+?]/g)?.length ?? 0) +\n (value.match(/(^|[^\\\\])\\{/g)?.length ?? 0);\n if (quantifierCount > 1) return true;\n return false;\n}\n\nexport function isSafeRouteNamespace(namespace: string): boolean {\n const value = normalizeNamespace(namespace);\n // The route limit is 64 Unicode code points. On-disk identity tokens may\n // exceed 64 characters because namespaceIdentityToken hex-encodes UTF-8.\n // A route can use at most 64 code points, but the reversible on-disk token\n // must also fit one filesystem component (255 characters).\n if (value.length === 0) return false;\n if (Array.from(value).length > 64) return false;\n if (namespaceIdentityToken(value).length > 255) return false;\n if (value === \".\") return false;\n if (value.includes(\"/\") || value.includes(\"\\\\\")) return false;\n if (value.includes(\"..\")) return false;\n return SAFE_ROUTE_NAMESPACE_CHARS.test(value);\n}\n\nexport function validateRouteTarget(target: RouteTarget | null | undefined, options?: RoutingEngineOptions): {\n ok: boolean;\n error?: string;\n target?: RouteTarget;\n} {\n if (!target || typeof target !== \"object\") {\n return { ok: false, error: \"target must be an object\" };\n }\n\n const allowedCategories = new Set(options?.allowedCategories ?? DEFAULT_CATEGORIES);\n const allowedNamespaces = options?.allowedNamespaces\n ? new Set(options.allowedNamespaces.map((v) => normalizeNamespace(v.trim())).filter((v) => v.length > 0))\n : null;\n\n const normalized: RouteTarget = {};\n\n if (typeof target.category === \"string\") {\n if (!allowedCategories.has(target.category)) {\n return { ok: false, error: `invalid category: ${target.category}` };\n }\n normalized.category = target.category;\n }\n\n if (typeof target.namespace === \"string\") {\n const namespace = normalizeNamespace(target.namespace);\n if (!isSafeRouteNamespace(namespace)) {\n return { ok: false, error: `invalid namespace: ${target.namespace}` };\n }\n if (allowedNamespaces && !allowedNamespaces.has(namespace)) {\n return { ok: false, error: `namespace not allowed: ${namespace}` };\n }\n normalized.namespace = namespace;\n }\n\n if (!normalized.category && !normalized.namespace) {\n return { ok: false, error: \"target must include category or namespace\" };\n }\n\n return { ok: true, target: normalized };\n}\n\nexport function doesRuleMatch(rule: RouteRule, text: string): boolean {\n if (!rule || typeof rule !== \"object\") return false;\n if (rule.enabled === false) return false;\n if (typeof rule.pattern !== \"string\") return false;\n const pattern = rule.pattern.trim();\n if (pattern.length === 0) return false;\n\n if (rule.patternType === \"keyword\") {\n return text.toLowerCase().includes(pattern.toLowerCase());\n }\n if (rule.patternType !== \"regex\") {\n return false;\n }\n\n if (isLikelyUnsafeRegex(pattern)) {\n return false;\n }\n\n try {\n return new RegExp(pattern, \"i\").test(text);\n } catch {\n return false;\n }\n}\n\nexport function selectRouteRule(text: string, rules: RouteRule[], options?: RoutingEngineOptions): RouteSelection | null {\n const ranked = rules\n .map((rule, index) => ({ rule, index }))\n .sort((a, b) => {\n if (b.rule.priority !== a.rule.priority) return b.rule.priority - a.rule.priority;\n return a.index - b.index;\n });\n\n for (const entry of ranked) {\n if (!doesRuleMatch(entry.rule, text)) continue;\n\n const validation = validateRouteTarget(entry.rule.target, options);\n if (!validation.ok || !validation.target) continue;\n\n return {\n rule: entry.rule,\n target: validation.target,\n };\n }\n\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -13,10 +13,10 @@ import {
|
|
|
13
13
|
} from "./chunk-2AD5SXO4.js";
|
|
14
14
|
import {
|
|
15
15
|
ExtractionDeadlineError
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-4UUF2EX3.js";
|
|
17
17
|
import {
|
|
18
18
|
resolvePrincipal
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-MRV7VSQK.js";
|
|
20
20
|
import {
|
|
21
21
|
resolvePresentationCapabilities
|
|
22
22
|
} from "./chunk-JUOZ62Y7.js";
|
|
@@ -792,4 +792,4 @@ var SmartBuffer = class {
|
|
|
792
792
|
export {
|
|
793
793
|
SmartBuffer
|
|
794
794
|
};
|
|
795
|
-
//# sourceMappingURL=chunk-
|
|
795
|
+
//# sourceMappingURL=chunk-PEDYKCJI.js.map
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
expandScopeProfileReadNamespaces,
|
|
3
3
|
resolveScopeProfilePlan
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
namespaceIdentityFromToken,
|
|
7
|
-
namespaceIdentityToken
|
|
8
|
-
} from "./chunk-EGGE52UU.js";
|
|
4
|
+
} from "./chunk-KCT36JAW.js";
|
|
9
5
|
import {
|
|
10
6
|
canReadNamespace,
|
|
11
7
|
canWriteNamespace,
|
|
12
8
|
defaultNamespaceForPrincipal,
|
|
13
9
|
recallNamespacesForPrincipal,
|
|
14
10
|
resolvePrincipal
|
|
15
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-MRV7VSQK.js";
|
|
12
|
+
import {
|
|
13
|
+
namespaceIdentityFromToken,
|
|
14
|
+
namespaceIdentityLegacyToken,
|
|
15
|
+
namespaceIdentityToken,
|
|
16
|
+
normalizeNamespaceIdentity
|
|
17
|
+
} from "./chunk-VUFL7X3H.js";
|
|
16
18
|
import {
|
|
17
19
|
resolveNamespaceCapabilities
|
|
18
20
|
} from "./chunk-JUOZ62Y7.js";
|
|
@@ -104,7 +106,7 @@ function getConfiguredNamespaces(config) {
|
|
|
104
106
|
config.defaultNamespace,
|
|
105
107
|
config.sharedNamespace,
|
|
106
108
|
...config.namespacePolicies.map((policy) => policy.name)
|
|
107
|
-
].map((value) => value
|
|
109
|
+
].map((value) => normalizeNamespaceIdentity(value)).filter(Boolean)
|
|
108
110
|
)
|
|
109
111
|
);
|
|
110
112
|
}
|
|
@@ -131,7 +133,7 @@ function resolveNamespaceFromStorageDir(storageDir, options) {
|
|
|
131
133
|
if (hintedNamespace) return hintedNamespace;
|
|
132
134
|
}
|
|
133
135
|
const decoded = namespaceIdentityFromToken(dirName);
|
|
134
|
-
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
136
|
+
if (decoded && (namespaceIdentityToken(decoded) === dirName || namespaceIdentityLegacyToken(decoded.normalize("NFD")) === dirName)) {
|
|
135
137
|
return decoded;
|
|
136
138
|
}
|
|
137
139
|
return dirName;
|
|
@@ -202,4 +204,4 @@ export {
|
|
|
202
204
|
resolveWritableNamespaceValue,
|
|
203
205
|
resolveScopedWritableNamespaceValue
|
|
204
206
|
};
|
|
205
|
-
//# sourceMappingURL=chunk-
|
|
207
|
+
//# sourceMappingURL=chunk-Q57GOHLF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/scopes/scope-plan.ts"],"sourcesContent":["/**\n * ScopePlan resolver (issue #1521).\n *\n * A single pure function that resolves every namespace-bearing read/write path\n * to one {@link ScopePlan} value object. Consumers (recall tiers, QMD router,\n * LCM reads, maintenance) never call the ad-hoc resolution helpers directly —\n * they receive a resolved plan and read its fields.\n *\n * The resolver DELEGATES to the existing helpers (`resolvePrincipal`,\n * `recallNamespacesForPrincipal`, `resolveCodingNamespaceOverlay`,\n * `resolveScopeProfilePlan`, `expandScopeProfileReadNamespaces`,\n * `combineNamespaces`, `lcmReadSessionIdsForNamespaces`) — no logic rewrite.\n * The ad-hoc inline resolution that previously lived in the orchestrator's\n * `recallInternal` and `enqueueDirectAnswerObservation` paths is replaced by a\n * single call to {@link resolveScopePlan}, eliminating the duplicated\n * namespace-set construction that produced the largest share of #1519's review\n * threads (scope-profile recall paths missing `readFallbacks` appends).\n *\n * Migration tranches (issue #1521 step 4):\n * - recall tiers (this PR): the two orchestrator recall entry points consume\n * the plan instead of building `recallNamespaces`/`observationNamespaces`\n * inline;\n * - QMD router calls, LCM reads, maintenance: follow-up PRs.\n *\n * CLAUDE.md rules honoured (pitfalls from the issue):\n * - rule 42: read/write resolve through the same layer; the coding overlay is\n * COMBINED with the principal base via `combineNamespaces`;\n * - rule 39: feature gates identical across every path the plan feeds;\n * - rule 22/48: LCM keys derived through `lcmSessionKeyForNamespace` (via\n * `lcmReadSessionIdsForNamespaces`), never hardcoded `:`-joins; unscoped LCM\n * search stays suppressed when `namespacesEnabled`.\n */\n\nimport { resolveNamespaceCapabilities } from \"../capabilities.js\";\nimport {\n canReadNamespace,\n canWriteNamespace,\n defaultNamespaceForPrincipal,\n recallNamespacesForPrincipal,\n resolvePrincipal,\n} from \"../namespaces/principal.js\";\nimport {\n namespaceIdentityFromToken,\n namespaceIdentityToken,\n} from \"../namespaces/identity.js\";\nimport path from \"node:path\";\nimport {\n capabilityAllowsOp,\n isNamespaceAllowed,\n resolveEffectiveNamespace,\n type TokenCapabilities,\n} from \"../access-token-capabilities.js\";\nimport type { ResolvedScopeProfilePlan } from \"../namespaces/scope-profiles.js\";\nimport {\n expandScopeProfileReadNamespaces,\n resolveScopeProfilePlan,\n} from \"../namespaces/scope-profiles.js\";\nimport {\n combineNamespaces,\n lcmReadSessionIdsForNamespaces,\n resolveCodingNamespaceOverlay,\n type CodingNamespaceOverlay,\n} from \"../coding/coding-namespace.js\";\nimport type { CodingContext, PluginConfig } from \"../types.js\";\n\n/**\n * A resolved scope plan: every namespace-bearing field a read or write path\n * needs, produced by ONE call to {@link resolveScopePlan}.\n *\n * Consumers read these fields directly — they never re-resolve namespaces.\n */\nexport interface ScopePlan {\n /**\n * Resolved principal under which the operation runs. `undefined` only when\n * `namespacesEnabled` is false (collapses to `\"default\"`) or no session key\n * was supplied in single-user mode.\n */\n readonly principal: string | undefined;\n\n /**\n * Explicit namespace override, trimmed, when the caller supplied one AND it\n * is readable by the principal. `undefined` when no override was given or the\n * override is not readable (the plan falls through to the coding/scope-profile\n * resolution in that case, mirroring the pre-existing observe-path behavior).\n */\n readonly namespaceOverride: string | undefined;\n\n /**\n * Resolved base (self) namespace — the effective namespace absent an explicit\n * override. This is the principal-self namespace, optionally substituted by a\n * scope-profile write layer or a coding overlay.\n *\n * Callers that persisted this as the `selfNamespace` / response namespace\n * read it here.\n */\n readonly baseNamespace: string;\n\n /**\n * Ordered, deduped read-namespace set. Includes the coding overlay fallbacks\n * (branch → project → root) combined with the principal base exactly once, so\n * the #1519 miss (scope-profile path omitted `readFallbacks` appends) cannot\n * recur — the fallback appends live in ONE place.\n */\n readonly readNamespaces: string[];\n\n /**\n * Coding overlay fallback namespaces combined with the principal base (rule\n * 42). Empty when no coding overlay applies. These are the SAME entries\n * appended into {@link readNamespaces} for the coding-overlay branch,\n * surfaced separately so consumers that need just the fallback set (e.g.\n * LCM read-key derivation) can read them without re-deriving.\n */\n readonly readFallbacks: string[];\n\n /**\n * LCM read namespace set. May differ from {@link readNamespaces} for\n * read-authorization reasons: when the principal self base is NOT in the\n * readable recall set, the overlay LCM keys collapse to the default store\n * (rule 42 read/write parity; rule 48 least-privilege) even though\n * {@link readNamespaces} still searches the overlay for QMD/file recall.\n */\n readonly lcmReadNamespaces: string[];\n\n /**\n * LCM read `session_id` set, encoded via\n * `lcmReadSessionIdsForNamespaces` (which delegates to\n * `lcmSessionKeyForNamespace`). Ordered and deduped; the primary overlay key\n * is first. Single-user / no-overlay recall collapses to `[sessionKey]` —\n * byte-for-byte the pre-#1495 behavior.\n *\n * Empty (`[]`) when a scope-profile plan is active and no `sessionKey` was\n * supplied (mirrors the pre-existing guard).\n */\n readonly lcmReadSessionIds: ReadonlyArray<string | undefined>;\n\n /**\n * Resolved coding overlay, or `null` when none applies (no coding context,\n * `codingMode.projectScope` false, `namespacesEnabled` false, or an explicit\n * readable namespace override is set).\n */\n readonly codingOverlay: { readonly namespace: string; readonly readFallbacks: readonly string[] } | null;\n\n /**\n * Resolved scope-profile plan, or `null` when no scope profile is active.\n * Consumers that branched on \"profile vs. non-profile\" read this directly.\n */\n readonly scopeProfilePlan: ResolvedScopeProfilePlan | null;\n}\n\n/**\n * Options for {@link resolveScopePlan}.\n */\nexport interface ResolveScopePlanOptions {\n /** Plugin config (namespace policies, coding mode, default namespace, …). */\n readonly config: PluginConfig;\n /** Session key (may derive the principal and/or coding context). */\n readonly sessionKey?: string;\n /**\n * Explicit namespace override (raw — the resolver trims it). When supplied\n * AND readable by the resolved principal, it wins over every other layer.\n */\n readonly namespace?: string;\n /**\n * Authenticated principal override. Access surfaces that already resolved\n * identity at the transport layer pass it here so namespace ACL decisions\n * use the same identity the surface authorized.\n */\n readonly principalOverride?: string;\n /**\n * Coding context for the session. Callers that track this on the orchestrator\n * pass `getCodingContextForSession(sessionKey)` here; the resolver never\n * reaches back into orchestrator state.\n */\n readonly codingContext?: CodingContext | null;\n /**\n * Whether namespace routing is enabled. Callers that have already read the\n * namespaces-enabled flag pass it here so the resolver does NOT re-read it\n * (keeps the scattered-read ratchet from growing, #1523).\n */\n readonly namespacesEnabled: boolean;\n}\n\n/**\n * Resolve a {@link ScopePlan} from the inputs by delegating to the existing\n * namespace-resolution helpers. Pure — no side effects, no orchestrator state.\n *\n * The caller is responsible for authorization checks that should THROW (e.g.\n * `namespacesEnabled && !principal`, or an unreadable explicit override in the\n * recall path). The resolver computes the plan; enforcement stays at the call\n * site so error semantics are unchanged.\n */\nexport function resolveScopePlan(options: ResolveScopePlanOptions): ScopePlan {\n const { config, sessionKey } = options;\n\n const namespaceOverride = options.namespace?.trim() || undefined;\n\n const principal =\n typeof options.principalOverride === \"string\" && options.principalOverride.length > 0\n ? options.principalOverride\n : resolvePrincipal(sessionKey, config);\n\n // A namespace override gates the overlay/scope-profile layers. When it is\n // readable it wins; when it is NOT readable the plan falls through to the\n // coding/scope-profile/legacy branches (mirrors the observe-path behavior\n // where an unreadable override does not throw but simply does not suppress\n // the overlay). The recall path validates readability and throws BEFORE\n // calling the resolver, so a reachable override is always readable there.\n const namespaceOverrideReadable =\n namespaceOverride !== undefined && canReadNamespace(principal, namespaceOverride, config);\n\n const readableRecallNamespaces = recallNamespacesForPrincipal(principal, config);\n\n // The orchestrator's `applyCodingRecallOverlay` gates on `namespacesEnabled`\n // (returning null when disabled), so the resolver must too — otherwise\n // single-store mode would produce apparent route separation with no actual\n // storage isolation (false-isolation trap, rule 39). The caller passes the\n // pre-read flag so the resolver does not add a scattered `config.*Enabled`\n // read (ratchet, #1523).\n const namespacesEnabled = options.namespacesEnabled;\n const codingOverlay: CodingNamespaceOverlay | null =\n namespaceOverrideReadable || !namespacesEnabled\n ? null\n : resolveCodingNamespaceOverlay(\n options.codingContext ?? null,\n config.codingMode,\n config.defaultNamespace,\n );\n\n const principalSelfNamespace = defaultNamespaceForPrincipal(principal, config);\n const codingSelfNamespace = codingOverlay\n ? combineNamespaces(principalSelfNamespace, codingOverlay.namespace)\n : null;\n\n const scopeProfilePlan = namespaceOverrideReadable\n ? null\n : resolveScopeProfilePlan({\n config,\n principal,\n codingContext: options.codingContext ?? null,\n codingOverlay,\n });\n\n const profileEffectiveNamespace =\n scopeProfilePlan?.writeNamespace || scopeProfilePlan?.readNamespaces[0];\n\n const baseNamespace = namespaceOverrideReadable\n ? namespaceOverride!\n : profileEffectiveNamespace ?? codingSelfNamespace ?? principalSelfNamespace;\n\n // ── Read namespace set ────────────────────────────────────────────────────\n let readNamespaces: string[];\n if (namespaceOverrideReadable) {\n readNamespaces = [namespaceOverride!];\n } else if (scopeProfilePlan) {\n readNamespaces = expandScopeProfileReadNamespaces({\n profilePlan: scopeProfilePlan,\n principalSelfNamespace: scopeProfilePlan.baseNamespace,\n config,\n principal,\n codingOverlay,\n legacyRecallNamespaces: readableRecallNamespaces,\n });\n } else if (codingOverlay && codingSelfNamespace) {\n // Substitute the principal's self namespace with the coding-scoped one, and\n // append any read fallbacks (branch → project, rule 42) combined with the\n // principal base so principal isolation is preserved on fallback entries.\n const mapped = readableRecallNamespaces.map((ns) =>\n ns === principalSelfNamespace ? codingSelfNamespace : ns,\n );\n const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>\n combineNamespaces(principalSelfNamespace, fallback),\n );\n readNamespaces = Array.from(new Set<string>([...mapped, ...fallbackNs]));\n } else {\n readNamespaces = readableRecallNamespaces;\n }\n\n const readFallbacks = codingOverlay\n ? codingOverlay.readFallbacks.map((fb) => combineNamespaces(principalSelfNamespace, fb))\n : [];\n\n // ── LCM read namespace set ────────────────────────────────────────────────\n // The LCM overlay keys are `<principal>-project-*` sub-namespaces authorized\n // transitively by the principal SELF base. Include them ONLY when that base\n // is in the readable recall set (rule 42 / 48). When it is NOT readable, the\n // overlay rows are unauthorized for this reader, so the LCM read collapses to\n // the default store — exactly what QMD/file recall surfaces for such a\n // principal.\n const codingOverlaySelfReadable =\n codingOverlay !== null &&\n (scopeProfilePlan\n ? scopeProfilePlan.layers.some((layer) => layer.id === \"userProject\" && layer.readable)\n : readableRecallNamespaces.includes(principalSelfNamespace));\n\n let lcmReadNamespaces: string[];\n if (namespaceOverrideReadable) {\n lcmReadNamespaces = [namespaceOverride!];\n } else if (scopeProfilePlan) {\n // Scope profiles define a layered read stack; LCM-backed evidence uses the\n // same namespace set as QMD/file recall so team/global/shared observations\n // are not silently skipped.\n lcmReadNamespaces = readNamespaces;\n } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {\n const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>\n combineNamespaces(principalSelfNamespace, fallback),\n );\n lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];\n } else {\n // No overlay, OR overlay present but self base unreadable → collapse to the\n // default store (raw sessionKey). No `<principal>-project-*` overlay key is\n // searched.\n lcmReadNamespaces = [config.defaultNamespace];\n }\n\n const lcmReadSessionIds =\n scopeProfilePlan && !sessionKey\n ? []\n : lcmReadSessionIdsForNamespaces(lcmReadNamespaces, sessionKey, config.defaultNamespace);\n\n return {\n principal,\n namespaceOverride: namespaceOverrideReadable ? namespaceOverride : undefined,\n baseNamespace,\n readNamespaces,\n readFallbacks,\n lcmReadNamespaces,\n lcmReadSessionIds,\n codingOverlay: codingOverlay\n ? { namespace: codingOverlay.namespace, readFallbacks: codingOverlay.readFallbacks }\n : null,\n scopeProfilePlan,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Centralized namespace-resolution helpers (issue #1521 step 3–4)\n//\n// The three functions below consolidate the ad-hoc namespace-resolution logic\n// that was previously scattered across orchestrator.ts (namespaceFromStorageDir,\n// configuredNamespaces) and access-service.ts (resolveWritableNamespace). By\n// living in THIS module they are excluded from the adHocNamespaceResolutions\n// ratchet, and every consumer reaches them through a single well-known import\n// instead of reimplementing the derivation inline (#1519's failure mode).\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * The configured namespace set: default + shared + every namespace-policy\n * name, trimmed and de-duplicated. Pure — reads only from `config`.\n *\n * Replaces the `configuredNamespaces()` private method that lived on both\n * `Orchestrator` and the maintenance namespace-planner (two copies of the\n * same logic).\n */\nexport function getConfiguredNamespaces(config: PluginConfig): string[] {\n return Array.from(\n new Set(\n [\n config.defaultNamespace,\n config.sharedNamespace,\n ...config.namespacePolicies.map((policy) => policy.name),\n ]\n .map((value) => value.trim())\n .filter(Boolean),\n ),\n );\n}\n\n/**\n * Options for {@link resolveNamespaceFromStorageDir}.\n */\nexport interface ResolveNamespaceFromStorageDirOptions {\n /** Plugin config (memoryDir, defaultNamespace, namespacesEnabled, …). */\n readonly config: PluginConfig;\n /**\n * Pre-computed configured-namespace set (from {@link getConfiguredNamespaces}).\n * Passed by the caller so this function does not re-derive it on every call.\n */\n readonly configuredNamespaces: readonly string[];\n /**\n * Catalog-owned dir→namespace hints (best-effort union of configured +\n * catalog-discovered). May be `undefined` when no catalog hints are loaded.\n */\n readonly hints?: ReadonlyMap<string, ReadonlySet<string>> | undefined;\n /**\n * Lazy catalog-hint loader. Called ONLY after the early returns (disabled,\n * memory root, no namespace segment, configured namespace) — matching the\n * original lazy behavior so an eager load does not mark hints as loaded\n * before the catalog file exists (codex P2).\n */\n readonly loadHints?: () => void;\n}\n\n/**\n * Derive the namespace a storage directory belongs to.\n *\n * Pure (no orchestrator state): the caller passes `config`, the configured set,\n * and any catalog hints. The logic is byte-identical to the private\n * `namespaceFromStorageDir` method that previously lived on `Orchestrator`\n * (including the token round-trip guard from codex round 6).\n */\nexport function resolveNamespaceFromStorageDir(\n storageDir: string,\n options: ResolveNamespaceFromStorageDirOptions,\n): string {\n const { config } = options;\n if (!resolveNamespaceCapabilities(config).namespaces) return config.defaultNamespace;\n const resolvedStorageDir = path.resolve(storageDir);\n const resolvedMemoryDir = path.resolve(config.memoryDir);\n if (resolvedStorageDir === resolvedMemoryDir) return config.defaultNamespace;\n const m = resolvedStorageDir.match(/[\\\\/]namespaces[\\\\/]([^\\\\/]+)$/);\n if (!m?.[1]) return config.defaultNamespace;\n const dirName = m[1];\n // Token-shaped raw names (codex P2 — NBsFz): a dir name might be a tokenized\n // identity OR a literal raw namespace name that merely LOOKS like a token.\n // A dir name that is itself a KNOWN namespace is preserved BEFORE decoding.\n if (options.configuredNamespaces.includes(dirName)) {\n return dirName;\n }\n // Load catalog hints ONLY after the early returns (codex P2: an eager load\n // before the catalog file exists would mark hints as loaded and skip later\n // catalog-derived rows).\n options.loadHints?.();\n const hints = options.hints;\n const hintedNamespaces = hints?.get(resolvedStorageDir);\n if (hintedNamespaces?.has(dirName)) {\n return dirName;\n }\n if (hintedNamespaces?.size === 1) {\n const [hintedNamespace] = hintedNamespaces;\n if (hintedNamespace) return hintedNamespace;\n }\n const decoded = namespaceIdentityFromToken(dirName);\n if (decoded && namespaceIdentityToken(decoded) === dirName) {\n return decoded;\n }\n return dirName;\n}\n\n/**\n * Result of resolving a writable namespace: either the authorized namespace\n * or a structured rejection. The caller decides how to surface the rejection\n * (e.g. as an `EngramAccessInputError` in the access-service layer), keeping\n * this module free of transport-specific error types.\n */\nexport type WritableNamespaceResult =\n | { readonly ok: true; readonly namespace: string }\n | { readonly ok: false; readonly reason: \"unsupported\" | \"not_writable\"; readonly namespace: string };\n\n/**\n * Resolve and authorize a writable namespace.\n *\n * Combines the three steps that `AccessService.resolveWritableNamespace`\n * performed inline: (1) resolve the namespace value (trim → default fallback →\n * validate against `namespacesEnabled`), (2) resolve the principal, (3) check\n * `canWriteNamespace`. Returns a structured result so this module stays free of\n * `EngramAccessInputError` (avoiding a circular dependency on access-service.ts).\n *\n * The caller throws `EngramAccessInputError` when `ok === false` — the access-\n * service wrapper does this in one place.\n */\nexport function resolveWritableNamespaceValue(\n namespace: string | undefined,\n sessionKey: string | undefined,\n authenticatedPrincipal: string | undefined,\n config: PluginConfig,\n): WritableNamespaceResult {\n const requested = namespace?.trim();\n let resolved: string;\n if (!requested) {\n resolved = config.defaultNamespace;\n } else if (\n !resolveNamespaceCapabilities(config).namespaces &&\n requested !== config.defaultNamespace\n ) {\n return { ok: false, reason: \"unsupported\", namespace: requested };\n } else {\n resolved = requested;\n }\n\n const trusted = authenticatedPrincipal?.trim();\n const principal =\n typeof trusted === \"string\" && trusted.length > 0\n ? trusted\n : resolvePrincipal(sessionKey, config);\n\n if (!canWriteNamespace(principal, resolved, config)) {\n return { ok: false, reason: \"not_writable\", namespace: resolved };\n }\n return { ok: true, namespace: resolved };\n}\n\n/**\n * Inputs already derived by the access-service's read-only coding-context\n * resolver. Keeping context discovery outside this pure function lets writes\n * and preflight checks apply identical namespace and ACL rules.\n */\nexport interface ScopedWritableNamespaceInput {\n readonly namespace?: string;\n readonly sessionKey?: string;\n readonly authenticatedPrincipal?: string;\n readonly principal: string | undefined;\n readonly codingOverlay: CodingNamespaceOverlay | null;\n readonly scopeProfile: ResolvedScopeProfilePlan | null;\n readonly config: PluginConfig;\n}\n\n/**\n * Resolve the namespace an explicit write or implicit coding-scoped write\n * would use. The caller supplies the same derived coding inputs for the real\n * write and the read-only preflight.\n */\nexport function resolveScopedWritableNamespaceValue(\n input: ScopedWritableNamespaceInput,\n): WritableNamespaceResult {\n const requested = input.namespace?.trim();\n if (requested) {\n return resolveWritableNamespaceValue(\n requested,\n input.sessionKey,\n input.authenticatedPrincipal,\n input.config,\n );\n }\n if (input.scopeProfile) {\n const selectedLayer = input.scopeProfile.layers.find(\n (layer) => layer.id === input.scopeProfile?.writeLayer,\n );\n const writeNamespaceReadable =\n input.scopeProfile.writeNamespace.length > 0 &&\n input.scopeProfile.readNamespaces.includes(input.scopeProfile.writeNamespace);\n if (!selectedLayer?.writable || !writeNamespaceReadable) {\n return {\n ok: false,\n reason: \"not_writable\",\n namespace: input.scopeProfile.writeNamespace,\n };\n }\n return { ok: true, namespace: input.scopeProfile.writeNamespace };\n }\n if (!input.codingOverlay) {\n return resolveWritableNamespaceValue(\n undefined,\n input.sessionKey,\n input.authenticatedPrincipal,\n input.config,\n );\n }\n const baseNamespace = defaultNamespaceForPrincipal(input.principal, input.config);\n if (!canWriteNamespace(input.principal, baseNamespace, input.config)) {\n return { ok: false, reason: \"not_writable\", namespace: baseNamespace };\n }\n return {\n ok: true,\n namespace: combineNamespaces(baseNamespace, input.codingOverlay.namespace),\n };\n}\n\n/**\n * Namespace preflight (issue #1888 part 3): would a write to `namespace` by a\n * caller carrying `caps` succeed? Combines the token's write-op scope, its\n * namespace allow-list, and policy writability so one call answers the real\n * question. A token that cannot actually write (no `observe`/`memory_store` in\n * its ops allow-list) or is asking about a namespace outside its allow-list\n * gets a definitive `not_writable` — revealing nothing about the namespace's\n * policy beyond what the token's own scope already implies, and never throwing\n * a hard authorization error the caller cannot distinguish from a transport\n * fault. (Unrestricted tokens pass both scope checks unchanged.)\n */\nexport function resolveNamespacePreflight(\n caps: TokenCapabilities | undefined | null,\n namespace: string | undefined,\n sessionKey: string | undefined,\n authenticatedPrincipal: string | undefined,\n config: PluginConfig,\n writeOp: string,\n): WritableNamespaceResult {\n const notWritable = (): WritableNamespaceResult => ({\n ok: false,\n reason: \"not_writable\",\n namespace: resolveEffectiveNamespace(namespace, config.defaultNamespace) ?? namespace ?? \"\",\n });\n // The token must be able to perform the write op the caller's ENABLED write\n // path actually uses (`observe` for automatic turn capture — the silent\n // #1888 data-loss path — or `memory_store` for an explicit-only install).\n // A token that cannot perform that op would drop the write, so an otherwise\n // policy-writable namespace is still reported not_writable rather than a\n // false ok:true that promises a write the token cannot make.\n if (!capabilityAllowsOp(caps, writeOp)) {\n return notWritable();\n }\n if (!isNamespaceAllowed(caps, namespace, config.defaultNamespace)) {\n return notWritable();\n }\n return resolveWritableNamespaceValue(namespace, sessionKey, authenticatedPrincipal, config);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/scopes/scope-plan.ts"],"sourcesContent":["/**\n * ScopePlan resolver (issue #1521).\n *\n * A single pure function that resolves every namespace-bearing read/write path\n * to one {@link ScopePlan} value object. Consumers (recall tiers, QMD router,\n * LCM reads, maintenance) never call the ad-hoc resolution helpers directly —\n * they receive a resolved plan and read its fields.\n *\n * The resolver DELEGATES to the existing helpers (`resolvePrincipal`,\n * `recallNamespacesForPrincipal`, `resolveCodingNamespaceOverlay`,\n * `resolveScopeProfilePlan`, `expandScopeProfileReadNamespaces`,\n * `combineNamespaces`, `lcmReadSessionIdsForNamespaces`) — no logic rewrite.\n * The ad-hoc inline resolution that previously lived in the orchestrator's\n * `recallInternal` and `enqueueDirectAnswerObservation` paths is replaced by a\n * single call to {@link resolveScopePlan}, eliminating the duplicated\n * namespace-set construction that produced the largest share of #1519's review\n * threads (scope-profile recall paths missing `readFallbacks` appends).\n *\n * Migration tranches (issue #1521 step 4):\n * - recall tiers (this PR): the two orchestrator recall entry points consume\n * the plan instead of building `recallNamespaces`/`observationNamespaces`\n * inline;\n * - QMD router calls, LCM reads, maintenance: follow-up PRs.\n *\n * CLAUDE.md rules honoured (pitfalls from the issue):\n * - rule 42: read/write resolve through the same layer; the coding overlay is\n * COMBINED with the principal base via `combineNamespaces`;\n * - rule 39: feature gates identical across every path the plan feeds;\n * - rule 22/48: LCM keys derived through `lcmSessionKeyForNamespace` (via\n * `lcmReadSessionIdsForNamespaces`), never hardcoded `:`-joins; unscoped LCM\n * search stays suppressed when `namespacesEnabled`.\n */\n\nimport { resolveNamespaceCapabilities } from \"../capabilities.js\";\nimport {\n canReadNamespace,\n canWriteNamespace,\n defaultNamespaceForPrincipal,\n recallNamespacesForPrincipal,\n resolvePrincipal,\n} from \"../namespaces/principal.js\";\nimport {\n namespaceIdentityFromToken,\n namespaceIdentityLegacyToken,\n namespaceIdentityToken,\n normalizeNamespaceIdentity,\n} from \"../namespaces/identity.js\";\nimport path from \"node:path\";\nimport {\n capabilityAllowsOp,\n isNamespaceAllowed,\n resolveEffectiveNamespace,\n type TokenCapabilities,\n} from \"../access-token-capabilities.js\";\nimport type { ResolvedScopeProfilePlan } from \"../namespaces/scope-profiles.js\";\nimport {\n expandScopeProfileReadNamespaces,\n resolveScopeProfilePlan,\n} from \"../namespaces/scope-profiles.js\";\nimport {\n combineNamespaces,\n lcmReadSessionIdsForNamespaces,\n resolveCodingNamespaceOverlay,\n type CodingNamespaceOverlay,\n} from \"../coding/coding-namespace.js\";\nimport type { CodingContext, PluginConfig } from \"../types.js\";\n\n/**\n * A resolved scope plan: every namespace-bearing field a read or write path\n * needs, produced by ONE call to {@link resolveScopePlan}.\n *\n * Consumers read these fields directly — they never re-resolve namespaces.\n */\nexport interface ScopePlan {\n /**\n * Resolved principal under which the operation runs. `undefined` only when\n * `namespacesEnabled` is false (collapses to `\"default\"`) or no session key\n * was supplied in single-user mode.\n */\n readonly principal: string | undefined;\n\n /**\n * Explicit namespace override, trimmed, when the caller supplied one AND it\n * is readable by the principal. `undefined` when no override was given or the\n * override is not readable (the plan falls through to the coding/scope-profile\n * resolution in that case, mirroring the pre-existing observe-path behavior).\n */\n readonly namespaceOverride: string | undefined;\n\n /**\n * Resolved base (self) namespace — the effective namespace absent an explicit\n * override. This is the principal-self namespace, optionally substituted by a\n * scope-profile write layer or a coding overlay.\n *\n * Callers that persisted this as the `selfNamespace` / response namespace\n * read it here.\n */\n readonly baseNamespace: string;\n\n /**\n * Ordered, deduped read-namespace set. Includes the coding overlay fallbacks\n * (branch → project → root) combined with the principal base exactly once, so\n * the #1519 miss (scope-profile path omitted `readFallbacks` appends) cannot\n * recur — the fallback appends live in ONE place.\n */\n readonly readNamespaces: string[];\n\n /**\n * Coding overlay fallback namespaces combined with the principal base (rule\n * 42). Empty when no coding overlay applies. These are the SAME entries\n * appended into {@link readNamespaces} for the coding-overlay branch,\n * surfaced separately so consumers that need just the fallback set (e.g.\n * LCM read-key derivation) can read them without re-deriving.\n */\n readonly readFallbacks: string[];\n\n /**\n * LCM read namespace set. May differ from {@link readNamespaces} for\n * read-authorization reasons: when the principal self base is NOT in the\n * readable recall set, the overlay LCM keys collapse to the default store\n * (rule 42 read/write parity; rule 48 least-privilege) even though\n * {@link readNamespaces} still searches the overlay for QMD/file recall.\n */\n readonly lcmReadNamespaces: string[];\n\n /**\n * LCM read `session_id` set, encoded via\n * `lcmReadSessionIdsForNamespaces` (which delegates to\n * `lcmSessionKeyForNamespace`). Ordered and deduped; the primary overlay key\n * is first. Single-user / no-overlay recall collapses to `[sessionKey]` —\n * byte-for-byte the pre-#1495 behavior.\n *\n * Empty (`[]`) when a scope-profile plan is active and no `sessionKey` was\n * supplied (mirrors the pre-existing guard).\n */\n readonly lcmReadSessionIds: ReadonlyArray<string | undefined>;\n\n /**\n * Resolved coding overlay, or `null` when none applies (no coding context,\n * `codingMode.projectScope` false, `namespacesEnabled` false, or an explicit\n * readable namespace override is set).\n */\n readonly codingOverlay: { readonly namespace: string; readonly readFallbacks: readonly string[] } | null;\n\n /**\n * Resolved scope-profile plan, or `null` when no scope profile is active.\n * Consumers that branched on \"profile vs. non-profile\" read this directly.\n */\n readonly scopeProfilePlan: ResolvedScopeProfilePlan | null;\n}\n\n/**\n * Options for {@link resolveScopePlan}.\n */\nexport interface ResolveScopePlanOptions {\n /** Plugin config (namespace policies, coding mode, default namespace, …). */\n readonly config: PluginConfig;\n /** Session key (may derive the principal and/or coding context). */\n readonly sessionKey?: string;\n /**\n * Explicit namespace override (raw — the resolver trims it). When supplied\n * AND readable by the resolved principal, it wins over every other layer.\n */\n readonly namespace?: string;\n /**\n * Authenticated principal override. Access surfaces that already resolved\n * identity at the transport layer pass it here so namespace ACL decisions\n * use the same identity the surface authorized.\n */\n readonly principalOverride?: string;\n /**\n * Coding context for the session. Callers that track this on the orchestrator\n * pass `getCodingContextForSession(sessionKey)` here; the resolver never\n * reaches back into orchestrator state.\n */\n readonly codingContext?: CodingContext | null;\n /**\n * Whether namespace routing is enabled. Callers that have already read the\n * namespaces-enabled flag pass it here so the resolver does NOT re-read it\n * (keeps the scattered-read ratchet from growing, #1523).\n */\n readonly namespacesEnabled: boolean;\n}\n\n/**\n * Resolve a {@link ScopePlan} from the inputs by delegating to the existing\n * namespace-resolution helpers. Pure — no side effects, no orchestrator state.\n *\n * The caller is responsible for authorization checks that should THROW (e.g.\n * `namespacesEnabled && !principal`, or an unreadable explicit override in the\n * recall path). The resolver computes the plan; enforcement stays at the call\n * site so error semantics are unchanged.\n */\nexport function resolveScopePlan(options: ResolveScopePlanOptions): ScopePlan {\n const { config, sessionKey } = options;\n\n const namespaceOverride = options.namespace?.trim() || undefined;\n\n const principal =\n typeof options.principalOverride === \"string\" && options.principalOverride.length > 0\n ? options.principalOverride\n : resolvePrincipal(sessionKey, config);\n\n // A namespace override gates the overlay/scope-profile layers. When it is\n // readable it wins; when it is NOT readable the plan falls through to the\n // coding/scope-profile/legacy branches (mirrors the observe-path behavior\n // where an unreadable override does not throw but simply does not suppress\n // the overlay). The recall path validates readability and throws BEFORE\n // calling the resolver, so a reachable override is always readable there.\n const namespaceOverrideReadable =\n namespaceOverride !== undefined && canReadNamespace(principal, namespaceOverride, config);\n\n const readableRecallNamespaces = recallNamespacesForPrincipal(principal, config);\n\n // The orchestrator's `applyCodingRecallOverlay` gates on `namespacesEnabled`\n // (returning null when disabled), so the resolver must too — otherwise\n // single-store mode would produce apparent route separation with no actual\n // storage isolation (false-isolation trap, rule 39). The caller passes the\n // pre-read flag so the resolver does not add a scattered `config.*Enabled`\n // read (ratchet, #1523).\n const namespacesEnabled = options.namespacesEnabled;\n const codingOverlay: CodingNamespaceOverlay | null =\n namespaceOverrideReadable || !namespacesEnabled\n ? null\n : resolveCodingNamespaceOverlay(\n options.codingContext ?? null,\n config.codingMode,\n config.defaultNamespace,\n );\n\n const principalSelfNamespace = defaultNamespaceForPrincipal(principal, config);\n const codingSelfNamespace = codingOverlay\n ? combineNamespaces(principalSelfNamespace, codingOverlay.namespace)\n : null;\n\n const scopeProfilePlan = namespaceOverrideReadable\n ? null\n : resolveScopeProfilePlan({\n config,\n principal,\n codingContext: options.codingContext ?? null,\n codingOverlay,\n });\n\n const profileEffectiveNamespace =\n scopeProfilePlan?.writeNamespace || scopeProfilePlan?.readNamespaces[0];\n\n const baseNamespace = namespaceOverrideReadable\n ? namespaceOverride!\n : profileEffectiveNamespace ?? codingSelfNamespace ?? principalSelfNamespace;\n\n // ── Read namespace set ────────────────────────────────────────────────────\n let readNamespaces: string[];\n if (namespaceOverrideReadable) {\n readNamespaces = [namespaceOverride!];\n } else if (scopeProfilePlan) {\n readNamespaces = expandScopeProfileReadNamespaces({\n profilePlan: scopeProfilePlan,\n principalSelfNamespace: scopeProfilePlan.baseNamespace,\n config,\n principal,\n codingOverlay,\n legacyRecallNamespaces: readableRecallNamespaces,\n });\n } else if (codingOverlay && codingSelfNamespace) {\n // Substitute the principal's self namespace with the coding-scoped one, and\n // append any read fallbacks (branch → project, rule 42) combined with the\n // principal base so principal isolation is preserved on fallback entries.\n const mapped = readableRecallNamespaces.map((ns) =>\n ns === principalSelfNamespace ? codingSelfNamespace : ns,\n );\n const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>\n combineNamespaces(principalSelfNamespace, fallback),\n );\n readNamespaces = Array.from(new Set<string>([...mapped, ...fallbackNs]));\n } else {\n readNamespaces = readableRecallNamespaces;\n }\n\n const readFallbacks = codingOverlay\n ? codingOverlay.readFallbacks.map((fb) => combineNamespaces(principalSelfNamespace, fb))\n : [];\n\n // ── LCM read namespace set ────────────────────────────────────────────────\n // The LCM overlay keys are `<principal>-project-*` sub-namespaces authorized\n // transitively by the principal SELF base. Include them ONLY when that base\n // is in the readable recall set (rule 42 / 48). When it is NOT readable, the\n // overlay rows are unauthorized for this reader, so the LCM read collapses to\n // the default store — exactly what QMD/file recall surfaces for such a\n // principal.\n const codingOverlaySelfReadable =\n codingOverlay !== null &&\n (scopeProfilePlan\n ? scopeProfilePlan.layers.some((layer) => layer.id === \"userProject\" && layer.readable)\n : readableRecallNamespaces.includes(principalSelfNamespace));\n\n let lcmReadNamespaces: string[];\n if (namespaceOverrideReadable) {\n lcmReadNamespaces = [namespaceOverride!];\n } else if (scopeProfilePlan) {\n // Scope profiles define a layered read stack; LCM-backed evidence uses the\n // same namespace set as QMD/file recall so team/global/shared observations\n // are not silently skipped.\n lcmReadNamespaces = readNamespaces;\n } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {\n const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>\n combineNamespaces(principalSelfNamespace, fallback),\n );\n lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];\n } else {\n // No overlay, OR overlay present but self base unreadable → collapse to the\n // default store (raw sessionKey). No `<principal>-project-*` overlay key is\n // searched.\n lcmReadNamespaces = [config.defaultNamespace];\n }\n\n const lcmReadSessionIds =\n scopeProfilePlan && !sessionKey\n ? []\n : lcmReadSessionIdsForNamespaces(lcmReadNamespaces, sessionKey, config.defaultNamespace);\n\n return {\n principal,\n namespaceOverride: namespaceOverrideReadable ? namespaceOverride : undefined,\n baseNamespace,\n readNamespaces,\n readFallbacks,\n lcmReadNamespaces,\n lcmReadSessionIds,\n codingOverlay: codingOverlay\n ? { namespace: codingOverlay.namespace, readFallbacks: codingOverlay.readFallbacks }\n : null,\n scopeProfilePlan,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Centralized namespace-resolution helpers (issue #1521 step 3–4)\n//\n// The three functions below consolidate the ad-hoc namespace-resolution logic\n// that was previously scattered across orchestrator.ts (namespaceFromStorageDir,\n// configuredNamespaces) and access-service.ts (resolveWritableNamespace). By\n// living in THIS module they are excluded from the adHocNamespaceResolutions\n// ratchet, and every consumer reaches them through a single well-known import\n// instead of reimplementing the derivation inline (#1519's failure mode).\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * The configured namespace set: default + shared + every namespace-policy\n * name, trimmed and de-duplicated. Pure — reads only from `config`.\n *\n * Replaces the `configuredNamespaces()` private method that lived on both\n * `Orchestrator` and the maintenance namespace-planner (two copies of the\n * same logic).\n */\nexport function getConfiguredNamespaces(config: PluginConfig): string[] {\n return Array.from(\n new Set(\n [\n config.defaultNamespace,\n config.sharedNamespace,\n ...config.namespacePolicies.map((policy) => policy.name),\n ]\n .map((value) => normalizeNamespaceIdentity(value))\n .filter(Boolean),\n ),\n );\n}\n\n/**\n * Options for {@link resolveNamespaceFromStorageDir}.\n */\nexport interface ResolveNamespaceFromStorageDirOptions {\n /** Plugin config (memoryDir, defaultNamespace, namespacesEnabled, …). */\n readonly config: PluginConfig;\n /**\n * Pre-computed configured-namespace set (from {@link getConfiguredNamespaces}).\n * Passed by the caller so this function does not re-derive it on every call.\n */\n readonly configuredNamespaces: readonly string[];\n /**\n * Catalog-owned dir→namespace hints (best-effort union of configured +\n * catalog-discovered). May be `undefined` when no catalog hints are loaded.\n */\n readonly hints?: ReadonlyMap<string, ReadonlySet<string>> | undefined;\n /**\n * Lazy catalog-hint loader. Called ONLY after the early returns (disabled,\n * memory root, no namespace segment, configured namespace) — matching the\n * original lazy behavior so an eager load does not mark hints as loaded\n * before the catalog file exists (codex P2).\n */\n readonly loadHints?: () => void;\n}\n\n/**\n * Derive the namespace a storage directory belongs to.\n *\n * Pure (no orchestrator state): the caller passes `config`, the configured set,\n * and any catalog hints. The logic is byte-identical to the private\n * `namespaceFromStorageDir` method that previously lived on `Orchestrator`\n * (including the token round-trip guard from codex round 6).\n */\nexport function resolveNamespaceFromStorageDir(\n storageDir: string,\n options: ResolveNamespaceFromStorageDirOptions,\n): string {\n const { config } = options;\n if (!resolveNamespaceCapabilities(config).namespaces) return config.defaultNamespace;\n const resolvedStorageDir = path.resolve(storageDir);\n const resolvedMemoryDir = path.resolve(config.memoryDir);\n if (resolvedStorageDir === resolvedMemoryDir) return config.defaultNamespace;\n const m = resolvedStorageDir.match(/[\\\\/]namespaces[\\\\/]([^\\\\/]+)$/);\n if (!m?.[1]) return config.defaultNamespace;\n const dirName = m[1];\n // Token-shaped raw names (codex P2 — NBsFz): a dir name might be a tokenized\n // identity OR a literal raw namespace name that merely LOOKS like a token.\n // A dir name that is itself a KNOWN namespace is preserved BEFORE decoding.\n if (options.configuredNamespaces.includes(dirName)) {\n return dirName;\n }\n // Load catalog hints ONLY after the early returns (codex P2: an eager load\n // before the catalog file exists would mark hints as loaded and skip later\n // catalog-derived rows).\n options.loadHints?.();\n const hints = options.hints;\n const hintedNamespaces = hints?.get(resolvedStorageDir);\n if (hintedNamespaces?.has(dirName)) {\n return dirName;\n }\n if (hintedNamespaces?.size === 1) {\n const [hintedNamespace] = hintedNamespaces;\n if (hintedNamespace) return hintedNamespace;\n }\n const decoded = namespaceIdentityFromToken(dirName);\n if (\n decoded &&\n (namespaceIdentityToken(decoded) === dirName ||\n namespaceIdentityLegacyToken(decoded.normalize(\"NFD\")) === dirName)\n ) {\n return decoded;\n }\n return dirName;\n}\n\n/**\n * Result of resolving a writable namespace: either the authorized namespace\n * or a structured rejection. The caller decides how to surface the rejection\n * (e.g. as an `EngramAccessInputError` in the access-service layer), keeping\n * this module free of transport-specific error types.\n */\nexport type WritableNamespaceResult =\n | { readonly ok: true; readonly namespace: string }\n | { readonly ok: false; readonly reason: \"unsupported\" | \"not_writable\"; readonly namespace: string };\n\n/**\n * Resolve and authorize a writable namespace.\n *\n * Combines the three steps that `AccessService.resolveWritableNamespace`\n * performed inline: (1) resolve the namespace value (trim → default fallback →\n * validate against `namespacesEnabled`), (2) resolve the principal, (3) check\n * `canWriteNamespace`. Returns a structured result so this module stays free of\n * `EngramAccessInputError` (avoiding a circular dependency on access-service.ts).\n *\n * The caller throws `EngramAccessInputError` when `ok === false` — the access-\n * service wrapper does this in one place.\n */\nexport function resolveWritableNamespaceValue(\n namespace: string | undefined,\n sessionKey: string | undefined,\n authenticatedPrincipal: string | undefined,\n config: PluginConfig,\n): WritableNamespaceResult {\n const requested = namespace?.trim();\n let resolved: string;\n if (!requested) {\n resolved = config.defaultNamespace;\n } else if (\n !resolveNamespaceCapabilities(config).namespaces &&\n requested !== config.defaultNamespace\n ) {\n return { ok: false, reason: \"unsupported\", namespace: requested };\n } else {\n resolved = requested;\n }\n\n const trusted = authenticatedPrincipal?.trim();\n const principal =\n typeof trusted === \"string\" && trusted.length > 0\n ? trusted\n : resolvePrincipal(sessionKey, config);\n\n if (!canWriteNamespace(principal, resolved, config)) {\n return { ok: false, reason: \"not_writable\", namespace: resolved };\n }\n return { ok: true, namespace: resolved };\n}\n\n/**\n * Inputs already derived by the access-service's read-only coding-context\n * resolver. Keeping context discovery outside this pure function lets writes\n * and preflight checks apply identical namespace and ACL rules.\n */\nexport interface ScopedWritableNamespaceInput {\n readonly namespace?: string;\n readonly sessionKey?: string;\n readonly authenticatedPrincipal?: string;\n readonly principal: string | undefined;\n readonly codingOverlay: CodingNamespaceOverlay | null;\n readonly scopeProfile: ResolvedScopeProfilePlan | null;\n readonly config: PluginConfig;\n}\n\n/**\n * Resolve the namespace an explicit write or implicit coding-scoped write\n * would use. The caller supplies the same derived coding inputs for the real\n * write and the read-only preflight.\n */\nexport function resolveScopedWritableNamespaceValue(\n input: ScopedWritableNamespaceInput,\n): WritableNamespaceResult {\n const requested = input.namespace?.trim();\n if (requested) {\n return resolveWritableNamespaceValue(\n requested,\n input.sessionKey,\n input.authenticatedPrincipal,\n input.config,\n );\n }\n if (input.scopeProfile) {\n const selectedLayer = input.scopeProfile.layers.find(\n (layer) => layer.id === input.scopeProfile?.writeLayer,\n );\n const writeNamespaceReadable =\n input.scopeProfile.writeNamespace.length > 0 &&\n input.scopeProfile.readNamespaces.includes(input.scopeProfile.writeNamespace);\n if (!selectedLayer?.writable || !writeNamespaceReadable) {\n return {\n ok: false,\n reason: \"not_writable\",\n namespace: input.scopeProfile.writeNamespace,\n };\n }\n return { ok: true, namespace: input.scopeProfile.writeNamespace };\n }\n if (!input.codingOverlay) {\n return resolveWritableNamespaceValue(\n undefined,\n input.sessionKey,\n input.authenticatedPrincipal,\n input.config,\n );\n }\n const baseNamespace = defaultNamespaceForPrincipal(input.principal, input.config);\n if (!canWriteNamespace(input.principal, baseNamespace, input.config)) {\n return { ok: false, reason: \"not_writable\", namespace: baseNamespace };\n }\n return {\n ok: true,\n namespace: combineNamespaces(baseNamespace, input.codingOverlay.namespace),\n };\n}\n\n/**\n * Namespace preflight (issue #1888 part 3): would a write to `namespace` by a\n * caller carrying `caps` succeed? Combines the token's write-op scope, its\n * namespace allow-list, and policy writability so one call answers the real\n * question. A token that cannot actually write (no `observe`/`memory_store` in\n * its ops allow-list) or is asking about a namespace outside its allow-list\n * gets a definitive `not_writable` — revealing nothing about the namespace's\n * policy beyond what the token's own scope already implies, and never throwing\n * a hard authorization error the caller cannot distinguish from a transport\n * fault. (Unrestricted tokens pass both scope checks unchanged.)\n */\nexport function resolveNamespacePreflight(\n caps: TokenCapabilities | undefined | null,\n namespace: string | undefined,\n sessionKey: string | undefined,\n authenticatedPrincipal: string | undefined,\n config: PluginConfig,\n writeOp: string,\n): WritableNamespaceResult {\n const notWritable = (): WritableNamespaceResult => ({\n ok: false,\n reason: \"not_writable\",\n namespace: resolveEffectiveNamespace(namespace, config.defaultNamespace) ?? namespace ?? \"\",\n });\n // The token must be able to perform the write op the caller's ENABLED write\n // path actually uses (`observe` for automatic turn capture — the silent\n // #1888 data-loss path — or `memory_store` for an explicit-only install).\n // A token that cannot perform that op would drop the write, so an otherwise\n // policy-writable namespace is still reported not_writable rather than a\n // false ok:true that promises a write the token cannot make.\n if (!capabilityAllowsOp(caps, writeOp)) {\n return notWritable();\n }\n if (!isNamespaceAllowed(caps, namespace, config.defaultNamespace)) {\n return notWritable();\n }\n return resolveWritableNamespaceValue(namespace, sessionKey, authenticatedPrincipal, config);\n}\n"],"mappings":"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","names":[]}
|