@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
package/src/chunking.ts
CHANGED
|
@@ -47,51 +47,136 @@ function estimateTokens(text: string): number {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Split text into sentences.
|
|
50
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* The scan stays linear and does not use a backtracking regular expression.
|
|
52
|
+
* ASCII punctuation keeps its old whitespace rule. Unicode terminators also
|
|
53
|
+
* split when the next sentence starts immediately, as in CJK text.
|
|
51
54
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
const UNICODE_SENTENCE_TERMINATORS = "。.!?؟۔।॥。…";
|
|
56
|
+
const ASCII_SENTENCE_TERMINATORS = ".!?";
|
|
57
|
+
const CJK_NO_SPACE_TERMINATORS = "。.!?。";
|
|
58
|
+
const CLOSING_PUNCTUATION = "\"'”’»」』)]】〉》)]}";
|
|
59
|
+
const DIGITS = "01234567890123456789";
|
|
60
|
+
const SENTENCE_SEGMENTER =
|
|
61
|
+
typeof Intl.Segmenter === "function"
|
|
62
|
+
? new Intl.Segmenter(undefined, { granularity: "sentence" })
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
65
|
+
function isSentenceTerminator(character: string): boolean {
|
|
66
|
+
return (
|
|
67
|
+
ASCII_SENTENCE_TERMINATORS.includes(character) ||
|
|
68
|
+
UNICODE_SENTENCE_TERMINATORS.includes(character)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isCjk(character: string): boolean {
|
|
73
|
+
return /[\u3000-\u9fff\uf900-\ufaff\uac00-\ud7af]/u.test(character);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type SentenceList = string[] & { separators: string[] };
|
|
77
|
+
|
|
78
|
+
function splitSentencesFallback(text: string): SentenceList {
|
|
79
|
+
const sentences = [] as unknown as SentenceList;
|
|
80
|
+
Object.defineProperty(sentences, "separators", {
|
|
81
|
+
value: [],
|
|
82
|
+
writable: true,
|
|
83
|
+
});
|
|
66
84
|
let start = 0;
|
|
85
|
+
let separator = "";
|
|
67
86
|
for (let i = 0; i < text.length; i++) {
|
|
68
87
|
const ch = text[i];
|
|
69
|
-
if (
|
|
70
|
-
|
|
88
|
+
if (!isSentenceTerminator(ch)) continue;
|
|
89
|
+
|
|
71
90
|
let end = i;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
let hasUnicodeTerminator = UNICODE_SENTENCE_TERMINATORS.includes(ch);
|
|
92
|
+
while (end + 1 < text.length && isSentenceTerminator(text[end + 1])) {
|
|
93
|
+
end++;
|
|
94
|
+
hasUnicodeTerminator ||= UNICODE_SENTENCE_TERMINATORS.includes(text[end]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isFullwidthDecimal =
|
|
98
|
+
ch === "." &&
|
|
99
|
+
DIGITS.includes(text[i - 1] ?? "") &&
|
|
100
|
+
DIGITS.includes(text[end + 1] ?? "");
|
|
101
|
+
while (end + 1 < text.length && CLOSING_PUNCTUATION.includes(text[end + 1])) {
|
|
75
102
|
end++;
|
|
76
103
|
}
|
|
104
|
+
|
|
77
105
|
const after = text[end + 1];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
106
|
+
const boundary =
|
|
107
|
+
!isFullwidthDecimal &&
|
|
108
|
+
(hasUnicodeTerminator || after === undefined || /\s/u.test(after));
|
|
109
|
+
if (boundary) {
|
|
82
110
|
const sentence = text.slice(start, end + 1).trim();
|
|
83
|
-
if (sentence.length > 0)
|
|
84
|
-
|
|
111
|
+
if (sentence.length > 0) {
|
|
112
|
+
sentences.push(sentence);
|
|
113
|
+
if (sentences.length > 1) sentences.separators.push(separator);
|
|
114
|
+
}
|
|
115
|
+
let nextStart = end + 1;
|
|
116
|
+
while (nextStart < text.length && /\s/u.test(text[nextStart])) nextStart++;
|
|
117
|
+
separator = text.slice(end + 1, nextStart);
|
|
118
|
+
start = nextStart;
|
|
85
119
|
}
|
|
86
120
|
i = end;
|
|
87
121
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
|
|
123
|
+
const remaining = text.slice(start).trim();
|
|
124
|
+
if (remaining.length > 0) {
|
|
125
|
+
sentences.push(remaining);
|
|
126
|
+
if (sentences.length > 1) sentences.separators.push(separator);
|
|
92
127
|
}
|
|
93
128
|
return sentences;
|
|
94
129
|
}
|
|
130
|
+
export function splitSentences(text: string): string[] {
|
|
131
|
+
const canUseSegmenter =
|
|
132
|
+
SENTENCE_SEGMENTER !== undefined &&
|
|
133
|
+
!/\s/u.test(text) &&
|
|
134
|
+
[...text].some((character) => UNICODE_SENTENCE_TERMINATORS.includes(character));
|
|
135
|
+
if (canUseSegmenter) {
|
|
136
|
+
const segments = Array.from(SENTENCE_SEGMENTER.segment(text), ({ segment }) =>
|
|
137
|
+
segment.trim(),
|
|
138
|
+
).filter((segment) => segment.length > 0) as SentenceList;
|
|
139
|
+
if (
|
|
140
|
+
segments.length > 1 &&
|
|
141
|
+
segments.slice(0, -1).every((segment) =>
|
|
142
|
+
[...segment].some((character) =>
|
|
143
|
+
UNICODE_SENTENCE_TERMINATORS.includes(character),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
Object.defineProperty(segments, "separators", {
|
|
148
|
+
value: Array(segments.length - 1).fill(""),
|
|
149
|
+
writable: true,
|
|
150
|
+
});
|
|
151
|
+
return segments;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return splitSentencesFallback(text);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function endsWithoutSpace(text: string): boolean {
|
|
158
|
+
let index = text.length - 1;
|
|
159
|
+
while (index >= 0 && CLOSING_PUNCTUATION.includes(text[index])) index--;
|
|
160
|
+
const terminator = text[index] ?? "";
|
|
161
|
+
if (CJK_NO_SPACE_TERMINATORS.includes(terminator)) {
|
|
162
|
+
return isCjk(text[index - 1] ?? "");
|
|
163
|
+
}
|
|
164
|
+
return terminator === "…" && isCjk(text[index - 1] ?? "");
|
|
165
|
+
}
|
|
166
|
+
export function joinSentences(sentences: string[]): string {
|
|
167
|
+
let result = "";
|
|
168
|
+
const separators = (sentences as Partial<SentenceList>).separators;
|
|
169
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
170
|
+
const sentence = sentences[i];
|
|
171
|
+
if (result.length > 0) {
|
|
172
|
+
const separator = separators?.[i - 1];
|
|
173
|
+
result += separator ?? (endsWithoutSpace(result) ? "" : " ");
|
|
174
|
+
}
|
|
175
|
+
result += sentence;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
95
180
|
|
|
96
181
|
/**
|
|
97
182
|
* Chunk content into overlapping segments at sentence boundaries.
|
|
@@ -152,7 +237,7 @@ export function chunkContent(
|
|
|
152
237
|
|
|
153
238
|
if (atTarget || isLastSentence) {
|
|
154
239
|
// Create chunk from accumulated sentences
|
|
155
|
-
const chunkContent = currentChunkSentences
|
|
240
|
+
const chunkContent = joinSentences(currentChunkSentences);
|
|
156
241
|
chunks.push({
|
|
157
242
|
content: chunkContent,
|
|
158
243
|
index: chunkIndex,
|
|
@@ -216,14 +301,14 @@ export function reassembleChunks(chunks: string[]): string {
|
|
|
216
301
|
const prevEnd = prevSentences.slice(-(j + 1));
|
|
217
302
|
const currStart = currSentences.slice(0, j + 1);
|
|
218
303
|
|
|
219
|
-
if (prevEnd
|
|
304
|
+
if (joinSentences(prevEnd) === joinSentences(currStart)) {
|
|
220
305
|
overlapCount = j + 1;
|
|
221
306
|
}
|
|
222
307
|
}
|
|
223
308
|
|
|
224
309
|
// Add non-overlapping portion
|
|
225
310
|
if (overlapCount > 0 && overlapCount < currSentences.length) {
|
|
226
|
-
result.push(currSentences.slice(overlapCount)
|
|
311
|
+
result.push(joinSentences(currSentences.slice(overlapCount)));
|
|
227
312
|
} else if (overlapCount === 0) {
|
|
228
313
|
// No detected overlap, add full chunk
|
|
229
314
|
result.push(currChunk);
|
|
@@ -231,5 +316,5 @@ export function reassembleChunks(chunks: string[]): string {
|
|
|
231
316
|
// If overlapCount === currSentences.length, skip (fully contained)
|
|
232
317
|
}
|
|
233
318
|
|
|
234
|
-
return result
|
|
319
|
+
return joinSentences(result);
|
|
235
320
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
withHeldFileLock,
|
|
20
20
|
} from "../utils/serialize-mutations.js";
|
|
21
21
|
import { isSafeRouteNamespace } from "../routing/engine.js";
|
|
22
|
-
import { namespaceIdentityFromToken, namespaceIdentityToken, normalizeNamespaceIdentity } from "./identity.js";
|
|
22
|
+
import { namespaceIdentityFromToken, namespaceIdentityLegacyToken, namespaceIdentityToken, normalizeNamespaceIdentity } from "./identity.js";
|
|
23
23
|
import { resolveDefaultNamespaceRoot, resolveNamespaceStorageRoot } from "./storage.js";
|
|
24
24
|
import { ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
|
|
25
25
|
|
|
@@ -1721,8 +1721,7 @@ export class NamespaceCatalog {
|
|
|
1721
1721
|
// sourced from their tokenized dir so a later legacy-named `readdir` entry
|
|
1722
1722
|
// cannot overwrite the tokenized record (and vice-versa: a tokenized entry
|
|
1723
1723
|
// always wins over a previously-set legacy one).
|
|
1724
|
-
const scannedFromTokenized = new
|
|
1725
|
-
|
|
1724
|
+
const scannedFromTokenized = new Map<string, number>();
|
|
1726
1725
|
for (const entry of entries) {
|
|
1727
1726
|
const token = entry.name;
|
|
1728
1727
|
const fullPath = path.join(namespacesDir, token);
|
|
@@ -1798,7 +1797,7 @@ export class NamespaceCatalog {
|
|
|
1798
1797
|
const tokenDecoded = literalOwnsRoot ? null : namespaceIdentityFromToken(token);
|
|
1799
1798
|
const rawDecoded = tokenDecoded && tokenDecoded.length > 0 ? tokenDecoded : token;
|
|
1800
1799
|
const decoded = normalizeNamespaceIdentity(rawDecoded);
|
|
1801
|
-
if (decoded.length === 0 || rawDecoded !== decoded) {
|
|
1800
|
+
if (decoded.length === 0 || (rawDecoded !== decoded && rawDecoded.normalize("NFC") !== decoded)) {
|
|
1802
1801
|
skipped.push({ token, reason: "unsafe", detail: rawDecoded });
|
|
1803
1802
|
continue;
|
|
1804
1803
|
}
|
|
@@ -1841,11 +1840,12 @@ export class NamespaceCatalog {
|
|
|
1841
1840
|
// is the on-disk dir name; it is the tokenized dir iff it equals the
|
|
1842
1841
|
// namespace's identity token. If we already recorded this namespace from
|
|
1843
1842
|
// its tokenized dir, a later legacy-named entry must not clobber it.
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
if (
|
|
1843
|
+
const canonicalToken = namespaceIdentityToken(decoded);
|
|
1844
|
+
const legacyToken = namespaceIdentityLegacyToken(decoded.normalize("NFD"));
|
|
1845
|
+
const tokenKind = token === canonicalToken ? 2 : token === legacyToken ? 1 : 0;
|
|
1846
|
+
const priorTokenKind = scannedFromTokenized.get(decoded) ?? 0;
|
|
1847
|
+
if (rebuilt.has(decoded) && priorTokenKind > tokenKind) continue;
|
|
1848
|
+
if (tokenKind > 0) scannedFromTokenized.set(decoded, tokenKind);
|
|
1849
1849
|
|
|
1850
1850
|
const prior = existing.get(decoded);
|
|
1851
1851
|
rebuilt.set(
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
export function normalizeNamespaceIdentity(namespace: string | null | undefined): string {
|
|
2
|
-
return namespace?.trim() ?? "";
|
|
2
|
+
return namespace?.trim().normalize("NFC") ?? "";
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const bytes = new TextEncoder().encode(normalized);
|
|
5
|
+
function encodeNamespaceIdentityToken(namespace: string): string {
|
|
6
|
+
const bytes = new TextEncoder().encode(namespace);
|
|
8
7
|
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9
8
|
return `ns-${hex || "default"}`;
|
|
10
9
|
}
|
|
11
10
|
|
|
11
|
+
export function namespaceIdentityToken(namespace: string): string {
|
|
12
|
+
return encodeNamespaceIdentityToken(normalizeNamespaceIdentity(namespace));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function namespaceIdentityLegacyToken(namespace: string): string {
|
|
16
|
+
return encodeNamespaceIdentityToken(namespace.trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export function namespaceIdentityFromToken(token: string): string | null {
|
|
13
20
|
if (!token.startsWith("ns-")) return null;
|
|
14
21
|
const hex = token.slice(3);
|
|
@@ -17,7 +24,9 @@ export function namespaceIdentityFromToken(token: string): string | null {
|
|
|
17
24
|
return null;
|
|
18
25
|
}
|
|
19
26
|
const decoded = Buffer.from(hex, "hex").toString("utf8");
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
const normalized = normalizeNamespaceIdentity(decoded);
|
|
28
|
+
return namespaceIdentityToken(decoded).toLowerCase() === token.toLowerCase() ||
|
|
29
|
+
namespaceIdentityLegacyToken(decoded).toLowerCase() === token.toLowerCase()
|
|
30
|
+
? normalized
|
|
22
31
|
: null;
|
|
23
32
|
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import test from "node:test";
|
|
14
14
|
|
|
15
|
-
import { canReadNamespace, isNamespacePolicyCovered, resolvePrincipal } from "./principal.js";
|
|
16
15
|
import type { PluginConfig } from "../types.js";
|
|
16
|
+
import { canReadNamespace, canWriteNamespace, isNamespacePolicyCovered, resolvePrincipal } from "./principal.js";
|
|
17
17
|
|
|
18
18
|
// ──────────────────────────────────────────────────────────────────────────
|
|
19
19
|
// Helpers
|
|
@@ -285,3 +285,12 @@ test("isNamespacePolicyCovered: an explicit default-namespace policy overrides t
|
|
|
285
285
|
const grantedDefault = makeConfig(true, [{ name: "default", readPrincipals: [], writePrincipals: ["alice"] }]);
|
|
286
286
|
assert.equal(isNamespacePolicyCovered("default", grantedDefault), true);
|
|
287
287
|
});
|
|
288
|
+
|
|
289
|
+
test("namespace ACL matches canonically equivalent Unicode identities", () => {
|
|
290
|
+
const config = makeConfig(true, [
|
|
291
|
+
{ name: "Cafe\u0301", readPrincipals: ["reader"], writePrincipals: ["writer"] },
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
assert.equal(canReadNamespace("reader", "Café", config), true);
|
|
295
|
+
assert.equal(canWriteNamespace("writer", "Café", config), true);
|
|
296
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveNamespaceCapabilities } from "../capabilities.js";
|
|
2
2
|
import type { PluginConfig } from "../types.js";
|
|
3
3
|
import { isLikelyUnsafeRegex } from "../routing/engine.js";
|
|
4
|
-
|
|
4
|
+
import { normalizeNamespaceIdentity } from "./identity.js";
|
|
5
5
|
const MAX_REGEX_SESSION_KEY_LENGTH = 512;
|
|
6
6
|
|
|
7
7
|
function compileSafePrincipalRegex(pattern: string): RegExp | null {
|
|
@@ -54,16 +54,21 @@ export function resolvePrincipal(sessionKey: string | undefined, config: PluginC
|
|
|
54
54
|
export function canReadNamespace(principal: string | undefined, namespace: string, config: PluginConfig): boolean {
|
|
55
55
|
if (!resolveNamespaceCapabilities(config).namespaces) return true;
|
|
56
56
|
if (!principal) return false;
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
58
|
+
const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);
|
|
59
|
+
if (!policy) {
|
|
60
|
+
return identity === normalizeNamespaceIdentity(config.defaultNamespace) ||
|
|
61
|
+
identity === normalizeNamespaceIdentity(config.sharedNamespace);
|
|
62
|
+
}
|
|
59
63
|
return policy.readPrincipals.includes(principal) || policy.readPrincipals.includes("*");
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export function canWriteNamespace(principal: string | undefined, namespace: string, config: PluginConfig): boolean {
|
|
63
67
|
if (!resolveNamespaceCapabilities(config).namespaces) return true;
|
|
64
68
|
if (!principal) return false;
|
|
65
|
-
const
|
|
66
|
-
|
|
69
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
70
|
+
const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);
|
|
71
|
+
if (!policy) return identity === normalizeNamespaceIdentity(config.defaultNamespace);
|
|
67
72
|
return policy.writePrincipals.includes(principal) || policy.writePrincipals.includes("*");
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -87,8 +92,9 @@ export function isNamespacePolicyCovered(namespace: string, config: PluginConfig
|
|
|
87
92
|
// An explicit policy wins over the default fallback (exactly as
|
|
88
93
|
// canWriteNamespace), so a policy naming the default with no writers is NOT
|
|
89
94
|
// covered. Only fall back to defaultNamespace when no policy matches.
|
|
90
|
-
const
|
|
91
|
-
|
|
95
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
96
|
+
const policy = config.namespacePolicies.find((p) => normalizeNamespaceIdentity(p.name) === identity);
|
|
97
|
+
if (!policy) return identity === normalizeNamespaceIdentity(config.defaultNamespace);
|
|
92
98
|
return policy.writePrincipals.some((w) => w.trim().length > 0);
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -102,8 +108,10 @@ export function isNamespacePolicyCovered(namespace: string, config: PluginConfig
|
|
|
102
108
|
export function defaultNamespaceForPrincipal(principal: string | undefined, config: PluginConfig): string {
|
|
103
109
|
if (!resolveNamespaceCapabilities(config).namespaces) return config.defaultNamespace;
|
|
104
110
|
if (!principal) return config.defaultNamespace;
|
|
105
|
-
const
|
|
106
|
-
|
|
111
|
+
const policy = config.namespacePolicies.find(
|
|
112
|
+
(p) => normalizeNamespaceIdentity(p.name) === normalizeNamespaceIdentity(principal),
|
|
113
|
+
);
|
|
114
|
+
return policy ? normalizeNamespaceIdentity(policy.name) : config.defaultNamespace;
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
export function recallNamespacesForPrincipal(principal: string | undefined, config: PluginConfig): string[] {
|
|
@@ -12,7 +12,12 @@ import type {
|
|
|
12
12
|
ScopeProfilePromotionTarget,
|
|
13
13
|
ScopeTeamConfig,
|
|
14
14
|
} from "../types.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
canReadNamespace,
|
|
17
|
+
canWriteNamespace,
|
|
18
|
+
defaultNamespaceForPrincipal,
|
|
19
|
+
} from "./principal.js";
|
|
20
|
+
import { normalizeNamespaceIdentity } from "./identity.js";
|
|
16
21
|
|
|
17
22
|
type ScopeProfileCodingOverlay = Pick<CodingNamespaceOverlay, "namespace" | "readFallbacks">;
|
|
18
23
|
|
|
@@ -65,19 +70,28 @@ function principalListed(list: string[], principal: string | undefined): boolean
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function derivedScopeProfileSelfNamespace(principal: string | undefined, config: PluginConfig): string | null {
|
|
68
|
-
if (
|
|
73
|
+
if (
|
|
74
|
+
!principal ||
|
|
75
|
+
normalizeNamespaceIdentity(principal) === normalizeNamespaceIdentity(config.defaultNamespace) ||
|
|
76
|
+
normalizeNamespaceIdentity(principal) === normalizeNamespaceIdentity(config.sharedNamespace)
|
|
77
|
+
) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
69
80
|
if (isSafeRouteNamespace(principal)) return principal;
|
|
70
81
|
return "principal-" + createHash("sha256").update(principal).digest("hex").slice(0, 54);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
function scopeProfileSelfNamespace(principal: string | undefined, config: PluginConfig): string {
|
|
74
85
|
const existing = defaultNamespaceForPrincipal(principal, config);
|
|
75
|
-
if (existing !== config.defaultNamespace) return existing;
|
|
86
|
+
if (normalizeNamespaceIdentity(existing) !== normalizeNamespaceIdentity(config.defaultNamespace)) return existing;
|
|
76
87
|
return derivedScopeProfileSelfNamespace(principal, config) ?? existing;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
function hasExplicitNamespacePolicy(namespace: string, config: PluginConfig): boolean {
|
|
80
|
-
|
|
91
|
+
const identity = normalizeNamespaceIdentity(namespace);
|
|
92
|
+
return (config.namespacePolicies ?? []).some(
|
|
93
|
+
(policy) => normalizeNamespaceIdentity(policy.name) === identity,
|
|
94
|
+
);
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
function isScopeProfileImplicitSelfNamespace(
|
|
@@ -88,9 +102,9 @@ function isScopeProfileImplicitSelfNamespace(
|
|
|
88
102
|
const derived = derivedScopeProfileSelfNamespace(principal, config);
|
|
89
103
|
return Boolean(
|
|
90
104
|
derived &&
|
|
91
|
-
namespace === derived &&
|
|
92
|
-
namespace !== config.defaultNamespace &&
|
|
93
|
-
namespace !== config.sharedNamespace &&
|
|
105
|
+
normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(derived) &&
|
|
106
|
+
normalizeNamespaceIdentity(namespace) !== normalizeNamespaceIdentity(config.defaultNamespace) &&
|
|
107
|
+
normalizeNamespaceIdentity(namespace) !== normalizeNamespaceIdentity(config.sharedNamespace) &&
|
|
94
108
|
isSafeRouteNamespace(namespace) &&
|
|
95
109
|
!hasExplicitNamespacePolicy(namespace, config),
|
|
96
110
|
);
|
|
@@ -297,16 +311,24 @@ function resolveLayer(params: {
|
|
|
297
311
|
const userProjectBase = namespace.endsWith(userProjectSuffix)
|
|
298
312
|
? namespace.slice(0, -userProjectSuffix.length)
|
|
299
313
|
: "";
|
|
314
|
+
const userProjectBaseIdentity = normalizeNamespaceIdentity(userProjectBase);
|
|
315
|
+
const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
316
|
+
const sharedIdentity = normalizeNamespaceIdentity(config.sharedNamespace);
|
|
317
|
+
const namespaceIdentity = normalizeNamespaceIdentity(namespace);
|
|
300
318
|
const dynamicUserProjectCollision =
|
|
301
319
|
userProjectBase.length > 0 &&
|
|
302
|
-
(
|
|
303
|
-
|
|
304
|
-
(config.namespacePolicies ?? []).some(
|
|
320
|
+
(userProjectBaseIdentity === defaultIdentity ||
|
|
321
|
+
userProjectBaseIdentity === sharedIdentity ||
|
|
322
|
+
(config.namespacePolicies ?? []).some(
|
|
323
|
+
(policy) => normalizeNamespaceIdentity(policy.name) === userProjectBaseIdentity,
|
|
324
|
+
));
|
|
305
325
|
const protectedNamespace =
|
|
306
|
-
|
|
307
|
-
|
|
326
|
+
namespaceIdentity === defaultIdentity ||
|
|
327
|
+
namespaceIdentity === sharedIdentity ||
|
|
308
328
|
dynamicUserProjectCollision ||
|
|
309
|
-
(config.namespacePolicies ?? []).some(
|
|
329
|
+
(config.namespacePolicies ?? []).some(
|
|
330
|
+
(policy) => normalizeNamespaceIdentity(policy.name) === namespaceIdentity,
|
|
331
|
+
);
|
|
310
332
|
const policyReadable = !protectedNamespace || canReadNamespace(principal, namespace, config);
|
|
311
333
|
const policyWritable = !protectedNamespace || canWriteNamespace(principal, namespace, config);
|
|
312
334
|
const policyBlocked = protectedNamespace && (!policyReadable || !policyWritable);
|
|
@@ -6,7 +6,11 @@ import { StorageManager } from "../storage.js";
|
|
|
6
6
|
import { keyring, secureStoreDir } from "../secure-store/index.js";
|
|
7
7
|
import type { PluginConfig } from "../types.js";
|
|
8
8
|
import { ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
namespaceIdentityLegacyToken,
|
|
11
|
+
namespaceIdentityToken,
|
|
12
|
+
normalizeNamespaceIdentity,
|
|
13
|
+
} from "./identity.js";
|
|
10
14
|
import type { NamespaceCatalog } from "./catalog.js";
|
|
11
15
|
import { MutationSerializer } from "../utils/serialize-mutations.js";
|
|
12
16
|
|
|
@@ -153,19 +157,43 @@ export async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise
|
|
|
153
157
|
// back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes
|
|
154
158
|
// internally, so the tokenized path is unaffected.
|
|
155
159
|
const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
160
|
+
const defaultNfdIdentity = defaultIdentity.normalize("NFD");
|
|
156
161
|
const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);
|
|
162
|
+
const legacyNfdNsDir = resolveNamespaceDir(config.memoryDir, defaultNfdIdentity);
|
|
157
163
|
const tokenizedNsDir = resolveNamespaceDir(
|
|
158
164
|
config.memoryDir,
|
|
159
|
-
namespaceIdentityToken(
|
|
165
|
+
namespaceIdentityToken(defaultIdentity),
|
|
166
|
+
);
|
|
167
|
+
const legacyTokenNsDir = resolveNamespaceDir(
|
|
168
|
+
config.memoryDir,
|
|
169
|
+
namespaceIdentityLegacyToken(defaultNfdIdentity),
|
|
160
170
|
);
|
|
161
171
|
const tokenizedHasData =
|
|
162
172
|
(await exists(tokenizedNsDir)) &&
|
|
163
173
|
(await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));
|
|
174
|
+
const legacyTokenHasData =
|
|
175
|
+
(await exists(legacyTokenNsDir)) &&
|
|
176
|
+
(await hasAnyNamespaceStorageMarker(legacyTokenNsDir, { includeRuntimeState: true }));
|
|
177
|
+
const legacyHasData =
|
|
178
|
+
(await exists(legacyNsDir)) &&
|
|
179
|
+
(await hasAnyNamespaceStorageMarker(legacyNsDir, { includeRuntimeState: true }));
|
|
180
|
+
const legacyNfdHasData =
|
|
181
|
+
legacyNfdNsDir !== legacyNsDir &&
|
|
182
|
+
(await exists(legacyNfdNsDir)) &&
|
|
183
|
+
(await hasAnyNamespaceStorageMarker(legacyNfdNsDir, { includeRuntimeState: true }));
|
|
164
184
|
const nsDir = tokenizedHasData
|
|
165
185
|
? tokenizedNsDir
|
|
166
|
-
:
|
|
167
|
-
?
|
|
168
|
-
:
|
|
186
|
+
: legacyTokenHasData
|
|
187
|
+
? legacyTokenNsDir
|
|
188
|
+
: legacyHasData
|
|
189
|
+
? legacyNsDir
|
|
190
|
+
: legacyNfdHasData
|
|
191
|
+
? legacyNfdNsDir
|
|
192
|
+
: (await exists(legacyNsDir))
|
|
193
|
+
? legacyNsDir
|
|
194
|
+
: legacyNfdNsDir !== legacyNsDir && (await exists(legacyNfdNsDir))
|
|
195
|
+
? legacyNfdNsDir
|
|
196
|
+
: tokenizedNsDir;
|
|
169
197
|
return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))
|
|
170
198
|
? nsDir
|
|
171
199
|
: config.memoryDir;
|
|
@@ -192,15 +220,37 @@ export async function resolveNamespaceStorageRoot(
|
|
|
192
220
|
if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {
|
|
193
221
|
return resolveDefaultNamespaceRoot(config);
|
|
194
222
|
}
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
const normalized = normalizeNamespaceIdentity(namespace);
|
|
224
|
+
const legacyExactNamespace = namespace.trim();
|
|
225
|
+
const legacyNfdNamespace = normalized.normalize("NFD");
|
|
226
|
+
const legacyRoot = resolveNamespaceDir(config.memoryDir, normalized);
|
|
227
|
+
const legacyExactRoot =
|
|
228
|
+
legacyExactNamespace !== normalized && isSafeRouteNamespace(legacyExactNamespace)
|
|
229
|
+
? resolveNamespaceDir(config.memoryDir, legacyExactNamespace)
|
|
230
|
+
: null;
|
|
231
|
+
const legacyNfdRoot =
|
|
232
|
+
legacyNfdNamespace !== normalized
|
|
233
|
+
? resolveNamespaceDir(config.memoryDir, legacyNfdNamespace)
|
|
234
|
+
: null;
|
|
235
|
+
const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(normalized));
|
|
236
|
+
const legacyTokenRoots = [
|
|
237
|
+
namespaceIdentityLegacyToken(legacyExactNamespace),
|
|
238
|
+
namespaceIdentityLegacyToken(legacyNfdNamespace),
|
|
239
|
+
]
|
|
240
|
+
.filter((token, index, tokens) => tokens.indexOf(token) === index)
|
|
241
|
+
.map((token) => resolveNamespaceDir(config.memoryDir, token));
|
|
242
|
+
for (const candidate of [tokenizedRoot, ...legacyTokenRoots]) {
|
|
243
|
+
if (
|
|
244
|
+
(await exists(candidate)) &&
|
|
245
|
+
(await hasAnyNamespaceStorageMarker(candidate, { includeRuntimeState: true }))
|
|
246
|
+
) {
|
|
247
|
+
return candidate;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const candidate of [legacyRoot, legacyExactRoot, legacyNfdRoot]) {
|
|
251
|
+
if (candidate && (await exists(candidate))) return candidate;
|
|
202
252
|
}
|
|
203
|
-
return
|
|
253
|
+
return tokenizedRoot;
|
|
204
254
|
}
|
|
205
255
|
|
|
206
256
|
export class NamespaceStorageRouter {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import type { PluginConfig } from "../types.js";
|
|
8
|
+
import { NamespaceCatalog } from "./catalog.js";
|
|
9
|
+
import { namespaceIdentityFromToken, namespaceIdentityLegacyToken, namespaceIdentityToken } from "./identity.js";
|
|
10
|
+
import { resolveDefaultNamespaceRoot, resolveNamespaceStorageRoot } from "./storage.js";
|
|
11
|
+
|
|
12
|
+
function makeConfig(memoryDir: string): PluginConfig {
|
|
13
|
+
return {
|
|
14
|
+
memoryDir,
|
|
15
|
+
namespacesEnabled: true,
|
|
16
|
+
namespaceCatalogEnabled: true,
|
|
17
|
+
defaultNamespace: "default",
|
|
18
|
+
sharedNamespace: "shared",
|
|
19
|
+
namespacePolicies: [],
|
|
20
|
+
entitySchemas: {},
|
|
21
|
+
} as unknown as PluginConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("Unicode namespaces decode from disk and list in stable order", async () => {
|
|
25
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-unicode-routing-"));
|
|
26
|
+
try {
|
|
27
|
+
const config = makeConfig(memoryDir);
|
|
28
|
+
const catalog = new NamespaceCatalog(config);
|
|
29
|
+
const cjk = "项目";
|
|
30
|
+
const ascii = "alpha";
|
|
31
|
+
|
|
32
|
+
assert.equal(namespaceIdentityFromToken(namespaceIdentityToken(cjk)), cjk);
|
|
33
|
+
assert.equal(namespaceIdentityToken("Cafe\u0301"), namespaceIdentityToken("Café"));
|
|
34
|
+
const nfd = "Café".normalize("NFD");
|
|
35
|
+
const legacyToken = namespaceIdentityLegacyToken(nfd);
|
|
36
|
+
assert.equal(namespaceIdentityFromToken(legacyToken), "Café");
|
|
37
|
+
await mkdir(path.join(memoryDir, "namespaces", legacyToken, "facts"), { recursive: true });
|
|
38
|
+
assert.equal(
|
|
39
|
+
await resolveNamespaceStorageRoot(config, "Café"),
|
|
40
|
+
path.join(memoryDir, "namespaces", legacyToken),
|
|
41
|
+
);
|
|
42
|
+
const rebuilt = await catalog.rebuildFromDisk({ dryRun: true });
|
|
43
|
+
const legacyRecord = rebuilt.records.find((record) => record.namespace === "Café");
|
|
44
|
+
assert.equal(legacyRecord?.storageDir, path.join(memoryDir, "namespaces", legacyToken));
|
|
45
|
+
await catalog.markWrite(cjk, { discoveredBy: "write" });
|
|
46
|
+
await catalog.markWrite(ascii, { discoveredBy: "write" });
|
|
47
|
+
|
|
48
|
+
const first = await catalog.listNamespaces();
|
|
49
|
+
const second = await catalog.listNamespaces();
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
first.map((record) => record.namespace),
|
|
52
|
+
second.map((record) => record.namespace)
|
|
53
|
+
);
|
|
54
|
+
assert.ok(first.some((record) => record.namespace === cjk));
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("default Unicode namespace resolves legacy NFD roots", async () => {
|
|
61
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-unicode-default-"));
|
|
62
|
+
try {
|
|
63
|
+
const config = { ...makeConfig(memoryDir), defaultNamespace: "Café" };
|
|
64
|
+
const nfd = config.defaultNamespace.normalize("NFD");
|
|
65
|
+
const nfdRoot = path.join(memoryDir, "namespaces", nfd);
|
|
66
|
+
await mkdir(path.join(nfdRoot, "facts"), { recursive: true });
|
|
67
|
+
assert.equal(await resolveDefaultNamespaceRoot(config), nfdRoot);
|
|
68
|
+
} finally {
|
|
69
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|