@remnic/core 9.66.11 → 9.66.12
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 +81 -81
- 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 +25 -25
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +41 -41
- 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 +32 -32
- 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-operations-batch.js +26 -26
- package/dist/access-operations.d.ts +6 -6
- package/dist/access-operations.js +31 -31
- 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 +89 -89
- package/dist/access-schema.js +4 -4
- 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/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-BCM6ZAJ3.js → chunk-GHSJBVVQ.js} +95 -55
- package/dist/chunk-GHSJBVVQ.js.map +1 -0
- 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-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-CFpMhqz5.d.ts → cli-Bvy4jdBu.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- 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.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-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 +700 -681
- 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/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/memory-subject-cli.d.ts +1 -1
- package/dist/memory-subject.d.ts +1 -1
- 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/{public-http-C9-CHc40.d.ts → public-http-2MyshWxl.d.ts} +1 -1
- 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/schemas.d.ts +176 -176
- 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/shared-context/manager.d.ts +6 -6
- 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.d.ts +4 -4
- 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/transfer/types.d.ts +78 -78
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.d.ts +1 -1
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.d.ts +1 -1
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- 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/{access-service-DcfM0Vf0.d.ts → access-service-LbwWl05g.d.ts} +64 -64
- /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,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
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MAX_REFINES_PER_INVOCATION,
|
|
6
|
+
validateRefineRewrite,
|
|
7
|
+
} from "./recall-deep-rewrite.js";
|
|
8
|
+
|
|
9
|
+
const base = {
|
|
10
|
+
currentQuery: "who met at the cafe",
|
|
11
|
+
refinesUsed: 0,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test("genuine rewrite succeeds and is trimmed", () => {
|
|
15
|
+
assert.deepEqual(
|
|
16
|
+
validateRefineRewrite({
|
|
17
|
+
...base,
|
|
18
|
+
refinedQuery: " which people met at the cafe on friday ",
|
|
19
|
+
}),
|
|
20
|
+
{ ok: true, refinedQuery: "which people met at the cafe on friday" },
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("success keeps internal whitespace as written", () => {
|
|
25
|
+
assert.deepEqual(
|
|
26
|
+
validateRefineRewrite({
|
|
27
|
+
...base,
|
|
28
|
+
refinedQuery: "which people met at the cafe",
|
|
29
|
+
}),
|
|
30
|
+
{ ok: true, refinedQuery: "which people met at the cafe" },
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("refinesUsed 0 is allowed", () => {
|
|
35
|
+
assert.equal(
|
|
36
|
+
validateRefineRewrite({
|
|
37
|
+
...base,
|
|
38
|
+
refinesUsed: 0,
|
|
39
|
+
refinedQuery: "which people met at the cafe",
|
|
40
|
+
}).ok,
|
|
41
|
+
true,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("refinesUsed 1 is allowed", () => {
|
|
46
|
+
assert.equal(
|
|
47
|
+
validateRefineRewrite({
|
|
48
|
+
...base,
|
|
49
|
+
refinesUsed: 1,
|
|
50
|
+
refinedQuery: "which people met at the cafe",
|
|
51
|
+
}).ok,
|
|
52
|
+
true,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("refinesUsed 2 gives refine_budget_spent", () => {
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
validateRefineRewrite({
|
|
59
|
+
...base,
|
|
60
|
+
refinesUsed: 2,
|
|
61
|
+
refinedQuery: "which people met at the cafe",
|
|
62
|
+
}),
|
|
63
|
+
{ ok: false, stop: true, reason: "refine_budget_spent" },
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("refinesUsed 3 gives refine_budget_spent", () => {
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
validateRefineRewrite({
|
|
70
|
+
...base,
|
|
71
|
+
refinesUsed: 3,
|
|
72
|
+
refinedQuery: "which people met at the cafe",
|
|
73
|
+
}),
|
|
74
|
+
{ ok: false, stop: true, reason: "refine_budget_spent" },
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("budget check wins over a valid rewrite", () => {
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
validateRefineRewrite({
|
|
81
|
+
currentQuery: "who met at the cafe",
|
|
82
|
+
refinedQuery: "completely different and clearly better query",
|
|
83
|
+
refinesUsed: MAX_REFINES_PER_INVOCATION,
|
|
84
|
+
}),
|
|
85
|
+
{ ok: false, stop: true, reason: "refine_budget_spent" },
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("negative refinesUsed throws", () => {
|
|
90
|
+
assert.throws(
|
|
91
|
+
() => validateRefineRewrite({ ...base, refinedQuery: "new query", refinesUsed: -1 }),
|
|
92
|
+
RangeError,
|
|
93
|
+
);
|
|
94
|
+
assert.throws(
|
|
95
|
+
() => validateRefineRewrite({ ...base, refinedQuery: "new query", refinesUsed: -1 }),
|
|
96
|
+
/refinesUsed/,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("float refinesUsed throws", () => {
|
|
101
|
+
assert.throws(
|
|
102
|
+
() => validateRefineRewrite({ ...base, refinedQuery: "new query", refinesUsed: 1.5 }),
|
|
103
|
+
/refinesUsed/,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("NaN refinesUsed throws", () => {
|
|
108
|
+
assert.throws(
|
|
109
|
+
() => validateRefineRewrite({ ...base, refinedQuery: "new query", refinesUsed: Number.NaN }),
|
|
110
|
+
/refinesUsed/,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("empty string rewrite gives empty_rewrite", () => {
|
|
115
|
+
assert.deepEqual(validateRefineRewrite({ ...base, refinedQuery: "" }), {
|
|
116
|
+
ok: false,
|
|
117
|
+
stop: true,
|
|
118
|
+
reason: "empty_rewrite",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("whitespace-only rewrite gives empty_rewrite", () => {
|
|
123
|
+
assert.deepEqual(validateRefineRewrite({ ...base, refinedQuery: " " }), {
|
|
124
|
+
ok: false,
|
|
125
|
+
stop: true,
|
|
126
|
+
reason: "empty_rewrite",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("null rewrite gives empty_rewrite", () => {
|
|
131
|
+
assert.deepEqual(validateRefineRewrite({ ...base, refinedQuery: null }), {
|
|
132
|
+
ok: false,
|
|
133
|
+
stop: true,
|
|
134
|
+
reason: "empty_rewrite",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("undefined rewrite gives empty_rewrite", () => {
|
|
139
|
+
assert.deepEqual(validateRefineRewrite({ ...base, refinedQuery: undefined }), {
|
|
140
|
+
ok: false,
|
|
141
|
+
stop: true,
|
|
142
|
+
reason: "empty_rewrite",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("number rewrite gives empty_rewrite", () => {
|
|
147
|
+
assert.deepEqual(validateRefineRewrite({ ...base, refinedQuery: 42 }), {
|
|
148
|
+
ok: false,
|
|
149
|
+
stop: true,
|
|
150
|
+
reason: "empty_rewrite",
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("exact duplicate gives identical_rewrite", () => {
|
|
155
|
+
assert.deepEqual(
|
|
156
|
+
validateRefineRewrite({
|
|
157
|
+
...base,
|
|
158
|
+
refinedQuery: "who met at the cafe",
|
|
159
|
+
}),
|
|
160
|
+
{ ok: false, stop: true, reason: "identical_rewrite" },
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("case-different duplicate gives identical_rewrite", () => {
|
|
165
|
+
assert.deepEqual(
|
|
166
|
+
validateRefineRewrite({
|
|
167
|
+
...base,
|
|
168
|
+
refinedQuery: " Who Met At The Cafe ",
|
|
169
|
+
}),
|
|
170
|
+
{ ok: false, stop: true, reason: "identical_rewrite" },
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("extra internal whitespace duplicate gives identical_rewrite", () => {
|
|
175
|
+
assert.deepEqual(
|
|
176
|
+
validateRefineRewrite({
|
|
177
|
+
currentQuery: "who met",
|
|
178
|
+
refinedQuery: "who met",
|
|
179
|
+
refinesUsed: 0,
|
|
180
|
+
}),
|
|
181
|
+
{ ok: false, stop: true, reason: "identical_rewrite" },
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("every failure carries stop true", () => {
|
|
186
|
+
const failures = [
|
|
187
|
+
validateRefineRewrite({
|
|
188
|
+
...base,
|
|
189
|
+
refinedQuery: "fine",
|
|
190
|
+
refinesUsed: 2,
|
|
191
|
+
}),
|
|
192
|
+
validateRefineRewrite({ ...base, refinedQuery: " " }),
|
|
193
|
+
validateRefineRewrite({ ...base, refinedQuery: "who met at the cafe" }),
|
|
194
|
+
];
|
|
195
|
+
for (const failure of failures) {
|
|
196
|
+
assert.equal(failure.ok, false);
|
|
197
|
+
assert.equal(failure.stop, true);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a deep-recall REFINE rewrite (issue #2332).
|
|
3
|
+
*
|
|
4
|
+
* Pure. The budget check runs before the rewrite is inspected, so an
|
|
5
|
+
* exhausted budget cannot be bypassed by a valid-looking query. An empty
|
|
6
|
+
* or no-progress rewrite is rejected and treated as STOP.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_REFINES_PER_INVOCATION = 2;
|
|
9
|
+
|
|
10
|
+
export type RefineRewriteResult =
|
|
11
|
+
| { ok: true; refinedQuery: string }
|
|
12
|
+
| {
|
|
13
|
+
ok: false;
|
|
14
|
+
stop: true;
|
|
15
|
+
reason: "empty_rewrite" | "identical_rewrite" | "refine_budget_spent";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function validateRefineRewrite(input: {
|
|
19
|
+
currentQuery: string;
|
|
20
|
+
refinedQuery: unknown;
|
|
21
|
+
refinesUsed: number;
|
|
22
|
+
}): RefineRewriteResult {
|
|
23
|
+
if (!Number.isInteger(input.refinesUsed) || input.refinesUsed < 0) {
|
|
24
|
+
throw new RangeError(
|
|
25
|
+
`refinesUsed must be a non-negative integer; got ${JSON.stringify(input.refinesUsed)}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (input.refinesUsed >= MAX_REFINES_PER_INVOCATION) {
|
|
29
|
+
return { ok: false, stop: true, reason: "refine_budget_spent" };
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
typeof input.refinedQuery !== "string" ||
|
|
33
|
+
input.refinedQuery.trim() === ""
|
|
34
|
+
) {
|
|
35
|
+
return { ok: false, stop: true, reason: "empty_rewrite" };
|
|
36
|
+
}
|
|
37
|
+
const trimmed = input.refinedQuery.trim();
|
|
38
|
+
// Comparison key: case-folded with internal whitespace runs collapsed, so
|
|
39
|
+
// pure case/spacing drift is not progress. The returned rewrite keeps
|
|
40
|
+
// internal whitespace as the model wrote it.
|
|
41
|
+
const currentKey = input.currentQuery
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/\s+/g, " ");
|
|
45
|
+
const refinedKey = trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
46
|
+
if (refinedKey === currentKey) {
|
|
47
|
+
return { ok: false, stop: true, reason: "identical_rewrite" };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, refinedQuery: trimmed };
|
|
50
|
+
}
|