@remnic/core 9.66.11 → 9.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +23 -23
- package/dist/access-authorization-probe.js +24 -24
- package/dist/access-boundary.js +23 -23
- package/dist/access-cli.js +81 -81
- package/dist/access-extraction-force-flush.js +23 -23
- package/dist/access-http-query.js +25 -25
- package/dist/access-http.js +41 -41
- package/dist/access-identity-continuity-surface.js +23 -23
- package/dist/access-lcm-surface.js +23 -23
- package/dist/access-mcp.js +32 -32
- package/dist/access-namespace-preflight.js +23 -23
- package/dist/access-observe-write-surface.js +23 -23
- package/dist/access-operations-batch.js +26 -26
- package/dist/access-operations.js +31 -31
- package/dist/access-recall-concurrency.js +23 -23
- package/dist/access-recall-response.js +23 -23
- package/dist/access-recall-surface.js +23 -23
- package/dist/access-schema.js +4 -4
- package/dist/access-service.js +23 -23
- package/dist/access-surface-catalog.js +25 -25
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +10 -10
- package/dist/buffer.js +4 -4
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-XN33JLRF.js → chunk-46XRYYOJ.js} +3 -3
- package/dist/{chunk-WD424KYT.js → chunk-4L7OKRGP.js} +2 -2
- package/dist/{chunk-XZ7QLOTJ.js → chunk-6KGBI54C.js} +2 -2
- package/dist/{chunk-245QROHZ.js → chunk-6SRUPUTY.js} +7 -7
- package/dist/{chunk-ZIGSNLYI.js → chunk-A6HVSRGM.js} +3 -3
- package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
- package/dist/{chunk-7BS5UVXA.js → chunk-AWMS3IFB.js} +3 -3
- package/dist/{chunk-HJQZQUPC.js → chunk-BE4Y6GK6.js} +6 -6
- package/dist/{chunk-D3AUQXTJ.js → chunk-BT7AQZQX.js} +2 -2
- package/dist/{chunk-IPLYGWQF.js → chunk-BUKK5SWA.js} +6 -6
- package/dist/{chunk-CEH2AFXG.js → chunk-C24UZSFW.js} +6 -6
- package/dist/{chunk-HCERYDRI.js → chunk-CJTZO5MW.js} +2 -2
- package/dist/{chunk-4QKWBVEE.js → chunk-CR73YTWC.js} +13 -13
- package/dist/{chunk-LTWFU6VJ.js → chunk-DDAHG5ZD.js} +2 -2
- package/dist/{chunk-S47XNAWD.js → chunk-EU5PDHU2.js} +2 -2
- package/dist/{chunk-KT3ERTF6.js → chunk-F3H7VNM4.js} +2 -2
- package/dist/{chunk-EG4FHU5C.js → chunk-FJP6MVRK.js} +2 -2
- package/dist/{chunk-7FLINMUE.js → chunk-GEHEBLVY.js} +2 -2
- package/dist/{chunk-G3B3LP6A.js → chunk-GRZYF6KK.js} +14 -14
- package/dist/{chunk-E2PORO7O.js → chunk-H6VYVOVP.js} +2 -2
- package/dist/{chunk-QKTGCL6M.js → chunk-HGRRJDUH.js} +2 -2
- package/dist/{chunk-J2R275FN.js → chunk-HQJM3BKH.js} +2 -2
- package/dist/{chunk-ZRQPX2DW.js → chunk-I3X5NWDU.js} +2 -2
- package/dist/{chunk-TGKLVAML.js → chunk-I7FVQMAA.js} +2 -2
- package/dist/{chunk-UWOA5L6N.js → chunk-IDPFBRPJ.js} +35 -35
- package/dist/{chunk-CNZVEU4X.js → chunk-IE7N2YK6.js} +2 -2
- package/dist/{chunk-PVV42HM4.js → chunk-IYFL3534.js} +2 -2
- package/dist/{chunk-X5SDZMO7.js → chunk-JGXMA6HL.js} +2 -2
- package/dist/{chunk-SQEIPTDV.js → chunk-JKXPFIPC.js} +2 -2
- package/dist/{chunk-Y2ECPAJI.js → chunk-K5JFIA44.js} +7 -7
- package/dist/{chunk-JNZSNJ43.js → chunk-K7VTD5AZ.js} +2 -2
- package/dist/{chunk-E62M4H4M.js → chunk-KDCPIXIY.js} +2 -2
- package/dist/{chunk-TS37MXC7.js → chunk-KZAN4JXY.js} +2 -2
- package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
- package/dist/{chunk-5E7KXID5.js → chunk-M3BFW3VX.js} +1 -1
- package/dist/{chunk-PWVRMD37.js → chunk-MUYJQ273.js} +2 -2
- package/dist/{chunk-5KVMT25I.js → chunk-MWLBRFLY.js} +2 -2
- package/dist/{chunk-2OXBS2LV.js → chunk-NROLO2HY.js} +2 -2
- package/dist/{chunk-6H7JWYTC.js → chunk-O2HDDAUM.js} +3 -3
- package/dist/{chunk-RLEJULGN.js → chunk-OMA2B7GY.js} +2 -2
- package/dist/{chunk-K6PWPC7O.js → chunk-OQT34EVH.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-4PWANX5V.js → chunk-PKRYQ222.js} +6 -6
- package/dist/{chunk-BCM6ZAJ3.js → chunk-QJLTAH5X.js} +208 -61
- package/dist/chunk-QJLTAH5X.js.map +1 -0
- package/dist/{chunk-DJLH3XWM.js → chunk-QMIKXIAM.js} +2 -2
- package/dist/{chunk-Y6KDJQLK.js → chunk-QRHKJT7F.js} +7 -7
- package/dist/{chunk-7MOYAT4Y.js → chunk-QXDICB4J.js} +3 -3
- package/dist/{chunk-KWLDB64F.js → chunk-QZPSIQRB.js} +2 -2
- package/dist/{chunk-54BRW3BL.js → chunk-REJCULWA.js} +2 -2
- package/dist/{chunk-ISX7JODR.js → chunk-SBUQ4KIE.js} +2 -2
- package/dist/{chunk-HPGXB56F.js → chunk-STUPJFG7.js} +2 -2
- package/dist/{chunk-LPSTQR26.js → chunk-SV3MDTJI.js} +2 -2
- package/dist/{chunk-MK7EJLB4.js → chunk-UJICQ77Y.js} +2 -2
- package/dist/{chunk-4PHT7KE2.js → chunk-UUGU26P2.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-QB66LBXA.js → chunk-WGVAEVT7.js} +2 -2
- package/dist/{chunk-2L5FSNPC.js → chunk-X2NFFBTO.js} +7 -7
- package/dist/{chunk-EMAEDM2J.js → chunk-YWD5UIWY.js} +2 -2
- package/dist/{chunk-C6TMLF3A.js → chunk-ZFE7NXB7.js} +4 -4
- package/dist/cli.js +57 -57
- package/dist/coding/export-okf-codegraph.js +10 -10
- package/dist/compounding/engine.js +10 -10
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/convergence-refresh.js +12 -12
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +12 -12
- package/dist/entity-retrieval.js +10 -10
- package/dist/external-wiki-access.js +24 -24
- package/dist/extraction-span-provenance.d.ts +28 -0
- package/dist/extraction-span-provenance.js +24 -0
- package/dist/extraction-span-provenance.js.map +1 -0
- package/dist/extraction.js +13 -13
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +34 -6
- package/dist/index.js +115 -109
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +6 -6
- package/dist/location/index.js +12 -12
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.js +10 -10
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/maintenance.js +18 -18
- package/dist/orchestrator.js +82 -82
- package/dist/qmd.js +4 -4
- package/dist/recall-deep-rewrite.d.ts +23 -0
- package/dist/recall-deep-rewrite.js +29 -0
- package/dist/recall-deep-rewrite.js.map +1 -0
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +11 -11
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/source-agent-qualifier.js +12 -12
- package/dist/standup/build.js +10 -10
- package/dist/storage.js +9 -9
- package/dist/support-passport/index.js +26 -26
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +11 -11
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- package/src/activity/index.ts +1 -0
- package/src/activity/journal-section.ts +1 -1
- package/src/activity/journal-strip.test.ts +126 -0
- package/src/activity/journal-strip.ts +115 -0
- package/src/activity/timeline/analysis-failure.test.ts +104 -0
- package/src/activity/timeline/analysis-failure.ts +60 -0
- package/src/activity/timeline/index.ts +1 -0
- package/src/dedup/index.ts +7 -0
- package/src/dedup/merge-verdict.test.ts +74 -0
- package/src/dedup/merge-verdict.ts +31 -0
- package/src/extraction-span-provenance.test.ts +85 -0
- package/src/extraction-span-provenance.ts +41 -0
- package/src/recall-deep-rewrite.test.ts +199 -0
- package/src/recall-deep-rewrite.ts +50 -0
- package/dist/chunk-BCM6ZAJ3.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-XN33JLRF.js.map → chunk-46XRYYOJ.js.map} +0 -0
- /package/dist/{chunk-WD424KYT.js.map → chunk-4L7OKRGP.js.map} +0 -0
- /package/dist/{chunk-XZ7QLOTJ.js.map → chunk-6KGBI54C.js.map} +0 -0
- /package/dist/{chunk-245QROHZ.js.map → chunk-6SRUPUTY.js.map} +0 -0
- /package/dist/{chunk-ZIGSNLYI.js.map → chunk-A6HVSRGM.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
- /package/dist/{chunk-7BS5UVXA.js.map → chunk-AWMS3IFB.js.map} +0 -0
- /package/dist/{chunk-HJQZQUPC.js.map → chunk-BE4Y6GK6.js.map} +0 -0
- /package/dist/{chunk-D3AUQXTJ.js.map → chunk-BT7AQZQX.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-CEH2AFXG.js.map → chunk-C24UZSFW.js.map} +0 -0
- /package/dist/{chunk-HCERYDRI.js.map → chunk-CJTZO5MW.js.map} +0 -0
- /package/dist/{chunk-4QKWBVEE.js.map → chunk-CR73YTWC.js.map} +0 -0
- /package/dist/{chunk-LTWFU6VJ.js.map → chunk-DDAHG5ZD.js.map} +0 -0
- /package/dist/{chunk-S47XNAWD.js.map → chunk-EU5PDHU2.js.map} +0 -0
- /package/dist/{chunk-KT3ERTF6.js.map → chunk-F3H7VNM4.js.map} +0 -0
- /package/dist/{chunk-EG4FHU5C.js.map → chunk-FJP6MVRK.js.map} +0 -0
- /package/dist/{chunk-7FLINMUE.js.map → chunk-GEHEBLVY.js.map} +0 -0
- /package/dist/{chunk-G3B3LP6A.js.map → chunk-GRZYF6KK.js.map} +0 -0
- /package/dist/{chunk-E2PORO7O.js.map → chunk-H6VYVOVP.js.map} +0 -0
- /package/dist/{chunk-QKTGCL6M.js.map → chunk-HGRRJDUH.js.map} +0 -0
- /package/dist/{chunk-J2R275FN.js.map → chunk-HQJM3BKH.js.map} +0 -0
- /package/dist/{chunk-ZRQPX2DW.js.map → chunk-I3X5NWDU.js.map} +0 -0
- /package/dist/{chunk-TGKLVAML.js.map → chunk-I7FVQMAA.js.map} +0 -0
- /package/dist/{chunk-UWOA5L6N.js.map → chunk-IDPFBRPJ.js.map} +0 -0
- /package/dist/{chunk-CNZVEU4X.js.map → chunk-IE7N2YK6.js.map} +0 -0
- /package/dist/{chunk-PVV42HM4.js.map → chunk-IYFL3534.js.map} +0 -0
- /package/dist/{chunk-X5SDZMO7.js.map → chunk-JGXMA6HL.js.map} +0 -0
- /package/dist/{chunk-SQEIPTDV.js.map → chunk-JKXPFIPC.js.map} +0 -0
- /package/dist/{chunk-Y2ECPAJI.js.map → chunk-K5JFIA44.js.map} +0 -0
- /package/dist/{chunk-JNZSNJ43.js.map → chunk-K7VTD5AZ.js.map} +0 -0
- /package/dist/{chunk-E62M4H4M.js.map → chunk-KDCPIXIY.js.map} +0 -0
- /package/dist/{chunk-TS37MXC7.js.map → chunk-KZAN4JXY.js.map} +0 -0
- /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-5E7KXID5.js.map → chunk-M3BFW3VX.js.map} +0 -0
- /package/dist/{chunk-PWVRMD37.js.map → chunk-MUYJQ273.js.map} +0 -0
- /package/dist/{chunk-5KVMT25I.js.map → chunk-MWLBRFLY.js.map} +0 -0
- /package/dist/{chunk-2OXBS2LV.js.map → chunk-NROLO2HY.js.map} +0 -0
- /package/dist/{chunk-6H7JWYTC.js.map → chunk-O2HDDAUM.js.map} +0 -0
- /package/dist/{chunk-RLEJULGN.js.map → chunk-OMA2B7GY.js.map} +0 -0
- /package/dist/{chunk-K6PWPC7O.js.map → chunk-OQT34EVH.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-4PWANX5V.js.map → chunk-PKRYQ222.js.map} +0 -0
- /package/dist/{chunk-DJLH3XWM.js.map → chunk-QMIKXIAM.js.map} +0 -0
- /package/dist/{chunk-Y6KDJQLK.js.map → chunk-QRHKJT7F.js.map} +0 -0
- /package/dist/{chunk-7MOYAT4Y.js.map → chunk-QXDICB4J.js.map} +0 -0
- /package/dist/{chunk-KWLDB64F.js.map → chunk-QZPSIQRB.js.map} +0 -0
- /package/dist/{chunk-54BRW3BL.js.map → chunk-REJCULWA.js.map} +0 -0
- /package/dist/{chunk-ISX7JODR.js.map → chunk-SBUQ4KIE.js.map} +0 -0
- /package/dist/{chunk-HPGXB56F.js.map → chunk-STUPJFG7.js.map} +0 -0
- /package/dist/{chunk-LPSTQR26.js.map → chunk-SV3MDTJI.js.map} +0 -0
- /package/dist/{chunk-MK7EJLB4.js.map → chunk-UJICQ77Y.js.map} +0 -0
- /package/dist/{chunk-4PHT7KE2.js.map → chunk-UUGU26P2.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-QB66LBXA.js.map → chunk-WGVAEVT7.js.map} +0 -0
- /package/dist/{chunk-2L5FSNPC.js.map → chunk-X2NFFBTO.js.map} +0 -0
- /package/dist/{chunk-EMAEDM2J.js.map → chunk-YWD5UIWY.js.map} +0 -0
- /package/dist/{chunk-C6TMLF3A.js.map → chunk-ZFE7NXB7.js.map} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { stripRemnicOwnedRegions } from "./journal-strip.js";
|
|
5
|
+
|
|
6
|
+
test("stripRemnicOwnedRegions removes a marker pair and keeps user text", () => {
|
|
7
|
+
const text = [
|
|
8
|
+
"Morning standup.",
|
|
9
|
+
"<!-- remnic:timeline:start -->",
|
|
10
|
+
"9:00 deep work",
|
|
11
|
+
"<!-- remnic:timeline:end -->",
|
|
12
|
+
"Wrote the parser.",
|
|
13
|
+
].join("\n");
|
|
14
|
+
assert.equal(stripRemnicOwnedRegions(text, []), "Morning standup.\nWrote the parser.");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("stripRemnicOwnedRegions keeps one blank line where a removal doubled it", () => {
|
|
18
|
+
const text = [
|
|
19
|
+
"Before.",
|
|
20
|
+
"",
|
|
21
|
+
"<!-- remnic:timeline:start -->",
|
|
22
|
+
"generated",
|
|
23
|
+
"<!-- remnic:timeline:end -->",
|
|
24
|
+
"",
|
|
25
|
+
"After.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
assert.equal(stripRemnicOwnedRegions(text, []), "Before.\n\nAfter.");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("stripRemnicOwnedRegions removes two distinct marker pairs", () => {
|
|
31
|
+
const text = [
|
|
32
|
+
"head",
|
|
33
|
+
"<!-- remnic:timeline:start -->",
|
|
34
|
+
"timeline body",
|
|
35
|
+
"<!-- remnic:timeline:end -->",
|
|
36
|
+
"middle",
|
|
37
|
+
"<!-- remnic:digest:start -->",
|
|
38
|
+
"digest body",
|
|
39
|
+
"<!-- remnic:digest:end -->",
|
|
40
|
+
"tail",
|
|
41
|
+
].join("\n");
|
|
42
|
+
assert.equal(stripRemnicOwnedRegions(text, []), "head\nmiddle\ntail");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("stripRemnicOwnedRegions strips an unterminated start marker to the end of input", () => {
|
|
46
|
+
const text = ["kept", "<!-- remnic:timeline:start -->", "generated", "later user text"].join("\n");
|
|
47
|
+
assert.equal(stripRemnicOwnedRegions(text, []), "kept");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("stripRemnicOwnedRegions removes an orphan end marker and keeps surrounding text", () => {
|
|
51
|
+
const text = ["Before.", "<!-- remnic:timeline:end -->", "After."].join("\n");
|
|
52
|
+
assert.equal(stripRemnicOwnedRegions(text, []), "Before.\nAfter.");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("stripRemnicOwnedRegions removes an owned heading section up to the next same-level heading", () => {
|
|
56
|
+
const text = ["## Journal", "intro", "## Timeline", "card one", "## Notes", "kept"].join("\n");
|
|
57
|
+
assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), "## Journal\nintro\n## Notes\nkept");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("stripRemnicOwnedRegions does not end an owned section at a deeper heading", () => {
|
|
61
|
+
const text = [
|
|
62
|
+
"## Journal",
|
|
63
|
+
"## Timeline",
|
|
64
|
+
"card one",
|
|
65
|
+
"### Deep",
|
|
66
|
+
"deep card",
|
|
67
|
+
"## Notes",
|
|
68
|
+
"kept",
|
|
69
|
+
].join("\n");
|
|
70
|
+
assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), "## Journal\n## Notes\nkept");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("stripRemnicOwnedRegions preserves a non-owned heading section", () => {
|
|
74
|
+
const text = ["## Journal", "## Timeline", "card one", "## Notes", "kept"].join("\n");
|
|
75
|
+
assert.equal(stripRemnicOwnedRegions(text, ["Digest"]), text);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("stripRemnicOwnedRegions ignores blank and whitespace owned heading entries", () => {
|
|
79
|
+
const text = ["## Timeline", "card one", ""].join("\n");
|
|
80
|
+
assert.equal(stripRemnicOwnedRegions(text, ["", " ", "\t"]), text);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("stripRemnicOwnedRegions trims configured owned heading entries", () => {
|
|
84
|
+
const text = ["## Timeline", "card", "## Notes", "kept"].join("\n");
|
|
85
|
+
assert.equal(stripRemnicOwnedRegions(text, [" Timeline "]), "## Notes\nkept");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("stripRemnicOwnedRegions returns empty string for empty input", () => {
|
|
89
|
+
assert.equal(stripRemnicOwnedRegions("", ["Timeline"]), "");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("stripRemnicOwnedRegions returns unchanged text when nothing is owned", () => {
|
|
93
|
+
const text = ["first", "", "second", "", "third"].join("\n");
|
|
94
|
+
assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), text);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Regression: the journal-section reader normalizes tab separators, extra
|
|
98
|
+
// spaces, trailing whitespace, and closing # sequences. The stripper must
|
|
99
|
+
// recognize exactly the same set, or an owned heading the reader treats as
|
|
100
|
+
// the journal section would keep its generated body in the journal text.
|
|
101
|
+
test("owned heading matching accepts every ATX form the section reader accepts", () => {
|
|
102
|
+
const body = ["generated line", "", "## User Notes", "kept"].join("\n");
|
|
103
|
+
for (const headingLine of [
|
|
104
|
+
"## Timeline",
|
|
105
|
+
"## Timeline",
|
|
106
|
+
"##\tTimeline",
|
|
107
|
+
"## Timeline ",
|
|
108
|
+
"## Timeline ##",
|
|
109
|
+
"## Timeline ### ",
|
|
110
|
+
]) {
|
|
111
|
+
const stripped = stripRemnicOwnedRegions(
|
|
112
|
+
[headingLine, body].join("\n"),
|
|
113
|
+
["Timeline"],
|
|
114
|
+
);
|
|
115
|
+
assert.equal(
|
|
116
|
+
stripped,
|
|
117
|
+
["## User Notes", "kept"].join("\n"),
|
|
118
|
+
`heading line ${JSON.stringify(headingLine)} must strip as owned`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("an indented heading is not owned, matching the section reader", () => {
|
|
124
|
+
const text = [" ## Timeline", "not owned body"].join("\n");
|
|
125
|
+
assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), text);
|
|
126
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip Remnic-owned regions from a vault journal section (issue #1987).
|
|
3
|
+
*
|
|
4
|
+
* Loop prevention for vault journals: before daily-note text is treated
|
|
5
|
+
* as journal text, every managed marker region and every configured
|
|
6
|
+
* owned heading section is removed, so published output can never
|
|
7
|
+
* re-enter the journal. An unterminated start marker fails closed and
|
|
8
|
+
* strips to the end of the section. Pure string-in/string-out. No
|
|
9
|
+
* filesystem. Heading lines parse through the SAME exported ATX
|
|
10
|
+
* parser the journal-section reader uses, so a heading the reader
|
|
11
|
+
* recognizes is always a heading this stripper strips.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseAtxHeading } from "./journal-section.js";
|
|
15
|
+
|
|
16
|
+
const MARKER_OPEN = "<!-- remnic:";
|
|
17
|
+
const MARKER_CLOSE = "-->";
|
|
18
|
+
const START_SUFFIX = ":start";
|
|
19
|
+
const END_SUFFIX = ":end";
|
|
20
|
+
|
|
21
|
+
type RegionMarker = { kind: "start" | "end"; name: string };
|
|
22
|
+
|
|
23
|
+
export function stripRemnicOwnedRegions(
|
|
24
|
+
sectionText: string,
|
|
25
|
+
ownedHeadings: readonly string[],
|
|
26
|
+
): string {
|
|
27
|
+
return stripOwnedHeadings(stripMarkerRegions(sectionText), ownedHeadings);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripMarkerRegions(text: string): string {
|
|
31
|
+
const lines = text.split("\n");
|
|
32
|
+
const keep = lines.map(() => true);
|
|
33
|
+
let openName: string | null = null;
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const marker = parseRegionMarker(lines[i]!);
|
|
36
|
+
if (openName !== null) {
|
|
37
|
+
keep[i] = false;
|
|
38
|
+
if (marker && marker.kind === "end" && marker.name === openName) openName = null;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (marker) {
|
|
42
|
+
keep[i] = false;
|
|
43
|
+
if (marker.kind === "start") openName = marker.name;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return emitKeptLines(lines, keep);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stripOwnedHeadings(text: string, ownedHeadings: readonly string[]): string {
|
|
50
|
+
const wanted = ownedHeadingSet(ownedHeadings);
|
|
51
|
+
if (wanted.size === 0 || text.length === 0) return text;
|
|
52
|
+
const lines = text.split("\n");
|
|
53
|
+
const keep = lines.map(() => true);
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
if (!keep[i]) continue;
|
|
56
|
+
const heading = parseAtxHeading(lines[i]!);
|
|
57
|
+
if (!heading || !wanted.has(heading.text)) continue;
|
|
58
|
+
keep[i] = false;
|
|
59
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
60
|
+
const next = parseAtxHeading(lines[j]!);
|
|
61
|
+
if (next && next.level <= heading.level) break;
|
|
62
|
+
keep[j] = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return emitKeptLines(lines, keep);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Emit kept lines, collapsing the one blank line each removal would
|
|
70
|
+
* otherwise double. Text outside removals is preserved byte-for-byte.
|
|
71
|
+
*/
|
|
72
|
+
function emitKeptLines(lines: readonly string[], keep: readonly boolean[]): string {
|
|
73
|
+
const out: string[] = [];
|
|
74
|
+
let dropped = false;
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
if (!keep[i]) {
|
|
77
|
+
dropped = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const line = lines[i]!;
|
|
81
|
+
const previous = out.length > 0 ? out[out.length - 1]! : null;
|
|
82
|
+
if (dropped && previous !== null && previous.trim().length === 0 && line.trim().length === 0) {
|
|
83
|
+
dropped = false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
out.push(line);
|
|
87
|
+
dropped = false;
|
|
88
|
+
}
|
|
89
|
+
return out.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRegionMarker(line: string): RegionMarker | null {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed.startsWith(MARKER_OPEN) || !trimmed.endsWith(MARKER_CLOSE)) return null;
|
|
95
|
+
const inner = trimmed.slice(MARKER_OPEN.length, trimmed.length - MARKER_CLOSE.length).trim();
|
|
96
|
+
if (inner.endsWith(START_SUFFIX)) {
|
|
97
|
+
const name = inner.slice(0, -START_SUFFIX.length);
|
|
98
|
+
return name.length > 0 ? { kind: "start", name } : null;
|
|
99
|
+
}
|
|
100
|
+
if (inner.endsWith(END_SUFFIX)) {
|
|
101
|
+
const name = inner.slice(0, -END_SUFFIX.length);
|
|
102
|
+
return name.length > 0 ? { kind: "end", name } : null;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ownedHeadingSet(ownedHeadings: readonly string[]): Set<string> {
|
|
108
|
+
const wanted = new Set<string>();
|
|
109
|
+
for (const entry of ownedHeadings) {
|
|
110
|
+
if (typeof entry !== "string") continue;
|
|
111
|
+
const trimmed = entry.trim();
|
|
112
|
+
if (trimmed.length > 0) wanted.add(trimmed);
|
|
113
|
+
}
|
|
114
|
+
return wanted;
|
|
115
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ANALYSIS_FAILURE_KINDS,
|
|
6
|
+
classifyAnalysisFailure,
|
|
7
|
+
isAnalysisFailureKind,
|
|
8
|
+
type AnalysisFailureKind,
|
|
9
|
+
} from "./analysis-failure.js";
|
|
10
|
+
|
|
11
|
+
test("every known kind classifies with the explicit retryable split", () => {
|
|
12
|
+
const retryableKinds: readonly AnalysisFailureKind[] = [
|
|
13
|
+
"provider_unavailable",
|
|
14
|
+
"timeout",
|
|
15
|
+
"rate_limited",
|
|
16
|
+
];
|
|
17
|
+
const nonRetryableKinds: readonly AnalysisFailureKind[] = [
|
|
18
|
+
"aborted",
|
|
19
|
+
"malformed_json",
|
|
20
|
+
"invalid_schema",
|
|
21
|
+
"partial_output",
|
|
22
|
+
"invalid_config",
|
|
23
|
+
];
|
|
24
|
+
for (const kind of ANALYSIS_FAILURE_KINDS) {
|
|
25
|
+
const failure = classifyAnalysisFailure(kind);
|
|
26
|
+
assert.equal(failure.kind, kind);
|
|
27
|
+
assert.equal(failure.preservesDeterministic, true);
|
|
28
|
+
const expectedRetryable = retryableKinds.includes(kind);
|
|
29
|
+
assert.ok(
|
|
30
|
+
expectedRetryable || nonRetryableKinds.includes(kind),
|
|
31
|
+
`${kind} missing from the explicit retryable split`,
|
|
32
|
+
);
|
|
33
|
+
assert.equal(
|
|
34
|
+
failure.retryable,
|
|
35
|
+
expectedRetryable,
|
|
36
|
+
`${kind} retryable must be ${expectedRetryable}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the explicit retryable split covers all kinds exactly once", () => {
|
|
42
|
+
const split: readonly string[] = [
|
|
43
|
+
"provider_unavailable",
|
|
44
|
+
"timeout",
|
|
45
|
+
"rate_limited",
|
|
46
|
+
"aborted",
|
|
47
|
+
"malformed_json",
|
|
48
|
+
"invalid_schema",
|
|
49
|
+
"partial_output",
|
|
50
|
+
"invalid_config",
|
|
51
|
+
];
|
|
52
|
+
assert.equal(new Set(split).size, split.length);
|
|
53
|
+
assert.deepEqual(
|
|
54
|
+
[...ANALYSIS_FAILURE_KINDS].sort(),
|
|
55
|
+
[...split].sort(),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("unknown string kind throws TypeError naming the allowed kinds", () => {
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => classifyAnalysisFailure("provider_on_fire"),
|
|
62
|
+
(err: unknown) => {
|
|
63
|
+
assert.ok(err instanceof TypeError);
|
|
64
|
+
assert.match((err as Error).message, /unknown analysis failure/);
|
|
65
|
+
for (const kind of ANALYSIS_FAILURE_KINDS) {
|
|
66
|
+
assert.ok((err as Error).message.includes(kind));
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("empty, null, undefined, and non-string kinds throw", () => {
|
|
74
|
+
for (const bad of ["", undefined, null, 42]) {
|
|
75
|
+
assert.throws(
|
|
76
|
+
() => classifyAnalysisFailure(bad as string),
|
|
77
|
+
TypeError,
|
|
78
|
+
`expected throw for ${String(bad)}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("isAnalysisFailureKind is false for non-strings, empty, and unknown", () => {
|
|
84
|
+
assert.equal(isAnalysisFailureKind(undefined), false);
|
|
85
|
+
assert.equal(isAnalysisFailureKind(null), false);
|
|
86
|
+
assert.equal(isAnalysisFailureKind(42), false);
|
|
87
|
+
assert.equal(isAnalysisFailureKind({ kind: "timeout" }), false);
|
|
88
|
+
assert.equal(isAnalysisFailureKind(""), false);
|
|
89
|
+
assert.equal(isAnalysisFailureKind("timeout "), false);
|
|
90
|
+
assert.equal(isAnalysisFailureKind("TIMEOUT"), false);
|
|
91
|
+
assert.equal(isAnalysisFailureKind("provider_on_fire"), false);
|
|
92
|
+
for (const kind of ANALYSIS_FAILURE_KINDS) {
|
|
93
|
+
assert.equal(isAnalysisFailureKind(kind), true);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("preservesDeterministic is true for every kind", () => {
|
|
98
|
+
for (const kind of ANALYSIS_FAILURE_KINDS) {
|
|
99
|
+
assert.deepEqual(
|
|
100
|
+
classifyAnalysisFailure(kind).preservesDeterministic,
|
|
101
|
+
true,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure classification for timeline analysis (issue #2050).
|
|
3
|
+
*
|
|
4
|
+
* Unknown kinds fail closed with a TypeError. Pure: no I/O, no clock, no randomness.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const ANALYSIS_FAILURE_KINDS = [
|
|
8
|
+
"provider_unavailable",
|
|
9
|
+
"timeout",
|
|
10
|
+
"aborted",
|
|
11
|
+
"rate_limited",
|
|
12
|
+
"malformed_json",
|
|
13
|
+
"invalid_schema",
|
|
14
|
+
"partial_output",
|
|
15
|
+
"invalid_config",
|
|
16
|
+
] as const;
|
|
17
|
+
export type AnalysisFailureKind = (typeof ANALYSIS_FAILURE_KINDS)[number];
|
|
18
|
+
|
|
19
|
+
export interface AnalysisFailure {
|
|
20
|
+
kind: AnalysisFailureKind;
|
|
21
|
+
/** True only when retrying the same request could plausibly succeed. */
|
|
22
|
+
retryable: boolean;
|
|
23
|
+
/** True when the deterministic cards must be preserved unchanged. */
|
|
24
|
+
preservesDeterministic: true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RETRYABLE_BY_KIND: Readonly<Record<AnalysisFailureKind, boolean>> = {
|
|
28
|
+
provider_unavailable: true,
|
|
29
|
+
timeout: true,
|
|
30
|
+
rate_limited: true,
|
|
31
|
+
aborted: false,
|
|
32
|
+
malformed_json: false,
|
|
33
|
+
invalid_schema: false,
|
|
34
|
+
partial_output: false,
|
|
35
|
+
invalid_config: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** True when the value is one of the known failure kinds. */
|
|
39
|
+
export function isAnalysisFailureKind(
|
|
40
|
+
value: unknown,
|
|
41
|
+
): value is AnalysisFailureKind {
|
|
42
|
+
return (
|
|
43
|
+
typeof value === "string" &&
|
|
44
|
+
(ANALYSIS_FAILURE_KINDS as readonly string[]).includes(value)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Classify a failure kind. Unknown kinds throw; there is no permissive default. */
|
|
49
|
+
export function classifyAnalysisFailure(kind: string): AnalysisFailure {
|
|
50
|
+
if (!isAnalysisFailureKind(kind)) {
|
|
51
|
+
throw new TypeError(
|
|
52
|
+
`unknown analysis failure kind: ${String(kind)}; allowed kinds: ${ANALYSIS_FAILURE_KINDS.join(", ")}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
kind,
|
|
57
|
+
retryable: RETRYABLE_BY_KIND[kind],
|
|
58
|
+
preservesDeterministic: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/dedup/index.ts
CHANGED
|
@@ -467,3 +467,10 @@ export {
|
|
|
467
467
|
type MergeDecision,
|
|
468
468
|
type MergeDecisionOptions,
|
|
469
469
|
} from "./merge-decision.js";
|
|
470
|
+
|
|
471
|
+
export {
|
|
472
|
+
MERGE_JUDGE_VERDICTS,
|
|
473
|
+
parseMergeJudgeVerdict,
|
|
474
|
+
type MergeJudgeVerdictName,
|
|
475
|
+
type ParseMergeVerdictResult,
|
|
476
|
+
} from "./merge-verdict.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { MERGE_JUDGE_VERDICTS, parseMergeJudgeVerdict } from "./merge-verdict.js";
|
|
5
|
+
|
|
6
|
+
test("every allow-list name parses (loops the constant)", () => {
|
|
7
|
+
for (const name of MERGE_JUDGE_VERDICTS) {
|
|
8
|
+
const result = parseMergeJudgeVerdict(name);
|
|
9
|
+
assert.equal(result.ok, true);
|
|
10
|
+
assert.ok(result.ok);
|
|
11
|
+
assert.equal(result.verdict, name);
|
|
12
|
+
assert.equal(result.decision, name === "merge" ? "merge" : "create");
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("accepts mixed case and surrounding whitespace", () => {
|
|
17
|
+
for (const raw of ["MERGE", " merge ", "Create", "\tcreate\n", "UnCertain", " uncertain "]) {
|
|
18
|
+
const result = parseMergeJudgeVerdict(raw);
|
|
19
|
+
assert.ok(result.ok, `expected "${raw}" to parse`);
|
|
20
|
+
assert.equal(result.verdict, raw.trim().toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("only merge yields decision merge", () => {
|
|
25
|
+
const merged = parseMergeJudgeVerdict("merge");
|
|
26
|
+
assert.ok(merged.ok);
|
|
27
|
+
assert.equal(merged.decision, "merge");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("uncertain yields decision create", () => {
|
|
31
|
+
const result = parseMergeJudgeVerdict("uncertain");
|
|
32
|
+
assert.ok(result.ok);
|
|
33
|
+
assert.equal(result.verdict, "uncertain");
|
|
34
|
+
assert.equal(result.decision, "create");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("empty and non-string inputs give empty_verdict with decision create", () => {
|
|
38
|
+
const empties: unknown[] = ["", " ", null, undefined, 42, { verdict: "merge" }, ["merge"]];
|
|
39
|
+
for (const raw of empties) {
|
|
40
|
+
const result = parseMergeJudgeVerdict(raw);
|
|
41
|
+
assert.ok(!result.ok, `expected ${JSON.stringify(raw)} to fail`);
|
|
42
|
+
assert.equal(result.error, "empty_verdict");
|
|
43
|
+
assert.equal(result.decision, "create");
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("unrecognized strings give unknown_verdict with decision create", () => {
|
|
48
|
+
for (const raw of ["I think merge", "maybe", "merge them", "merg", "MERGE!"]) {
|
|
49
|
+
const result = parseMergeJudgeVerdict(raw);
|
|
50
|
+
assert.ok(!result.ok, `expected "${raw}" to fail`);
|
|
51
|
+
assert.equal(result.error, "unknown_verdict");
|
|
52
|
+
assert.equal(result.decision, "create");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("no input throws", () => {
|
|
57
|
+
const hostile: unknown[] = [
|
|
58
|
+
"",
|
|
59
|
+
" ",
|
|
60
|
+
null,
|
|
61
|
+
undefined,
|
|
62
|
+
0,
|
|
63
|
+
NaN,
|
|
64
|
+
{},
|
|
65
|
+
[],
|
|
66
|
+
() => "merge",
|
|
67
|
+
Symbol("merge"),
|
|
68
|
+
"I think merge",
|
|
69
|
+
"merge",
|
|
70
|
+
];
|
|
71
|
+
for (const raw of hostile) {
|
|
72
|
+
assert.doesNotThrow(() => parseMergeJudgeVerdict(raw));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict parser for free-form merge-judge verdicts (issue #2330 slice).
|
|
3
|
+
*
|
|
4
|
+
* Keeps "unparseable answer" distinguishable from a deliberate "create"
|
|
5
|
+
* while still failing closed: every failure carries decision "create".
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const MERGE_JUDGE_VERDICTS = ["merge", "create", "uncertain"] as const;
|
|
9
|
+
export type MergeJudgeVerdictName = (typeof MERGE_JUDGE_VERDICTS)[number];
|
|
10
|
+
|
|
11
|
+
export type ParseMergeVerdictResult =
|
|
12
|
+
| { ok: true; verdict: MergeJudgeVerdictName; decision: "merge" | "create" }
|
|
13
|
+
| { ok: false; error: "empty_verdict" | "unknown_verdict"; decision: "create" };
|
|
14
|
+
|
|
15
|
+
export function parseMergeJudgeVerdict(raw: unknown): ParseMergeVerdictResult {
|
|
16
|
+
if (typeof raw !== "string") {
|
|
17
|
+
return { ok: false, error: "empty_verdict", decision: "create" };
|
|
18
|
+
}
|
|
19
|
+
const normalized = raw.trim().toLowerCase();
|
|
20
|
+
if (normalized === "") {
|
|
21
|
+
return { ok: false, error: "empty_verdict", decision: "create" };
|
|
22
|
+
}
|
|
23
|
+
if (!(MERGE_JUDGE_VERDICTS as readonly string[]).includes(normalized)) {
|
|
24
|
+
return { ok: false, error: "unknown_verdict", decision: "create" };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
verdict: normalized as MergeJudgeVerdictName,
|
|
29
|
+
decision: normalized === "merge" ? "merge" : "create",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { spanToProvenance } from "./extraction-span-provenance.js";
|
|
4
|
+
|
|
5
|
+
const TEXT = "The quick brown fox jumps over the lazy dog";
|
|
6
|
+
|
|
7
|
+
test("spanToProvenance: mid-text span yields exact slice with echoed offsets", () => {
|
|
8
|
+
const result = spanToProvenance({ text: TEXT, start: 4, end: 19 });
|
|
9
|
+
assert.deepEqual(result, {
|
|
10
|
+
ok: true,
|
|
11
|
+
provenance: { quote: "quick brown fox", charStart: 4, charEnd: 19 },
|
|
12
|
+
});
|
|
13
|
+
if (result.ok) {
|
|
14
|
+
assert.ok(result.provenance.charEnd >= result.provenance.charStart);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("spanToProvenance: full-text span succeeds", () => {
|
|
19
|
+
const result = spanToProvenance({ text: TEXT, start: 0, end: TEXT.length });
|
|
20
|
+
assert.deepEqual(result, {
|
|
21
|
+
ok: true,
|
|
22
|
+
provenance: { quote: TEXT, charStart: 0, charEnd: TEXT.length },
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("spanToProvenance: matching supplied quote succeeds", () => {
|
|
27
|
+
const result = spanToProvenance({ text: TEXT, start: 4, end: 9, quote: "quick" });
|
|
28
|
+
assert.deepEqual(result, {
|
|
29
|
+
ok: true,
|
|
30
|
+
provenance: { quote: "quick", charStart: 4, charEnd: 9 },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("spanToProvenance: whitespace-differing quote is quote_mismatch", () => {
|
|
35
|
+
const result = spanToProvenance({ text: TEXT, start: 4, end: 15, quote: "quick brown" });
|
|
36
|
+
assert.deepEqual(result, { ok: false, error: "quote_mismatch" });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("spanToProvenance: paraphrase is quote_mismatch", () => {
|
|
40
|
+
const result = spanToProvenance({ text: TEXT, start: 4, end: 19, quote: "a fast fox" });
|
|
41
|
+
assert.deepEqual(result, { ok: false, error: "quote_mismatch" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("spanToProvenance: empty span", () => {
|
|
45
|
+
assert.deepEqual(spanToProvenance({ text: TEXT, start: 3, end: 3 }), {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: "empty_span",
|
|
48
|
+
});
|
|
49
|
+
assert.deepEqual(spanToProvenance({ text: "", start: 0, end: 0 }), {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: "empty_span",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("spanToProvenance: out of range", () => {
|
|
56
|
+
assert.deepEqual(spanToProvenance({ text: TEXT, start: -1, end: 4 }), {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: "out_of_range",
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(spanToProvenance({ text: TEXT, start: 0, end: TEXT.length + 1 }), {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: "out_of_range",
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(spanToProvenance({ text: TEXT, start: 10, end: 4 }), {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: "out_of_range",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("spanToProvenance: non-integers and non-finite offsets throw", () => {
|
|
71
|
+
assert.throws(() => spanToProvenance({ text: TEXT, start: 1.5, end: 4 }), /integers/);
|
|
72
|
+
assert.throws(() => spanToProvenance({ text: TEXT, start: 0, end: 3.2 }), /integers/);
|
|
73
|
+
assert.throws(() => spanToProvenance({ text: TEXT, start: Number.NaN, end: 4 }), /integers/);
|
|
74
|
+
assert.throws(
|
|
75
|
+
() => spanToProvenance({ text: TEXT, start: 0, end: Number.POSITIVE_INFINITY }),
|
|
76
|
+
/integers/,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("spanToProvenance: does not mutate the input object", () => {
|
|
81
|
+
const input = { text: TEXT, start: 4, end: 19, quote: "quick brown fox" };
|
|
82
|
+
const snapshot = { ...input };
|
|
83
|
+
spanToProvenance(input);
|
|
84
|
+
assert.deepEqual(input, snapshot);
|
|
85
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated span-provenance builder (issue #2333).
|
|
3
|
+
*
|
|
4
|
+
* Pure. Builds the ProvenanceSource quote/charStart/charEnd triple from
|
|
5
|
+
* offsets and verifies a model-supplied quote equals the sliced text, so a
|
|
6
|
+
* bad offset pair can never persist a quote absent from the source.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface SpanProvenance {
|
|
10
|
+
quote: string;
|
|
11
|
+
charStart: number;
|
|
12
|
+
charEnd: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SpanProvenanceResult =
|
|
16
|
+
| { ok: true; provenance: SpanProvenance }
|
|
17
|
+
| { ok: false; error: "empty_span" | "out_of_range" | "quote_mismatch" };
|
|
18
|
+
|
|
19
|
+
export function spanToProvenance(input: {
|
|
20
|
+
text: string;
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
/** Optional model-supplied quote to verify against the slice. */
|
|
24
|
+
quote?: string;
|
|
25
|
+
}): SpanProvenanceResult {
|
|
26
|
+
const { text, start, end, quote } = input;
|
|
27
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) {
|
|
28
|
+
throw new TypeError("span offsets must be integers");
|
|
29
|
+
}
|
|
30
|
+
if (start < 0 || end > text.length || end < start) {
|
|
31
|
+
return { ok: false, error: "out_of_range" };
|
|
32
|
+
}
|
|
33
|
+
if (start === end) {
|
|
34
|
+
return { ok: false, error: "empty_span" };
|
|
35
|
+
}
|
|
36
|
+
const sliced = text.slice(start, end);
|
|
37
|
+
if (quote !== undefined && quote !== sliced) {
|
|
38
|
+
return { ok: false, error: "quote_mismatch" };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, provenance: { quote: sliced, charStart: start, charEnd: end } };
|
|
41
|
+
}
|