@remnic/core 9.69.0 → 9.69.2
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 +18 -18
- package/dist/access-authorization-probe.js +19 -19
- package/dist/access-boundary.js +18 -18
- package/dist/access-cli.js +76 -76
- package/dist/access-extraction-force-flush.js +18 -18
- package/dist/access-http-query.js +20 -20
- package/dist/access-http.js +34 -34
- package/dist/access-identity-continuity-surface.js +18 -18
- package/dist/access-lcm-surface.js +18 -18
- package/dist/access-mcp.js +25 -25
- package/dist/access-namespace-preflight.js +18 -18
- package/dist/access-observe-write-surface.js +18 -18
- package/dist/access-operations-batch.js +19 -19
- package/dist/access-operations.js +24 -24
- package/dist/access-recall-concurrency.js +18 -18
- package/dist/access-recall-response.js +18 -18
- package/dist/access-recall-surface.js +18 -18
- package/dist/access-service.js +18 -18
- package/dist/access-surface-catalog.js +20 -20
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +10 -10
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-4PEZMGZD.js → chunk-42JAVCED.js} +2 -2
- package/dist/{chunk-QOE3EM5Z.js → chunk-42LQMCEE.js} +8 -8
- package/dist/{chunk-PIBQIYF2.js → chunk-4EQBBIFH.js} +2 -2
- package/dist/{chunk-DJ7TZLGU.js → chunk-4FRP3R44.js} +2 -2
- package/dist/{chunk-25P3LW6U.js → chunk-5H5TKPBA.js} +2 -2
- package/dist/{chunk-L7MJJAG2.js → chunk-6FLNUDEO.js} +2 -2
- package/dist/{chunk-2MHMHF76.js → chunk-6LDZNG3T.js} +2 -2
- package/dist/{chunk-KAY2C6VX.js → chunk-7ATQD2GP.js} +2 -2
- package/dist/{chunk-P342D5BP.js → chunk-AEZCLP3C.js} +2 -2
- package/dist/{chunk-4PMPOZK3.js → chunk-AF67CHFU.js} +2 -2
- package/dist/{chunk-YYAWJRGL.js → chunk-AI6TT4Y3.js} +2 -2
- package/dist/{chunk-VLSUKXCJ.js → chunk-B6D3O5X2.js} +3 -3
- package/dist/{chunk-5UI7WQA6.js → chunk-C7KYMSL5.js} +2 -2
- package/dist/{chunk-BS5BD3DF.js → chunk-CCKIHXU5.js} +2 -2
- package/dist/{chunk-KJUH3QIQ.js → chunk-DKTIW44X.js} +2 -2
- package/dist/{chunk-3IRH5MYM.js → chunk-DMR6GVTW.js} +5 -5
- package/dist/{chunk-USQAH6PN.js → chunk-F6B3RUR4.js} +2 -2
- package/dist/{chunk-XZBXINXZ.js → chunk-GIC6BASP.js} +4 -4
- package/dist/{chunk-BGD3T743.js → chunk-GPTZFSMQ.js} +2 -2
- package/dist/{chunk-327JMA2N.js → chunk-IJCC5TSX.js} +2 -2
- package/dist/{chunk-NYQHD7H4.js → chunk-J7RZ4NJO.js} +11 -11
- package/dist/{chunk-6FUMA6AA.js → chunk-JAQXUGLN.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-FOT5GP2U.js → chunk-JC5GCR3K.js} +2 -2
- package/dist/{chunk-363VZTVH.js → chunk-JU5SBSV7.js} +2 -2
- package/dist/{chunk-WQCET3N3.js → chunk-KXJ3IDD5.js} +2 -2
- package/dist/{chunk-YFK64FYK.js → chunk-LSGQRUTX.js} +2 -2
- package/dist/{chunk-X2NFFBTO.js → chunk-LVFI5LG7.js} +10 -10
- package/dist/{chunk-ABPCF4MW.js → chunk-LW5DFCGW.js} +1 -1
- package/dist/{chunk-KRNFJKMH.js → chunk-M73TXJJW.js} +2 -2
- package/dist/{chunk-RACZB5HT.js → chunk-N37X5RU7.js} +4 -4
- package/dist/{chunk-AG46P24C.js → chunk-NNBZS3DY.js} +4 -4
- package/dist/{chunk-HDQQRRCZ.js → chunk-PSFQ4HDM.js} +3 -3
- package/dist/{chunk-D35XLYQ7.js → chunk-RGCF4XNH.js} +9 -9
- package/dist/{chunk-F25CL3IQ.js → chunk-RJ7NOBKA.js} +2 -2
- package/dist/{chunk-A7NMS5II.js → chunk-RSPIDEY3.js} +3 -3
- package/dist/{chunk-ND6NL2YR.js → chunk-S7WPER23.js} +2 -2
- package/dist/{chunk-52GQNSLC.js → chunk-SB7PMYWV.js} +7 -7
- package/dist/{chunk-WSYACIKD.js → chunk-THWUR7JO.js} +2 -2
- package/dist/{chunk-BZ26443Y.js → chunk-TL2THUFH.js} +101 -53
- package/dist/chunk-TL2THUFH.js.map +1 -0
- package/dist/{chunk-XW3G4BJI.js → chunk-TV52QFXK.js} +7 -7
- package/dist/{chunk-ERKFG4RV.js → chunk-UXEAJ765.js} +34 -34
- package/dist/{chunk-ZVW724ZK.js → chunk-WFHBC4BQ.js} +2 -2
- package/dist/{chunk-7KG5V4VV.js → chunk-WJPSCTZY.js} +2 -2
- package/dist/{chunk-BHH736UQ.js → chunk-WKZSKFZC.js} +3 -3
- package/dist/{chunk-NFITGYMQ.js → chunk-XCVGQKTR.js} +3 -3
- package/dist/{chunk-BTRY6UH3.js → chunk-YJLGCUKZ.js} +2 -2
- package/dist/{chunk-Z2FXSE5P.js → chunk-YQPVXUJG.js} +2 -2
- package/dist/{chunk-ZDJMCXC7.js → chunk-ZBGODUSY.js} +12 -12
- package/dist/{chunk-XPDWNWH6.js → chunk-ZMVMYZMX.js} +3 -3
- package/dist/{chunk-I6T4SYFW.js → chunk-ZOMXROJ6.js} +3 -3
- package/dist/cli.js +50 -50
- 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.js +19 -19
- package/dist/extraction.js +10 -10
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +94 -88
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.js +13 -13
- 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/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 +76 -76
- package/dist/recall-deep-expand-select.d.ts +23 -0
- package/dist/recall-deep-expand-select.js +50 -0
- package/dist/recall-deep-expand-select.js.map +1 -0
- 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/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/source-agent-qualifier.js +10 -10
- package/dist/standup/build.js +10 -10
- package/dist/storage.js +9 -9
- package/dist/support-passport/index.js +21 -21
- package/dist/transfer/export-okf.js +11 -11
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- 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/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-deep-expand-select.test.ts +163 -0
- package/src/recall-deep-expand-select.ts +65 -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-BZ26443Y.js.map +0 -1
- /package/dist/{chunk-4PEZMGZD.js.map → chunk-42JAVCED.js.map} +0 -0
- /package/dist/{chunk-QOE3EM5Z.js.map → chunk-42LQMCEE.js.map} +0 -0
- /package/dist/{chunk-PIBQIYF2.js.map → chunk-4EQBBIFH.js.map} +0 -0
- /package/dist/{chunk-DJ7TZLGU.js.map → chunk-4FRP3R44.js.map} +0 -0
- /package/dist/{chunk-25P3LW6U.js.map → chunk-5H5TKPBA.js.map} +0 -0
- /package/dist/{chunk-L7MJJAG2.js.map → chunk-6FLNUDEO.js.map} +0 -0
- /package/dist/{chunk-2MHMHF76.js.map → chunk-6LDZNG3T.js.map} +0 -0
- /package/dist/{chunk-KAY2C6VX.js.map → chunk-7ATQD2GP.js.map} +0 -0
- /package/dist/{chunk-P342D5BP.js.map → chunk-AEZCLP3C.js.map} +0 -0
- /package/dist/{chunk-4PMPOZK3.js.map → chunk-AF67CHFU.js.map} +0 -0
- /package/dist/{chunk-YYAWJRGL.js.map → chunk-AI6TT4Y3.js.map} +0 -0
- /package/dist/{chunk-VLSUKXCJ.js.map → chunk-B6D3O5X2.js.map} +0 -0
- /package/dist/{chunk-5UI7WQA6.js.map → chunk-C7KYMSL5.js.map} +0 -0
- /package/dist/{chunk-BS5BD3DF.js.map → chunk-CCKIHXU5.js.map} +0 -0
- /package/dist/{chunk-KJUH3QIQ.js.map → chunk-DKTIW44X.js.map} +0 -0
- /package/dist/{chunk-3IRH5MYM.js.map → chunk-DMR6GVTW.js.map} +0 -0
- /package/dist/{chunk-USQAH6PN.js.map → chunk-F6B3RUR4.js.map} +0 -0
- /package/dist/{chunk-XZBXINXZ.js.map → chunk-GIC6BASP.js.map} +0 -0
- /package/dist/{chunk-BGD3T743.js.map → chunk-GPTZFSMQ.js.map} +0 -0
- /package/dist/{chunk-327JMA2N.js.map → chunk-IJCC5TSX.js.map} +0 -0
- /package/dist/{chunk-NYQHD7H4.js.map → chunk-J7RZ4NJO.js.map} +0 -0
- /package/dist/{chunk-6FUMA6AA.js.map → chunk-JAQXUGLN.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-FOT5GP2U.js.map → chunk-JC5GCR3K.js.map} +0 -0
- /package/dist/{chunk-363VZTVH.js.map → chunk-JU5SBSV7.js.map} +0 -0
- /package/dist/{chunk-WQCET3N3.js.map → chunk-KXJ3IDD5.js.map} +0 -0
- /package/dist/{chunk-YFK64FYK.js.map → chunk-LSGQRUTX.js.map} +0 -0
- /package/dist/{chunk-X2NFFBTO.js.map → chunk-LVFI5LG7.js.map} +0 -0
- /package/dist/{chunk-ABPCF4MW.js.map → chunk-LW5DFCGW.js.map} +0 -0
- /package/dist/{chunk-KRNFJKMH.js.map → chunk-M73TXJJW.js.map} +0 -0
- /package/dist/{chunk-RACZB5HT.js.map → chunk-N37X5RU7.js.map} +0 -0
- /package/dist/{chunk-AG46P24C.js.map → chunk-NNBZS3DY.js.map} +0 -0
- /package/dist/{chunk-HDQQRRCZ.js.map → chunk-PSFQ4HDM.js.map} +0 -0
- /package/dist/{chunk-D35XLYQ7.js.map → chunk-RGCF4XNH.js.map} +0 -0
- /package/dist/{chunk-F25CL3IQ.js.map → chunk-RJ7NOBKA.js.map} +0 -0
- /package/dist/{chunk-A7NMS5II.js.map → chunk-RSPIDEY3.js.map} +0 -0
- /package/dist/{chunk-ND6NL2YR.js.map → chunk-S7WPER23.js.map} +0 -0
- /package/dist/{chunk-52GQNSLC.js.map → chunk-SB7PMYWV.js.map} +0 -0
- /package/dist/{chunk-WSYACIKD.js.map → chunk-THWUR7JO.js.map} +0 -0
- /package/dist/{chunk-XW3G4BJI.js.map → chunk-TV52QFXK.js.map} +0 -0
- /package/dist/{chunk-ERKFG4RV.js.map → chunk-UXEAJ765.js.map} +0 -0
- /package/dist/{chunk-ZVW724ZK.js.map → chunk-WFHBC4BQ.js.map} +0 -0
- /package/dist/{chunk-7KG5V4VV.js.map → chunk-WJPSCTZY.js.map} +0 -0
- /package/dist/{chunk-BHH736UQ.js.map → chunk-WKZSKFZC.js.map} +0 -0
- /package/dist/{chunk-NFITGYMQ.js.map → chunk-XCVGQKTR.js.map} +0 -0
- /package/dist/{chunk-BTRY6UH3.js.map → chunk-YJLGCUKZ.js.map} +0 -0
- /package/dist/{chunk-Z2FXSE5P.js.map → chunk-YQPVXUJG.js.map} +0 -0
- /package/dist/{chunk-ZDJMCXC7.js.map → chunk-ZBGODUSY.js.map} +0 -0
- /package/dist/{chunk-XPDWNWH6.js.map → chunk-ZMVMYZMX.js.map} +0 -0
- /package/dist/{chunk-I6T4SYFW.js.map → chunk-ZOMXROJ6.js.map} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge frontmatter builder (issue #2330).
|
|
3
|
+
*
|
|
4
|
+
* On a create-or-update merge the target keeps its own provenance and gains
|
|
5
|
+
* the incoming fact's, so a merge can never erase where a claim came from.
|
|
6
|
+
*
|
|
7
|
+
* Fields here match the canonical schema in `types.ts` deliberately: the
|
|
8
|
+
* serializer rejects a `derived_via` outside its allow-list and drops keys
|
|
9
|
+
* `MemoryFrontmatter` does not define, so an invented operator name or
|
|
10
|
+
* counter would vanish on write. Pure — no I/O, no clock.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The serializer's allow-listed operator for a create-or-update merge. */
|
|
14
|
+
export const MERGE_DERIVED_VIA = "merge" as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Structural mirror of `ProvenanceSource` (types.ts). `sessionKey`,
|
|
18
|
+
* `observedAt`, and `quote` are required; `turnId` and the character
|
|
19
|
+
* offsets are optional and MUST survive a merge untouched.
|
|
20
|
+
*/
|
|
21
|
+
export interface MergeProvenanceSource {
|
|
22
|
+
sessionKey: string;
|
|
23
|
+
observedAt: string;
|
|
24
|
+
quote: string;
|
|
25
|
+
turnId?: string;
|
|
26
|
+
charStart?: number;
|
|
27
|
+
charEnd?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MergeFrontmatterUpdate {
|
|
31
|
+
updated: string;
|
|
32
|
+
derived_via: typeof MERGE_DERIVED_VIA;
|
|
33
|
+
/** Follows the `reinforcement_count` convention; the serializer requires > 0. */
|
|
34
|
+
reinforcement_count: number;
|
|
35
|
+
sources: MergeProvenanceSource[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ISO_TIMESTAMP =
|
|
39
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Require a full ISO 8601 instant whose calendar components round-trip.
|
|
43
|
+
* `Date.parse` alone accepts `123` and rolls `2026-02-30` over into March,
|
|
44
|
+
* and this value is echoed verbatim into frontmatter.
|
|
45
|
+
*/
|
|
46
|
+
function assertIsoInstant(value: unknown, field: string): string {
|
|
47
|
+
if (typeof value !== "string" || !ISO_TIMESTAMP.test(value)) {
|
|
48
|
+
throw new RangeError(`${field} must be a full ISO 8601 instant, got ${JSON.stringify(value)}`);
|
|
49
|
+
}
|
|
50
|
+
const ms = Date.parse(value);
|
|
51
|
+
if (Number.isNaN(ms)) {
|
|
52
|
+
throw new RangeError(`${field} must be a full ISO 8601 instant, got ${JSON.stringify(value)}`);
|
|
53
|
+
}
|
|
54
|
+
// Reject a rolled-over calendar date (2026-02-30 -> 2026-03-02).
|
|
55
|
+
if (!new Date(ms).toISOString().startsWith(value.slice(0, 10))) {
|
|
56
|
+
throw new RangeError(`${field} is not a real calendar date: ${JSON.stringify(value)}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertOffset(value: unknown, field: string): void {
|
|
62
|
+
if (value === undefined) return;
|
|
63
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
64
|
+
throw new RangeError(`${field} must be a non-negative integer when present, got ${String(value)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSource(source: MergeProvenanceSource, index: number): MergeProvenanceSource {
|
|
69
|
+
const label = `sources[${index}]`;
|
|
70
|
+
for (const field of ["sessionKey", "quote"] as const) {
|
|
71
|
+
const value = source[field];
|
|
72
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
73
|
+
throw new RangeError(`${label}.${field} must be a non-blank string`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
assertIsoInstant(source.observedAt, `${label}.observedAt`);
|
|
77
|
+
if (source.turnId !== undefined && (typeof source.turnId !== "string" || source.turnId.trim() === "")) {
|
|
78
|
+
throw new RangeError(`${label}.turnId must be a non-blank string when present`);
|
|
79
|
+
}
|
|
80
|
+
assertOffset(source.charStart, `${label}.charStart`);
|
|
81
|
+
assertOffset(source.charEnd, `${label}.charEnd`);
|
|
82
|
+
if (source.charStart !== undefined && source.charEnd !== undefined && source.charEnd < source.charStart) {
|
|
83
|
+
throw new RangeError(`${label}.charEnd must be >= charStart`);
|
|
84
|
+
}
|
|
85
|
+
// Copy every canonical field, including the optional ones: dropping
|
|
86
|
+
// observedAt or the offsets here would make the serializer discard the
|
|
87
|
+
// clone, turning "append provenance" into "erase provenance".
|
|
88
|
+
const copy: MergeProvenanceSource = {
|
|
89
|
+
sessionKey: source.sessionKey,
|
|
90
|
+
observedAt: source.observedAt,
|
|
91
|
+
quote: source.quote,
|
|
92
|
+
};
|
|
93
|
+
if (source.turnId !== undefined) copy.turnId = source.turnId;
|
|
94
|
+
if (source.charStart !== undefined) copy.charStart = source.charStart;
|
|
95
|
+
if (source.charEnd !== undefined) copy.charEnd = source.charEnd;
|
|
96
|
+
return copy;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sourceKey(source: MergeProvenanceSource): string {
|
|
100
|
+
return JSON.stringify([
|
|
101
|
+
source.sessionKey,
|
|
102
|
+
source.turnId ?? null,
|
|
103
|
+
source.observedAt,
|
|
104
|
+
source.quote,
|
|
105
|
+
source.charStart ?? null,
|
|
106
|
+
source.charEnd ?? null,
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildMergeFrontmatterUpdate(input: {
|
|
111
|
+
/** Existing sources already on the merge target. */
|
|
112
|
+
targetSources?: readonly MergeProvenanceSource[];
|
|
113
|
+
/** Sources from the incoming fact being merged in. */
|
|
114
|
+
incomingSources: readonly MergeProvenanceSource[];
|
|
115
|
+
/** Existing reinforcement_count on the target, if any. */
|
|
116
|
+
targetReinforcementCount?: number;
|
|
117
|
+
nowIso: string;
|
|
118
|
+
}): MergeFrontmatterUpdate {
|
|
119
|
+
const updated = assertIsoInstant(input.nowIso, "nowIso");
|
|
120
|
+
|
|
121
|
+
const previous = input.targetReinforcementCount;
|
|
122
|
+
if (
|
|
123
|
+
previous !== undefined &&
|
|
124
|
+
(typeof previous !== "number" || !Number.isInteger(previous) || previous < 0)
|
|
125
|
+
) {
|
|
126
|
+
throw new RangeError(
|
|
127
|
+
`reinforcement_count must be a non-negative integer when supplied, got ${String(previous)}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const seen = new Set<string>();
|
|
132
|
+
const sources: MergeProvenanceSource[] = [];
|
|
133
|
+
const append = (source: MergeProvenanceSource, index: number): void => {
|
|
134
|
+
const copy = normalizeSource(source, index);
|
|
135
|
+
const key = sourceKey(copy);
|
|
136
|
+
if (seen.has(key)) return;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
sources.push(copy);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const target = input.targetSources ?? [];
|
|
142
|
+
target.forEach(append);
|
|
143
|
+
input.incomingSources.forEach((source, i) => append(source, target.length + i));
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
updated,
|
|
147
|
+
derived_via: MERGE_DERIVED_VIA,
|
|
148
|
+
reinforcement_count: (previous ?? 0) + 1,
|
|
149
|
+
sources,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_MAX_EXPAND_PER_STEP,
|
|
6
|
+
selectExpandNodeIds,
|
|
7
|
+
} from "./recall-deep-expand-select.js";
|
|
8
|
+
|
|
9
|
+
const FRONTIER = ["n1", "n2", "n3", "n4", "n5"] as const;
|
|
10
|
+
|
|
11
|
+
test("subset request passes untruncated", () => {
|
|
12
|
+
const result = selectExpandNodeIds({
|
|
13
|
+
frontierIds: FRONTIER,
|
|
14
|
+
requestedIds: ["n3", "n1"],
|
|
15
|
+
});
|
|
16
|
+
assert.deepEqual(result, {
|
|
17
|
+
ok: true,
|
|
18
|
+
nodeIds: ["n3", "n1"],
|
|
19
|
+
truncated: false,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("five-id valid request truncates to three in requested order", () => {
|
|
24
|
+
const result = selectExpandNodeIds({
|
|
25
|
+
frontierIds: FRONTIER,
|
|
26
|
+
requestedIds: ["n5", "n2", "n4", "n1", "n3"],
|
|
27
|
+
});
|
|
28
|
+
assert.deepEqual(result, {
|
|
29
|
+
ok: true,
|
|
30
|
+
nodeIds: ["n5", "n2", "n4"],
|
|
31
|
+
truncated: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("one foreign id among valid ones refuses and lists only the foreign id", () => {
|
|
36
|
+
const result = selectExpandNodeIds({
|
|
37
|
+
frontierIds: FRONTIER,
|
|
38
|
+
requestedIds: ["n1", "ghost", "n2"],
|
|
39
|
+
});
|
|
40
|
+
assert.deepEqual(result, {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: "invalid_policy_output",
|
|
43
|
+
foreignIds: ["ghost"],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("several foreign ids are sorted and deduplicated", () => {
|
|
48
|
+
const result = selectExpandNodeIds({
|
|
49
|
+
frontierIds: FRONTIER,
|
|
50
|
+
requestedIds: ["z9", "a1", "z9", "n1"],
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(result, {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: "invalid_policy_output",
|
|
55
|
+
foreignIds: ["a1", "z9"],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("duplicates collapse before the cap", () => {
|
|
60
|
+
const result = selectExpandNodeIds({
|
|
61
|
+
frontierIds: FRONTIER,
|
|
62
|
+
requestedIds: ["n1", "n1", "n1", "n2"],
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(result, {
|
|
65
|
+
ok: true,
|
|
66
|
+
nodeIds: ["n1", "n2"],
|
|
67
|
+
truncated: false,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("padded and case-different ids are foreign", () => {
|
|
72
|
+
const result = selectExpandNodeIds({
|
|
73
|
+
frontierIds: FRONTIER,
|
|
74
|
+
requestedIds: [" n1", "N1"],
|
|
75
|
+
});
|
|
76
|
+
assert.deepEqual(result, {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: "invalid_policy_output",
|
|
79
|
+
foreignIds: [" n1", "N1"],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("non-array requestedIds refuses with empty foreignIds", () => {
|
|
84
|
+
for (const bad of [undefined, null, "n1"]) {
|
|
85
|
+
const result = selectExpandNodeIds({
|
|
86
|
+
frontierIds: FRONTIER,
|
|
87
|
+
requestedIds: bad,
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(result, {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: "invalid_policy_output",
|
|
92
|
+
foreignIds: [],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("non-string array entry is reported as its String form", () => {
|
|
98
|
+
const result = selectExpandNodeIds({
|
|
99
|
+
frontierIds: FRONTIER,
|
|
100
|
+
requestedIds: ["n1", 42],
|
|
101
|
+
});
|
|
102
|
+
assert.deepEqual(result, {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: "invalid_policy_output",
|
|
105
|
+
foreignIds: ["42"],
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("blank string entry is foreign", () => {
|
|
110
|
+
const result = selectExpandNodeIds({
|
|
111
|
+
frontierIds: FRONTIER,
|
|
112
|
+
requestedIds: [" "],
|
|
113
|
+
});
|
|
114
|
+
assert.deepEqual(result, {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: "invalid_policy_output",
|
|
117
|
+
foreignIds: [" "],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("empty array succeeds and expands nothing", () => {
|
|
122
|
+
const result = selectExpandNodeIds({
|
|
123
|
+
frontierIds: FRONTIER,
|
|
124
|
+
requestedIds: [],
|
|
125
|
+
});
|
|
126
|
+
assert.deepEqual(result, { ok: true, nodeIds: [], truncated: false });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("default cap is three", () => {
|
|
130
|
+
assert.equal(DEFAULT_MAX_EXPAND_PER_STEP, 3);
|
|
131
|
+
const result = selectExpandNodeIds({
|
|
132
|
+
frontierIds: FRONTIER,
|
|
133
|
+
requestedIds: ["n1", "n2", "n3", "n4"],
|
|
134
|
+
});
|
|
135
|
+
assert.deepEqual(result, {
|
|
136
|
+
ok: true,
|
|
137
|
+
nodeIds: ["n1", "n2", "n3"],
|
|
138
|
+
truncated: true,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("maxExpandPerStep 0, -1, and 1.5 throw RangeError naming the field", () => {
|
|
143
|
+
for (const bad of [0, -1, 1.5]) {
|
|
144
|
+
assert.throws(
|
|
145
|
+
() =>
|
|
146
|
+
selectExpandNodeIds({
|
|
147
|
+
frontierIds: FRONTIER,
|
|
148
|
+
requestedIds: ["n1"],
|
|
149
|
+
maxExpandPerStep: bad,
|
|
150
|
+
}),
|
|
151
|
+
(err: unknown) =>
|
|
152
|
+
err instanceof RangeError && /maxExpandPerStep/.test(err.message),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("inputs are not mutated", () => {
|
|
158
|
+
const frontierIds = ["n1", "n2"];
|
|
159
|
+
const requestedIds = ["n2", "n2", "ghost"];
|
|
160
|
+
selectExpandNodeIds({ frontierIds, requestedIds });
|
|
161
|
+
assert.deepEqual(frontierIds, ["n1", "n2"]);
|
|
162
|
+
assert.deepEqual(requestedIds, ["n2", "n2", "ghost"]);
|
|
163
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select EXPAND node ids for deep recall (issue #2332).
|
|
3
|
+
*
|
|
4
|
+
* A foreign id refuses the whole selection; an oversized request truncates.
|
|
5
|
+
* Duplicates collapse before the cap. Pure. Surfaces wait.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_MAX_EXPAND_PER_STEP = 3;
|
|
8
|
+
|
|
9
|
+
export type ExpandSelectionResult =
|
|
10
|
+
| { ok: true; nodeIds: string[]; truncated: boolean }
|
|
11
|
+
| { ok: false; error: "invalid_policy_output"; foreignIds: string[] };
|
|
12
|
+
|
|
13
|
+
export function selectExpandNodeIds(input: {
|
|
14
|
+
frontierIds: readonly string[];
|
|
15
|
+
requestedIds: unknown;
|
|
16
|
+
maxExpandPerStep?: number;
|
|
17
|
+
}): ExpandSelectionResult {
|
|
18
|
+
const max =
|
|
19
|
+
input.maxExpandPerStep === undefined
|
|
20
|
+
? DEFAULT_MAX_EXPAND_PER_STEP
|
|
21
|
+
: input.maxExpandPerStep;
|
|
22
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
23
|
+
throw new RangeError(
|
|
24
|
+
`maxExpandPerStep must be an integer >= 1; got ${JSON.stringify(max)}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!Array.isArray(input.requestedIds)) {
|
|
29
|
+
return { ok: false, error: "invalid_policy_output", foreignIds: [] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const frontier = new Set(input.frontierIds);
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const foreign = new Set<string>();
|
|
35
|
+
const ordered: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const entry of input.requestedIds) {
|
|
38
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
39
|
+
foreign.add(String(entry));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!frontier.has(entry)) {
|
|
43
|
+
foreign.add(entry);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (seen.has(entry)) continue;
|
|
47
|
+
seen.add(entry);
|
|
48
|
+
ordered.push(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (foreign.size > 0) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: "invalid_policy_output",
|
|
55
|
+
foreignIds: [...foreign].sort(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const truncated = ordered.length > max;
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
nodeIds: truncated ? ordered.slice(0, max) : ordered,
|
|
63
|
+
truncated,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
disclosureRank,
|
|
6
|
+
planDisclosureStep,
|
|
7
|
+
} from "./recall-navigate-disclosure.js";
|
|
8
|
+
import { RECALL_DISCLOSURE_LEVELS } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// The declared input type is strings; agents send arbitrary JSON. Cast only
|
|
11
|
+
// at this boundary so the invalid-shape cases stay one line each.
|
|
12
|
+
const plan = (from: unknown, to: unknown) =>
|
|
13
|
+
planDisclosureStep({ from, to } as unknown as { from: string; to: string });
|
|
14
|
+
|
|
15
|
+
test("disclosureRank returns the index of every level, shallowest first", () => {
|
|
16
|
+
for (const [i, level] of RECALL_DISCLOSURE_LEVELS.entries()) {
|
|
17
|
+
assert.equal(disclosureRank(level), i);
|
|
18
|
+
}
|
|
19
|
+
assert.ok(
|
|
20
|
+
disclosureRank(RECALL_DISCLOSURE_LEVELS[0]) < disclosureRank(RECALL_DISCLOSURE_LEVELS[1]),
|
|
21
|
+
);
|
|
22
|
+
assert.ok(
|
|
23
|
+
disclosureRank(RECALL_DISCLOSURE_LEVELS[1]) < disclosureRank(RECALL_DISCLOSURE_LEVELS[2]),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("disclosureRank throws a RangeError listing allowed values", () => {
|
|
28
|
+
assert.throws(
|
|
29
|
+
() => disclosureRank("full"),
|
|
30
|
+
(err: unknown) =>
|
|
31
|
+
err instanceof RangeError &&
|
|
32
|
+
/disclosure level/.test(err.message) &&
|
|
33
|
+
err.message.includes("chunk") &&
|
|
34
|
+
err.message.includes("section") &&
|
|
35
|
+
err.message.includes("raw"),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("chunk→section is one step", () => {
|
|
40
|
+
assert.deepEqual(planDisclosureStep({ from: "chunk", to: "section" }), {
|
|
41
|
+
ok: true,
|
|
42
|
+
from: "chunk",
|
|
43
|
+
to: "section",
|
|
44
|
+
steps: 1,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("section→raw is one step", () => {
|
|
49
|
+
assert.deepEqual(planDisclosureStep({ from: "section", to: "raw" }), {
|
|
50
|
+
ok: true,
|
|
51
|
+
from: "section",
|
|
52
|
+
to: "raw",
|
|
53
|
+
steps: 1,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("chunk→raw is two steps", () => {
|
|
58
|
+
assert.deepEqual(planDisclosureStep({ from: "chunk", to: "raw" }), {
|
|
59
|
+
ok: true,
|
|
60
|
+
from: "chunk",
|
|
61
|
+
to: "raw",
|
|
62
|
+
steps: 2,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("every level pair is only deeper in one direction", () => {
|
|
67
|
+
for (const shallower of RECALL_DISCLOSURE_LEVELS) {
|
|
68
|
+
for (const deeper of RECALL_DISCLOSURE_LEVELS) {
|
|
69
|
+
const result = planDisclosureStep({ from: shallower, to: deeper });
|
|
70
|
+
if (disclosureRank(shallower) < disclosureRank(deeper)) {
|
|
71
|
+
assert.deepEqual(result, {
|
|
72
|
+
ok: true,
|
|
73
|
+
from: shallower,
|
|
74
|
+
to: deeper,
|
|
75
|
+
steps: disclosureRank(deeper) - disclosureRank(shallower),
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
assert.deepEqual(result, { ok: false, error: "not_deeper" });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("equal levels are not_deeper", () => {
|
|
85
|
+
for (const level of RECALL_DISCLOSURE_LEVELS) {
|
|
86
|
+
assert.deepEqual(planDisclosureStep({ from: level, to: level }), {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: "not_deeper",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("shallower pairs are not_deeper", () => {
|
|
94
|
+
for (const [from, to] of [
|
|
95
|
+
["section", "chunk"],
|
|
96
|
+
["raw", "section"],
|
|
97
|
+
["raw", "chunk"],
|
|
98
|
+
] as const) {
|
|
99
|
+
assert.deepEqual(planDisclosureStep({ from, to }), {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: "not_deeper",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("unknown level on either side is unknown_level, not a throw", () => {
|
|
107
|
+
assert.deepEqual(plan("full", "raw"), { ok: false, error: "unknown_level" });
|
|
108
|
+
assert.deepEqual(plan("chunk", "full"), { ok: false, error: "unknown_level" });
|
|
109
|
+
assert.deepEqual(plan("full", "worse"), { ok: false, error: "unknown_level" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("non-string level on either side is unknown_level", () => {
|
|
113
|
+
for (const bad of [5, null, undefined, true, { level: "raw" }, ["raw"]]) {
|
|
114
|
+
assert.deepEqual(plan(bad, "raw"), { ok: false, error: "unknown_level" });
|
|
115
|
+
assert.deepEqual(plan("chunk", bad), { ok: false, error: "unknown_level" });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("empty string level is unknown_level", () => {
|
|
120
|
+
assert.deepEqual(plan("", "raw"), { ok: false, error: "unknown_level" });
|
|
121
|
+
assert.deepEqual(plan("chunk", ""), { ok: false, error: "unknown_level" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("levels match exactly — no case folding, no trimming", () => {
|
|
125
|
+
assert.deepEqual(plan("Raw", "chunk"), { ok: false, error: "unknown_level" });
|
|
126
|
+
assert.deepEqual(plan(" raw", "raw"), { ok: false, error: "unknown_level" });
|
|
127
|
+
assert.deepEqual(plan("raw", "RAW"), { ok: false, error: "unknown_level" });
|
|
128
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disclosure-step planning for recall navigation (issue #1956).
|
|
3
|
+
*
|
|
4
|
+
* The ladder itself is NOT redefined here: `types.ts` designates
|
|
5
|
+
* `RECALL_DISCLOSURE_LEVELS` the single source of truth and forbids
|
|
6
|
+
* hard-coding disclosure strings elsewhere, so a second copy could drift out
|
|
7
|
+
* of agreement with recall validation and rendering. This module only adds
|
|
8
|
+
* the rule that an expansion must go strictly deeper, so a caller cannot
|
|
9
|
+
* re-pay recall budget for output it already holds. Pure — no I/O.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
isRecallDisclosure,
|
|
14
|
+
RECALL_DISCLOSURE_LEVELS,
|
|
15
|
+
type RecallDisclosure,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
export type DisclosureStepResult =
|
|
19
|
+
| { ok: true; from: RecallDisclosure; to: RecallDisclosure; steps: number }
|
|
20
|
+
| { ok: false; error: "unknown_level" | "not_deeper" };
|
|
21
|
+
|
|
22
|
+
/** Index in the canonical shallow-to-deep ladder. Throws for a non-level. */
|
|
23
|
+
export function disclosureRank(level: string): number {
|
|
24
|
+
const index = (RECALL_DISCLOSURE_LEVELS as readonly string[]).indexOf(level);
|
|
25
|
+
if (index < 0) {
|
|
26
|
+
throw new RangeError(
|
|
27
|
+
`unknown disclosure level ${JSON.stringify(level)}; expected one of ${RECALL_DISCLOSURE_LEVELS.join(", ")}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Plan one expansion. A bad level from an agent is a typed refusal rather
|
|
35
|
+
* than a throw; equal or shallower targets are `not_deeper`.
|
|
36
|
+
*/
|
|
37
|
+
export function planDisclosureStep(input: { from: string; to: string }): DisclosureStepResult {
|
|
38
|
+
if (!isRecallDisclosure(input.from) || !isRecallDisclosure(input.to)) {
|
|
39
|
+
return { ok: false, error: "unknown_level" };
|
|
40
|
+
}
|
|
41
|
+
const fromRank = disclosureRank(input.from);
|
|
42
|
+
const toRank = disclosureRank(input.to);
|
|
43
|
+
if (toRank <= fromRank) return { ok: false, error: "not_deeper" };
|
|
44
|
+
return { ok: true, from: input.from, to: input.to, steps: toRank - fromRank };
|
|
45
|
+
}
|