@remnic/core 9.69.2 → 9.69.4
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 +25 -25
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +26 -26
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +25 -25
- package/dist/access-cli.js +75 -75
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +25 -25
- 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 +27 -27
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +40 -40
- package/dist/access-identity-continuity-surface.js +25 -25
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +25 -25
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +34 -34
- package/dist/access-namespace-preflight.js +25 -25
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +25 -25
- package/dist/access-operations-batch.js +28 -28
- package/dist/access-operations.d.ts +6 -6
- package/dist/access-operations.js +33 -33
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +25 -25
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +25 -25
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +25 -25
- package/dist/access-schema.d.ts +89 -89
- package/dist/access-schema.js +3 -3
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +25 -25
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +27 -27
- package/dist/active-recall.js +3 -3
- package/dist/briefing.js +7 -7
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +10 -10
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-LVFI5LG7.js → chunk-2L5FSNPC.js} +5 -5
- package/dist/{chunk-5H5TKPBA.js → chunk-35J5ABJC.js} +2 -2
- package/dist/{chunk-AI6TT4Y3.js → chunk-3HEYRDNF.js} +2 -2
- package/dist/{chunk-CCKIHXU5.js → chunk-3KURGLO7.js} +2 -2
- package/dist/{chunk-WKZSKFZC.js → chunk-5FBA6UXN.js} +7 -7
- package/dist/{chunk-THWUR7JO.js → chunk-6AYPXHOF.js} +2 -2
- package/dist/{chunk-JU5SBSV7.js → chunk-6CT4HSVJ.js} +2 -2
- package/dist/{chunk-DMR6GVTW.js → chunk-76X3F4WV.js} +6 -6
- package/dist/{chunk-6FLNUDEO.js → chunk-7DQQYJKU.js} +2 -2
- package/dist/{chunk-DKTIW44X.js → chunk-7SL6EJLT.js} +2 -2
- package/dist/{chunk-JC5GCR3K.js → chunk-ASDK4GEV.js} +2 -2
- package/dist/{chunk-YMOPYG3K.js → chunk-BIJAWZYW.js} +6 -6
- package/dist/{chunk-AWGXR2PV.js → chunk-BX6GW4KC.js} +2 -2
- package/dist/{chunk-ZOMXROJ6.js → chunk-D65XGENZ.js} +3 -3
- package/dist/{chunk-SB7PMYWV.js → chunk-DA2XDNS5.js} +5 -5
- package/dist/{chunk-LSGQRUTX.js → chunk-DT7LKLET.js} +2 -2
- package/dist/{chunk-RSPIDEY3.js → chunk-DZPX5WSG.js} +3 -3
- package/dist/{chunk-UXEAJ765.js → chunk-EA2C5Z7V.js} +31 -31
- package/dist/{chunk-ZBGODUSY.js → chunk-EB3V7PJC.js} +10 -10
- package/dist/{chunk-GPTZFSMQ.js → chunk-EJCKJS4X.js} +2 -2
- package/dist/{chunk-RGCF4XNH.js → chunk-EZPBQVGZ.js} +4 -4
- package/dist/{chunk-WJPSCTZY.js → chunk-FGORNFTZ.js} +2 -2
- package/dist/{chunk-JAQXUGLN.js → chunk-FNNWK7G7.js} +2 -2
- package/dist/{chunk-XCVGQKTR.js → chunk-GH477IGR.js} +3 -3
- package/dist/{chunk-TV52QFXK.js → chunk-HBOQO4GB.js} +10 -10
- package/dist/{chunk-D6Q5EPOV.js → chunk-HCERYDRI.js} +2 -2
- package/dist/{chunk-HCM7ZYJD.js → chunk-IEZ6CGTW.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-MB6GLPRY.js → chunk-IVKIODHO.js} +4 -4
- package/dist/{chunk-WFHBC4BQ.js → chunk-J6SWAE74.js} +2 -2
- package/dist/{chunk-PUTLM67J.js → chunk-JHTF22NQ.js} +2 -2
- package/dist/{chunk-M73TXJJW.js → chunk-LXBVTRBG.js} +2 -2
- package/dist/{chunk-LW5DFCGW.js → chunk-LXMYIJOO.js} +1 -1
- package/dist/{chunk-YJLGCUKZ.js → chunk-MU4FAKGV.js} +2 -2
- package/dist/{chunk-7ATQD2GP.js → chunk-N3VPKMU3.js} +2 -2
- package/dist/{chunk-LTBKKSCX.js → chunk-OLBZ2WGG.js} +2 -2
- package/dist/{chunk-4EQBBIFH.js → chunk-OM4VYQPH.js} +2 -2
- package/dist/{chunk-HACVBEAP.js → chunk-OMA2B7GY.js} +2 -2
- package/dist/{chunk-PD66CVSD.js → chunk-OUKBMQ2O.js} +2 -2
- package/dist/{chunk-PSFQ4HDM.js → chunk-OZFHE6FY.js} +3 -3
- package/dist/{chunk-TR76RJNY.js → chunk-P6GKRT2H.js} +1 -1
- package/dist/{chunk-42LQMCEE.js → chunk-PBSZQBFM.js} +11 -11
- package/dist/{chunk-B6D3O5X2.js → chunk-PRCJDCDD.js} +4 -4
- package/dist/{chunk-4FRP3R44.js → chunk-RFYKLES2.js} +2 -2
- package/dist/{chunk-AEZCLP3C.js → chunk-S7363OIV.js} +2 -2
- package/dist/{chunk-S7WPER23.js → chunk-TFN3PPYO.js} +2 -2
- package/dist/{chunk-AF67CHFU.js → chunk-TOUZM2RQ.js} +2 -2
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-J7RZ4NJO.js → chunk-UFY4G6EC.js} +13 -13
- package/dist/{chunk-52I6AHO6.js → chunk-V3KLIYTW.js} +2 -2
- package/dist/{chunk-RJ7NOBKA.js → chunk-V63BBOFS.js} +2 -2
- package/dist/{chunk-ZMVMYZMX.js → chunk-VJERIMIX.js} +3 -3
- package/dist/{chunk-YQPVXUJG.js → chunk-VVE5SLO5.js} +2 -2
- package/dist/{chunk-IJCC5TSX.js → chunk-WJTB4SF5.js} +2 -2
- package/dist/{chunk-TL2THUFH.js → chunk-WOA7DS7O.js} +114 -38
- package/dist/chunk-WOA7DS7O.js.map +1 -0
- package/dist/{chunk-42JAVCED.js → chunk-WRPRUMUI.js} +2 -2
- package/dist/{chunk-C7KYMSL5.js → chunk-WZBDOI2C.js} +2 -2
- package/dist/{chunk-6LDZNG3T.js → chunk-XVI2TR26.js} +2 -2
- package/dist/{chunk-KXJ3IDD5.js → chunk-YHSZTMM3.js} +2 -2
- package/dist/{chunk-KIJCXLW7.js → chunk-YNF4ZT3X.js} +2 -2
- package/dist/{chunk-F6B3RUR4.js → chunk-YSD4MK5Q.js} +2 -2
- package/dist/{cli-CFpMhqz5.d.ts → cli-Bvy4jdBu.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +59 -59
- package/dist/coding/export-okf-codegraph.js +7 -7
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +7 -7
- package/dist/config.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +7 -7
- package/dist/convergence-refresh.js +9 -9
- package/dist/corpus-watermark.js +9 -9
- package/dist/entity-retrieval.js +7 -7
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +26 -26
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/extraction-span-fallback.d.ts +35 -0
- package/dist/extraction-span-fallback.js +33 -0
- package/dist/extraction-span-fallback.js.map +1 -0
- package/dist/extraction.js +10 -10
- package/dist/{graph-edge-decay-A4SDFMMS.js → graph-edge-decay-FXGELBFA.js} +4 -4
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +3 -3
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +732 -681
- package/dist/index.js +109 -101
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +9 -9
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- 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 +8 -8
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-doctor-corpus.js +10 -10
- package/dist/operator-toolkit.js +20 -20
- package/dist/orchestration/maintenance.js +15 -15
- package/dist/orchestrator.js +76 -76
- package/dist/{public-http-C9-CHc40.d.ts → public-http-2MyshWxl.d.ts} +1 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/schemas.d.ts +176 -176
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/shared-context/manager.d.ts +6 -6
- package/dist/source-agent-qualifier.js +7 -7
- package/dist/standup/build.js +7 -7
- package/dist/storage.js +6 -6
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +28 -28
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/export-okf.js +8 -8
- package/dist/transfer/types.d.ts +78 -78
- package/dist/verified-recall.js +7 -7
- package/dist/who-knows-cli.d.ts +1 -1
- package/dist/who-knows-cli.js +8 -8
- package/dist/who-knows.d.ts +1 -1
- package/dist/who-knows.js +7 -7
- package/package.json +2 -2
- package/src/activity/index.ts +1 -0
- package/src/activity/journal-vault-prereq.test.ts +121 -0
- package/src/activity/journal-vault-prereq.ts +41 -0
- package/src/activity/timeline/analysis-metadata.test.ts +147 -0
- package/src/activity/timeline/analysis-metadata.ts +97 -0
- package/src/activity/timeline/index.ts +1 -0
- package/src/dedup/index.ts +6 -0
- package/src/dedup/merge-content.test.ts +116 -0
- package/src/dedup/merge-content.ts +40 -0
- package/src/extraction-span-fallback.test.ts +103 -0
- package/src/extraction-span-fallback.ts +62 -0
- package/src/shared-context/curation-lint.test.ts +155 -0
- package/src/shared-context/curation-lint.ts +68 -0
- package/dist/chunk-TL2THUFH.js.map +0 -1
- package/dist/{access-service-DcfM0Vf0.d.ts → access-service-LbwWl05g.d.ts} +64 -64
- /package/dist/{chunk-LVFI5LG7.js.map → chunk-2L5FSNPC.js.map} +0 -0
- /package/dist/{chunk-5H5TKPBA.js.map → chunk-35J5ABJC.js.map} +0 -0
- /package/dist/{chunk-AI6TT4Y3.js.map → chunk-3HEYRDNF.js.map} +0 -0
- /package/dist/{chunk-CCKIHXU5.js.map → chunk-3KURGLO7.js.map} +0 -0
- /package/dist/{chunk-WKZSKFZC.js.map → chunk-5FBA6UXN.js.map} +0 -0
- /package/dist/{chunk-THWUR7JO.js.map → chunk-6AYPXHOF.js.map} +0 -0
- /package/dist/{chunk-JU5SBSV7.js.map → chunk-6CT4HSVJ.js.map} +0 -0
- /package/dist/{chunk-DMR6GVTW.js.map → chunk-76X3F4WV.js.map} +0 -0
- /package/dist/{chunk-6FLNUDEO.js.map → chunk-7DQQYJKU.js.map} +0 -0
- /package/dist/{chunk-DKTIW44X.js.map → chunk-7SL6EJLT.js.map} +0 -0
- /package/dist/{chunk-JC5GCR3K.js.map → chunk-ASDK4GEV.js.map} +0 -0
- /package/dist/{chunk-YMOPYG3K.js.map → chunk-BIJAWZYW.js.map} +0 -0
- /package/dist/{chunk-AWGXR2PV.js.map → chunk-BX6GW4KC.js.map} +0 -0
- /package/dist/{chunk-ZOMXROJ6.js.map → chunk-D65XGENZ.js.map} +0 -0
- /package/dist/{chunk-SB7PMYWV.js.map → chunk-DA2XDNS5.js.map} +0 -0
- /package/dist/{chunk-LSGQRUTX.js.map → chunk-DT7LKLET.js.map} +0 -0
- /package/dist/{chunk-RSPIDEY3.js.map → chunk-DZPX5WSG.js.map} +0 -0
- /package/dist/{chunk-UXEAJ765.js.map → chunk-EA2C5Z7V.js.map} +0 -0
- /package/dist/{chunk-ZBGODUSY.js.map → chunk-EB3V7PJC.js.map} +0 -0
- /package/dist/{chunk-GPTZFSMQ.js.map → chunk-EJCKJS4X.js.map} +0 -0
- /package/dist/{chunk-RGCF4XNH.js.map → chunk-EZPBQVGZ.js.map} +0 -0
- /package/dist/{chunk-WJPSCTZY.js.map → chunk-FGORNFTZ.js.map} +0 -0
- /package/dist/{chunk-JAQXUGLN.js.map → chunk-FNNWK7G7.js.map} +0 -0
- /package/dist/{chunk-XCVGQKTR.js.map → chunk-GH477IGR.js.map} +0 -0
- /package/dist/{chunk-TV52QFXK.js.map → chunk-HBOQO4GB.js.map} +0 -0
- /package/dist/{chunk-D6Q5EPOV.js.map → chunk-HCERYDRI.js.map} +0 -0
- /package/dist/{chunk-HCM7ZYJD.js.map → chunk-IEZ6CGTW.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-MB6GLPRY.js.map → chunk-IVKIODHO.js.map} +0 -0
- /package/dist/{chunk-WFHBC4BQ.js.map → chunk-J6SWAE74.js.map} +0 -0
- /package/dist/{chunk-PUTLM67J.js.map → chunk-JHTF22NQ.js.map} +0 -0
- /package/dist/{chunk-M73TXJJW.js.map → chunk-LXBVTRBG.js.map} +0 -0
- /package/dist/{chunk-LW5DFCGW.js.map → chunk-LXMYIJOO.js.map} +0 -0
- /package/dist/{chunk-YJLGCUKZ.js.map → chunk-MU4FAKGV.js.map} +0 -0
- /package/dist/{chunk-7ATQD2GP.js.map → chunk-N3VPKMU3.js.map} +0 -0
- /package/dist/{chunk-LTBKKSCX.js.map → chunk-OLBZ2WGG.js.map} +0 -0
- /package/dist/{chunk-4EQBBIFH.js.map → chunk-OM4VYQPH.js.map} +0 -0
- /package/dist/{chunk-HACVBEAP.js.map → chunk-OMA2B7GY.js.map} +0 -0
- /package/dist/{chunk-PD66CVSD.js.map → chunk-OUKBMQ2O.js.map} +0 -0
- /package/dist/{chunk-PSFQ4HDM.js.map → chunk-OZFHE6FY.js.map} +0 -0
- /package/dist/{chunk-TR76RJNY.js.map → chunk-P6GKRT2H.js.map} +0 -0
- /package/dist/{chunk-42LQMCEE.js.map → chunk-PBSZQBFM.js.map} +0 -0
- /package/dist/{chunk-B6D3O5X2.js.map → chunk-PRCJDCDD.js.map} +0 -0
- /package/dist/{chunk-4FRP3R44.js.map → chunk-RFYKLES2.js.map} +0 -0
- /package/dist/{chunk-AEZCLP3C.js.map → chunk-S7363OIV.js.map} +0 -0
- /package/dist/{chunk-S7WPER23.js.map → chunk-TFN3PPYO.js.map} +0 -0
- /package/dist/{chunk-AF67CHFU.js.map → chunk-TOUZM2RQ.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-J7RZ4NJO.js.map → chunk-UFY4G6EC.js.map} +0 -0
- /package/dist/{chunk-52I6AHO6.js.map → chunk-V3KLIYTW.js.map} +0 -0
- /package/dist/{chunk-RJ7NOBKA.js.map → chunk-V63BBOFS.js.map} +0 -0
- /package/dist/{chunk-ZMVMYZMX.js.map → chunk-VJERIMIX.js.map} +0 -0
- /package/dist/{chunk-YQPVXUJG.js.map → chunk-VVE5SLO5.js.map} +0 -0
- /package/dist/{chunk-IJCC5TSX.js.map → chunk-WJTB4SF5.js.map} +0 -0
- /package/dist/{chunk-42JAVCED.js.map → chunk-WRPRUMUI.js.map} +0 -0
- /package/dist/{chunk-C7KYMSL5.js.map → chunk-WZBDOI2C.js.map} +0 -0
- /package/dist/{chunk-6LDZNG3T.js.map → chunk-XVI2TR26.js.map} +0 -0
- /package/dist/{chunk-KXJ3IDD5.js.map → chunk-YHSZTMM3.js.map} +0 -0
- /package/dist/{chunk-KIJCXLW7.js.map → chunk-YNF4ZT3X.js.map} +0 -0
- /package/dist/{chunk-F6B3RUR4.js.map → chunk-YSD4MK5Q.js.map} +0 -0
- /package/dist/{graph-edge-decay-A4SDFMMS.js.map → graph-edge-decay-FXGELBFA.js.map} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
VAULT_JOURNAL_PREREQUISITES,
|
|
6
|
+
checkVaultJournalPrerequisites,
|
|
7
|
+
type VaultJournalPrereqResult,
|
|
8
|
+
} from "./journal-vault-prereq.js";
|
|
9
|
+
|
|
10
|
+
const SATISFIED = {
|
|
11
|
+
vaultEnabled: true,
|
|
12
|
+
dailyNotePath: "/vault/journal/2026-08-20.md",
|
|
13
|
+
journalSection: "Journal",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
test("all three satisfied returns exactly ok", () => {
|
|
17
|
+
assert.deepEqual(
|
|
18
|
+
checkVaultJournalPrerequisites(SATISFIED),
|
|
19
|
+
{ ok: true } satisfies VaultJournalPrereqResult,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("vault.enabled unmet alone", () => {
|
|
24
|
+
const result = checkVaultJournalPrerequisites({
|
|
25
|
+
...SATISFIED,
|
|
26
|
+
vaultEnabled: false,
|
|
27
|
+
});
|
|
28
|
+
assert.equal(result.ok, false);
|
|
29
|
+
assert.deepEqual(result.missing, ["vault.enabled"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("vault.dailyNotePath unmet alone", () => {
|
|
33
|
+
const result = checkVaultJournalPrerequisites({
|
|
34
|
+
...SATISFIED,
|
|
35
|
+
dailyNotePath: undefined,
|
|
36
|
+
});
|
|
37
|
+
assert.equal(result.ok, false);
|
|
38
|
+
assert.deepEqual(result.missing, ["vault.dailyNotePath"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("vault.readback.journalSection unmet alone", () => {
|
|
42
|
+
const result = checkVaultJournalPrerequisites({
|
|
43
|
+
...SATISFIED,
|
|
44
|
+
journalSection: "",
|
|
45
|
+
});
|
|
46
|
+
assert.equal(result.ok, false);
|
|
47
|
+
assert.deepEqual(result.missing, ["vault.readback.journalSection"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("all three unmet lists all three in declaration order", () => {
|
|
51
|
+
const result = checkVaultJournalPrerequisites({
|
|
52
|
+
vaultEnabled: undefined,
|
|
53
|
+
dailyNotePath: undefined,
|
|
54
|
+
journalSection: undefined,
|
|
55
|
+
});
|
|
56
|
+
assert.equal(result.ok, false);
|
|
57
|
+
assert.deepEqual(result.missing, [...VAULT_JOURNAL_PREREQUISITES]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("string \"true\" does not satisfy vault.enabled", () => {
|
|
61
|
+
const result = checkVaultJournalPrerequisites({
|
|
62
|
+
...SATISFIED,
|
|
63
|
+
vaultEnabled: "true",
|
|
64
|
+
});
|
|
65
|
+
assert.equal(result.ok, false);
|
|
66
|
+
assert.deepEqual(result.missing, ["vault.enabled"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("number 1 does not satisfy vault.enabled", () => {
|
|
70
|
+
const result = checkVaultJournalPrerequisites({
|
|
71
|
+
...SATISFIED,
|
|
72
|
+
vaultEnabled: 1,
|
|
73
|
+
});
|
|
74
|
+
assert.equal(result.ok, false);
|
|
75
|
+
assert.deepEqual(result.missing, ["vault.enabled"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("whitespace-only dailyNotePath is unmet", () => {
|
|
79
|
+
const result = checkVaultJournalPrerequisites({
|
|
80
|
+
...SATISFIED,
|
|
81
|
+
dailyNotePath: " ",
|
|
82
|
+
});
|
|
83
|
+
assert.equal(result.ok, false);
|
|
84
|
+
assert.deepEqual(result.missing, ["vault.dailyNotePath"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("whitespace-only journalSection is unmet", () => {
|
|
88
|
+
const result = checkVaultJournalPrerequisites({
|
|
89
|
+
...SATISFIED,
|
|
90
|
+
journalSection: "\t\n",
|
|
91
|
+
});
|
|
92
|
+
assert.equal(result.ok, false);
|
|
93
|
+
assert.deepEqual(result.missing, ["vault.readback.journalSection"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("message names every missing key and is a single line", () => {
|
|
97
|
+
const result = checkVaultJournalPrerequisites({
|
|
98
|
+
vaultEnabled: "true",
|
|
99
|
+
dailyNotePath: 42,
|
|
100
|
+
journalSection: " ",
|
|
101
|
+
});
|
|
102
|
+
assert.equal(result.ok, false);
|
|
103
|
+
for (const key of result.missing) {
|
|
104
|
+
assert.ok(result.message.includes(key), `message must name ${key}`);
|
|
105
|
+
}
|
|
106
|
+
for (const key of VAULT_JOURNAL_PREREQUISITES) {
|
|
107
|
+
assert.ok(result.message.includes(key), `message must name ${key}`);
|
|
108
|
+
}
|
|
109
|
+
assert.ok(!result.message.includes("\n"), "message must be a single line");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("input is not mutated", () => {
|
|
113
|
+
const input = Object.freeze({
|
|
114
|
+
vaultEnabled: false,
|
|
115
|
+
dailyNotePath: " ",
|
|
116
|
+
journalSection: "",
|
|
117
|
+
});
|
|
118
|
+
const snapshot = { ...input };
|
|
119
|
+
checkVaultJournalPrerequisites(input);
|
|
120
|
+
assert.deepEqual(input, snapshot);
|
|
121
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault journal read-back prerequisites (issue #1987).
|
|
3
|
+
*
|
|
4
|
+
* Pure. `journal.source: "vault"` requires all three prerequisites; the
|
|
5
|
+
* caller checks `source` first via the journal-source parser and calls this
|
|
6
|
+
* only for vault mode. Reports EVERY unmet prerequisite at once so the user
|
|
7
|
+
* gets one actionable error, not the first hit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const VAULT_JOURNAL_PREREQUISITES = [
|
|
11
|
+
"vault.enabled",
|
|
12
|
+
"vault.dailyNotePath",
|
|
13
|
+
"vault.readback.journalSection",
|
|
14
|
+
] as const;
|
|
15
|
+
export type VaultJournalPrerequisite = (typeof VAULT_JOURNAL_PREREQUISITES)[number];
|
|
16
|
+
|
|
17
|
+
export type VaultJournalPrereqResult =
|
|
18
|
+
| { ok: true }
|
|
19
|
+
| { ok: false; missing: VaultJournalPrerequisite[]; message: string };
|
|
20
|
+
|
|
21
|
+
export function checkVaultJournalPrerequisites(input: {
|
|
22
|
+
vaultEnabled?: unknown;
|
|
23
|
+
dailyNotePath?: unknown;
|
|
24
|
+
journalSection?: unknown;
|
|
25
|
+
}): VaultJournalPrereqResult {
|
|
26
|
+
const missing: VaultJournalPrerequisite[] = [];
|
|
27
|
+
if (input.vaultEnabled !== true) missing.push("vault.enabled");
|
|
28
|
+
// Exact values are validated: trim() only detects blankness, never rewrites.
|
|
29
|
+
if (typeof input.dailyNotePath !== "string" || input.dailyNotePath.trim().length === 0) {
|
|
30
|
+
missing.push("vault.dailyNotePath");
|
|
31
|
+
}
|
|
32
|
+
if (typeof input.journalSection !== "string" || input.journalSection.trim().length === 0) {
|
|
33
|
+
missing.push("vault.readback.journalSection");
|
|
34
|
+
}
|
|
35
|
+
if (missing.length === 0) return { ok: true };
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
missing,
|
|
39
|
+
message: `vault journal read-back prerequisites unmet: ${missing.join(", ")}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ANALYSIS_METADATA_MAX_FIELD_LENGTH,
|
|
6
|
+
buildAnalysisRunMetadata,
|
|
7
|
+
} from "./analysis-metadata.js";
|
|
8
|
+
|
|
9
|
+
const VALID = {
|
|
10
|
+
provider: "openai",
|
|
11
|
+
model: "gpt-5.2",
|
|
12
|
+
promptVersion: "timeline-analysis.v2",
|
|
13
|
+
observationCount: 12,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const STRING_FIELDS = ["provider", "model", "promptVersion"] as const;
|
|
17
|
+
|
|
18
|
+
test("a valid record round-trips with exactly the documented keys", () => {
|
|
19
|
+
const result = buildAnalysisRunMetadata(VALID);
|
|
20
|
+
assert.deepEqual(result, VALID);
|
|
21
|
+
assert.deepEqual(Object.keys(result).sort(), [
|
|
22
|
+
"model",
|
|
23
|
+
"observationCount",
|
|
24
|
+
"promptVersion",
|
|
25
|
+
"provider",
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("real provider and model slugs are accepted", () => {
|
|
30
|
+
for (const model of ["gpt-5.2", "claude-4_1", "anthropic/claude-4", "llama3:8b", "o1"]) {
|
|
31
|
+
assert.equal(buildAnalysisRunMetadata({ ...VALID, model }).model, model);
|
|
32
|
+
}
|
|
33
|
+
for (const promptVersion of ["v3", "2026-08-01", "timeline.v2"]) {
|
|
34
|
+
assert.equal(
|
|
35
|
+
buildAnalysisRunMetadata({ ...VALID, promptVersion }).promptVersion,
|
|
36
|
+
promptVersion,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// The guarantee this record makes is that it cannot carry content. A length
|
|
42
|
+
// cap does not deliver that: a short single-line string is happily prose.
|
|
43
|
+
test("prose is rejected even when short and single-line", () => {
|
|
44
|
+
for (const field of STRING_FIELDS) {
|
|
45
|
+
for (const prose of [
|
|
46
|
+
"Summarize this user's activity",
|
|
47
|
+
"The user said they were tired",
|
|
48
|
+
"api key lives in the operator profile",
|
|
49
|
+
]) {
|
|
50
|
+
assert.throws(
|
|
51
|
+
() => buildAnalysisRunMetadata({ ...VALID, [field]: prose }),
|
|
52
|
+
/must be an identifier/,
|
|
53
|
+
`${field} must reject ${JSON.stringify(prose)}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("every line-break character is rejected, including U+2028 and U+2029", () => {
|
|
60
|
+
for (const field of STRING_FIELDS) {
|
|
61
|
+
for (const sep of ["\n", "\r", "\u2028", "\u2029"]) {
|
|
62
|
+
assert.throws(
|
|
63
|
+
() => buildAnalysisRunMetadata({ ...VALID, [field]: `openai${sep}gpt` }),
|
|
64
|
+
/must not contain a line break/,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("blank, padded, and non-string values are rejected", () => {
|
|
71
|
+
for (const field of STRING_FIELDS) {
|
|
72
|
+
assert.throws(() => buildAnalysisRunMetadata({ ...VALID, [field]: "" }), /must not be empty/);
|
|
73
|
+
assert.throws(
|
|
74
|
+
() => buildAnalysisRunMetadata({ ...VALID, [field]: " " }),
|
|
75
|
+
/must be an identifier/,
|
|
76
|
+
);
|
|
77
|
+
// Validate the exact value: a padded identifier is not trimmed into validity.
|
|
78
|
+
assert.throws(
|
|
79
|
+
() => buildAnalysisRunMetadata({ ...VALID, [field]: " openai" }),
|
|
80
|
+
/must be an identifier/,
|
|
81
|
+
);
|
|
82
|
+
assert.throws(
|
|
83
|
+
() => buildAnalysisRunMetadata({ ...VALID, [field]: 42 as unknown as string }),
|
|
84
|
+
/must be a string; received number/,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("the field length cap is enforced at the boundary", () => {
|
|
90
|
+
const max = "a".repeat(ANALYSIS_METADATA_MAX_FIELD_LENGTH);
|
|
91
|
+
assert.equal(buildAnalysisRunMetadata({ ...VALID, model: max }).model, max);
|
|
92
|
+
assert.throws(
|
|
93
|
+
() => buildAnalysisRunMetadata({ ...VALID, model: `${max}a` }),
|
|
94
|
+
/is too long/,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("observationCount accepts 0 and rejects every non-count", () => {
|
|
99
|
+
assert.equal(buildAnalysisRunMetadata({ ...VALID, observationCount: 0 }).observationCount, 0);
|
|
100
|
+
for (const bad of [-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
101
|
+
assert.throws(
|
|
102
|
+
() => buildAnalysisRunMetadata({ ...VALID, observationCount: bad }),
|
|
103
|
+
/observationCount must be a non-negative integer/,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// An error message is a log line. A caller that mis-passes prompt text or a
|
|
109
|
+
// secret must not have it echoed there through the failure path.
|
|
110
|
+
test("a rejected value is never echoed in the error message", () => {
|
|
111
|
+
const secret = "my-api-key-value-goes-here-0123456789";
|
|
112
|
+
assert.throws(
|
|
113
|
+
() => buildAnalysisRunMetadata({ ...VALID, provider: `${secret} leaked` }),
|
|
114
|
+
(error: unknown) => {
|
|
115
|
+
assert.ok(error instanceof RangeError);
|
|
116
|
+
assert.equal(error.message.includes(secret), false, "message must not echo the value");
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
const circular: Record<string, unknown> = {};
|
|
121
|
+
circular.self = circular;
|
|
122
|
+
assert.throws(
|
|
123
|
+
() =>
|
|
124
|
+
buildAnalysisRunMetadata({
|
|
125
|
+
...VALID,
|
|
126
|
+
observationCount: circular as unknown as number,
|
|
127
|
+
}),
|
|
128
|
+
(error: unknown) => {
|
|
129
|
+
// JSON.stringify would throw a TypeError here instead of the intended
|
|
130
|
+
// RangeError, and would echo content for a plain object.
|
|
131
|
+
assert.ok(error instanceof RangeError);
|
|
132
|
+
assert.equal(error.message.includes("self"), false);
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
assert.throws(
|
|
137
|
+
() =>
|
|
138
|
+
buildAnalysisRunMetadata({
|
|
139
|
+
...VALID,
|
|
140
|
+
observationCount: 10n as unknown as number,
|
|
141
|
+
}),
|
|
142
|
+
(error: unknown) => {
|
|
143
|
+
assert.ok(error instanceof RangeError, "a bigint must not escape as a TypeError");
|
|
144
|
+
return true;
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis run metadata (issue #2050).
|
|
3
|
+
*
|
|
4
|
+
* Pure builder for the provider/model/prompt-version record attached to
|
|
5
|
+
* stored analysis results and telemetry.
|
|
6
|
+
*
|
|
7
|
+
* The no-content guarantee is enforced by SHAPE, not by length: each field
|
|
8
|
+
* must match an identifier/version syntax, so prose cannot be stored as a
|
|
9
|
+
* "model name". A 200-character single-line string is happily prose
|
|
10
|
+
* ("Summarize this user\u0027s activity"), which is why a length cap alone was
|
|
11
|
+
* not the boundary this record needs. Errors never echo a rejected value,
|
|
12
|
+
* so a mis-passed prompt cannot reach logs through the failure path either.
|
|
13
|
+
*/
|
|
14
|
+
export interface AnalysisRunMetadata {
|
|
15
|
+
provider: string;
|
|
16
|
+
model: string;
|
|
17
|
+
promptVersion: string;
|
|
18
|
+
/** Observation ids the run consumed, for evidence tracing. */
|
|
19
|
+
observationCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ANALYSIS_METADATA_MAX_FIELD_LENGTH = 120;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Identifier syntax for provider and model: letters, digits, and the
|
|
26
|
+
* separators real provider/model slugs use (`openai`, `gpt-5.2`,
|
|
27
|
+
* `anthropic/claude-4_1`, `llama3:8b`). No spaces, so a sentence cannot pass.
|
|
28
|
+
*/
|
|
29
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:\-\/]*$/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prompt versions are identifiers or dates (`v3`, `2026-08-01`,
|
|
33
|
+
* `timeline-analysis.v2`), so the same syntax applies.
|
|
34
|
+
*/
|
|
35
|
+
const FIELD_PATTERNS: Readonly<Record<string, RegExp>> = {
|
|
36
|
+
provider: IDENTIFIER,
|
|
37
|
+
model: IDENTIFIER,
|
|
38
|
+
promptVersion: IDENTIFIER,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function validateMetadataField(field: string, value: unknown): string {
|
|
42
|
+
// Never interpolate the value itself: an error string is a log line, and a
|
|
43
|
+
// caller that mis-passes prompt text must not have it echoed there.
|
|
44
|
+
if (typeof value !== "string") {
|
|
45
|
+
throw new RangeError(
|
|
46
|
+
`analysis run metadata ${field} must be a string; received ${typeof value}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (value.length === 0) {
|
|
50
|
+
throw new RangeError(`analysis run metadata ${field} must not be empty`);
|
|
51
|
+
}
|
|
52
|
+
if (value.length > ANALYSIS_METADATA_MAX_FIELD_LENGTH) {
|
|
53
|
+
throw new RangeError(
|
|
54
|
+
`analysis run metadata ${field} is too long (max ${ANALYSIS_METADATA_MAX_FIELD_LENGTH} characters)`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
// Report a line break as a line break even though the pattern would also
|
|
58
|
+
// reject it. U+2028 and U+2029 are line separators too: they break
|
|
59
|
+
// line-oriented log consumers exactly like CR and LF.
|
|
60
|
+
if (/[\n\r\u2028\u2029]/.test(value)) {
|
|
61
|
+
throw new RangeError(`analysis run metadata ${field} must not contain a line break`);
|
|
62
|
+
}
|
|
63
|
+
const pattern = FIELD_PATTERNS[field];
|
|
64
|
+
if (pattern === undefined) {
|
|
65
|
+
throw new RangeError(`analysis run metadata has no pattern for field ${field}`);
|
|
66
|
+
}
|
|
67
|
+
if (!pattern.test(value)) {
|
|
68
|
+
throw new RangeError(
|
|
69
|
+
`analysis run metadata ${field} must be an identifier (letters, digits, and ._:-/ only)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildAnalysisRunMetadata(input: {
|
|
76
|
+
provider: string;
|
|
77
|
+
model: string;
|
|
78
|
+
promptVersion: string;
|
|
79
|
+
observationCount: number;
|
|
80
|
+
}): AnalysisRunMetadata {
|
|
81
|
+
const provider = validateMetadataField("provider", input.provider);
|
|
82
|
+
const model = validateMetadataField("model", input.model);
|
|
83
|
+
const promptVersion = validateMetadataField("promptVersion", input.promptVersion);
|
|
84
|
+
const observationCount = input.observationCount;
|
|
85
|
+
if (
|
|
86
|
+
typeof observationCount !== "number" ||
|
|
87
|
+
!Number.isInteger(observationCount) ||
|
|
88
|
+
observationCount < 0
|
|
89
|
+
) {
|
|
90
|
+
// Report the type only. JSON.stringify would echo mis-passed content into
|
|
91
|
+
// the log, and it throws outright on a bigint or a circular object.
|
|
92
|
+
throw new RangeError(
|
|
93
|
+
`analysis run metadata observationCount must be a non-negative integer; received ${typeof observationCount}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return { provider, model, promptVersion, observationCount };
|
|
97
|
+
}
|
package/src/dedup/index.ts
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MERGE_CONTENT_LENGTH_FACTOR,
|
|
6
|
+
checkMergedContent,
|
|
7
|
+
} from "./merge-content.js";
|
|
8
|
+
|
|
9
|
+
const INCOMING = "alpha";
|
|
10
|
+
const TARGET = "beta";
|
|
11
|
+
|
|
12
|
+
test("normal merge passes and returns the content unchanged", () => {
|
|
13
|
+
const merged = "alpha and beta combined";
|
|
14
|
+
const result = checkMergedContent({
|
|
15
|
+
mergedContent: merged,
|
|
16
|
+
incomingContent: INCOMING,
|
|
17
|
+
targetContent: TARGET,
|
|
18
|
+
});
|
|
19
|
+
assert.deepEqual(result, { ok: true, content: merged });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("merged content at exactly the limit is accepted", () => {
|
|
23
|
+
const limit = MERGE_CONTENT_LENGTH_FACTOR * (INCOMING.length + TARGET.length);
|
|
24
|
+
const merged = "x".repeat(limit);
|
|
25
|
+
const result = checkMergedContent({
|
|
26
|
+
mergedContent: merged,
|
|
27
|
+
incomingContent: INCOMING,
|
|
28
|
+
targetContent: TARGET,
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(result, { ok: true, content: merged });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("merged content one character over the limit is oversized with the reported limit", () => {
|
|
34
|
+
const limit = MERGE_CONTENT_LENGTH_FACTOR * (INCOMING.length + TARGET.length);
|
|
35
|
+
const result = checkMergedContent({
|
|
36
|
+
mergedContent: "x".repeat(limit + 1),
|
|
37
|
+
incomingContent: INCOMING,
|
|
38
|
+
targetContent: TARGET,
|
|
39
|
+
});
|
|
40
|
+
assert.deepEqual(result, { ok: false, reason: "oversized", limit });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("empty, whitespace-only, null, undefined, object, and bigint merged content are all empty refusals", () => {
|
|
44
|
+
const cases: unknown[] = ["", " ", "\t\n", null, undefined, { body: 1 }, 42n];
|
|
45
|
+
for (const mergedContent of cases) {
|
|
46
|
+
const result = checkMergedContent({
|
|
47
|
+
mergedContent,
|
|
48
|
+
incomingContent: INCOMING,
|
|
49
|
+
targetContent: TARGET,
|
|
50
|
+
});
|
|
51
|
+
assert.deepEqual(result, {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: "empty",
|
|
54
|
+
limit: MERGE_CONTENT_LENGTH_FACTOR * (INCOMING.length + TARGET.length),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("two empty inputs make the limit zero, so any content is oversized", () => {
|
|
60
|
+
const result = checkMergedContent({
|
|
61
|
+
mergedContent: "x",
|
|
62
|
+
incomingContent: "",
|
|
63
|
+
targetContent: "",
|
|
64
|
+
});
|
|
65
|
+
assert.deepEqual(result, { ok: false, reason: "oversized", limit: 0 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("non-string incomingContent throws RangeError naming the field", () => {
|
|
69
|
+
assert.throws(
|
|
70
|
+
() =>
|
|
71
|
+
checkMergedContent({
|
|
72
|
+
mergedContent: "ok",
|
|
73
|
+
incomingContent: 7 as unknown as string,
|
|
74
|
+
targetContent: TARGET,
|
|
75
|
+
}),
|
|
76
|
+
(error: unknown) =>
|
|
77
|
+
error instanceof RangeError && /incomingContent/.test(error.message),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("non-string targetContent throws RangeError naming the field", () => {
|
|
82
|
+
assert.throws(
|
|
83
|
+
() =>
|
|
84
|
+
checkMergedContent({
|
|
85
|
+
mergedContent: "ok",
|
|
86
|
+
incomingContent: INCOMING,
|
|
87
|
+
targetContent: null as unknown as string,
|
|
88
|
+
}),
|
|
89
|
+
(error: unknown) =>
|
|
90
|
+
error instanceof RangeError && /targetContent/.test(error.message),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("the rejected merged content never appears in an error message or refusal result", () => {
|
|
95
|
+
const sentinel = "UNIQUE-LEAK-SENTINEL";
|
|
96
|
+
const leakyObject = { body: sentinel };
|
|
97
|
+
|
|
98
|
+
const refusal = checkMergedContent({
|
|
99
|
+
mergedContent: leakyObject,
|
|
100
|
+
incomingContent: INCOMING,
|
|
101
|
+
targetContent: TARGET,
|
|
102
|
+
});
|
|
103
|
+
assert.ok(!refusal.ok);
|
|
104
|
+
assert.ok(!JSON.stringify(refusal).includes(sentinel));
|
|
105
|
+
|
|
106
|
+
assert.throws(
|
|
107
|
+
() =>
|
|
108
|
+
checkMergedContent({
|
|
109
|
+
mergedContent: sentinel,
|
|
110
|
+
incomingContent: undefined as unknown as string,
|
|
111
|
+
targetContent: TARGET,
|
|
112
|
+
}),
|
|
113
|
+
(error: unknown) =>
|
|
114
|
+
error instanceof RangeError && !error.message.includes(sentinel),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merged-content acceptance check (issue #2330).
|
|
3
|
+
*
|
|
4
|
+
* A judge-merged body that is blank or a runaway continuation is refused so
|
|
5
|
+
* the write path can fall back to create. `mergedContent` arrives from a
|
|
6
|
+
* model and gets a typed refusal; non-string `incomingContent`/
|
|
7
|
+
* `targetContent` is a caller bug and throws. Internal helper; wiring into
|
|
8
|
+
* the write path is a later slice.
|
|
9
|
+
*/
|
|
10
|
+
export const MERGE_CONTENT_LENGTH_FACTOR = 4;
|
|
11
|
+
|
|
12
|
+
export type MergeContentCheck =
|
|
13
|
+
| { ok: true; content: string }
|
|
14
|
+
| { ok: false; reason: "empty" | "oversized"; limit: number };
|
|
15
|
+
|
|
16
|
+
function requireString(value: unknown, field: string): string {
|
|
17
|
+
if (typeof value !== "string") {
|
|
18
|
+
throw new RangeError(`merge ${field} must be a string`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function checkMergedContent(input: {
|
|
24
|
+
mergedContent: unknown;
|
|
25
|
+
incomingContent: string;
|
|
26
|
+
targetContent: string;
|
|
27
|
+
}): MergeContentCheck {
|
|
28
|
+
const incoming = requireString(input.incomingContent, "incomingContent");
|
|
29
|
+
const target = requireString(input.targetContent, "targetContent");
|
|
30
|
+
const limit = MERGE_CONTENT_LENGTH_FACTOR * (incoming.length + target.length);
|
|
31
|
+
|
|
32
|
+
const merged = input.mergedContent;
|
|
33
|
+
if (typeof merged !== "string" || merged.trim() === "") {
|
|
34
|
+
return { ok: false, reason: "empty", limit };
|
|
35
|
+
}
|
|
36
|
+
if (merged.length > limit) {
|
|
37
|
+
return { ok: false, reason: "oversized", limit };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, content: merged };
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { SPAN_OUTCOMES, tallySpanFallbacks } from "./extraction-span-fallback.js";
|
|
4
|
+
import {
|
|
5
|
+
SPAN_GATE_MAX_FALLBACK_RATE_PCT,
|
|
6
|
+
evaluateSpanPhaseGate,
|
|
7
|
+
} from "./extraction-span-gate.js";
|
|
8
|
+
|
|
9
|
+
test("tallySpanFallbacks: mixed outcomes compute the expected percentage", () => {
|
|
10
|
+
const tally = tallySpanFallbacks(["span", "fallback", "span", "span", "fallback"]);
|
|
11
|
+
assert.equal(tally.attempts, 5);
|
|
12
|
+
assert.equal(tally.fallbacks, 2);
|
|
13
|
+
assert.equal(tally.fallbackRatePct, 40);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("tallySpanFallbacks: all-span is exactly 0", () => {
|
|
17
|
+
const tally = tallySpanFallbacks(["span", "span", "span"]);
|
|
18
|
+
assert.equal(tally.fallbacks, 0);
|
|
19
|
+
assert.equal(tally.fallbackRatePct, 0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("tallySpanFallbacks: all-fallback is exactly 100", () => {
|
|
23
|
+
const tally = tallySpanFallbacks(["fallback", "fallback"]);
|
|
24
|
+
assert.equal(tally.fallbacks, 2);
|
|
25
|
+
assert.equal(tally.fallbackRatePct, 100);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("tallySpanFallbacks: zero attempts means no measurement, never NaN", () => {
|
|
29
|
+
const tally = tallySpanFallbacks([]);
|
|
30
|
+
assert.equal(tally.attempts, 0);
|
|
31
|
+
assert.equal(tally.fallbacks, 0);
|
|
32
|
+
assert.equal(tally.fallbackRatePct, null);
|
|
33
|
+
assert.equal(Number.isNaN(tally.fallbackRatePct as unknown as number), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("tallySpanFallbacks: unknown outcome throws and lists the allow-list", () => {
|
|
37
|
+
assert.throws(() => tallySpanFallbacks(["span", "fall back"]), /unknown span outcome/);
|
|
38
|
+
// No normalization: case variants are unknown outcomes, never successes.
|
|
39
|
+
assert.throws(() => tallySpanFallbacks(["Span"]), /unknown span outcome/);
|
|
40
|
+
assert.throws(() => tallySpanFallbacks(["FALLBACK"]), /unknown span outcome/);
|
|
41
|
+
assert.throws(
|
|
42
|
+
() => tallySpanFallbacks(["span", "fallback", "recovered"]),
|
|
43
|
+
/unknown span outcome "recovered"; expected one of \[span, fallback\]/,
|
|
44
|
+
);
|
|
45
|
+
assert.deepEqual(SPAN_OUTCOMES, ["span", "fallback"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("tallySpanFallbacks: non-array input throws", () => {
|
|
49
|
+
assert.throws(() => tallySpanFallbacks("span" as never), /array/);
|
|
50
|
+
assert.throws(() => tallySpanFallbacks(null as never), /array/);
|
|
51
|
+
assert.throws(() => tallySpanFallbacks(undefined as never), /array/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("tallySpanFallbacks: 1-of-3 rounds deterministically across calls", () => {
|
|
55
|
+
const first = tallySpanFallbacks(["fallback", "span", "span"]);
|
|
56
|
+
const second = tallySpanFallbacks(["fallback", "span", "span"]);
|
|
57
|
+
assert.equal(first.fallbackRatePct, 33.3333);
|
|
58
|
+
assert.deepEqual(first, second);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("tallySpanFallbacks: a rate at or above the gate bar is what the gate rejects", () => {
|
|
62
|
+
const outcomes = [
|
|
63
|
+
"span",
|
|
64
|
+
"fallback",
|
|
65
|
+
"span",
|
|
66
|
+
"span",
|
|
67
|
+
"fallback",
|
|
68
|
+
"span",
|
|
69
|
+
"span",
|
|
70
|
+
"span",
|
|
71
|
+
"span",
|
|
72
|
+
"span",
|
|
73
|
+
];
|
|
74
|
+
const tally = tallySpanFallbacks(outcomes);
|
|
75
|
+
assert.equal(tally.fallbackRatePct, 20); // 2 of 10
|
|
76
|
+
assert.ok(tally.fallbackRatePct >= SPAN_GATE_MAX_FALLBACK_RATE_PCT);
|
|
77
|
+
const verdict = evaluateSpanPhaseGate({
|
|
78
|
+
wallClockReductionPct: 30,
|
|
79
|
+
judgeScoreDropPoints: 0,
|
|
80
|
+
fallbackRatePct: tally.fallbackRatePct,
|
|
81
|
+
});
|
|
82
|
+
assert.equal(verdict.conditions.fallbackRate, false);
|
|
83
|
+
assert.equal(verdict.pass, false);
|
|
84
|
+
assert.ok(verdict.failed.includes("fallbackRate"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("tallySpanFallbacks: does not mutate the input", () => {
|
|
88
|
+
const outcomes = ["span", "fallback", "span"];
|
|
89
|
+
tallySpanFallbacks(outcomes);
|
|
90
|
+
assert.deepEqual(outcomes, ["span", "fallback", "span"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Review: a 0 rate satisfies the gate's `fallbackRatePct < 15`, so returning 0
|
|
94
|
+
// for an empty sample let an unmeasured run approve Phase B. null cannot be
|
|
95
|
+
// passed to the gate at all, which is the enforcement the comment lacked.
|
|
96
|
+
test("tallySpanFallbacks: an empty sample is null, which the gate cannot accept as a rate", () => {
|
|
97
|
+
const tally = tallySpanFallbacks([]);
|
|
98
|
+
assert.equal(tally.fallbackRatePct, null);
|
|
99
|
+
assert.equal(tally.attempts, 0);
|
|
100
|
+
assert.equal(Number.isNaN(tally.fallbackRatePct as unknown as number), false);
|
|
101
|
+
// A measured all-span run is a real 0 and stays distinguishable from null.
|
|
102
|
+
assert.equal(tallySpanFallbacks(["span"]).fallbackRatePct, 0);
|
|
103
|
+
});
|