@remnic/core 10.0.82 → 10.0.84
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 +22 -22
- package/dist/access-authorization-probe.js +23 -23
- package/dist/access-boundary.js +22 -22
- package/dist/access-cli.js +66 -66
- package/dist/access-extraction-force-flush.js +22 -22
- package/dist/access-http-briefing.js +23 -23
- package/dist/access-http-query.js +24 -24
- package/dist/access-http.js +38 -38
- package/dist/access-identity-continuity-surface.js +22 -22
- package/dist/access-lcm-surface.js +22 -22
- package/dist/access-mcp.js +31 -31
- package/dist/access-namespace-preflight.js +22 -22
- package/dist/access-observe-write-surface.js +22 -22
- package/dist/access-operations-batch.js +23 -23
- package/dist/access-operations.d.ts +1 -1
- package/dist/access-operations.js +30 -30
- package/dist/access-recall-concurrency.js +22 -22
- package/dist/access-recall-response.js +22 -22
- package/dist/access-recall-surface.js +22 -22
- package/dist/access-service.js +22 -22
- package/dist/access-surface-catalog.js +24 -24
- package/dist/active-recall.js +4 -4
- package/dist/{auto-sync-XHSS3XPL.js → auto-sync-C7E6G7Y2.js} +5 -5
- package/dist/briefing.js +5 -5
- package/dist/buffer.js +3 -3
- package/dist/calibration.js +3 -3
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-XBNO56HC.js → chunk-2IW5PT7U.js} +2 -2
- package/dist/{chunk-USMMLI6W.js → chunk-2XJZLSXE.js} +10 -10
- package/dist/{chunk-E2ROXLW7.js → chunk-3JPTUCD7.js} +5 -5
- package/dist/{chunk-6OGFQ5GH.js → chunk-3T3VHXUH.js} +2 -2
- package/dist/{chunk-HK3CUTAG.js → chunk-4EACCIPO.js} +2 -2
- package/dist/{chunk-NVYY2TJG.js → chunk-54FDG7K7.js} +2 -2
- package/dist/{chunk-YH5AF3BU.js → chunk-6LCWKS62.js} +17 -17
- package/dist/{chunk-2XG7QECM.js → chunk-6QYPL6NI.js} +3 -3
- package/dist/{chunk-JHJP2IAU.js → chunk-B4ZU34JQ.js} +3 -3
- package/dist/{chunk-P44DNJC5.js → chunk-B74NPUHT.js} +17 -17
- package/dist/{chunk-SW6OAWK7.js → chunk-BDN3TDGV.js} +2 -2
- package/dist/{chunk-TSITK6QK.js → chunk-BK4FBHZ3.js} +2 -2
- package/dist/{chunk-ROEAEZPX.js → chunk-BK7AJUWB.js} +5 -5
- package/dist/{chunk-S57QBPTA.js → chunk-DPPLNZ3A.js} +4 -6
- package/dist/chunk-DPPLNZ3A.js.map +1 -0
- package/dist/{chunk-RIBCP4DJ.js → chunk-EBOHFZI4.js} +2 -2
- package/dist/{chunk-YSA4CYTK.js → chunk-FUOAIJKQ.js} +2 -2
- package/dist/{chunk-SPYFXSFZ.js → chunk-FVRTCYZ3.js} +2 -2
- package/dist/{chunk-YGECOOTF.js → chunk-GCAQ3VUJ.js} +2 -2
- package/dist/{chunk-MNVIZDHU.js → chunk-GGJFRVRD.js} +2 -2
- package/dist/{chunk-4MTJ2PZ6.js → chunk-GQZSGXR6.js} +2 -2
- package/dist/{chunk-IG2ZBW4R.js → chunk-HLKUZW42.js} +32 -32
- package/dist/{chunk-35RSL5B4.js → chunk-HPFVZPRH.js} +2 -2
- package/dist/{chunk-IDGCL5OP.js → chunk-HQGATKG2.js} +2 -2
- package/dist/{chunk-LNHBYT3F.js → chunk-IN4OFAHX.js} +2 -2
- package/dist/{chunk-Y5HZFIQO.js → chunk-IR4C3DP2.js} +1 -1
- package/dist/{chunk-PCSQRQPJ.js → chunk-K3B72DKJ.js} +3 -3
- package/dist/{chunk-UKTUWCAN.js → chunk-K6RTQZA4.js} +2 -2
- package/dist/{chunk-B7PLF3HW.js → chunk-KESJ5XX3.js} +2 -2
- package/dist/{chunk-B36ACJG7.js → chunk-KFLD7ZZG.js} +2 -2
- package/dist/{chunk-SMSJOWVE.js → chunk-KJCL2FZO.js} +2 -2
- package/dist/{chunk-BK6JG2SF.js → chunk-KO3YBHOV.js} +2 -2
- package/dist/{chunk-UFXST7UZ.js → chunk-LECSS7EE.js} +2 -2
- package/dist/{chunk-ROZA6YVV.js → chunk-LTDLJCMQ.js} +2 -2
- package/dist/{chunk-TA5ALUX2.js → chunk-MPGLM2VU.js} +2 -2
- package/dist/{chunk-BSBGI4BD.js → chunk-MSIRVXQ3.js} +2 -2
- package/dist/{chunk-7IQ5HYBJ.js → chunk-NUSAAN75.js} +2 -2
- package/dist/{chunk-QOP7PR7O.js → chunk-OQSSQOKQ.js} +2 -2
- package/dist/{chunk-36B5VPRU.js → chunk-PCQWKMOL.js} +2 -2
- package/dist/{chunk-X3KFSJLQ.js → chunk-PJYK7HH2.js} +4 -4
- package/dist/{chunk-JDQEQE2R.js → chunk-QFEDITXM.js} +2 -2
- package/dist/{chunk-5XS4CYJ4.js → chunk-QZBTIYKD.js} +2 -2
- package/dist/{chunk-TWN75QO6.js → chunk-QZSIDSB2.js} +2 -2
- package/dist/{chunk-EFRMEOZQ.js → chunk-T77AX6DG.js} +3 -3
- package/dist/{chunk-VBNXDC7P.js → chunk-TFLPDNQV.js} +2 -2
- package/dist/{chunk-K4SVTZWP.js → chunk-TLHF75OR.js} +7 -7
- package/dist/{chunk-FR3CAR2P.js → chunk-VLMBSGQF.js} +5 -5
- package/dist/{chunk-3M7YP42P.js → chunk-VZELUMKK.js} +2 -2
- package/dist/{chunk-D4WBMLPD.js → chunk-WAD32CZG.js} +2 -2
- package/dist/{chunk-FOQ7TW2T.js → chunk-WBDIZWJQ.js} +2 -2
- package/dist/{chunk-GMZUF3GN.js → chunk-WC475JOG.js} +3 -3
- package/dist/{chunk-LE4MQ2WF.js → chunk-WHQXWFH7.js} +2 -2
- package/dist/{chunk-SLORPQZT.js → chunk-WV254W4V.js} +2 -2
- package/dist/{chunk-ORVPS4ZW.js → chunk-WYPCJK6K.js} +2 -2
- package/dist/{chunk-YXUNSOHD.js → chunk-XHGYZRVW.js} +3 -3
- package/dist/{chunk-NBFHOQIO.js → chunk-XRBHIR7Q.js} +2 -2
- package/dist/{chunk-D3WNJF7D.js → chunk-XT45WCKX.js} +10 -10
- package/dist/{chunk-NBFXHX6E.js → chunk-XYP5WBDO.js} +2 -2
- package/dist/{chunk-GXVK754R.js → chunk-XYTQJD7F.js} +2 -2
- package/dist/{chunk-L7BOS7KV.js → chunk-YAUHSNPB.js} +2 -2
- package/dist/{chunk-JNW5N6FZ.js → chunk-YOEYL6CH.js} +2 -2
- package/dist/{chunk-HUF4S4MY.js → chunk-YUTG35M4.js} +2 -2
- package/dist/{chunk-UO4YPMZG.js → chunk-Z3DDQXM4.js} +3 -3
- package/dist/{chunk-TLTBACYX.js → chunk-ZGFLIK5H.js} +2 -2
- package/dist/{chunk-6WULHFNJ.js → chunk-ZJJFUEPO.js} +2 -2
- package/dist/{chunk-DJDJBMFX.js → chunk-ZU3NJNO7.js} +5 -2
- package/dist/chunk-ZU3NJNO7.js.map +1 -0
- package/dist/{chunk-UCPH56YX.js → chunk-ZU5JDLOH.js} +2 -2
- package/dist/cli.js +48 -48
- package/dist/coding/export-okf-codegraph.js +4 -4
- package/dist/compounding/engine.js +6 -6
- package/dist/config.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/index.js +5 -5
- package/dist/convergence-refresh.js +7 -7
- package/dist/corpus-watermark.js +7 -7
- package/dist/deep-recall-policy-llm.js +3 -3
- package/dist/entity-mention-index-cache.d.ts.map +1 -1
- package/dist/entity-mention-index-cache.js +2 -1
- package/dist/entity-retrieval.js +7 -6
- package/dist/external-wiki-access.js +23 -23
- package/dist/extraction-faithfulness.js +3 -3
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +10 -10
- package/dist/fallback-llm-codex-subscription.d.ts.map +1 -1
- package/dist/fallback-llm-codex-subscription.js +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/harmonic-retrieval.js +5 -5
- package/dist/index.js +77 -77
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +7 -7
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection-runner.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/memory-browse-operations.js +23 -23
- package/dist/memory-browse.js +3 -3
- package/dist/namespaces/migrate.js +6 -6
- package/dist/namespaces/storage.js +5 -5
- package/dist/operator-doctor-corpus.js +8 -8
- package/dist/operator-toolkit.js +13 -13
- package/dist/orchestration/backlog-scheduler.js +67 -67
- package/dist/orchestration/buffer-drain-runner.js +67 -67
- package/dist/orchestration/maintenance.js +16 -16
- package/dist/orchestrator.js +66 -66
- package/dist/recall-navigation-operations.js +23 -23
- package/dist/recall-navigation.js +22 -22
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-state-view-admission.js +3 -3
- package/dist/recall-why-service.js +3 -3
- package/dist/resume-bundles.js +4 -4
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/source-agent-qualifier.js +8 -8
- package/dist/standup/build.js +5 -5
- package/dist/storage.js +4 -4
- package/dist/summarizer.js +4 -4
- package/dist/support-passport/index.js +25 -25
- package/dist/transfer/export-okf.js +5 -5
- package/dist/verified-recall.js +5 -5
- package/dist/who-knows-cli.js +6 -6
- package/dist/who-knows.js +5 -5
- package/package.json +2 -2
- package/src/access-mcp-correction-tools.test.ts +45 -0
- package/src/compounding/weekly-evidence.test.ts +237 -0
- package/src/entity-mention-index-cache.ts +1 -6
- package/src/fallback-llm-codex-subscription.ts +4 -0
- package/src/local-llm-json-schema-fallback.test.ts +33 -0
- package/src/maintenance/rebuild-memory-projection-runner.test.ts +90 -0
- package/src/orchestration/full-corpus-heap-guard.test.ts +52 -0
- package/src/projection-rebuild-config.test.ts +79 -0
- package/src/storage/entity-store-symlink.test.ts +44 -0
- package/src/utils/malformed-entity-record.test.ts +69 -0
- package/dist/chunk-DJDJBMFX.js.map +0 -1
- package/dist/chunk-S57QBPTA.js.map +0 -1
- /package/dist/{auto-sync-XHSS3XPL.js.map → auto-sync-C7E6G7Y2.js.map} +0 -0
- /package/dist/{chunk-XBNO56HC.js.map → chunk-2IW5PT7U.js.map} +0 -0
- /package/dist/{chunk-USMMLI6W.js.map → chunk-2XJZLSXE.js.map} +0 -0
- /package/dist/{chunk-E2ROXLW7.js.map → chunk-3JPTUCD7.js.map} +0 -0
- /package/dist/{chunk-6OGFQ5GH.js.map → chunk-3T3VHXUH.js.map} +0 -0
- /package/dist/{chunk-HK3CUTAG.js.map → chunk-4EACCIPO.js.map} +0 -0
- /package/dist/{chunk-NVYY2TJG.js.map → chunk-54FDG7K7.js.map} +0 -0
- /package/dist/{chunk-YH5AF3BU.js.map → chunk-6LCWKS62.js.map} +0 -0
- /package/dist/{chunk-2XG7QECM.js.map → chunk-6QYPL6NI.js.map} +0 -0
- /package/dist/{chunk-JHJP2IAU.js.map → chunk-B4ZU34JQ.js.map} +0 -0
- /package/dist/{chunk-P44DNJC5.js.map → chunk-B74NPUHT.js.map} +0 -0
- /package/dist/{chunk-SW6OAWK7.js.map → chunk-BDN3TDGV.js.map} +0 -0
- /package/dist/{chunk-TSITK6QK.js.map → chunk-BK4FBHZ3.js.map} +0 -0
- /package/dist/{chunk-ROEAEZPX.js.map → chunk-BK7AJUWB.js.map} +0 -0
- /package/dist/{chunk-RIBCP4DJ.js.map → chunk-EBOHFZI4.js.map} +0 -0
- /package/dist/{chunk-YSA4CYTK.js.map → chunk-FUOAIJKQ.js.map} +0 -0
- /package/dist/{chunk-SPYFXSFZ.js.map → chunk-FVRTCYZ3.js.map} +0 -0
- /package/dist/{chunk-YGECOOTF.js.map → chunk-GCAQ3VUJ.js.map} +0 -0
- /package/dist/{chunk-MNVIZDHU.js.map → chunk-GGJFRVRD.js.map} +0 -0
- /package/dist/{chunk-4MTJ2PZ6.js.map → chunk-GQZSGXR6.js.map} +0 -0
- /package/dist/{chunk-IG2ZBW4R.js.map → chunk-HLKUZW42.js.map} +0 -0
- /package/dist/{chunk-35RSL5B4.js.map → chunk-HPFVZPRH.js.map} +0 -0
- /package/dist/{chunk-IDGCL5OP.js.map → chunk-HQGATKG2.js.map} +0 -0
- /package/dist/{chunk-LNHBYT3F.js.map → chunk-IN4OFAHX.js.map} +0 -0
- /package/dist/{chunk-Y5HZFIQO.js.map → chunk-IR4C3DP2.js.map} +0 -0
- /package/dist/{chunk-PCSQRQPJ.js.map → chunk-K3B72DKJ.js.map} +0 -0
- /package/dist/{chunk-UKTUWCAN.js.map → chunk-K6RTQZA4.js.map} +0 -0
- /package/dist/{chunk-B7PLF3HW.js.map → chunk-KESJ5XX3.js.map} +0 -0
- /package/dist/{chunk-B36ACJG7.js.map → chunk-KFLD7ZZG.js.map} +0 -0
- /package/dist/{chunk-SMSJOWVE.js.map → chunk-KJCL2FZO.js.map} +0 -0
- /package/dist/{chunk-BK6JG2SF.js.map → chunk-KO3YBHOV.js.map} +0 -0
- /package/dist/{chunk-UFXST7UZ.js.map → chunk-LECSS7EE.js.map} +0 -0
- /package/dist/{chunk-ROZA6YVV.js.map → chunk-LTDLJCMQ.js.map} +0 -0
- /package/dist/{chunk-TA5ALUX2.js.map → chunk-MPGLM2VU.js.map} +0 -0
- /package/dist/{chunk-BSBGI4BD.js.map → chunk-MSIRVXQ3.js.map} +0 -0
- /package/dist/{chunk-7IQ5HYBJ.js.map → chunk-NUSAAN75.js.map} +0 -0
- /package/dist/{chunk-QOP7PR7O.js.map → chunk-OQSSQOKQ.js.map} +0 -0
- /package/dist/{chunk-36B5VPRU.js.map → chunk-PCQWKMOL.js.map} +0 -0
- /package/dist/{chunk-X3KFSJLQ.js.map → chunk-PJYK7HH2.js.map} +0 -0
- /package/dist/{chunk-JDQEQE2R.js.map → chunk-QFEDITXM.js.map} +0 -0
- /package/dist/{chunk-5XS4CYJ4.js.map → chunk-QZBTIYKD.js.map} +0 -0
- /package/dist/{chunk-TWN75QO6.js.map → chunk-QZSIDSB2.js.map} +0 -0
- /package/dist/{chunk-EFRMEOZQ.js.map → chunk-T77AX6DG.js.map} +0 -0
- /package/dist/{chunk-VBNXDC7P.js.map → chunk-TFLPDNQV.js.map} +0 -0
- /package/dist/{chunk-K4SVTZWP.js.map → chunk-TLHF75OR.js.map} +0 -0
- /package/dist/{chunk-FR3CAR2P.js.map → chunk-VLMBSGQF.js.map} +0 -0
- /package/dist/{chunk-3M7YP42P.js.map → chunk-VZELUMKK.js.map} +0 -0
- /package/dist/{chunk-D4WBMLPD.js.map → chunk-WAD32CZG.js.map} +0 -0
- /package/dist/{chunk-FOQ7TW2T.js.map → chunk-WBDIZWJQ.js.map} +0 -0
- /package/dist/{chunk-GMZUF3GN.js.map → chunk-WC475JOG.js.map} +0 -0
- /package/dist/{chunk-LE4MQ2WF.js.map → chunk-WHQXWFH7.js.map} +0 -0
- /package/dist/{chunk-SLORPQZT.js.map → chunk-WV254W4V.js.map} +0 -0
- /package/dist/{chunk-ORVPS4ZW.js.map → chunk-WYPCJK6K.js.map} +0 -0
- /package/dist/{chunk-YXUNSOHD.js.map → chunk-XHGYZRVW.js.map} +0 -0
- /package/dist/{chunk-NBFHOQIO.js.map → chunk-XRBHIR7Q.js.map} +0 -0
- /package/dist/{chunk-D3WNJF7D.js.map → chunk-XT45WCKX.js.map} +0 -0
- /package/dist/{chunk-NBFXHX6E.js.map → chunk-XYP5WBDO.js.map} +0 -0
- /package/dist/{chunk-GXVK754R.js.map → chunk-XYTQJD7F.js.map} +0 -0
- /package/dist/{chunk-L7BOS7KV.js.map → chunk-YAUHSNPB.js.map} +0 -0
- /package/dist/{chunk-JNW5N6FZ.js.map → chunk-YOEYL6CH.js.map} +0 -0
- /package/dist/{chunk-HUF4S4MY.js.map → chunk-YUTG35M4.js.map} +0 -0
- /package/dist/{chunk-UO4YPMZG.js.map → chunk-Z3DDQXM4.js.map} +0 -0
- /package/dist/{chunk-TLTBACYX.js.map → chunk-ZGFLIK5H.js.map} +0 -0
- /package/dist/{chunk-6WULHFNJ.js.map → chunk-ZJJFUEPO.js.map} +0 -0
- /package/dist/{chunk-UCPH56YX.js.map → chunk-ZU5JDLOH.js.map} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import type { MemoryFile } from "../types.js";
|
|
8
|
+
import { rememberRawFrontmatter } from "../storage/memory-frontmatter-metadata.js";
|
|
9
|
+
import {
|
|
10
|
+
CORRECTION_FILE_BYTE_LIMIT,
|
|
11
|
+
WEEKLY_CORRECTION_READ_LIMIT,
|
|
12
|
+
WEEKLY_CORRECTION_SCAN_LIMIT,
|
|
13
|
+
buildActionFailurePatterns,
|
|
14
|
+
capWeeklyPatternDetails,
|
|
15
|
+
isoWeekStartMs,
|
|
16
|
+
provenanceVisibleInStoreRecall,
|
|
17
|
+
readFeedbackEntriesForWeekFile,
|
|
18
|
+
readWeeklyCorrections,
|
|
19
|
+
type WeeklyActionEvent,
|
|
20
|
+
type WeeklyEvidenceStorage,
|
|
21
|
+
} from "./weekly-evidence.js";
|
|
22
|
+
import { isoWeekId } from "./iso-week.js";
|
|
23
|
+
|
|
24
|
+
test("isoWeekStartMs returns Monday UTC for valid ids and null otherwise", () => {
|
|
25
|
+
const start = isoWeekStartMs("2026-W01");
|
|
26
|
+
assert.ok(start !== null);
|
|
27
|
+
assert.equal(new Date(start).toISOString(), "2025-12-29T00:00:00.000Z");
|
|
28
|
+
assert.equal(isoWeekId(new Date(start)), "2026-W01");
|
|
29
|
+
|
|
30
|
+
const w02 = isoWeekStartMs("2026-W02");
|
|
31
|
+
assert.ok(w02 !== null);
|
|
32
|
+
assert.equal(w02 - start!, 7 * 86_400_000);
|
|
33
|
+
|
|
34
|
+
assert.equal(isoWeekStartMs("2026-W1"), null);
|
|
35
|
+
assert.equal(isoWeekStartMs("2026-W00"), null);
|
|
36
|
+
assert.equal(isoWeekStartMs("not-a-week"), null);
|
|
37
|
+
assert.equal(isoWeekStartMs("2026-W54"), null);
|
|
38
|
+
// 2026 has 53 weeks only if the calendar emits it; W53 that does not land is null.
|
|
39
|
+
const w53 = isoWeekStartMs("2015-W53");
|
|
40
|
+
assert.ok(w53 !== null);
|
|
41
|
+
assert.equal(isoWeekId(new Date(w53)), "2015-W53");
|
|
42
|
+
assert.equal(isoWeekStartMs("2016-W53"), null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("readFeedbackEntriesForWeekFile filters by ISO week and skips bad lines", async () => {
|
|
46
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-weekly-feedback-"));
|
|
47
|
+
try {
|
|
48
|
+
const inbox = path.join(root, "feedback.jsonl");
|
|
49
|
+
const inWeek = "2026-01-02T12:00:00.000Z"; // 2026-W01
|
|
50
|
+
const otherWeek = "2026-01-08T12:00:00.000Z"; // 2026-W02
|
|
51
|
+
await writeFile(
|
|
52
|
+
inbox,
|
|
53
|
+
[
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
agent: "alpha",
|
|
56
|
+
decision: "approved",
|
|
57
|
+
reason: "looks good",
|
|
58
|
+
date: inWeek,
|
|
59
|
+
}),
|
|
60
|
+
"not-json",
|
|
61
|
+
JSON.stringify({ agent: "", decision: "approved", reason: "x", date: inWeek }),
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
agent: "beta",
|
|
64
|
+
decision: "rejected",
|
|
65
|
+
reason: "no",
|
|
66
|
+
date: otherWeek,
|
|
67
|
+
}),
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
agent: "gamma",
|
|
70
|
+
decision: "approved_with_feedback",
|
|
71
|
+
reason: "tweak",
|
|
72
|
+
date: "2025-12-30T08:00:00.000Z",
|
|
73
|
+
}),
|
|
74
|
+
"",
|
|
75
|
+
].join("\n"),
|
|
76
|
+
"utf8",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const entries = await readFeedbackEntriesForWeekFile(inbox, "2026-W01");
|
|
80
|
+
assert.equal(entries.length, 2);
|
|
81
|
+
assert.deepEqual(
|
|
82
|
+
entries.map((row) => row.entry.agent).sort(),
|
|
83
|
+
["alpha", "gamma"],
|
|
84
|
+
);
|
|
85
|
+
assert.equal(entries.find((row) => row.entry.agent === "alpha")?.sourceLine, 1);
|
|
86
|
+
assert.equal(entries.find((row) => row.entry.agent === "gamma")?.sourceLine, 5);
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(await readFeedbackEntriesForWeekFile(path.join(root, "missing.jsonl"), "2026-W01"), []);
|
|
89
|
+
} finally {
|
|
90
|
+
await rm(root, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("buildActionFailurePatterns keeps failures, skips, and policy decisions", () => {
|
|
95
|
+
const events: WeeklyActionEvent[] = [
|
|
96
|
+
{
|
|
97
|
+
line: 1,
|
|
98
|
+
action: "forget",
|
|
99
|
+
outcome: "failed",
|
|
100
|
+
policyDecision: null,
|
|
101
|
+
namespace: "default",
|
|
102
|
+
reason: "locked",
|
|
103
|
+
ledgerLabel: "memory-actions.jsonl",
|
|
104
|
+
actionId: "a1",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
line: 2,
|
|
108
|
+
action: "promote",
|
|
109
|
+
outcome: "applied",
|
|
110
|
+
policyDecision: null,
|
|
111
|
+
namespace: "default",
|
|
112
|
+
reason: null,
|
|
113
|
+
ledgerLabel: "memory-actions.jsonl",
|
|
114
|
+
actionId: "a2",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
line: 3,
|
|
118
|
+
action: "edit",
|
|
119
|
+
outcome: "applied",
|
|
120
|
+
policyDecision: "deny",
|
|
121
|
+
namespace: "ns1",
|
|
122
|
+
reason: " blocked by policy ",
|
|
123
|
+
ledgerLabel: "namespaces/ns1/state/memory-actions.jsonl",
|
|
124
|
+
actionId: "a3",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
line: 4,
|
|
128
|
+
action: "merge",
|
|
129
|
+
outcome: "skipped",
|
|
130
|
+
policyDecision: null,
|
|
131
|
+
namespace: "default",
|
|
132
|
+
reason: null,
|
|
133
|
+
ledgerLabel: "memory-actions.jsonl",
|
|
134
|
+
actionId: "a4",
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const patterns = buildActionFailurePatterns(events);
|
|
139
|
+
assert.equal(patterns.length, 3);
|
|
140
|
+
assert.match(patterns[0]!.pattern, /memory-action\/default: forget failed - locked/);
|
|
141
|
+
assert.equal(patterns[0]!.provenance, "memory-actions.jsonl:*");
|
|
142
|
+
assert.match(patterns[1]!.pattern, /memory-action\/ns1: edit applied\/deny - blocked by policy/);
|
|
143
|
+
assert.match(patterns[2]!.pattern, /memory-action\/default: merge skipped$/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("readWeeklyCorrections skips files over the byte cap and keeps in-week active ones", async () => {
|
|
147
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-weekly-corr-"));
|
|
148
|
+
try {
|
|
149
|
+
const correctionsDir = path.join(root, "corrections");
|
|
150
|
+
await mkdir(correctionsDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
const inWeekMs = Date.parse("2026-01-02T15:00:00.000Z");
|
|
153
|
+
const smallName = `correction-${inWeekMs}.md`;
|
|
154
|
+
const largeName = `correction-${inWeekMs + 1000}.md`;
|
|
155
|
+
const smallBody = `---
|
|
156
|
+
id: corr-small
|
|
157
|
+
category: correction
|
|
158
|
+
status: active
|
|
159
|
+
created: 2026-01-02T15:00:00.000Z
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
Fix the pricing table.
|
|
163
|
+
`;
|
|
164
|
+
await writeFile(path.join(correctionsDir, smallName), smallBody, "utf8");
|
|
165
|
+
await writeFile(
|
|
166
|
+
path.join(correctionsDir, largeName),
|
|
167
|
+
`${"x".repeat(CORRECTION_FILE_BYTE_LIMIT + 1)}`,
|
|
168
|
+
"utf8",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// SAFETY: rememberRawFrontmatter reads only path, content and frontmatter; this literal sets all three.
|
|
172
|
+
const smallMemory = rememberRawFrontmatter(
|
|
173
|
+
{
|
|
174
|
+
path: path.join(correctionsDir, smallName),
|
|
175
|
+
content: "Fix the pricing table.",
|
|
176
|
+
frontmatter: {
|
|
177
|
+
id: "corr-small",
|
|
178
|
+
category: "correction",
|
|
179
|
+
status: "active",
|
|
180
|
+
created: "2026-01-02T15:00:00.000Z",
|
|
181
|
+
},
|
|
182
|
+
} as MemoryFile,
|
|
183
|
+
smallBody,
|
|
184
|
+
);
|
|
185
|
+
const byPath = new Map<string, MemoryFile>([[path.join(correctionsDir, smallName), smallMemory]]);
|
|
186
|
+
|
|
187
|
+
const storage: WeeklyEvidenceStorage = {
|
|
188
|
+
async readMemoryByPath(filePath) {
|
|
189
|
+
return byPath.get(filePath) ?? null;
|
|
190
|
+
},
|
|
191
|
+
async readMemoryActionEventRows() {
|
|
192
|
+
return [];
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const evidence = await readWeeklyCorrections({
|
|
197
|
+
storage,
|
|
198
|
+
memoryDir: root,
|
|
199
|
+
weekId: "2026-W01",
|
|
200
|
+
});
|
|
201
|
+
assert.equal(evidence.length, 1);
|
|
202
|
+
assert.equal(evidence[0]!.id, "corr-small");
|
|
203
|
+
assert.equal(evidence[0]!.note, "Fix the pricing table.");
|
|
204
|
+
assert.equal(evidence[0]!.provenance, `corrections/${smallName}`);
|
|
205
|
+
|
|
206
|
+
assert.ok(CORRECTION_FILE_BYTE_LIMIT === 256 * 1024);
|
|
207
|
+
assert.ok(WEEKLY_CORRECTION_READ_LIMIT === 500);
|
|
208
|
+
assert.ok(WEEKLY_CORRECTION_SCAN_LIMIT === 2000);
|
|
209
|
+
} finally {
|
|
210
|
+
await rm(root, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("provenanceVisibleInStoreRecall and capWeeklyPatternDetails prefer visible actions", () => {
|
|
215
|
+
assert.equal(provenanceVisibleInStoreRecall(["facts/a.md"]), true);
|
|
216
|
+
assert.equal(provenanceVisibleInStoreRecall(["namespaces/other/x.md"]), false);
|
|
217
|
+
assert.equal(provenanceVisibleInStoreRecall(["namespaces/home/x.md"], "namespaces/home/"), true);
|
|
218
|
+
assert.equal(provenanceVisibleInStoreRecall([]), true);
|
|
219
|
+
|
|
220
|
+
const details = [
|
|
221
|
+
{ pattern: "foreign", provenance: ["namespaces/other/a.md"] as const },
|
|
222
|
+
{ pattern: "action-a", provenance: ["memory-actions.jsonl:*"] as const },
|
|
223
|
+
{ pattern: "corr", provenance: ["corrections/c.md"] as const },
|
|
224
|
+
{ pattern: "action-b", provenance: ["memory-actions.jsonl:*"] as const },
|
|
225
|
+
{ pattern: "overflow", provenance: ["facts/x.md"] as const },
|
|
226
|
+
];
|
|
227
|
+
const capped = capWeeklyPatternDetails(
|
|
228
|
+
details,
|
|
229
|
+
(pattern) => (pattern.startsWith("action") ? "action" : "correction"),
|
|
230
|
+
null,
|
|
231
|
+
3,
|
|
232
|
+
);
|
|
233
|
+
assert.deepEqual(
|
|
234
|
+
capped.map((row) => row.pattern),
|
|
235
|
+
["action-a", "action-b", "corr"],
|
|
236
|
+
);
|
|
237
|
+
});
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { raceAbort } from "./abort-error.js";
|
|
4
4
|
import { entityMentionEpoch } from "./entity-mention-epoch.js";
|
|
5
5
|
import { log } from "./logger.js";
|
|
6
|
+
import { compareStrings } from "./eval-trajectory-contracts.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* In-memory entity mention index.
|
|
@@ -158,12 +159,6 @@ function throwIfMentionAborted(signal?: AbortSignal): void {
|
|
|
158
159
|
throw reason instanceof Error ? reason : new Error("entity mention rebuild aborted");
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
function compareStrings(left: string, right: string): number {
|
|
162
|
-
if (left < right) return -1;
|
|
163
|
-
if (left > right) return 1;
|
|
164
|
-
return 0;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
162
|
function deduped(values: readonly string[]): string[] {
|
|
168
163
|
const seen = new Set<string>();
|
|
169
164
|
const out: string[] = [];
|
|
@@ -200,6 +200,10 @@ export function isUnsupportedJsonSchemaError(error: unknown): boolean {
|
|
|
200
200
|
}
|
|
201
201
|
return (
|
|
202
202
|
/(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output)\b[^;,\n]{0,60}\b(?:unsupported|not supported|not implemented|not available)\b/i.test(message) ||
|
|
203
|
+
// "unavailable" counts only when it directly describes the format (DeepSeek:
|
|
204
|
+
// "This response_format type is unavailable now"). A wider window would also
|
|
205
|
+
// match an outage message that merely echoes the format.
|
|
206
|
+
/(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output)(?:\s{1,8}(?:type|mode|parameter|option|feature))?\s{1,8}(?:is\s{1,8})?unavailable\b/i.test(message) ||
|
|
203
207
|
/\b(?:unsupported|unknown|unrecognized)(?:[\s:"'=]|parameter|argument|request|supplied|field|type|value){1,40}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
|
|
204
208
|
/\b(?:does not|doesn't|cannot|can't) support(?:[\s:"'=]|the|native|type){1,20}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
|
|
205
209
|
// A strict provider parser rejecting the SCHEMA CONTENT itself (2026-11-05
|
|
@@ -170,6 +170,9 @@ test("a diagnostic echo of the format is not a schema capability rejection", asy
|
|
|
170
170
|
for (const message of [
|
|
171
171
|
"Context length exceeded; request included response_format json_schema",
|
|
172
172
|
"Unsupported parameter temperature; request included response_format json_schema",
|
|
173
|
+
"Model temporarily unavailable; request included response_format json_schema",
|
|
174
|
+
"Request included response_format json_schema but model temporarily unavailable",
|
|
175
|
+
"json_schema request failed: upstream unavailable",
|
|
173
176
|
]) {
|
|
174
177
|
const server = await startRecordingServer(() => ({ status: 400, body: { error: { message } } }));
|
|
175
178
|
try {
|
|
@@ -199,6 +202,36 @@ test("an explicit unknown response_format field retries without the format", asy
|
|
|
199
202
|
}
|
|
200
203
|
});
|
|
201
204
|
|
|
205
|
+
test("an unavailable response_format type retries without the format and persists the rejection", async () => {
|
|
206
|
+
const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
|
|
207
|
+
const server = await startRecordingServer((_req, count) =>
|
|
208
|
+
count === 1
|
|
209
|
+
? { status: 400, body: { error: { message: "This response_format type is unavailable now" } } }
|
|
210
|
+
: { status: 200, body: okCompletion },
|
|
211
|
+
);
|
|
212
|
+
try {
|
|
213
|
+
const registry = new ModelRegistry(memoryDir);
|
|
214
|
+
const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }), registry);
|
|
215
|
+
primeClient(client);
|
|
216
|
+
assert.equal(
|
|
217
|
+
(await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }))?.content,
|
|
218
|
+
"done",
|
|
219
|
+
);
|
|
220
|
+
assert.equal(server.requests.length, 2, "one schema-carrying request, then one retry without the format");
|
|
221
|
+
assert.deepEqual(server.requests[0].body.response_format, responseFormat);
|
|
222
|
+
assert.equal(server.requests[1].body.response_format, undefined);
|
|
223
|
+
// SAFETY: we wrote model-capabilities.json through ModelRegistry above; the
|
|
224
|
+
// document is an object keyed by normalized model id.
|
|
225
|
+
const persisted = JSON.parse(
|
|
226
|
+
fs.readFileSync(join(memoryDir, ".registry", "model-capabilities.json"), "utf8"),
|
|
227
|
+
) as { models: Record<string, { structuredOutput?: string }> };
|
|
228
|
+
assert.equal(persisted.models["test-local-model"]?.structuredOutput, "none");
|
|
229
|
+
} finally {
|
|
230
|
+
await server.close();
|
|
231
|
+
fs.rmSync(memoryDir, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
202
235
|
test("format removal persists for later 5xx attempts", async () => {
|
|
203
236
|
const server = await startRecordingServer((_req, count) =>
|
|
204
237
|
count === 1
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
import { PROJECTION_REBUILD_RUNNER_FLAG } from "./rebuild-memory-projection-runner.js";
|
|
10
|
+
|
|
11
|
+
const RUNNER = fileURLToPath(new URL("./rebuild-memory-projection-runner.ts", import.meta.url));
|
|
12
|
+
|
|
13
|
+
type RunnerMessage =
|
|
14
|
+
| { ok: true; currentRows: number; timelineRows: number; entityMentionRows: number }
|
|
15
|
+
| { ok: false; error: string };
|
|
16
|
+
|
|
17
|
+
async function runRunner(args: string[]): Promise<{
|
|
18
|
+
code: number | null;
|
|
19
|
+
messages: RunnerMessage[];
|
|
20
|
+
stderr: string;
|
|
21
|
+
}> {
|
|
22
|
+
const child = spawn(
|
|
23
|
+
process.execPath,
|
|
24
|
+
["--import", "tsx", RUNNER, PROJECTION_REBUILD_RUNNER_FLAG, ...args],
|
|
25
|
+
{
|
|
26
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
27
|
+
env: { ...process.env },
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
const messages: RunnerMessage[] = [];
|
|
31
|
+
let stderr = "";
|
|
32
|
+
child.stderr?.setEncoding("utf8");
|
|
33
|
+
child.stderr?.on("data", (chunk: string) => {
|
|
34
|
+
stderr += chunk;
|
|
35
|
+
});
|
|
36
|
+
child.on("message", (message) => {
|
|
37
|
+
// SAFETY: the runner posts only RunnerMessage values over IPC; the assertions below check each field.
|
|
38
|
+
messages.push(message as RunnerMessage);
|
|
39
|
+
});
|
|
40
|
+
const { promise: exitPromise, resolve, reject } = Promise.withResolvers<number | null>();
|
|
41
|
+
child.once("error", reject);
|
|
42
|
+
child.once("exit", (exitCode) => resolve(exitCode));
|
|
43
|
+
const code = await exitPromise;
|
|
44
|
+
return { code, messages, stderr };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("PROJECTION_REBUILD_RUNNER_FLAG is the documented argv marker", () => {
|
|
48
|
+
assert.equal(PROJECTION_REBUILD_RUNNER_FLAG, "--remnic-projection-rebuild-runner");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("runner rejects missing argv with an IPC failure message", async () => {
|
|
52
|
+
const result = await runRunner([]);
|
|
53
|
+
assert.equal(result.code, 1);
|
|
54
|
+
assert.equal(result.messages.length, 1);
|
|
55
|
+
assert.equal(result.messages[0]!.ok, false);
|
|
56
|
+
if (result.messages[0]!.ok === false) {
|
|
57
|
+
assert.match(result.messages[0]!.error, /requires memoryDir, defaultNamespace, and now/);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("runner rejects a non-timestamp now value", async () => {
|
|
62
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-runner-bad-now-"));
|
|
63
|
+
try {
|
|
64
|
+
const result = await runRunner([root, "default", "not-a-date"]);
|
|
65
|
+
assert.equal(result.code, 1);
|
|
66
|
+
assert.equal(result.messages[0]!.ok, false);
|
|
67
|
+
if (result.messages[0]!.ok === false) {
|
|
68
|
+
assert.match(result.messages[0]!.error, /not a timestamp/);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
await rm(root, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("runner rebuilds an empty memory dir and reports zero rows", async () => {
|
|
76
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-proj-runner-ok-"));
|
|
77
|
+
try {
|
|
78
|
+
const result = await runRunner([root, "default", "2026-06-01T00:00:00.000Z"]);
|
|
79
|
+
assert.equal(result.code, 0, result.stderr || JSON.stringify(result.messages));
|
|
80
|
+
assert.equal(result.messages.length, 1);
|
|
81
|
+
assert.deepEqual(result.messages[0], {
|
|
82
|
+
ok: true,
|
|
83
|
+
currentRows: 0,
|
|
84
|
+
timelineRows: 0,
|
|
85
|
+
entityMentionRows: 0,
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
await rm(root, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { fullCorpusHeapPressure } from "./full-corpus-heap-guard.js";
|
|
5
|
+
|
|
6
|
+
test("fullCorpusHeapPressure postpones only when usage is strictly above the limit", () => {
|
|
7
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: 61, heap_size_limit: 100 }, 0.6), {
|
|
8
|
+
postpone: true,
|
|
9
|
+
ratio: 0.61,
|
|
10
|
+
});
|
|
11
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: 60, heap_size_limit: 100 }, 0.6), {
|
|
12
|
+
postpone: false,
|
|
13
|
+
ratio: 0.6,
|
|
14
|
+
});
|
|
15
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: 59, heap_size_limit: 100 }, 0.6), {
|
|
16
|
+
postpone: false,
|
|
17
|
+
ratio: 0.59,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("fullCorpusHeapPressure never fires at limit ratio 1", () => {
|
|
22
|
+
const pressure = fullCorpusHeapPressure({ used_heap_size: 100, heap_size_limit: 100 }, 1);
|
|
23
|
+
assert.equal(pressure.postpone, false);
|
|
24
|
+
assert.equal(pressure.ratio, 1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("fullCorpusHeapPressure ignores unusable statistics or limits", () => {
|
|
28
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: 90, heap_size_limit: 0 }, 0.6), {
|
|
29
|
+
postpone: false,
|
|
30
|
+
ratio: 0,
|
|
31
|
+
});
|
|
32
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: 90, heap_size_limit: -1 }, 0.6), {
|
|
33
|
+
postpone: false,
|
|
34
|
+
ratio: 0,
|
|
35
|
+
});
|
|
36
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: Number.NaN, heap_size_limit: 100 }, 0.6), {
|
|
37
|
+
postpone: false,
|
|
38
|
+
ratio: 0,
|
|
39
|
+
});
|
|
40
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: -1, heap_size_limit: 100 }, 0.6), {
|
|
41
|
+
postpone: false,
|
|
42
|
+
ratio: 0,
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(fullCorpusHeapPressure({ used_heap_size: 90, heap_size_limit: 100 }, Number.NaN), {
|
|
45
|
+
postpone: false,
|
|
46
|
+
ratio: 0,
|
|
47
|
+
});
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
fullCorpusHeapPressure({ used_heap_size: Number.POSITIVE_INFINITY, heap_size_limit: 100 }, 0.6),
|
|
50
|
+
{ postpone: false, ratio: 0 },
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PROJECTION_REBUILD_HEAP_USAGE_LIMIT,
|
|
6
|
+
parseProjectionRebuildHeapUsageLimit,
|
|
7
|
+
parseProjectionRebuildScheduleConfig,
|
|
8
|
+
} from "./projection-rebuild-config.js";
|
|
9
|
+
|
|
10
|
+
const parseIntegerAtLeast: Parameters<typeof parseProjectionRebuildScheduleConfig>[1] = (
|
|
11
|
+
value,
|
|
12
|
+
fallback,
|
|
13
|
+
min,
|
|
14
|
+
keyName,
|
|
15
|
+
) => {
|
|
16
|
+
if (value === undefined || value === null) return fallback;
|
|
17
|
+
const n = Number(value);
|
|
18
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < min) {
|
|
19
|
+
throw new Error(`${keyName} must be an integer >= ${min}; got ${JSON.stringify(value)}`);
|
|
20
|
+
}
|
|
21
|
+
return n;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test("parseProjectionRebuildHeapUsageLimit defaults and accepts (0, 1]", () => {
|
|
25
|
+
assert.equal(parseProjectionRebuildHeapUsageLimit(undefined), DEFAULT_PROJECTION_REBUILD_HEAP_USAGE_LIMIT);
|
|
26
|
+
assert.equal(parseProjectionRebuildHeapUsageLimit(null), DEFAULT_PROJECTION_REBUILD_HEAP_USAGE_LIMIT);
|
|
27
|
+
assert.equal(DEFAULT_PROJECTION_REBUILD_HEAP_USAGE_LIMIT, 0.6);
|
|
28
|
+
assert.equal(parseProjectionRebuildHeapUsageLimit(0.5), 0.5);
|
|
29
|
+
assert.equal(parseProjectionRebuildHeapUsageLimit("0.75"), 0.75);
|
|
30
|
+
assert.equal(parseProjectionRebuildHeapUsageLimit(1), 1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("parseProjectionRebuildHeapUsageLimit rejects 0, negatives, and non-numbers", () => {
|
|
34
|
+
assert.throws(() => parseProjectionRebuildHeapUsageLimit(0), /projectionRebuildHeapUsageLimit/);
|
|
35
|
+
assert.throws(() => parseProjectionRebuildHeapUsageLimit(-0.1), /projectionRebuildHeapUsageLimit/);
|
|
36
|
+
assert.throws(() => parseProjectionRebuildHeapUsageLimit(1.01), /projectionRebuildHeapUsageLimit/);
|
|
37
|
+
assert.throws(() => parseProjectionRebuildHeapUsageLimit(Number.NaN), /projectionRebuildHeapUsageLimit/);
|
|
38
|
+
assert.throws(() => parseProjectionRebuildHeapUsageLimit("abc"), /projectionRebuildHeapUsageLimit/);
|
|
39
|
+
assert.throws(() => parseProjectionRebuildHeapUsageLimit({}), /projectionRebuildHeapUsageLimit/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("parseProjectionRebuildScheduleConfig defaults and coerces booleans", () => {
|
|
43
|
+
const defaults = parseProjectionRebuildScheduleConfig({}, parseIntegerAtLeast);
|
|
44
|
+
assert.equal(defaults.projectionRebuildEnabled, true);
|
|
45
|
+
assert.equal(defaults.projectionRebuildIntervalMs, 6 * 60 * 60 * 1000);
|
|
46
|
+
assert.equal(defaults.projectionRebuildHeapUsageLimit, 0.6);
|
|
47
|
+
|
|
48
|
+
const disabled = parseProjectionRebuildScheduleConfig(
|
|
49
|
+
{ projectionRebuildEnabled: "false", projectionRebuildIntervalMs: 120_000, projectionRebuildHeapUsageLimit: 0.4 },
|
|
50
|
+
parseIntegerAtLeast,
|
|
51
|
+
);
|
|
52
|
+
assert.equal(disabled.projectionRebuildEnabled, false);
|
|
53
|
+
assert.equal(disabled.projectionRebuildIntervalMs, 120_000);
|
|
54
|
+
assert.equal(disabled.projectionRebuildHeapUsageLimit, 0.4);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("parseProjectionRebuildScheduleConfig floors the interval via the integer parser", () => {
|
|
58
|
+
assert.throws(
|
|
59
|
+
() =>
|
|
60
|
+
parseProjectionRebuildScheduleConfig(
|
|
61
|
+
{ projectionRebuildIntervalMs: 59_999 },
|
|
62
|
+
parseIntegerAtLeast,
|
|
63
|
+
),
|
|
64
|
+
/projectionRebuildIntervalMs/,
|
|
65
|
+
);
|
|
66
|
+
assert.throws(
|
|
67
|
+
() =>
|
|
68
|
+
parseProjectionRebuildScheduleConfig(
|
|
69
|
+
{ projectionRebuildIntervalMs: 90_000.5 },
|
|
70
|
+
parseIntegerAtLeast,
|
|
71
|
+
),
|
|
72
|
+
/projectionRebuildIntervalMs/,
|
|
73
|
+
);
|
|
74
|
+
const ok = parseProjectionRebuildScheduleConfig(
|
|
75
|
+
{ projectionRebuildIntervalMs: 60_000 },
|
|
76
|
+
parseIntegerAtLeast,
|
|
77
|
+
);
|
|
78
|
+
assert.equal(ok.projectionRebuildIntervalMs, 60_000);
|
|
79
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { StorageManager } from "../storage.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* COS-388c Finding C2: ordinary entity reads must not follow a symlink at
|
|
11
|
+
* `entities/<id>.md` to content outside the memory dir. List skips the
|
|
12
|
+
* same entry. A normal entity file still reads.
|
|
13
|
+
*/
|
|
14
|
+
test("readEntity refuses entities/evil.md symlink outside the memory dir (C2)", async () => {
|
|
15
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-entity-c2-"));
|
|
16
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-entity-c2-out-"));
|
|
17
|
+
try {
|
|
18
|
+
const storage = new StorageManager(dir);
|
|
19
|
+
await storage.ensureDirectories({ waitForEntityMigration: false });
|
|
20
|
+
const id = await storage.writeEntity("Alice", "person", ["Normal entity inside the store."]);
|
|
21
|
+
|
|
22
|
+
await writeFile(path.join(outside, "secret.md"), "OUTSIDE_SECRET_CONTENT\n");
|
|
23
|
+
await symlink(path.join(outside, "secret.md"), path.join(dir, "entities", "evil.md"));
|
|
24
|
+
|
|
25
|
+
const evil = await storage.readEntity("evil");
|
|
26
|
+
assert.equal(
|
|
27
|
+
evil.includes("OUTSIDE_SECRET_CONTENT"),
|
|
28
|
+
false,
|
|
29
|
+
"entity read must not return the outside symlink target",
|
|
30
|
+
);
|
|
31
|
+
assert.equal(evil, "", "symlinked entity id is treated as absent, not as escaped content");
|
|
32
|
+
|
|
33
|
+
const alice = await storage.readEntity(id);
|
|
34
|
+
assert.match(alice, /Normal entity inside the store/);
|
|
35
|
+
|
|
36
|
+
const names = await storage.listEntityNames();
|
|
37
|
+
assert.equal(names.includes("evil"), false, "list must skip the symlinked entity entry");
|
|
38
|
+
assert.ok(names.includes(id), "normal entity remains listable");
|
|
39
|
+
} finally {
|
|
40
|
+
StorageManager.clearAllStaticCaches();
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
await rm(outside, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isMalformedEntityRecord,
|
|
6
|
+
isPopulatedUnnamedEntityRecord,
|
|
7
|
+
isShapedUnnamedEntityRecord as isEntityLikeUnnamedRecord,
|
|
8
|
+
isUnnamedEntityRecord,
|
|
9
|
+
} from "./malformed-entity-record.js";
|
|
10
|
+
|
|
11
|
+
const NAMED = "# Alice\n\n**Type:** person\n\nNotes about Alice.\n";
|
|
12
|
+
const NAMED_NO_TYPE = "# Alice\n\nNotes about Alice.\n";
|
|
13
|
+
const TYPE_ONLY = "**Type:** person\n\nNo heading name.\n";
|
|
14
|
+
const TAB_HEADING = "#\tAlice\n\n**Type:** person\n";
|
|
15
|
+
const BLANK_HEADING = "# \n\n**Type:** person\n";
|
|
16
|
+
const PROSE_ONLY = "just some content without structure\n";
|
|
17
|
+
const EMPTY_BODY = "---\nid: x\n---\n\n \n";
|
|
18
|
+
const FRONTMATTER_NAMED = "---\nid: alice\n---\n\n# Alice\n\n**Type:** person\n";
|
|
19
|
+
|
|
20
|
+
test("isUnnamedEntityRecord requires a hash-space heading with a name", () => {
|
|
21
|
+
assert.equal(isUnnamedEntityRecord(NAMED), false);
|
|
22
|
+
assert.equal(isUnnamedEntityRecord(NAMED_NO_TYPE), false);
|
|
23
|
+
assert.equal(isUnnamedEntityRecord(FRONTMATTER_NAMED), false);
|
|
24
|
+
assert.equal(isUnnamedEntityRecord(TYPE_ONLY), true);
|
|
25
|
+
assert.equal(isUnnamedEntityRecord(TAB_HEADING), true, "tab after # is not a heading");
|
|
26
|
+
assert.equal(isUnnamedEntityRecord(BLANK_HEADING), true, "blank # heading is unnamed");
|
|
27
|
+
assert.equal(isUnnamedEntityRecord(PROSE_ONLY), true);
|
|
28
|
+
assert.equal(isUnnamedEntityRecord(""), true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("isPopulatedUnnamedEntityRecord needs body text without a name", () => {
|
|
32
|
+
assert.equal(isPopulatedUnnamedEntityRecord(NAMED), false);
|
|
33
|
+
assert.equal(isPopulatedUnnamedEntityRecord(TYPE_ONLY), true);
|
|
34
|
+
assert.equal(isPopulatedUnnamedEntityRecord(TAB_HEADING), true);
|
|
35
|
+
assert.equal(isPopulatedUnnamedEntityRecord(PROSE_ONLY), true);
|
|
36
|
+
assert.equal(isPopulatedUnnamedEntityRecord(EMPTY_BODY), false, "whitespace-only body stays readable");
|
|
37
|
+
assert.equal(isPopulatedUnnamedEntityRecord(""), false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("isShapedUnnamedEntityRecord keeps entity-like unnamed files only", () => {
|
|
41
|
+
assert.equal(isEntityLikeUnnamedRecord(NAMED), false);
|
|
42
|
+
assert.equal(isEntityLikeUnnamedRecord(TYPE_ONLY), true);
|
|
43
|
+
assert.equal(isEntityLikeUnnamedRecord(TAB_HEADING), true, "any # line shapes the record");
|
|
44
|
+
assert.equal(isEntityLikeUnnamedRecord(BLANK_HEADING), true);
|
|
45
|
+
assert.equal(isEntityLikeUnnamedRecord(PROSE_ONLY), false, "unstructured placeholders stay for filename recall");
|
|
46
|
+
assert.equal(isEntityLikeUnnamedRecord(EMPTY_BODY), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("isMalformedEntityRecord requires both name and explicit type for migration", () => {
|
|
50
|
+
assert.equal(isMalformedEntityRecord(NAMED), false);
|
|
51
|
+
assert.equal(isMalformedEntityRecord(FRONTMATTER_NAMED), false);
|
|
52
|
+
assert.equal(isMalformedEntityRecord(NAMED_NO_TYPE), true, "missing type is malformed for migration");
|
|
53
|
+
assert.equal(isMalformedEntityRecord(TYPE_ONLY), true);
|
|
54
|
+
assert.equal(isMalformedEntityRecord(TAB_HEADING), true);
|
|
55
|
+
assert.equal(isMalformedEntityRecord(PROSE_ONLY), true);
|
|
56
|
+
assert.equal(isMalformedEntityRecord("# Bob\n\n**Type:** \n"), true, "blank type value is missing");
|
|
57
|
+
assert.equal(isMalformedEntityRecord("# Bob\n\n**Type:** org\r\n"), false, "CRLF type lines count");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("entity body ignores closed frontmatter and keeps unclosed frontmatter as body", () => {
|
|
61
|
+
const unclosed = "---\nid: stuck\n# Not a real heading yet\n";
|
|
62
|
+
// Unclosed frontmatter is not stripped, so the # line is not at body start of a closed block —
|
|
63
|
+
// but it still appears in the full content scan as a named heading.
|
|
64
|
+
assert.equal(isUnnamedEntityRecord(unclosed), false);
|
|
65
|
+
assert.equal(isMalformedEntityRecord(unclosed), true, "no type line");
|
|
66
|
+
|
|
67
|
+
const namedAfterFrontmatter = "---\na: 1\n---\r\n# Carol\r\n\r\n**Type:** place\r\n";
|
|
68
|
+
assert.equal(isMalformedEntityRecord(namedAfterFrontmatter), false);
|
|
69
|
+
});
|