@remnic/core 9.68.0 → 9.69.1
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 +19 -19
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +20 -20
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +19 -19
- package/dist/access-cli.js +65 -65
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +19 -19
- 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 +21 -21
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +33 -33
- package/dist/access-identity-continuity-surface.js +19 -19
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +19 -19
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +26 -26
- package/dist/access-namespace-preflight.js +19 -19
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +19 -19
- package/dist/access-operations-batch.js +20 -20
- package/dist/access-operations.d.ts +7 -7
- package/dist/access-operations.js +25 -25
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +19 -19
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +19 -19
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +19 -19
- package/dist/access-schema.d.ts +89 -89
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +19 -19
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +21 -21
- package/dist/briefing.js +10 -10
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-KAY2C6VX.js → chunk-2D7S3FZW.js} +2 -2
- package/dist/{chunk-WFFGUFFN.js → chunk-2FEBD4LN.js} +148 -41
- package/dist/chunk-2FEBD4LN.js.map +1 -0
- package/dist/{chunk-HDQQRRCZ.js → chunk-5DSPAWXQ.js} +3 -3
- package/dist/{chunk-VLSUKXCJ.js → chunk-5JUSOMNO.js} +3 -3
- package/dist/{chunk-P342D5BP.js → chunk-626ILV6J.js} +2 -2
- package/dist/{chunk-I6T4SYFW.js → chunk-63Z7D2YJ.js} +3 -3
- package/dist/{chunk-XW3G4BJI.js → chunk-6DPAZEGK.js} +7 -7
- package/dist/{chunk-XPDWNWH6.js → chunk-6LIILOP3.js} +3 -3
- package/dist/{chunk-3IRH5MYM.js → chunk-6OX6SNRC.js} +5 -5
- package/dist/{chunk-FOT5GP2U.js → chunk-73CA45TG.js} +2 -2
- package/dist/{chunk-2MHMHF76.js → chunk-7GFLU63V.js} +2 -2
- package/dist/{chunk-A7NMS5II.js → chunk-7HAZ6GZE.js} +3 -3
- package/dist/{chunk-X2NFFBTO.js → chunk-A7GQBJP5.js} +4 -4
- package/dist/{chunk-7KG5V4VV.js → chunk-BZFHCRO6.js} +2 -2
- package/dist/{chunk-4PEZMGZD.js → chunk-CUQI6DR3.js} +2 -2
- package/dist/{chunk-NFITGYMQ.js → chunk-CX6J53S5.js} +3 -3
- package/dist/{chunk-DJ7TZLGU.js → chunk-DHXPZZ5U.js} +2 -2
- package/dist/{chunk-XZBXINXZ.js → chunk-GIC6BASP.js} +4 -4
- package/dist/{chunk-ABPCF4MW.js → chunk-HB3GEHGF.js} +1 -1
- package/dist/{chunk-6FUMA6AA.js → chunk-JFPUQ3E2.js} +2 -2
- package/dist/{chunk-BTRY6UH3.js → chunk-JMMG5K7H.js} +2 -2
- package/dist/{chunk-ZDJMCXC7.js → chunk-JRLDQ6WS.js} +12 -12
- package/dist/{chunk-BGD3T743.js → chunk-JRYINMLF.js} +2 -2
- package/dist/{chunk-F25CL3IQ.js → chunk-KSTLZCR5.js} +2 -2
- package/dist/{chunk-363VZTVH.js → chunk-L5QVIATR.js} +2 -2
- package/dist/{chunk-BS5BD3DF.js → chunk-LFGSIMFS.js} +2 -2
- package/dist/{chunk-YYAWJRGL.js → chunk-MYMME2VL.js} +2 -2
- package/dist/{chunk-RACZB5HT.js → chunk-N37X5RU7.js} +4 -4
- package/dist/{chunk-Z2FXSE5P.js → chunk-NFZUZL44.js} +2 -2
- package/dist/{chunk-AG46P24C.js → chunk-NNBZS3DY.js} +4 -4
- package/dist/{chunk-L7MJJAG2.js → chunk-NRDLOFAI.js} +2 -2
- package/dist/{chunk-PIBQIYF2.js → chunk-NX2ECBRG.js} +2 -2
- package/dist/{chunk-QOE3EM5Z.js → chunk-PVN6DVWU.js} +8 -8
- package/dist/{chunk-327JMA2N.js → chunk-PZY4DLLO.js} +2 -2
- package/dist/{chunk-ZVW724ZK.js → chunk-Q4OITLPA.js} +2 -2
- package/dist/{chunk-ND6NL2YR.js → chunk-QBZMUF54.js} +2 -2
- package/dist/{chunk-25P3LW6U.js → chunk-QLT74C2M.js} +2 -2
- package/dist/{chunk-KJUH3QIQ.js → chunk-TITZPONM.js} +2 -2
- package/dist/{chunk-YFK64FYK.js → chunk-TPIO36PJ.js} +2 -2
- package/dist/{chunk-5UI7WQA6.js → chunk-TX55UTJT.js} +2 -2
- package/dist/{chunk-D35XLYQ7.js → chunk-UF65P4GF.js} +6 -6
- package/dist/{chunk-BHH736UQ.js → chunk-UJYNT3R2.js} +3 -3
- package/dist/{chunk-4PMPOZK3.js → chunk-V5I36Y2F.js} +2 -2
- package/dist/{chunk-WQCET3N3.js → chunk-VK7YWP5Q.js} +2 -2
- package/dist/{chunk-USQAH6PN.js → chunk-VPYAQYD5.js} +2 -2
- package/dist/{chunk-KRNFJKMH.js → chunk-VTEO5T6Z.js} +2 -2
- package/dist/{chunk-WSYACIKD.js → chunk-W6PZQY4L.js} +2 -2
- package/dist/{chunk-52GQNSLC.js → chunk-WM2N5DRB.js} +7 -7
- package/dist/{chunk-ERKFG4RV.js → chunk-XRT7ZG5B.js} +33 -33
- package/dist/{chunk-NYQHD7H4.js → chunk-YTWEK6CU.js} +17 -17
- package/dist/{cli-Bvy4jdBu.d.ts → cli-DDHGtWI1.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +48 -48
- package/dist/coding/export-okf-codegraph.js +10 -10
- package/dist/compounding/engine.js +10 -10
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/convergence-refresh.js +12 -12
- package/dist/corpus-watermark.js +12 -12
- package/dist/entity-retrieval.js +10 -10
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +20 -20
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/extraction.js +10 -10
- package/dist/{first-start-migration-KHIMCBIC.js → first-start-migration-UG32BSG2.js} +4 -4
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +732 -679
- package/dist/index.js +82 -72
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +7 -7
- package/dist/location/index.js +12 -12
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/memory-subject-cli.d.ts +1 -1
- package/dist/memory-subject.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.js +10 -10
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/maintenance.js +18 -18
- package/dist/orchestrator.js +65 -65
- package/dist/{public-http-2MyshWxl.d.ts → public-http-Bab7S0ED.d.ts} +1 -1
- package/dist/recall-navigate-disclosure.d.ts +44 -0
- package/dist/recall-navigate-disclosure.js +30 -0
- package/dist/recall-navigate-disclosure.js.map +1 -0
- package/dist/recall-state-view-packet.d.ts +30 -0
- package/dist/recall-state-view-packet.js +69 -0
- package/dist/recall-state-view-packet.js.map +1 -0
- package/dist/schemas.d.ts +152 -152
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +10 -10
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/shared-context/manager.d.ts +6 -6
- package/dist/source-agent-qualifier.js +10 -10
- package/dist/standup/build.js +10 -10
- package/dist/storage.js +9 -9
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +22 -22
- package/dist/transfer/export-okf.js +11 -11
- package/dist/transfer/types.d.ts +78 -78
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.d.ts +1 -1
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.d.ts +1 -1
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- package/src/activity/index.ts +1 -0
- package/src/activity/timeline/index.ts +1 -0
- package/src/activity/timeline/week-recurring.test.ts +177 -0
- package/src/activity/timeline/week-recurring.ts +88 -0
- package/src/activity/vault-status.test.ts +138 -0
- package/src/activity/vault-status.ts +96 -0
- package/src/dedup/index.ts +7 -0
- package/src/dedup/merge-provenance.test.ts +185 -0
- package/src/dedup/merge-provenance.ts +151 -0
- package/src/recall-navigate-disclosure.test.ts +128 -0
- package/src/recall-navigate-disclosure.ts +45 -0
- package/src/recall-state-view-packet.test.ts +158 -0
- package/src/recall-state-view-packet.ts +128 -0
- package/dist/chunk-WFFGUFFN.js.map +0 -1
- package/dist/{access-service-LbwWl05g.d.ts → access-service-CRZHXClX.d.ts} +58 -58
- /package/dist/{chunk-KAY2C6VX.js.map → chunk-2D7S3FZW.js.map} +0 -0
- /package/dist/{chunk-HDQQRRCZ.js.map → chunk-5DSPAWXQ.js.map} +0 -0
- /package/dist/{chunk-VLSUKXCJ.js.map → chunk-5JUSOMNO.js.map} +0 -0
- /package/dist/{chunk-P342D5BP.js.map → chunk-626ILV6J.js.map} +0 -0
- /package/dist/{chunk-I6T4SYFW.js.map → chunk-63Z7D2YJ.js.map} +0 -0
- /package/dist/{chunk-XW3G4BJI.js.map → chunk-6DPAZEGK.js.map} +0 -0
- /package/dist/{chunk-XPDWNWH6.js.map → chunk-6LIILOP3.js.map} +0 -0
- /package/dist/{chunk-3IRH5MYM.js.map → chunk-6OX6SNRC.js.map} +0 -0
- /package/dist/{chunk-FOT5GP2U.js.map → chunk-73CA45TG.js.map} +0 -0
- /package/dist/{chunk-2MHMHF76.js.map → chunk-7GFLU63V.js.map} +0 -0
- /package/dist/{chunk-A7NMS5II.js.map → chunk-7HAZ6GZE.js.map} +0 -0
- /package/dist/{chunk-X2NFFBTO.js.map → chunk-A7GQBJP5.js.map} +0 -0
- /package/dist/{chunk-7KG5V4VV.js.map → chunk-BZFHCRO6.js.map} +0 -0
- /package/dist/{chunk-4PEZMGZD.js.map → chunk-CUQI6DR3.js.map} +0 -0
- /package/dist/{chunk-NFITGYMQ.js.map → chunk-CX6J53S5.js.map} +0 -0
- /package/dist/{chunk-DJ7TZLGU.js.map → chunk-DHXPZZ5U.js.map} +0 -0
- /package/dist/{chunk-XZBXINXZ.js.map → chunk-GIC6BASP.js.map} +0 -0
- /package/dist/{chunk-ABPCF4MW.js.map → chunk-HB3GEHGF.js.map} +0 -0
- /package/dist/{chunk-6FUMA6AA.js.map → chunk-JFPUQ3E2.js.map} +0 -0
- /package/dist/{chunk-BTRY6UH3.js.map → chunk-JMMG5K7H.js.map} +0 -0
- /package/dist/{chunk-ZDJMCXC7.js.map → chunk-JRLDQ6WS.js.map} +0 -0
- /package/dist/{chunk-BGD3T743.js.map → chunk-JRYINMLF.js.map} +0 -0
- /package/dist/{chunk-F25CL3IQ.js.map → chunk-KSTLZCR5.js.map} +0 -0
- /package/dist/{chunk-363VZTVH.js.map → chunk-L5QVIATR.js.map} +0 -0
- /package/dist/{chunk-BS5BD3DF.js.map → chunk-LFGSIMFS.js.map} +0 -0
- /package/dist/{chunk-YYAWJRGL.js.map → chunk-MYMME2VL.js.map} +0 -0
- /package/dist/{chunk-RACZB5HT.js.map → chunk-N37X5RU7.js.map} +0 -0
- /package/dist/{chunk-Z2FXSE5P.js.map → chunk-NFZUZL44.js.map} +0 -0
- /package/dist/{chunk-AG46P24C.js.map → chunk-NNBZS3DY.js.map} +0 -0
- /package/dist/{chunk-L7MJJAG2.js.map → chunk-NRDLOFAI.js.map} +0 -0
- /package/dist/{chunk-PIBQIYF2.js.map → chunk-NX2ECBRG.js.map} +0 -0
- /package/dist/{chunk-QOE3EM5Z.js.map → chunk-PVN6DVWU.js.map} +0 -0
- /package/dist/{chunk-327JMA2N.js.map → chunk-PZY4DLLO.js.map} +0 -0
- /package/dist/{chunk-ZVW724ZK.js.map → chunk-Q4OITLPA.js.map} +0 -0
- /package/dist/{chunk-ND6NL2YR.js.map → chunk-QBZMUF54.js.map} +0 -0
- /package/dist/{chunk-25P3LW6U.js.map → chunk-QLT74C2M.js.map} +0 -0
- /package/dist/{chunk-KJUH3QIQ.js.map → chunk-TITZPONM.js.map} +0 -0
- /package/dist/{chunk-YFK64FYK.js.map → chunk-TPIO36PJ.js.map} +0 -0
- /package/dist/{chunk-5UI7WQA6.js.map → chunk-TX55UTJT.js.map} +0 -0
- /package/dist/{chunk-D35XLYQ7.js.map → chunk-UF65P4GF.js.map} +0 -0
- /package/dist/{chunk-BHH736UQ.js.map → chunk-UJYNT3R2.js.map} +0 -0
- /package/dist/{chunk-4PMPOZK3.js.map → chunk-V5I36Y2F.js.map} +0 -0
- /package/dist/{chunk-WQCET3N3.js.map → chunk-VK7YWP5Q.js.map} +0 -0
- /package/dist/{chunk-USQAH6PN.js.map → chunk-VPYAQYD5.js.map} +0 -0
- /package/dist/{chunk-KRNFJKMH.js.map → chunk-VTEO5T6Z.js.map} +0 -0
- /package/dist/{chunk-WSYACIKD.js.map → chunk-W6PZQY4L.js.map} +0 -0
- /package/dist/{chunk-52GQNSLC.js.map → chunk-WM2N5DRB.js.map} +0 -0
- /package/dist/{chunk-ERKFG4RV.js.map → chunk-XRT7ZG5B.js.map} +0 -0
- /package/dist/{chunk-NYQHD7H4.js.map → chunk-YTWEK6CU.js.map} +0 -0
- /package/dist/{first-start-migration-KHIMCBIC.js.map → first-start-migration-UG32BSG2.js.map} +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RECURRENCE_MIN_DAYS,
|
|
6
|
+
MIN_RECURRENCE_MIN_DAYS,
|
|
7
|
+
findRecurringPatterns,
|
|
8
|
+
type WeekDayOccurrence,
|
|
9
|
+
} from "./week-recurring.js";
|
|
10
|
+
|
|
11
|
+
function rangeErrorMatching(pattern: RegExp): (err: unknown) => boolean {
|
|
12
|
+
return (err: unknown) => err instanceof RangeError && pattern.test((err as Error).message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("default minDays is 3; a key on exactly minDays distinct dates qualifies", () => {
|
|
16
|
+
assert.equal(DEFAULT_RECURRENCE_MIN_DAYS, 3);
|
|
17
|
+
const result = findRecurringPatterns({
|
|
18
|
+
occurrences: [
|
|
19
|
+
{ date: "2026-08-10", key: "code", durationMs: 100 },
|
|
20
|
+
{ date: "2026-08-11", key: "code", durationMs: 100 },
|
|
21
|
+
{ date: "2026-08-12", key: "code", durationMs: 100 },
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
assert.deepEqual(result, [{ key: "code", dayCount: 3, totalDurationMs: 300 }]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a key on minDays - 1 distinct dates does not qualify", () => {
|
|
28
|
+
const result = findRecurringPatterns({
|
|
29
|
+
occurrences: [
|
|
30
|
+
{ date: "2026-08-10", key: "code", durationMs: 999_999 },
|
|
31
|
+
{ date: "2026-08-11", key: "code", durationMs: 999_999 },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
assert.deepEqual(result, []);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("same-date occurrences count as one day but all durations sum", () => {
|
|
38
|
+
const result = findRecurringPatterns({
|
|
39
|
+
occurrences: [
|
|
40
|
+
{ date: "2026-08-10", key: "code", durationMs: 100 },
|
|
41
|
+
{ date: "2026-08-10", key: "code", durationMs: 40 },
|
|
42
|
+
{ date: "2026-08-11", key: "code", durationMs: 100 },
|
|
43
|
+
{ date: "2026-08-12", key: "code", durationMs: 100 },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
assert.deepEqual(result, [{ key: "code", dayCount: 3, totalDurationMs: 340 }]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("blank keys are ignored", () => {
|
|
50
|
+
const result = findRecurringPatterns({
|
|
51
|
+
occurrences: [
|
|
52
|
+
{ date: "2026-08-10", key: "", durationMs: 100 },
|
|
53
|
+
{ date: "2026-08-11", key: " ", durationMs: 100 },
|
|
54
|
+
{ date: "2026-08-12", key: "\t", durationMs: 100 },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
assert.deepEqual(result, []);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("keys are compared trimmed and case-sensitively", () => {
|
|
61
|
+
const result = findRecurringPatterns({
|
|
62
|
+
occurrences: [
|
|
63
|
+
{ date: "2026-08-10", key: "code", durationMs: 100 },
|
|
64
|
+
{ date: "2026-08-11", key: " code ", durationMs: 100 },
|
|
65
|
+
{ date: "2026-08-12", key: "code", durationMs: 100 },
|
|
66
|
+
{ date: "2026-08-10", key: "Code", durationMs: 100 },
|
|
67
|
+
{ date: "2026-08-11", key: "Code", durationMs: 100 },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
// " code " folds into "code" (3 distinct days); "Code" stays separate at 2 days.
|
|
71
|
+
assert.deepEqual(result, [{ key: "code", dayCount: 3, totalDurationMs: 300 }]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("minDays must be an integer >= MIN_RECURRENCE_MIN_DAYS", () => {
|
|
75
|
+
for (const bad of [0, 1, -3, 1.5]) {
|
|
76
|
+
assert.throws(
|
|
77
|
+
() => findRecurringPatterns({ occurrences: [], minDays: bad }),
|
|
78
|
+
rangeErrorMatching(/minDays/),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
assert.doesNotThrow(() =>
|
|
82
|
+
findRecurringPatterns({
|
|
83
|
+
occurrences: [{ date: "2026-08-10", key: "code", durationMs: 1 }],
|
|
84
|
+
minDays: MIN_RECURRENCE_MIN_DAYS,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// minDays 1 would make a single-day key "recurring", which is the invariant
|
|
90
|
+
// this helper exists to enforce — configuration must not be able to disable it.
|
|
91
|
+
test("minDays 1 is refused rather than reporting one-off keys", () => {
|
|
92
|
+
assert.throws(
|
|
93
|
+
() =>
|
|
94
|
+
findRecurringPatterns({
|
|
95
|
+
occurrences: [{ date: "2026-08-10", key: "code", durationMs: 5 }],
|
|
96
|
+
minDays: 1,
|
|
97
|
+
}),
|
|
98
|
+
rangeErrorMatching(/minDays/),
|
|
99
|
+
);
|
|
100
|
+
assert.equal(MIN_RECURRENCE_MIN_DAYS, 2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("durationMs must be a finite number >= 0", () => {
|
|
104
|
+
for (const bad of [Number.NaN, Number.POSITIVE_INFINITY, -1]) {
|
|
105
|
+
assert.throws(
|
|
106
|
+
() =>
|
|
107
|
+
findRecurringPatterns({
|
|
108
|
+
occurrences: [{ date: "2026-08-10", key: "code", durationMs: bad }],
|
|
109
|
+
}),
|
|
110
|
+
rangeErrorMatching(/durationMs/),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("date must be a valid YYYY-MM-DD calendar date", () => {
|
|
116
|
+
for (const bad of ["", "not-a-date", "2026-8-10", "2026-02-30"]) {
|
|
117
|
+
assert.throws(
|
|
118
|
+
() =>
|
|
119
|
+
findRecurringPatterns({
|
|
120
|
+
occurrences: [{ date: bad, key: "code", durationMs: 0 }],
|
|
121
|
+
}),
|
|
122
|
+
rangeErrorMatching(/date/),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("ordering: dayCount desc, then totalDurationMs desc, then key asc", () => {
|
|
128
|
+
const result = findRecurringPatterns({
|
|
129
|
+
minDays: 2,
|
|
130
|
+
occurrences: [
|
|
131
|
+
// alpha: 2 days, biggest total — dayCount still ranks it last.
|
|
132
|
+
{ date: "2026-08-10", key: "alpha", durationMs: 999_999 },
|
|
133
|
+
{ date: "2026-08-11", key: "alpha", durationMs: 999_999 },
|
|
134
|
+
// beta: 3 days, 10 total — wins the dayCount tie on duration.
|
|
135
|
+
{ date: "2026-08-10", key: "beta", durationMs: 4 },
|
|
136
|
+
{ date: "2026-08-11", key: "beta", durationMs: 4 },
|
|
137
|
+
{ date: "2026-08-12", key: "beta", durationMs: 2 },
|
|
138
|
+
// gamma: 3 days, 8 total — ties delta on both, loses on key ascending.
|
|
139
|
+
{ date: "2026-08-10", key: "gamma", durationMs: 8 },
|
|
140
|
+
{ date: "2026-08-11", key: "gamma", durationMs: 0 },
|
|
141
|
+
{ date: "2026-08-12", key: "gamma", durationMs: 0 },
|
|
142
|
+
// delta: 3 days, 8 total.
|
|
143
|
+
{ date: "2026-08-10", key: "delta", durationMs: 8 },
|
|
144
|
+
{ date: "2026-08-11", key: "delta", durationMs: 0 },
|
|
145
|
+
{ date: "2026-08-12", key: "delta", durationMs: 0 },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
assert.deepEqual(result.map((p) => p.key), ["beta", "delta", "gamma", "alpha"]);
|
|
149
|
+
assert.deepEqual(result[0], { key: "beta", dayCount: 3, totalDurationMs: 10 });
|
|
150
|
+
assert.deepEqual(result[3], { key: "alpha", dayCount: 2, totalDurationMs: 1_999_998 });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("empty input and no-qualifier input return []", () => {
|
|
154
|
+
assert.deepEqual(findRecurringPatterns({ occurrences: [] }), []);
|
|
155
|
+
assert.deepEqual(
|
|
156
|
+
findRecurringPatterns({
|
|
157
|
+
occurrences: [{ date: "2026-08-10", key: "code", durationMs: 5000 }],
|
|
158
|
+
}),
|
|
159
|
+
[],
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("pure: two calls deep-equal and the input is not mutated", () => {
|
|
164
|
+
const occurrences: WeekDayOccurrence[] = [
|
|
165
|
+
{ date: "2026-08-10", key: "code", durationMs: 100 },
|
|
166
|
+
{ date: "2026-08-10", key: "code", durationMs: 50 },
|
|
167
|
+
{ date: "2026-08-11", key: "mail", durationMs: 10 },
|
|
168
|
+
{ date: "2026-08-11", key: "code", durationMs: 100 },
|
|
169
|
+
{ date: "2026-08-12", key: "code", durationMs: 100 },
|
|
170
|
+
];
|
|
171
|
+
const snapshot = structuredClone(occurrences);
|
|
172
|
+
const first = findRecurringPatterns({ occurrences });
|
|
173
|
+
const second = findRecurringPatterns({ occurrences });
|
|
174
|
+
assert.deepEqual(first, second);
|
|
175
|
+
assert.deepEqual(first, [{ key: "code", dayCount: 3, totalDurationMs: 350 }]);
|
|
176
|
+
assert.deepEqual(occurrences, snapshot);
|
|
177
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recurring weekly pattern gate (issue #2052).
|
|
3
|
+
*
|
|
4
|
+
* A category/application key counts as recurring only when it appeared on at
|
|
5
|
+
* least `minDays` DISTINCT days of the analyzed week, so a one-off blip is
|
|
6
|
+
* never reported as a pattern. Pure: no I/O, no clock, no input mutation.
|
|
7
|
+
* Validation happens on the exact occurrence values before any key trimming
|
|
8
|
+
* (AGENTS.md: validate before normalizing; reject non-finite numbers
|
|
9
|
+
* explicitly instead of letting NaN fall through a comparison).
|
|
10
|
+
*/
|
|
11
|
+
import { isValidActivityDate } from "../digest.js";
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_RECURRENCE_MIN_DAYS = 3;
|
|
14
|
+
|
|
15
|
+
/** Lowest threshold that still means "recurring": two distinct days. */
|
|
16
|
+
export const MIN_RECURRENCE_MIN_DAYS = 2;
|
|
17
|
+
|
|
18
|
+
export interface WeekDayOccurrence {
|
|
19
|
+
/** YYYY-MM-DD inside the analyzed week. */
|
|
20
|
+
date: string;
|
|
21
|
+
/** Category id or application/domain label. */
|
|
22
|
+
key: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RecurringPattern {
|
|
27
|
+
key: string;
|
|
28
|
+
/** Count of DISTINCT days the key appeared on. */
|
|
29
|
+
dayCount: number;
|
|
30
|
+
totalDurationMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findRecurringPatterns(input: {
|
|
34
|
+
occurrences: readonly WeekDayOccurrence[];
|
|
35
|
+
minDays?: number;
|
|
36
|
+
}): RecurringPattern[] {
|
|
37
|
+
const minDays = input.minDays ?? DEFAULT_RECURRENCE_MIN_DAYS;
|
|
38
|
+
// A floor of 2 is the module's defining invariant, not a preference: at
|
|
39
|
+
// minDays 1 every key that appeared once is "recurring", which is exactly
|
|
40
|
+
// the one-off blip this helper exists to exclude. Configuration must not be
|
|
41
|
+
// able to switch the invariant off.
|
|
42
|
+
if (!Number.isInteger(minDays) || minDays < MIN_RECURRENCE_MIN_DAYS) {
|
|
43
|
+
throw new RangeError(
|
|
44
|
+
`Invalid minDays ${minDays}; expected an integer >= ${MIN_RECURRENCE_MIN_DAYS}.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const daysByKey = new Map<string, Set<string>>();
|
|
49
|
+
const totalsByKey = new Map<string, number>();
|
|
50
|
+
|
|
51
|
+
for (const occurrence of input.occurrences) {
|
|
52
|
+
if (!isValidActivityDate(occurrence.date)) {
|
|
53
|
+
throw new RangeError(`Invalid date "${occurrence.date}"; expected YYYY-MM-DD.`);
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
typeof occurrence.durationMs !== "number" ||
|
|
57
|
+
!Number.isFinite(occurrence.durationMs) ||
|
|
58
|
+
occurrence.durationMs < 0
|
|
59
|
+
) {
|
|
60
|
+
throw new RangeError(`Invalid durationMs ${occurrence.durationMs}; expected a finite number >= 0.`);
|
|
61
|
+
}
|
|
62
|
+
const key = occurrence.key.trim();
|
|
63
|
+
if (key === "") continue;
|
|
64
|
+
let days = daysByKey.get(key);
|
|
65
|
+
if (!days) {
|
|
66
|
+
days = new Set<string>();
|
|
67
|
+
daysByKey.set(key, days);
|
|
68
|
+
totalsByKey.set(key, 0);
|
|
69
|
+
}
|
|
70
|
+
days.add(occurrence.date);
|
|
71
|
+
totalsByKey.set(key, totalsByKey.get(key)! + occurrence.durationMs);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const patterns: RecurringPattern[] = [];
|
|
75
|
+
for (const [key, days] of daysByKey) {
|
|
76
|
+
if (days.size < minDays) continue;
|
|
77
|
+
patterns.push({ key, dayCount: days.size, totalDurationMs: totalsByKey.get(key)! });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
patterns.sort((a, b) => {
|
|
81
|
+
if (a.dayCount !== b.dayCount) return b.dayCount - a.dayCount;
|
|
82
|
+
if (a.totalDurationMs !== b.totalDurationMs) return b.totalDurationMs - a.totalDurationMs;
|
|
83
|
+
if (a.key < b.key) return -1;
|
|
84
|
+
if (a.key > b.key) return 1;
|
|
85
|
+
return 0;
|
|
86
|
+
});
|
|
87
|
+
return patterns;
|
|
88
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { VAULT_PUBLISH_OUTCOMES, summarizeVaultPublish } from "./vault-status.js";
|
|
5
|
+
import type { VaultPublishResult } from "./vault-status.js";
|
|
6
|
+
|
|
7
|
+
test("summarizeVaultPublish counts a mixed list, sorts by path, and flags the error", () => {
|
|
8
|
+
const status = summarizeVaultPublish([
|
|
9
|
+
{ path: "notes/e.md", outcome: "updated" },
|
|
10
|
+
{ path: "notes/d.md", outcome: "error", reason: "missing_file" },
|
|
11
|
+
{ path: "notes/a.md", outcome: "updated" },
|
|
12
|
+
{ path: "notes/c.md", outcome: "skipped", reason: "no_marker" },
|
|
13
|
+
{ path: "notes/b.md", outcome: "unchanged" },
|
|
14
|
+
]);
|
|
15
|
+
assert.deepEqual(status.counts, { updated: 2, unchanged: 1, skipped: 1, error: 1 });
|
|
16
|
+
assert.equal(status.hasError, true);
|
|
17
|
+
assert.deepEqual(status.results, [
|
|
18
|
+
{ path: "notes/a.md", outcome: "updated" },
|
|
19
|
+
{ path: "notes/b.md", outcome: "unchanged" },
|
|
20
|
+
{ path: "notes/c.md", outcome: "skipped", reason: "no_marker" },
|
|
21
|
+
{ path: "notes/d.md", outcome: "error", reason: "missing_file" },
|
|
22
|
+
{ path: "notes/e.md", outcome: "updated" },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("empty input returns zeroed counts, no results, and hasError false", () => {
|
|
27
|
+
const status = summarizeVaultPublish([]);
|
|
28
|
+
assert.deepEqual(status.counts, { updated: 0, unchanged: 0, skipped: 0, error: 0 });
|
|
29
|
+
for (const outcome of VAULT_PUBLISH_OUTCOMES) {
|
|
30
|
+
assert.equal(status.counts[outcome], 0, `count key ${outcome} must exist at zero`);
|
|
31
|
+
}
|
|
32
|
+
assert.deepEqual(status.results, []);
|
|
33
|
+
assert.equal(status.hasError, false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("unknown outcome throws TypeError listing the allow-list", () => {
|
|
37
|
+
assert.throws(
|
|
38
|
+
() => summarizeVaultPublish([{ path: "notes/a.md", outcome: "deleted" }]),
|
|
39
|
+
(err: unknown) => {
|
|
40
|
+
assert.ok(err instanceof TypeError);
|
|
41
|
+
assert.match(err.message, /unknown vault publish outcome/);
|
|
42
|
+
for (const outcome of VAULT_PUBLISH_OUTCOMES) {
|
|
43
|
+
assert.ok(err.message.includes(outcome), `message lists ${outcome}`);
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("skipped without a reason throws TypeError", () => {
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => summarizeVaultPublish([{ path: "notes/a.md", outcome: "skipped" }]),
|
|
53
|
+
(err: unknown) => err instanceof TypeError && /reason/.test(err.message),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("error with a missing or whitespace-only reason throws TypeError", () => {
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => summarizeVaultPublish([{ path: "notes/a.md", outcome: "error" }]),
|
|
60
|
+
(err: unknown) => err instanceof TypeError && /reason/.test(err.message),
|
|
61
|
+
);
|
|
62
|
+
assert.throws(
|
|
63
|
+
() => summarizeVaultPublish([{ path: "notes/a.md", outcome: "error", reason: " " }]),
|
|
64
|
+
(err: unknown) => err instanceof TypeError && /reason/.test(err.message),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("updated and unchanged must not carry a reason", () => {
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => summarizeVaultPublish([{ path: "notes/a.md", outcome: "updated", reason: "no_marker" }]),
|
|
71
|
+
(err: unknown) => err instanceof TypeError && /reason/.test(err.message),
|
|
72
|
+
);
|
|
73
|
+
assert.throws(
|
|
74
|
+
() => summarizeVaultPublish([{ path: "notes/a.md", outcome: "unchanged", reason: "x" }]),
|
|
75
|
+
(err: unknown) => err instanceof TypeError && /reason/.test(err.message),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("blank path throws RangeError", () => {
|
|
80
|
+
assert.throws(
|
|
81
|
+
() => summarizeVaultPublish([{ path: "", outcome: "updated" }]),
|
|
82
|
+
(err: unknown) => err instanceof RangeError && /path/.test(err.message),
|
|
83
|
+
);
|
|
84
|
+
assert.throws(
|
|
85
|
+
() => summarizeVaultPublish([{ path: " \t ", outcome: "updated" }]),
|
|
86
|
+
(err: unknown) => err instanceof RangeError && /path/.test(err.message),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("duplicate paths are all preserved, not deduplicated", () => {
|
|
91
|
+
const status = summarizeVaultPublish([
|
|
92
|
+
{ path: "notes/a.md", outcome: "updated" },
|
|
93
|
+
{ path: "notes/a.md", outcome: "skipped", reason: "no_marker" },
|
|
94
|
+
{ path: "notes/a.md", outcome: "updated" },
|
|
95
|
+
]);
|
|
96
|
+
assert.equal(status.results.length, 3);
|
|
97
|
+
assert.deepEqual(status.counts, { updated: 2, unchanged: 0, skipped: 1, error: 0 });
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
status.results.map((entry) => entry.outcome),
|
|
100
|
+
["updated", "updated", "skipped"],
|
|
101
|
+
);
|
|
102
|
+
assert.equal(status.hasError, false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("order is deterministic across shuffled input", () => {
|
|
106
|
+
const base: VaultPublishResult[] = [
|
|
107
|
+
{ path: "days/2026-08-18.md", outcome: "error", reason: "missing_file" },
|
|
108
|
+
{ path: "days/2026-08-18.md", outcome: "updated" },
|
|
109
|
+
{ path: "days/2026-08-18.md", outcome: "skipped", reason: "no_marker" },
|
|
110
|
+
{ path: "days/2026-08-18.md", outcome: "skipped", reason: "duplicate_heading" },
|
|
111
|
+
{ path: "days/2026-08-17.md", outcome: "unchanged" },
|
|
112
|
+
{ path: "places/office.md", outcome: "updated" },
|
|
113
|
+
];
|
|
114
|
+
const forward = summarizeVaultPublish(base);
|
|
115
|
+
const reversed = summarizeVaultPublish([...base].reverse());
|
|
116
|
+
assert.deepEqual(forward, reversed);
|
|
117
|
+
assert.deepEqual(
|
|
118
|
+
forward.results.map((entry) => `${entry.path}:${entry.outcome}:${entry.reason ?? ""}`),
|
|
119
|
+
[
|
|
120
|
+
"days/2026-08-17.md:unchanged:",
|
|
121
|
+
"days/2026-08-18.md:updated:",
|
|
122
|
+
"days/2026-08-18.md:skipped:duplicate_heading",
|
|
123
|
+
"days/2026-08-18.md:skipped:no_marker",
|
|
124
|
+
"days/2026-08-18.md:error:missing_file",
|
|
125
|
+
"places/office.md:updated:",
|
|
126
|
+
],
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("input is not mutated", () => {
|
|
131
|
+
const input: VaultPublishResult[] = [
|
|
132
|
+
{ path: "notes/b.md", outcome: "updated" },
|
|
133
|
+
{ path: "notes/a.md", outcome: "skipped", reason: "no_marker" },
|
|
134
|
+
];
|
|
135
|
+
const snapshot = structuredClone(input);
|
|
136
|
+
summarizeVaultPublish(input);
|
|
137
|
+
assert.deepEqual(input, snapshot);
|
|
138
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure vault-publish outcome aggregator (issue #1985).
|
|
3
|
+
*
|
|
4
|
+
* `summarizeVaultPublish` folds per-file publish results into the status
|
|
5
|
+
* shape the publish report and the dry-run print: sorted per-file outcomes
|
|
6
|
+
* plus zero-filled counts. It performs no I/O and never mutates its input.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed contract: an unrecognized outcome is refused, never coerced
|
|
9
|
+
* into `skipped` or `error`, and `skipped`/`error` must carry a non-blank
|
|
10
|
+
* reason so every un-actionable row says why.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const VAULT_PUBLISH_OUTCOMES = ["updated", "unchanged", "skipped", "error"] as const;
|
|
14
|
+
export type VaultPublishOutcome = (typeof VAULT_PUBLISH_OUTCOMES)[number];
|
|
15
|
+
|
|
16
|
+
export interface VaultPublishResult {
|
|
17
|
+
/** Vault-relative note path. */
|
|
18
|
+
path: string;
|
|
19
|
+
outcome: string;
|
|
20
|
+
/** Required for skipped and error, e.g. "no_marker". */
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VaultPublishStatus {
|
|
25
|
+
results: Array<{ path: string; outcome: VaultPublishOutcome; reason?: string }>;
|
|
26
|
+
counts: Record<VaultPublishOutcome, number>;
|
|
27
|
+
/** True when at least one entry is an error. */
|
|
28
|
+
hasError: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ValidatedEntry {
|
|
32
|
+
path: string;
|
|
33
|
+
outcome: VaultPublishOutcome;
|
|
34
|
+
reason?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Total order: path asc, then outcome in allow-list order, then reason. */
|
|
38
|
+
function compareEntries(a: ValidatedEntry, b: ValidatedEntry): number {
|
|
39
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
40
|
+
const rankA = VAULT_PUBLISH_OUTCOMES.indexOf(a.outcome);
|
|
41
|
+
const rankB = VAULT_PUBLISH_OUTCOMES.indexOf(b.outcome);
|
|
42
|
+
if (rankA !== rankB) return rankA < rankB ? -1 : 1;
|
|
43
|
+
const reasonA = a.reason ?? "";
|
|
44
|
+
const reasonB = b.reason ?? "";
|
|
45
|
+
if (reasonA !== reasonB) return reasonA < reasonB ? -1 : 1;
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function summarizeVaultPublish(results: readonly VaultPublishResult[]): VaultPublishStatus {
|
|
50
|
+
if (!Array.isArray(results)) {
|
|
51
|
+
throw new TypeError(`summarizeVaultPublish expects an array of results (got ${typeof results})`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entries: ValidatedEntry[] = [];
|
|
55
|
+
for (const entry of results) {
|
|
56
|
+
if (typeof entry !== "object" || entry === null) {
|
|
57
|
+
throw new TypeError(`vault publish result entries must be objects (got ${typeof entry})`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof entry.path !== "string" || entry.path.trim() === "") {
|
|
60
|
+
throw new RangeError(`vault publish result requires a non-blank path (got ${JSON.stringify(entry.path)})`);
|
|
61
|
+
}
|
|
62
|
+
const outcome = VAULT_PUBLISH_OUTCOMES.find((candidate) => candidate === entry.outcome);
|
|
63
|
+
if (outcome === undefined) {
|
|
64
|
+
throw new TypeError(
|
|
65
|
+
`unknown vault publish outcome ${JSON.stringify(entry.outcome)}; expected one of: ${VAULT_PUBLISH_OUTCOMES.join(", ")}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (outcome === "skipped" || outcome === "error") {
|
|
69
|
+
if (typeof entry.reason !== "string" || entry.reason.trim() === "") {
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
`vault publish outcome "${outcome}" requires a non-blank reason, e.g. "no_marker" (got ${JSON.stringify(entry.reason)})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
entries.push({ path: entry.path, outcome, reason: entry.reason });
|
|
75
|
+
} else {
|
|
76
|
+
if (entry.reason !== undefined) {
|
|
77
|
+
throw new TypeError(
|
|
78
|
+
`vault publish outcome "${outcome}" must not carry a reason (got ${JSON.stringify(entry.reason)})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
entries.push({ path: entry.path, outcome });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sorted = [...entries].sort(compareEntries);
|
|
86
|
+
|
|
87
|
+
const counts = {} as Record<VaultPublishOutcome, number>;
|
|
88
|
+
for (const outcome of VAULT_PUBLISH_OUTCOMES) counts[outcome] = 0;
|
|
89
|
+
for (const entry of sorted) counts[entry.outcome] += 1;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
results: sorted,
|
|
93
|
+
counts,
|
|
94
|
+
hasError: counts.error > 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/dedup/index.ts
CHANGED
|
@@ -474,3 +474,10 @@ export {
|
|
|
474
474
|
type MergeJudgeVerdictName,
|
|
475
475
|
type ParseMergeVerdictResult,
|
|
476
476
|
} from "./merge-verdict.js";
|
|
477
|
+
|
|
478
|
+
export {
|
|
479
|
+
MERGE_DERIVED_VIA,
|
|
480
|
+
buildMergeFrontmatterUpdate,
|
|
481
|
+
type MergeFrontmatterUpdate,
|
|
482
|
+
type MergeProvenanceSource,
|
|
483
|
+
} from "./merge-provenance.js";
|