@remnic/core 9.66.9 → 9.66.10
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 +20 -20
- package/dist/access-authorization-probe.js +21 -21
- package/dist/access-boundary.js +20 -20
- package/dist/access-cli.js +86 -86
- package/dist/access-extraction-force-flush.js +20 -20
- package/dist/access-http-query.js +22 -22
- package/dist/access-http.js +37 -37
- package/dist/access-identity-continuity-surface.js +20 -20
- package/dist/access-lcm-surface.js +20 -20
- package/dist/access-mcp.js +29 -29
- package/dist/access-namespace-preflight.js +20 -20
- package/dist/access-observe-write-surface.js +20 -20
- package/dist/access-operations-batch.js +23 -23
- package/dist/access-operations.js +28 -28
- package/dist/access-recall-concurrency.js +20 -20
- package/dist/access-recall-response.js +20 -20
- package/dist/access-recall-surface.js +20 -20
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +20 -20
- package/dist/access-surface-catalog.js +22 -22
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +8 -8
- package/dist/causal-consolidation.js +9 -9
- package/dist/{chunk-5GZ6UWY5.js → chunk-245QROHZ.js} +5 -5
- package/dist/{chunk-A7GQBJP5.js → chunk-2L5FSNPC.js} +10 -10
- package/dist/{chunk-SJ5L4ZZX.js → chunk-2OXBS2LV.js} +2 -2
- package/dist/{chunk-G775FE2E.js → chunk-4PHT7KE2.js} +11 -11
- package/dist/{chunk-TKSBOV7N.js → chunk-4PWANX5V.js} +6 -6
- package/dist/{chunk-TEJKBBWS.js → chunk-4QKWBVEE.js} +11 -11
- package/dist/{chunk-MFI4QPW5.js → chunk-54BRW3BL.js} +2 -2
- package/dist/{chunk-23F7HGBV.js → chunk-5E7KXID5.js} +1 -1
- package/dist/{chunk-5CSKIWEB.js → chunk-7BS5UVXA.js} +3 -3
- package/dist/{chunk-Q5YKVBIV.js → chunk-7FLINMUE.js} +2 -2
- package/dist/{chunk-ZC5L6ME2.js → chunk-7MOYAT4Y.js} +3 -3
- package/dist/{chunk-KVXMN33W.js → chunk-C6TMLF3A.js} +4 -4
- package/dist/{chunk-47BX23NA.js → chunk-D3AUQXTJ.js} +2 -2
- package/dist/{chunk-3HHLOKKU.js → chunk-DJLH3XWM.js} +2 -2
- package/dist/{chunk-SRILZVR4.js → chunk-E2PORO7O.js} +2 -2
- package/dist/{chunk-G3TUQJFU.js → chunk-E62M4H4M.js} +2 -2
- package/dist/{chunk-GB4R6B5M.js → chunk-EG4FHU5C.js} +2 -2
- package/dist/{chunk-QU2V4IO3.js → chunk-EMAEDM2J.js} +2 -2
- package/dist/{chunk-FNYVWAIK.js → chunk-G3B3LP6A.js} +18 -18
- package/dist/{chunk-D6Q5EPOV.js → chunk-HCERYDRI.js} +2 -2
- package/dist/{chunk-5NZIRM4Q.js → chunk-HJQZQUPC.js} +7 -7
- package/dist/{chunk-CIWOW2FH.js → chunk-HPGXB56F.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-UAWSKMWQ.js → chunk-ISX7JODR.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-HWWGPS2N.js → chunk-JNZSNJ43.js} +2 -2
- package/dist/{chunk-JVGBDNXA.js → chunk-KT3ERTF6.js} +2 -2
- package/dist/{chunk-SOQLXIQX.js → chunk-KWLDB64F.js} +2 -2
- package/dist/{chunk-NABJLNLU.js → chunk-KWVXCRFZ.js} +190 -52
- package/dist/chunk-KWVXCRFZ.js.map +1 -0
- package/dist/{chunk-7JPKRZD5.js → chunk-LPSTQR26.js} +2 -2
- package/dist/{chunk-OT7WQ2I3.js → chunk-LTWFU6VJ.js} +2 -2
- package/dist/{chunk-NSP3HFJZ.js → chunk-M2BY5IZL.js} +2 -2
- package/dist/{chunk-ZJGHOPEO.js → chunk-NR36NW24.js} +34 -34
- package/dist/{chunk-AAJK3PZ6.js → chunk-QB66LBXA.js} +2 -2
- package/dist/{chunk-XVNDP3RO.js → chunk-QKTGCL6M.js} +2 -2
- package/dist/{chunk-LTDN5WYF.js → chunk-S47XNAWD.js} +2 -2
- package/dist/{chunk-IBHUZEKL.js → chunk-SQEIPTDV.js} +2 -2
- package/dist/{chunk-J67E4JNT.js → chunk-TGKLVAML.js} +2 -2
- package/dist/{chunk-VCYY7XEL.js → chunk-TS37MXC7.js} +2 -2
- package/dist/{chunk-JHU543UU.js → chunk-TXCLDIXJ.js} +2 -2
- package/dist/{chunk-TWMGWLHD.js → chunk-VSQG4AXL.js} +2 -2
- package/dist/{chunk-ZVUGRXSY.js → chunk-WD424KYT.js} +2 -2
- package/dist/{chunk-B3TZLAO4.js → chunk-X5SDZMO7.js} +2 -2
- package/dist/{chunk-FUPIAP6Q.js → chunk-XN33JLRF.js} +3 -3
- package/dist/{chunk-OJSCH7QT.js → chunk-Y2ECPAJI.js} +7 -7
- package/dist/{chunk-DGKVUPBM.js → chunk-Y6KDJQLK.js} +7 -7
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-VX5YHEGJ.js → chunk-YYDOUOYM.js} +3 -3
- package/dist/{chunk-Q3UMNZRK.js → chunk-ZIGSNLYI.js} +3 -3
- package/dist/{chunk-MBDUNO7J.js → chunk-ZPZLGN4Y.js} +2 -2
- package/dist/{chunk-RF3LMSQW.js → chunk-ZRQPX2DW.js} +2 -2
- package/dist/cli.js +60 -60
- package/dist/coding/export-okf-codegraph.js +8 -8
- package/dist/compounding/engine.js +8 -8
- package/dist/connectors/codex-materialize-runner.js +8 -8
- package/dist/connectors/index.js +8 -8
- package/dist/convergence-refresh.js +10 -10
- package/dist/corpus-watermark.js +10 -10
- package/dist/dashboard-runtime.js +2 -2
- package/dist/entity-retrieval.js +8 -8
- package/dist/external-wiki-access.js +21 -21
- package/dist/extraction-span-tokens.d.ts +16 -0
- package/dist/extraction-span-tokens.js +34 -0
- package/dist/extraction-span-tokens.js.map +1 -0
- package/dist/extraction.js +8 -8
- package/dist/{first-start-migration-UG32BSG2.js → first-start-migration-KHIMCBIC.js} +4 -4
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.js +125 -117
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.js +9 -9
- package/dist/location/index.js +10 -10
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.js +8 -8
- package/dist/operator-doctor-corpus.js +11 -11
- package/dist/operator-toolkit.js +16 -16
- package/dist/orchestration/maintenance.js +16 -16
- package/dist/orchestrator.js +87 -87
- package/dist/recall-deep-frontier.d.ts +15 -0
- package/dist/recall-deep-frontier.js +29 -0
- package/dist/recall-deep-frontier.js.map +1 -0
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +7 -7
- package/dist/semantic-consolidation.js +9 -9
- package/dist/semantic-rule-promotion.js +8 -8
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/source-agent-qualifier.js +8 -8
- package/dist/standup/build.js +8 -8
- package/dist/storage.js +7 -7
- package/dist/support-passport/index.js +23 -23
- 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 +9 -9
- package/dist/verified-recall.js +8 -8
- package/dist/who-knows-cli.js +9 -9
- package/dist/who-knows.js +8 -8
- package/package.json +2 -2
- package/src/activity/index.ts +2 -0
- package/src/activity/privacy-status.test.ts +117 -0
- package/src/activity/privacy-status.ts +65 -0
- package/src/activity/timeline/index.ts +1 -0
- package/src/activity/timeline/week-previous.test.ts +119 -0
- package/src/activity/timeline/week-previous.ts +83 -0
- package/src/activity/vault-insert.test.ts +105 -0
- package/src/activity/vault-insert.ts +86 -0
- package/src/extraction-span-tokens.test.ts +69 -0
- package/src/extraction-span-tokens.ts +44 -0
- package/src/recall-deep-frontier.test.ts +85 -0
- package/src/recall-deep-frontier.ts +39 -0
- package/dist/chunk-NABJLNLU.js.map +0 -1
- /package/dist/{chunk-5GZ6UWY5.js.map → chunk-245QROHZ.js.map} +0 -0
- /package/dist/{chunk-A7GQBJP5.js.map → chunk-2L5FSNPC.js.map} +0 -0
- /package/dist/{chunk-SJ5L4ZZX.js.map → chunk-2OXBS2LV.js.map} +0 -0
- /package/dist/{chunk-G775FE2E.js.map → chunk-4PHT7KE2.js.map} +0 -0
- /package/dist/{chunk-TKSBOV7N.js.map → chunk-4PWANX5V.js.map} +0 -0
- /package/dist/{chunk-TEJKBBWS.js.map → chunk-4QKWBVEE.js.map} +0 -0
- /package/dist/{chunk-MFI4QPW5.js.map → chunk-54BRW3BL.js.map} +0 -0
- /package/dist/{chunk-23F7HGBV.js.map → chunk-5E7KXID5.js.map} +0 -0
- /package/dist/{chunk-5CSKIWEB.js.map → chunk-7BS5UVXA.js.map} +0 -0
- /package/dist/{chunk-Q5YKVBIV.js.map → chunk-7FLINMUE.js.map} +0 -0
- /package/dist/{chunk-ZC5L6ME2.js.map → chunk-7MOYAT4Y.js.map} +0 -0
- /package/dist/{chunk-KVXMN33W.js.map → chunk-C6TMLF3A.js.map} +0 -0
- /package/dist/{chunk-47BX23NA.js.map → chunk-D3AUQXTJ.js.map} +0 -0
- /package/dist/{chunk-3HHLOKKU.js.map → chunk-DJLH3XWM.js.map} +0 -0
- /package/dist/{chunk-SRILZVR4.js.map → chunk-E2PORO7O.js.map} +0 -0
- /package/dist/{chunk-G3TUQJFU.js.map → chunk-E62M4H4M.js.map} +0 -0
- /package/dist/{chunk-GB4R6B5M.js.map → chunk-EG4FHU5C.js.map} +0 -0
- /package/dist/{chunk-QU2V4IO3.js.map → chunk-EMAEDM2J.js.map} +0 -0
- /package/dist/{chunk-FNYVWAIK.js.map → chunk-G3B3LP6A.js.map} +0 -0
- /package/dist/{chunk-D6Q5EPOV.js.map → chunk-HCERYDRI.js.map} +0 -0
- /package/dist/{chunk-5NZIRM4Q.js.map → chunk-HJQZQUPC.js.map} +0 -0
- /package/dist/{chunk-CIWOW2FH.js.map → chunk-HPGXB56F.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-UAWSKMWQ.js.map → chunk-ISX7JODR.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-HWWGPS2N.js.map → chunk-JNZSNJ43.js.map} +0 -0
- /package/dist/{chunk-JVGBDNXA.js.map → chunk-KT3ERTF6.js.map} +0 -0
- /package/dist/{chunk-SOQLXIQX.js.map → chunk-KWLDB64F.js.map} +0 -0
- /package/dist/{chunk-7JPKRZD5.js.map → chunk-LPSTQR26.js.map} +0 -0
- /package/dist/{chunk-OT7WQ2I3.js.map → chunk-LTWFU6VJ.js.map} +0 -0
- /package/dist/{chunk-NSP3HFJZ.js.map → chunk-M2BY5IZL.js.map} +0 -0
- /package/dist/{chunk-ZJGHOPEO.js.map → chunk-NR36NW24.js.map} +0 -0
- /package/dist/{chunk-AAJK3PZ6.js.map → chunk-QB66LBXA.js.map} +0 -0
- /package/dist/{chunk-XVNDP3RO.js.map → chunk-QKTGCL6M.js.map} +0 -0
- /package/dist/{chunk-LTDN5WYF.js.map → chunk-S47XNAWD.js.map} +0 -0
- /package/dist/{chunk-IBHUZEKL.js.map → chunk-SQEIPTDV.js.map} +0 -0
- /package/dist/{chunk-J67E4JNT.js.map → chunk-TGKLVAML.js.map} +0 -0
- /package/dist/{chunk-VCYY7XEL.js.map → chunk-TS37MXC7.js.map} +0 -0
- /package/dist/{chunk-JHU543UU.js.map → chunk-TXCLDIXJ.js.map} +0 -0
- /package/dist/{chunk-TWMGWLHD.js.map → chunk-VSQG4AXL.js.map} +0 -0
- /package/dist/{chunk-ZVUGRXSY.js.map → chunk-WD424KYT.js.map} +0 -0
- /package/dist/{chunk-B3TZLAO4.js.map → chunk-X5SDZMO7.js.map} +0 -0
- /package/dist/{chunk-FUPIAP6Q.js.map → chunk-XN33JLRF.js.map} +0 -0
- /package/dist/{chunk-OJSCH7QT.js.map → chunk-Y2ECPAJI.js.map} +0 -0
- /package/dist/{chunk-DGKVUPBM.js.map → chunk-Y6KDJQLK.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-VX5YHEGJ.js.map → chunk-YYDOUOYM.js.map} +0 -0
- /package/dist/{chunk-Q3UMNZRK.js.map → chunk-ZIGSNLYI.js.map} +0 -0
- /package/dist/{chunk-MBDUNO7J.js.map → chunk-ZPZLGN4Y.js.map} +0 -0
- /package/dist/{chunk-RF3LMSQW.js.map → chunk-ZRQPX2DW.js.map} +0 -0
- /package/dist/{first-start-migration-UG32BSG2.js.map → first-start-migration-KHIMCBIC.js.map} +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { compareWeeklyPreviousPeriod } from "./week-previous.js";
|
|
5
|
+
|
|
6
|
+
const CURRENT = {
|
|
7
|
+
activeMs: 3_600_000,
|
|
8
|
+
idleMs: 600_000,
|
|
9
|
+
pauseMs: 120_000,
|
|
10
|
+
gapMs: 300_000,
|
|
11
|
+
unclassifiedMs: 60_000,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const PREVIOUS = {
|
|
15
|
+
activeMs: 1_800_000,
|
|
16
|
+
idleMs: 900_000,
|
|
17
|
+
pauseMs: 120_000,
|
|
18
|
+
gapMs: 100_000,
|
|
19
|
+
unclassifiedMs: 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test("null previous is unavailable, not zero", () => {
|
|
23
|
+
const result = compareWeeklyPreviousPeriod({
|
|
24
|
+
previous: null,
|
|
25
|
+
current: CURRENT,
|
|
26
|
+
previousStartUtc: "2026-07-06T00:00:00Z",
|
|
27
|
+
previousEndUtc: "2026-07-13T00:00:00Z",
|
|
28
|
+
});
|
|
29
|
+
assert.deepEqual(result, { available: false });
|
|
30
|
+
assert.equal("deltaActiveMs" in result, false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("undefined previous is unavailable", () => {
|
|
34
|
+
const result = compareWeeklyPreviousPeriod({
|
|
35
|
+
previous: undefined,
|
|
36
|
+
current: CURRENT,
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(result, { available: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("previous without timestamps is unavailable", () => {
|
|
42
|
+
const result = compareWeeklyPreviousPeriod({ previous: PREVIOUS, current: CURRENT });
|
|
43
|
+
assert.deepEqual(result, { available: false });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("previous with blank timestamps is unavailable", () => {
|
|
47
|
+
const result = compareWeeklyPreviousPeriod({
|
|
48
|
+
previous: PREVIOUS,
|
|
49
|
+
current: CURRENT,
|
|
50
|
+
previousStartUtc: " ",
|
|
51
|
+
previousEndUtc: "\t",
|
|
52
|
+
});
|
|
53
|
+
assert.deepEqual(result, { available: false });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("mixed deltas keep sign and trim timestamps", () => {
|
|
57
|
+
const result = compareWeeklyPreviousPeriod({
|
|
58
|
+
previous: PREVIOUS,
|
|
59
|
+
current: CURRENT,
|
|
60
|
+
previousStartUtc: " 2026-07-06T00:00:00Z ",
|
|
61
|
+
previousEndUtc: " 2026-07-13T00:00:00Z ",
|
|
62
|
+
});
|
|
63
|
+
assert.equal(result.available, true);
|
|
64
|
+
if (!result.available) return;
|
|
65
|
+
assert.equal(result.previousStartUtc, "2026-07-06T00:00:00Z");
|
|
66
|
+
assert.equal(result.previousEndUtc, "2026-07-13T00:00:00Z");
|
|
67
|
+
assert.equal(result.deltaActiveMs, 1_800_000);
|
|
68
|
+
assert.equal(result.deltaIdleMs, -300_000);
|
|
69
|
+
assert.equal(result.deltaPauseMs, 0);
|
|
70
|
+
assert.equal(result.deltaGapMs, 200_000);
|
|
71
|
+
assert.equal(result.deltaUnclassifiedMs, 60_000);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("NaN duration on previous throws TypeError", () => {
|
|
75
|
+
assert.throws(
|
|
76
|
+
() =>
|
|
77
|
+
compareWeeklyPreviousPeriod({
|
|
78
|
+
previous: { ...PREVIOUS, gapMs: Number.NaN },
|
|
79
|
+
current: CURRENT,
|
|
80
|
+
previousStartUtc: "2026-07-06T00:00:00Z",
|
|
81
|
+
previousEndUtc: "2026-07-13T00:00:00Z",
|
|
82
|
+
}),
|
|
83
|
+
TypeError,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("Infinity duration on current throws TypeError", () => {
|
|
88
|
+
assert.throws(
|
|
89
|
+
() =>
|
|
90
|
+
compareWeeklyPreviousPeriod({
|
|
91
|
+
previous: PREVIOUS,
|
|
92
|
+
current: { ...CURRENT, activeMs: Number.POSITIVE_INFINITY },
|
|
93
|
+
}),
|
|
94
|
+
/current\.activeMs/,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("all-zero current with present previous is available with negative deltas", () => {
|
|
99
|
+
const zeros = {
|
|
100
|
+
activeMs: 0,
|
|
101
|
+
idleMs: 0,
|
|
102
|
+
pauseMs: 0,
|
|
103
|
+
gapMs: 0,
|
|
104
|
+
unclassifiedMs: 0,
|
|
105
|
+
};
|
|
106
|
+
const result = compareWeeklyPreviousPeriod({
|
|
107
|
+
previous: PREVIOUS,
|
|
108
|
+
current: zeros,
|
|
109
|
+
previousStartUtc: "2026-07-06T00:00:00Z",
|
|
110
|
+
previousEndUtc: "2026-07-13T00:00:00Z",
|
|
111
|
+
});
|
|
112
|
+
assert.equal(result.available, true);
|
|
113
|
+
if (!result.available) return;
|
|
114
|
+
assert.equal(result.deltaActiveMs, -PREVIOUS.activeMs);
|
|
115
|
+
assert.equal(result.deltaIdleMs, -PREVIOUS.idleMs);
|
|
116
|
+
assert.equal(result.deltaPauseMs, -PREVIOUS.pauseMs);
|
|
117
|
+
assert.equal(result.deltaGapMs, -PREVIOUS.gapMs);
|
|
118
|
+
assert.equal(result.deltaUnclassifiedMs, 0);
|
|
119
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Previous-week duration comparison (issue #2052).
|
|
3
|
+
*
|
|
4
|
+
* The prior week is explicit when unavailable. It is never silently treated
|
|
5
|
+
* as zero.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface WeeklyDurationTotals {
|
|
9
|
+
activeMs: number;
|
|
10
|
+
idleMs: number;
|
|
11
|
+
pauseMs: number;
|
|
12
|
+
gapMs: number;
|
|
13
|
+
unclassifiedMs: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type WeeklyPreviousPeriod =
|
|
17
|
+
| { available: false }
|
|
18
|
+
| {
|
|
19
|
+
available: true;
|
|
20
|
+
previousStartUtc: string;
|
|
21
|
+
previousEndUtc: string;
|
|
22
|
+
deltaActiveMs: number;
|
|
23
|
+
deltaIdleMs: number;
|
|
24
|
+
deltaPauseMs: number;
|
|
25
|
+
deltaGapMs: number;
|
|
26
|
+
deltaUnclassifiedMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DURATION_FIELDS = [
|
|
30
|
+
"activeMs",
|
|
31
|
+
"idleMs",
|
|
32
|
+
"pauseMs",
|
|
33
|
+
"gapMs",
|
|
34
|
+
"unclassifiedMs",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
function assertFiniteTotals(totals: WeeklyDurationTotals, label: string): void {
|
|
38
|
+
for (const field of DURATION_FIELDS) {
|
|
39
|
+
const value = totals[field];
|
|
40
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
41
|
+
throw new TypeError(`${label}.${field} must be a finite number; got ${String(value)}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compare this week's duration totals against the previous week's.
|
|
48
|
+
*
|
|
49
|
+
* `previous` absent (null/undefined) or lacking trimmed start/end timestamps
|
|
50
|
+
* returns `{ available: false }` — never zero deltas. Otherwise returns the
|
|
51
|
+
* per-field deltas (`current - previous`, may be negative). Pure.
|
|
52
|
+
*/
|
|
53
|
+
export function compareWeeklyPreviousPeriod(input: {
|
|
54
|
+
previous: WeeklyDurationTotals | null | undefined;
|
|
55
|
+
current: WeeklyDurationTotals;
|
|
56
|
+
previousStartUtc?: string;
|
|
57
|
+
previousEndUtc?: string;
|
|
58
|
+
}): WeeklyPreviousPeriod {
|
|
59
|
+
const { previous, current, previousStartUtc, previousEndUtc } = input;
|
|
60
|
+
assertFiniteTotals(current, "current");
|
|
61
|
+
|
|
62
|
+
if (previous === null || previous === undefined) {
|
|
63
|
+
return { available: false };
|
|
64
|
+
}
|
|
65
|
+
assertFiniteTotals(previous, "previous");
|
|
66
|
+
|
|
67
|
+
const start = previousStartUtc?.trim() ?? "";
|
|
68
|
+
const end = previousEndUtc?.trim() ?? "";
|
|
69
|
+
if (start === "" || end === "") {
|
|
70
|
+
return { available: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
available: true,
|
|
75
|
+
previousStartUtc: start,
|
|
76
|
+
previousEndUtc: end,
|
|
77
|
+
deltaActiveMs: current.activeMs - previous.activeMs,
|
|
78
|
+
deltaIdleMs: current.idleMs - previous.idleMs,
|
|
79
|
+
deltaPauseMs: current.pauseMs - previous.pauseMs,
|
|
80
|
+
deltaGapMs: current.gapMs - previous.gapMs,
|
|
81
|
+
deltaUnclassifiedMs: current.unclassifiedMs - previous.unclassifiedMs,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { insertMarkersUnderHeading } from "./vault-insert.js";
|
|
5
|
+
|
|
6
|
+
const START = "<!-- remnic:daily:start -->";
|
|
7
|
+
const END = "<!-- remnic:daily:end -->";
|
|
8
|
+
|
|
9
|
+
test("inserts markers under a unique heading before existing body", () => {
|
|
10
|
+
const fileText = ["# Notes", "", "## Journal", "", "Existing entry.", ""].join("\n");
|
|
11
|
+
const result = insertMarkersUnderHeading(fileText, {
|
|
12
|
+
heading: "Journal",
|
|
13
|
+
name: "daily",
|
|
14
|
+
content: "Line one.\nLine two.",
|
|
15
|
+
});
|
|
16
|
+
assert.ok(result.ok && result.inserted);
|
|
17
|
+
assert.equal(
|
|
18
|
+
result.text,
|
|
19
|
+
["# Notes", "", "## Journal", START, "Line one.", "Line two.", END, "", "Existing entry.", ""].join("\n"),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns text unchanged when both markers already exist", () => {
|
|
24
|
+
const fileText = ["## Journal", START, "old", END, "tail", ""].join("\n");
|
|
25
|
+
const result = insertMarkersUnderHeading(fileText, { heading: "Journal", name: "daily", content: "new" });
|
|
26
|
+
assert.deepEqual(result, { ok: true, text: fileText, inserted: false });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("fails with no_heading when no heading matches exactly", () => {
|
|
30
|
+
const fileText = ["## Journal entry", "body", ""].join("\n");
|
|
31
|
+
const result = insertMarkersUnderHeading(fileText, { heading: "Journal", name: "daily", content: "new" });
|
|
32
|
+
assert.deepEqual(result, { ok: false, reason: "no_heading", text: fileText });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("fails with duplicate_heading and both 1-based line numbers", () => {
|
|
36
|
+
const fileText = ["# Top", "## Journal", "a", "## Journal", "b", ""].join("\n");
|
|
37
|
+
const result = insertMarkersUnderHeading(fileText, { heading: "Journal", name: "daily", content: "new" });
|
|
38
|
+
assert.ok(!result.ok && result.reason === "duplicate_heading");
|
|
39
|
+
assert.deepEqual(result.lines, [2, 4]);
|
|
40
|
+
assert.equal(result.text, fileText);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("inserts before a later same-level heading and leaves that section intact", () => {
|
|
44
|
+
const fileText = ["## Journal", "existing", "## Other", "other body", ""].join("\n");
|
|
45
|
+
const result = insertMarkersUnderHeading(fileText, { heading: "Journal", name: "daily", content: "cards" });
|
|
46
|
+
assert.ok(result.ok && result.inserted);
|
|
47
|
+
assert.equal(
|
|
48
|
+
result.text,
|
|
49
|
+
["## Journal", START, "cards", END, "existing", "## Other", "other body", ""].join("\n"),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("deeper headings do not end the section", () => {
|
|
54
|
+
const fileText = ["## Journal", "### Nested", "nested body", "## Other", ""].join("\n");
|
|
55
|
+
const result = insertMarkersUnderHeading(fileText, { heading: "Journal", name: "daily", content: "cards" });
|
|
56
|
+
assert.ok(result.ok && result.inserted);
|
|
57
|
+
assert.equal(
|
|
58
|
+
result.text,
|
|
59
|
+
["## Journal", START, "cards", END, "### Nested", "nested body", "## Other", ""].join("\n"),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("inserts under a heading at EOF without trailing newline", () => {
|
|
64
|
+
const result = insertMarkersUnderHeading("## Journal", { heading: "Journal", name: "daily", content: "cards" });
|
|
65
|
+
assert.ok(result.ok && result.inserted);
|
|
66
|
+
assert.equal(result.text, `## Journal\n${START}\ncards\n${END}\n`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("keeps content ending in a newline without doubling it", () => {
|
|
70
|
+
const result = insertMarkersUnderHeading("## Journal\nbody\n", {
|
|
71
|
+
heading: "Journal",
|
|
72
|
+
name: "daily",
|
|
73
|
+
content: "cards\n",
|
|
74
|
+
});
|
|
75
|
+
assert.ok(result.ok && result.inserted);
|
|
76
|
+
assert.equal(result.text, `## Journal\n${START}\ncards\n${END}\nbody\n`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("empty content inserts adjacent markers", () => {
|
|
80
|
+
const result = insertMarkersUnderHeading("## Journal\n", { heading: "Journal", name: "daily", content: "" });
|
|
81
|
+
assert.ok(result.ok && result.inserted);
|
|
82
|
+
assert.equal(result.text, `## Journal\n${START}\n${END}\n`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("preserves CRLF as the dominant EOL", () => {
|
|
86
|
+
const fileText = ["## Journal", "body", ""].join("\r\n");
|
|
87
|
+
const result = insertMarkersUnderHeading(fileText, {
|
|
88
|
+
heading: "Journal",
|
|
89
|
+
name: "daily",
|
|
90
|
+
content: "line a\r\nline b",
|
|
91
|
+
});
|
|
92
|
+
assert.ok(result.ok && result.inserted);
|
|
93
|
+
assert.equal(result.text, `## Journal\r\n${START}\r\nline a\r\nline b\r\n${END}\r\nbody\r\n`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("empty heading or name throws RangeError", () => {
|
|
97
|
+
assert.throws(
|
|
98
|
+
() => insertMarkersUnderHeading("## Journal\n", { heading: "", name: "daily", content: "" }),
|
|
99
|
+
(err) => err instanceof RangeError && /empty/i.test(err.message),
|
|
100
|
+
);
|
|
101
|
+
assert.throws(
|
|
102
|
+
() => insertMarkersUnderHeading("## Journal\n", { heading: "Journal", name: "", content: "" }),
|
|
103
|
+
(err) => err instanceof RangeError && /empty/i.test(err.message),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed-region marker insertion (issue #1985).
|
|
3
|
+
*
|
|
4
|
+
* Inserts a `<!-- remnic:<name>:start/end -->` pair under a unique ATX
|
|
5
|
+
* heading when the region is absent. This helper never replaces (see
|
|
6
|
+
* `vault-publish.ts`), never creates files, and never invents a heading.
|
|
7
|
+
* Pure string-in/string-out.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type InsertVaultRegionResult =
|
|
11
|
+
| { ok: true; text: string; inserted: true }
|
|
12
|
+
| { ok: true; text: string; inserted: false }
|
|
13
|
+
| { ok: false; reason: "no_heading"; text: string }
|
|
14
|
+
| { ok: false; reason: "duplicate_heading"; lines: readonly number[]; text: string };
|
|
15
|
+
|
|
16
|
+
export function insertMarkersUnderHeading(
|
|
17
|
+
fileText: string,
|
|
18
|
+
opts: { heading: string; name: string; content: string },
|
|
19
|
+
): InsertVaultRegionResult {
|
|
20
|
+
if (typeof opts.heading !== "string" || opts.heading.length === 0) {
|
|
21
|
+
throw new RangeError("Heading must be non-empty.");
|
|
22
|
+
}
|
|
23
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
24
|
+
throw new RangeError("Region name must be non-empty.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const startMarker = `<!-- remnic:${opts.name}:start -->`;
|
|
28
|
+
const endMarker = `<!-- remnic:${opts.name}:end -->`;
|
|
29
|
+
if (fileText.includes(startMarker) && fileText.includes(endMarker)) {
|
|
30
|
+
return { ok: true, text: fileText, inserted: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rows = fileLines(fileText);
|
|
34
|
+
const hits: Array<{ row: (typeof rows)[number]; level: number }> = [];
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const heading = parseAtxHeading(row.line);
|
|
37
|
+
if (heading && heading.text === opts.heading) {
|
|
38
|
+
hits.push({ row, level: heading.level });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (hits.length === 0) return { ok: false, reason: "no_heading", text: fileText };
|
|
42
|
+
if (hits.length > 1) {
|
|
43
|
+
return { ok: false, reason: "duplicate_heading", lines: hits.map((hit) => hit.row.number), text: fileText };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hit = hits[0]!;
|
|
47
|
+
|
|
48
|
+
const eol = fileText.includes("\r\n") ? "\r\n" : "\n";
|
|
49
|
+
const prefix =
|
|
50
|
+
hit.row.next === fileText.length && !fileText.endsWith("\n") && !fileText.endsWith("\r")
|
|
51
|
+
? `${fileText}${eol}`
|
|
52
|
+
: fileText.slice(0, hit.row.next);
|
|
53
|
+
let content = opts.content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
54
|
+
if (eol === "\r\n") content = content.replace(/\n/g, "\r\n");
|
|
55
|
+
if (content.length > 0 && !content.endsWith(eol)) content += eol;
|
|
56
|
+
|
|
57
|
+
const text = `${prefix}${startMarker}${eol}${content}${endMarker}${eol}${fileText.slice(hit.row.next)}`;
|
|
58
|
+
return { ok: true, text, inserted: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type FileLine = { line: string; start: number; next: number; number: number };
|
|
62
|
+
|
|
63
|
+
function fileLines(fileText: string): FileLine[] {
|
|
64
|
+
const rows: FileLine[] = [];
|
|
65
|
+
let start = 0;
|
|
66
|
+
let number = 1;
|
|
67
|
+
while (start < fileText.length) {
|
|
68
|
+
let i = start;
|
|
69
|
+
while (i < fileText.length && fileText[i] !== "\n" && fileText[i] !== "\r") i++;
|
|
70
|
+
let next = i;
|
|
71
|
+
if (fileText[i] === "\r" && fileText[i + 1] === "\n") next = i + 2;
|
|
72
|
+
else if (fileText[i] === "\n" || fileText[i] === "\r") next = i + 1;
|
|
73
|
+
else next = fileText.length;
|
|
74
|
+
rows.push({ line: fileText.slice(start, i), start, next, number });
|
|
75
|
+
number += 1;
|
|
76
|
+
start = next;
|
|
77
|
+
}
|
|
78
|
+
return rows;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseAtxHeading(line: string): { level: number; text: string } | null {
|
|
82
|
+
let level = 0;
|
|
83
|
+
while (level < line.length && level < 6 && line[level] === "#") level++;
|
|
84
|
+
if (level === 0 || line[level] !== " ") return null;
|
|
85
|
+
return { level, text: line.slice(level + 1) };
|
|
86
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
estimateGeneratedTokens,
|
|
5
|
+
estimateOffsetTokens,
|
|
6
|
+
spanModeSavesTokens,
|
|
7
|
+
} from "./extraction-span-tokens.js";
|
|
8
|
+
|
|
9
|
+
test("estimateGeneratedTokens: 0 chars -> 0 tokens", () => {
|
|
10
|
+
assert.equal(estimateGeneratedTokens(0), 0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("estimateGeneratedTokens: 1..4 chars -> 1 token", () => {
|
|
14
|
+
for (const chars of [1, 2, 3, 4]) {
|
|
15
|
+
assert.equal(estimateGeneratedTokens(chars), 1);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("estimateGeneratedTokens: 5 chars -> 2 tokens", () => {
|
|
20
|
+
assert.equal(estimateGeneratedTokens(5), 2);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("estimateGeneratedTokens: invalid charCount throws", () => {
|
|
24
|
+
assert.throws(() => estimateGeneratedTokens(-1), /charCount/);
|
|
25
|
+
assert.throws(() => estimateGeneratedTokens(Number.NaN), /charCount/);
|
|
26
|
+
assert.throws(() => estimateGeneratedTokens(3.7), /charCount/);
|
|
27
|
+
assert.throws(() => estimateGeneratedTokens(-1), RangeError);
|
|
28
|
+
assert.throws(() => estimateGeneratedTokens(Number.NaN), RangeError);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("estimateOffsetTokens: (0, 12) cost is a positive integer", () => {
|
|
32
|
+
const cost = estimateOffsetTokens(0, 12);
|
|
33
|
+
assert.equal(Number.isInteger(cost), true);
|
|
34
|
+
assert.ok(cost > 0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("estimateOffsetTokens: offset 0 costs one character (one token)", () => {
|
|
38
|
+
assert.equal(estimateOffsetTokens(0, 0), 2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("estimateOffsetTokens: reversed span throws", () => {
|
|
42
|
+
assert.throws(() => estimateOffsetTokens(13, 12), RangeError);
|
|
43
|
+
assert.throws(() => estimateOffsetTokens(13, 12), /reversed/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("estimateOffsetTokens: non-finite or non-integer offsets throw", () => {
|
|
47
|
+
assert.throws(() => estimateOffsetTokens(Number.NaN, 12), RangeError);
|
|
48
|
+
assert.throws(() => estimateOffsetTokens(0, Number.POSITIVE_INFINITY), RangeError);
|
|
49
|
+
assert.throws(() => estimateOffsetTokens(0.5, 12), RangeError);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("spanModeSavesTokens: long generated value beats small offsets", () => {
|
|
53
|
+
assert.equal(
|
|
54
|
+
spanModeSavesTokens({ generatedCharCount: 1000, start: 0, end: 12 }),
|
|
55
|
+
true,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("spanModeSavesTokens: equal costs return false", () => {
|
|
60
|
+
// (0, 12) costs 2 tokens; 5..8 generated chars also cost 2 tokens.
|
|
61
|
+
assert.equal(
|
|
62
|
+
spanModeSavesTokens({ generatedCharCount: 8, start: 0, end: 12 }),
|
|
63
|
+
false,
|
|
64
|
+
);
|
|
65
|
+
assert.equal(
|
|
66
|
+
spanModeSavesTokens({ generatedCharCount: 5, start: 0, end: 0 }),
|
|
67
|
+
false,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated span token-cost estimator (issue #2333, Phase A).
|
|
3
|
+
*
|
|
4
|
+
* Pure. Estimates the output-token cost of emitting span offsets versus
|
|
5
|
+
* generating the memory value, so the Phase A bench can compare both sides
|
|
6
|
+
* without touching the extraction engine.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const CHARS_PER_TOKEN = 4;
|
|
10
|
+
|
|
11
|
+
export function estimateGeneratedTokens(charCount: number): number {
|
|
12
|
+
if (!Number.isInteger(charCount) || charCount < 0) {
|
|
13
|
+
throw new RangeError(`charCount must be a non-negative integer, got ${charCount}`);
|
|
14
|
+
}
|
|
15
|
+
return Math.ceil(charCount / CHARS_PER_TOKEN);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function estimateOffsetTokens(start: number, end: number): number {
|
|
19
|
+
if (!Number.isInteger(start)) {
|
|
20
|
+
throw new RangeError(`start must be a finite integer, got ${start}`);
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isInteger(end)) {
|
|
23
|
+
throw new RangeError(`end must be a finite integer, got ${end}`);
|
|
24
|
+
}
|
|
25
|
+
if (start > end) {
|
|
26
|
+
throw new RangeError(`reversed span: start ${start} > end ${end}`);
|
|
27
|
+
}
|
|
28
|
+
return offsetTokens(start) + offsetTokens(end);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function spanModeSavesTokens(input: {
|
|
32
|
+
generatedCharCount: number;
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
}): boolean {
|
|
36
|
+
return (
|
|
37
|
+
estimateOffsetTokens(input.start, input.end) <
|
|
38
|
+
estimateGeneratedTokens(input.generatedCharCount)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function offsetTokens(offset: number): number {
|
|
43
|
+
return Math.ceil(String(offset).length / CHARS_PER_TOKEN);
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { DEEP_RECALL_FRONTIER_CAP, rankDeepRecallFrontier } from "./recall-deep-frontier.js";
|
|
5
|
+
|
|
6
|
+
test("empty input returns empty", () => {
|
|
7
|
+
assert.deepEqual(rankDeepRecallFrontier([]), []);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("blank nodeIds and counts below 1 are dropped", () => {
|
|
11
|
+
const result = rankDeepRecallFrontier([
|
|
12
|
+
{ nodeId: "", sharedAnchorCount: 3 },
|
|
13
|
+
{ nodeId: " ", sharedAnchorCount: 3 },
|
|
14
|
+
{ nodeId: "a", sharedAnchorCount: 0 },
|
|
15
|
+
{ nodeId: "b", sharedAnchorCount: -2 },
|
|
16
|
+
{ nodeId: "c", sharedAnchorCount: 1 },
|
|
17
|
+
]);
|
|
18
|
+
assert.deepEqual(result, [{ nodeId: "c", sharedAnchorCount: 1 }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("NaN and infinite counts are dropped", () => {
|
|
22
|
+
const result = rankDeepRecallFrontier([
|
|
23
|
+
{ nodeId: "a", sharedAnchorCount: Number.NaN },
|
|
24
|
+
{ nodeId: "b", sharedAnchorCount: Number.POSITIVE_INFINITY },
|
|
25
|
+
{ nodeId: "c", sharedAnchorCount: 2 },
|
|
26
|
+
]);
|
|
27
|
+
assert.deepEqual(result, [{ nodeId: "c", sharedAnchorCount: 2 }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("dedup by nodeId keeps the higher count, first on tie", () => {
|
|
31
|
+
const result = rankDeepRecallFrontier([
|
|
32
|
+
{ nodeId: "a", sharedAnchorCount: 2 },
|
|
33
|
+
{ nodeId: "a", sharedAnchorCount: 5 },
|
|
34
|
+
{ nodeId: "b", sharedAnchorCount: 4 },
|
|
35
|
+
{ nodeId: "b", sharedAnchorCount: 4 },
|
|
36
|
+
]);
|
|
37
|
+
assert.deepEqual(result, [
|
|
38
|
+
{ nodeId: "a", sharedAnchorCount: 5 },
|
|
39
|
+
{ nodeId: "b", sharedAnchorCount: 4 },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("sorts by count desc then nodeId asc", () => {
|
|
44
|
+
const result = rankDeepRecallFrontier([
|
|
45
|
+
{ nodeId: "c", sharedAnchorCount: 3 },
|
|
46
|
+
{ nodeId: "a", sharedAnchorCount: 3 },
|
|
47
|
+
{ nodeId: "b", sharedAnchorCount: 9 },
|
|
48
|
+
{ nodeId: "d", sharedAnchorCount: 1 },
|
|
49
|
+
]);
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
result.map((candidate) => candidate.nodeId),
|
|
52
|
+
["b", "a", "c", "d"],
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("caps at 20 keeping the top 20 of 21", () => {
|
|
57
|
+
const candidates = Array.from({ length: 21 }, (_, i) => ({
|
|
58
|
+
nodeId: `n${String(i).padStart(2, "0")}`,
|
|
59
|
+
sharedAnchorCount: i + 1,
|
|
60
|
+
}));
|
|
61
|
+
const result = rankDeepRecallFrontier(candidates);
|
|
62
|
+
assert.equal(result.length, DEEP_RECALL_FRONTIER_CAP);
|
|
63
|
+
assert.equal(result[0]?.nodeId, "n20");
|
|
64
|
+
assert.equal(result.at(-1)?.nodeId, "n01");
|
|
65
|
+
assert.ok(!result.some((candidate) => candidate.nodeId === "n00"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("does not mutate the input array or its objects", () => {
|
|
69
|
+
const input = [
|
|
70
|
+
{ nodeId: " b ", sharedAnchorCount: 2 },
|
|
71
|
+
{ nodeId: "a", sharedAnchorCount: 1 },
|
|
72
|
+
];
|
|
73
|
+
const snapshot = structuredClone(input);
|
|
74
|
+
rankDeepRecallFrontier(input);
|
|
75
|
+
assert.deepEqual(input, snapshot);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("two calls on the same input are deep-equal", () => {
|
|
79
|
+
const input = [
|
|
80
|
+
{ nodeId: "a", sharedAnchorCount: 2 },
|
|
81
|
+
{ nodeId: "b", sharedAnchorCount: 2 },
|
|
82
|
+
{ nodeId: "a", sharedAnchorCount: 7 },
|
|
83
|
+
];
|
|
84
|
+
assert.deepEqual(rankDeepRecallFrontier(input), rankDeepRecallFrontier(input));
|
|
85
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-recall frontier ranker (issue #2332).
|
|
3
|
+
*
|
|
4
|
+
* Pure. After every working-set change the frontier is capped at
|
|
5
|
+
* DEEP_RECALL_FRONTIER_CAP items ordered by shared-anchor count
|
|
6
|
+
* descending, then nodeId ascending — a total comparator.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const DEEP_RECALL_FRONTIER_CAP = 20;
|
|
10
|
+
|
|
11
|
+
export interface DeepRecallFrontierCandidate {
|
|
12
|
+
nodeId: string;
|
|
13
|
+
sharedAnchorCount: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function rankDeepRecallFrontier(
|
|
17
|
+
candidates: readonly DeepRecallFrontierCandidate[],
|
|
18
|
+
): DeepRecallFrontierCandidate[] {
|
|
19
|
+
const byNodeId = new Map<string, DeepRecallFrontierCandidate>();
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
const nodeId = typeof candidate.nodeId === "string" ? candidate.nodeId.trim() : "";
|
|
22
|
+
if (nodeId === "") continue;
|
|
23
|
+
const count = candidate.sharedAnchorCount;
|
|
24
|
+
if (typeof count !== "number" || !Number.isFinite(count) || count < 1) continue;
|
|
25
|
+
const kept = byNodeId.get(nodeId);
|
|
26
|
+
if (kept === undefined || count > kept.sharedAnchorCount) {
|
|
27
|
+
byNodeId.set(nodeId, { nodeId, sharedAnchorCount: count });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [...byNodeId.values()]
|
|
31
|
+
.sort((a, b) => {
|
|
32
|
+
if (a.sharedAnchorCount !== b.sharedAnchorCount) {
|
|
33
|
+
return b.sharedAnchorCount - a.sharedAnchorCount;
|
|
34
|
+
}
|
|
35
|
+
if (a.nodeId !== b.nodeId) return a.nodeId < b.nodeId ? -1 : 1;
|
|
36
|
+
return 0;
|
|
37
|
+
})
|
|
38
|
+
.slice(0, DEEP_RECALL_FRONTIER_CAP);
|
|
39
|
+
}
|