agent-recall-core 3.4.27 → 3.4.30

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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Precision meter — `injection_precision = used / injected`
3
+ * (v3.5.0 P-C, Ambient Relevance Loop, spec §5 + §6 "Precision guardrail").
4
+ *
5
+ * Purpose: this is the north-star metric that proves "intelligence, not
6
+ * storage." Of the memories AgentRecall surfaces (`injected`), how many
7
+ * does the agent actually act on (`used`)? A falling number is the
8
+ * early-warning that the Surfacer threshold is mis-tuned (noise > signal).
9
+ *
10
+ * Storage shape — append-only JSONL, one file per kind, per project:
11
+ *
12
+ * ~/.agent-recall/projects/<slug>/_precision/injections.jsonl
13
+ * ~/.agent-recall/projects/<slug>/_precision/usages.jsonl
14
+ *
15
+ * Design contract:
16
+ * • `recordInjection`/`recordUsage` are fire-and-forget — they NEVER
17
+ * throw. Losing a measurement line is acceptable; killing a user turn
18
+ * is not. Errors are swallowed silently (mirrors stager.ts).
19
+ * • `getPrecisionKPI` is tolerant of malformed lines (skipped, not
20
+ * fatal), missing files (treated as empty), and missing fields.
21
+ * • `precision` is **null** (not NaN) when `injected === 0` — the
22
+ * dashboard can render "—" instead of "NaN%".
23
+ * • `precision` is clamped to `[0, 1]` via `Math.min(1, used / injected)`
24
+ * (matches the existing pattern in storage/corrections.ts:373 — under
25
+ * data corruption / duplicate-usage events, the raw ratio can exceed
26
+ * 1.0, which is nonsense as a "precision" metric).
27
+ * • `per_source` groups by the prefix of `candidate_id` before ':'
28
+ * (e.g. `palace:room:slug` → bucket `palace`). Unprefixed ids land in
29
+ * the `unknown` bucket so they're still visible.
30
+ *
31
+ * Worker done-definition self-check:
32
+ * (1) error paths: `readJsonl` returns [] on read failure, JSON.parse
33
+ * wrapped in try/catch per line; recordInjection/Usage's outer
34
+ * try/catch swallows fs errors.
35
+ * (2) no global binaries — pure node:fs/path/crypto, no shell-outs.
36
+ * (3) ternary ordering — only branch is `injected > 0 ? clamp : null`,
37
+ * null is the explicit zero-denominator case.
38
+ * (4) time logic — `cutoff` is computed in ms-since-epoch from `Date.now()`
39
+ * which is TZ-agnostic; `ts` strings are ISO-8601 (also TZ-agnostic).
40
+ * No local-date filename is used here so no TZ bug surface.
41
+ */
42
+ import * as fs from "node:fs";
43
+ import * as path from "node:path";
44
+ import { sanitizeProject } from "../storage/paths.js";
45
+ import { ensureDir } from "../storage/fs-utils.js";
46
+ import { getRoot } from "../types.js";
47
+ // ---------------------------------------------------------------------------
48
+ // Internal helpers
49
+ // ---------------------------------------------------------------------------
50
+ /** Resolve the per-project precision directory. Mirrors stager's `_staging/` convention. */
51
+ function precisionDir(project) {
52
+ const safe = sanitizeProject(project);
53
+ return path.join(getRoot(), "projects", safe, "_precision");
54
+ }
55
+ function injectionsPath(project) {
56
+ return path.join(precisionDir(project), "injections.jsonl");
57
+ }
58
+ function usagesPath(project) {
59
+ return path.join(precisionDir(project), "usages.jsonl");
60
+ }
61
+ /**
62
+ * Parse a JSONL file. Bad lines (truncated writes, manual edits) are
63
+ * silently skipped — never throws. Missing file → empty array.
64
+ *
65
+ * Generic over the row shape; caller is responsible for narrowing.
66
+ */
67
+ function readJsonl(filePath) {
68
+ if (!fs.existsSync(filePath))
69
+ return [];
70
+ let raw;
71
+ try {
72
+ raw = fs.readFileSync(filePath, "utf-8");
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ const out = [];
78
+ const lines = raw.split("\n");
79
+ for (const line of lines) {
80
+ const trimmed = line.trim();
81
+ if (!trimmed)
82
+ continue;
83
+ try {
84
+ out.push(JSON.parse(trimmed));
85
+ }
86
+ catch {
87
+ // Malformed line — skip, never throw.
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+ /**
93
+ * Append one row to a JSONL file. Wraps the `fs.appendFileSync` call so
94
+ * the public recorders can swallow fs errors at the boundary.
95
+ */
96
+ function appendJsonl(filePath, row) {
97
+ ensureDir(path.dirname(filePath));
98
+ fs.appendFileSync(filePath, JSON.stringify(row) + "\n", "utf-8");
99
+ }
100
+ /**
101
+ * Extract the source prefix from a candidate id.
102
+ * `palace:room:slug` → `palace`. Unprefixed → `unknown`.
103
+ */
104
+ function sourceOf(candidateId) {
105
+ if (typeof candidateId !== "string" || candidateId.length === 0)
106
+ return "unknown";
107
+ const idx = candidateId.indexOf(":");
108
+ if (idx <= 0)
109
+ return "unknown";
110
+ return candidateId.slice(0, idx);
111
+ }
112
+ /**
113
+ * Narrow an unknown value to `InjectionEvent` shape (defensive — JSONL may
114
+ * have been edited by hand). Returns null on miss.
115
+ */
116
+ function asInjectionEvent(v) {
117
+ if (!v || typeof v !== "object")
118
+ return null;
119
+ const o = v;
120
+ if (typeof o.ts !== "string")
121
+ return null;
122
+ if (!Array.isArray(o.candidate_ids))
123
+ return null;
124
+ if (typeof o.turn_hash !== "string")
125
+ return null;
126
+ const cids = o.candidate_ids.filter((c) => typeof c === "string");
127
+ return { ts: o.ts, candidate_ids: cids, turn_hash: o.turn_hash };
128
+ }
129
+ function asUsageEvent(v) {
130
+ if (!v || typeof v !== "object")
131
+ return null;
132
+ const o = v;
133
+ if (typeof o.ts !== "string")
134
+ return null;
135
+ if (typeof o.candidate_id !== "string")
136
+ return null;
137
+ if (typeof o.turn_hash !== "string")
138
+ return null;
139
+ return {
140
+ ts: o.ts,
141
+ candidate_id: o.candidate_id,
142
+ turn_hash: o.turn_hash,
143
+ evidence: typeof o.evidence === "string" ? o.evidence : "",
144
+ };
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Public API
148
+ // ---------------------------------------------------------------------------
149
+ /**
150
+ * Append one injection event to `_precision/injections.jsonl`.
151
+ *
152
+ * NEVER THROWS. Mirrors `stage()` — the read/write loop continues
153
+ * regardless of fs errors. Losing a measurement is acceptable; killing a
154
+ * user turn is not.
155
+ */
156
+ export function recordInjection(project, ev) {
157
+ try {
158
+ if (!ev || typeof ev !== "object")
159
+ return;
160
+ const safe = {
161
+ ts: typeof ev.ts === "string" && ev.ts.length > 0
162
+ ? ev.ts
163
+ : new Date().toISOString(),
164
+ candidate_ids: Array.isArray(ev.candidate_ids)
165
+ ? ev.candidate_ids.filter((c) => typeof c === "string")
166
+ : [],
167
+ turn_hash: typeof ev.turn_hash === "string" ? ev.turn_hash : "",
168
+ };
169
+ appendJsonl(injectionsPath(project), safe);
170
+ }
171
+ catch {
172
+ // Swallow — see contract above.
173
+ }
174
+ }
175
+ /**
176
+ * Append one usage event to `_precision/usages.jsonl`.
177
+ *
178
+ * NEVER THROWS. One row per used candidate; the caller is expected to
179
+ * dedupe at the detection layer (one usage row per (turn_hash, candidate_id)).
180
+ */
181
+ export function recordUsage(project, ev) {
182
+ try {
183
+ if (!ev || typeof ev !== "object")
184
+ return;
185
+ const safe = {
186
+ ts: typeof ev.ts === "string" && ev.ts.length > 0
187
+ ? ev.ts
188
+ : new Date().toISOString(),
189
+ candidate_id: typeof ev.candidate_id === "string" ? ev.candidate_id : "",
190
+ turn_hash: typeof ev.turn_hash === "string" ? ev.turn_hash : "",
191
+ evidence: typeof ev.evidence === "string" ? ev.evidence : "",
192
+ };
193
+ if (!safe.candidate_id)
194
+ return; // can't bucket without an id
195
+ appendJsonl(usagesPath(project), safe);
196
+ }
197
+ catch {
198
+ // Swallow.
199
+ }
200
+ }
201
+ /**
202
+ * Compute the precision KPI over the trailing `windowDays` (default 14).
203
+ *
204
+ * Algorithm (spec §6):
205
+ * • cutoff = now - windowDays * 86400_000 ms
206
+ * • injected = Σ ev.candidate_ids.length over injections with ts ≥ cutoff
207
+ * • used = count of usage rows with ts ≥ cutoff
208
+ * • precision = injected > 0 ? Math.min(1, used / injected) : null
209
+ * • per_source = { <prefix>: { injected, used } } grouped by id-prefix
210
+ *
211
+ * Invariants:
212
+ * • `precision` is never NaN; it is either a finite number in [0, 1] or null.
213
+ * • Malformed jsonl lines are tolerated (skipped by `readJsonl`).
214
+ * • Empty project → injected=0, used=0, precision=null, per_source={}.
215
+ */
216
+ export function getPrecisionKPI(project, windowDays) {
217
+ const days = typeof windowDays === "number" && Number.isFinite(windowDays) && windowDays > 0
218
+ ? windowDays
219
+ : 14;
220
+ const cutoffMs = Date.now() - days * 86400_000;
221
+ const rawInjections = readJsonl(injectionsPath(project));
222
+ const rawUsages = readJsonl(usagesPath(project));
223
+ const injections = [];
224
+ for (const r of rawInjections) {
225
+ const ev = asInjectionEvent(r);
226
+ if (!ev)
227
+ continue;
228
+ const t = Date.parse(ev.ts);
229
+ if (!Number.isFinite(t))
230
+ continue; // unparseable ts → skip
231
+ if (t < cutoffMs)
232
+ continue;
233
+ injections.push(ev);
234
+ }
235
+ const usages = [];
236
+ for (const r of rawUsages) {
237
+ const ev = asUsageEvent(r);
238
+ if (!ev)
239
+ continue;
240
+ const t = Date.parse(ev.ts);
241
+ if (!Number.isFinite(t))
242
+ continue;
243
+ if (t < cutoffMs)
244
+ continue;
245
+ usages.push(ev);
246
+ }
247
+ let injected = 0;
248
+ const perSource = {};
249
+ for (const ev of injections) {
250
+ for (const cid of ev.candidate_ids) {
251
+ injected++;
252
+ const src = sourceOf(cid);
253
+ if (!perSource[src])
254
+ perSource[src] = { injected: 0, used: 0 };
255
+ perSource[src].injected++;
256
+ }
257
+ }
258
+ const used = usages.length;
259
+ for (const ev of usages) {
260
+ const src = sourceOf(ev.candidate_id);
261
+ if (!perSource[src])
262
+ perSource[src] = { injected: 0, used: 0 };
263
+ perSource[src].used++;
264
+ }
265
+ // Clamp [0, 1]: data corruption (duplicate usages, late-arriving rows after
266
+ // injections were rotated out, etc.) can push raw used/injected above 1.0.
267
+ // Matches storage/corrections.ts:373 — "150% precision" is nonsense.
268
+ // null (not NaN) is the explicit zero-denominator case so the dashboard
269
+ // can render "—" rather than "NaN%".
270
+ const precision = injected > 0 ? Math.min(1, used / injected) : null;
271
+ return {
272
+ window_days: days,
273
+ injected,
274
+ used,
275
+ precision,
276
+ per_source: perSource,
277
+ };
278
+ }
279
+ //# sourceMappingURL=precision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"precision.js","sourceRoot":"","sources":["../../src/relevance/precision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AA0CtC,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,4FAA4F;AAC5F,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,cAAc,CAAC,OAAe;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,UAAU,CAAC,OAAe;IACjC,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAI,QAAgB;IACpC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,GAAG,GAAQ,EAAE,CAAC;IACpB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAM,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,sCAAsC;QACxC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,QAAgB,EAAE,GAAY;IACjD,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClC,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CAAC,WAAmB;IACnC,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAClF,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/B,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,CAAU;IAClC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IAC/E,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,CAAU;IAC9B,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;KAC3D,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,EAAkB;IACjE,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ;YAAE,OAAO;QAC1C,MAAM,IAAI,GAAmB;YAC3B,EAAE,EACA,OAAO,EAAE,CAAC,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC;gBAC3C,CAAC,CAAC,EAAE,CAAC,EAAE;gBACP,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,aAAa,CAAC;gBAC5C,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;gBACpE,CAAC,CAAC,EAAE;YACN,SAAS,EAAE,OAAO,EAAE,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;SAChE,CAAC;QACF,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,EAAc;IACzD,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ;YAAE,OAAO;QAC1C,MAAM,IAAI,GAAe;YACvB,EAAE,EACA,OAAO,EAAE,CAAC,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC;gBAC3C,CAAC,CAAC,EAAE,CAAC,EAAE;gBACP,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,YAAY,EAAE,OAAO,EAAE,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;YACxE,SAAS,EAAE,OAAO,EAAE,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;YAC/D,QAAQ,EAAE,OAAO,EAAE,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;SAC7D,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,YAAY;YAAE,OAAO,CAAC,6BAA6B;QAC7D,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,WAAW;IACb,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,UAAmB;IAClE,MAAM,IAAI,GACR,OAAO,UAAU,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC;QAC7E,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,EAAE,CAAC;IACT,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IAE/C,MAAM,aAAa,GAAG,SAAS,CAAU,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,SAAS,CAAU,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;IAE1D,MAAM,UAAU,GAAqB,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE;YAAE,SAAS;QAClB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS,CAAC,wBAAwB;QAC3D,IAAI,CAAC,GAAG,QAAQ;YAAE,SAAS;QAC3B,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,EAAE;YAAE,SAAS;QAClB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS;QAClC,IAAI,CAAC,GAAG,QAAQ;YAAE,SAAS;QAC3B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,MAAM,SAAS,GAAuD,EAAE,CAAC;IACzE,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,KAAK,MAAM,GAAG,IAAI,EAAE,CAAC,aAAa,EAAE,CAAC;YACnC,QAAQ,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;gBAAE,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAC/D,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3B,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/D,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,qEAAqE;IACrE,wEAAwE;IACxE,qCAAqC;IACrC,MAAM,SAAS,GACb,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAErD,OAAO;QACL,WAAW,EAAE,IAAI;QACjB,QAAQ;QACR,IAAI;QACJ,SAAS;QACT,UAAU,EAAE,SAAS;KACtB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Promoter — curates the staging buffer into the palace/journal at session
3
+ * boundaries (v3.5.0 P-A, spec §4 "Promote", §5).
4
+ *
5
+ * Flow:
6
+ * 1. Read up to N most-recent staged exchanges via `readStaging`.
7
+ * 2. Apply a pure-function durability heuristic — `isDurableSignal` — to
8
+ * drop chitchat, single-word turns, tool-only exchanges.
9
+ * 3. Dedupe survivors against the project's recent journal titles
10
+ * (`tooSimilarToExistingJournal`, threshold 0.7).
11
+ * 4. Write the survivors via the existing `journalWrite` tool, then clear
12
+ * the staging file up to the newest promoted timestamp.
13
+ *
14
+ * Design notes:
15
+ * • The heuristic is intentionally cheap and pure. It is the FIRST pass —
16
+ * the in-room model (called separately by `sessionEndReflect`) is the
17
+ * true fine-grain judge. Here we just trim the obvious noise.
18
+ * • `opts.dryRun` runs the whole pipeline without writing the journal or
19
+ * clearing staging — used by tests and the dashboard preview.
20
+ * • Threshold ordering inside `isDurableSignal` is high-confidence first
21
+ * (explicit markers) then weaker signals (length + lexical proxy)
22
+ * (worker done-def rule 3).
23
+ */
24
+ import { type StagedExchange } from "./stager.js";
25
+ export interface PromoteResult {
26
+ promoted: number;
27
+ skipped: number;
28
+ /** Counters keyed by reason for downstream dashboards and tests. */
29
+ reasons: Record<string, number>;
30
+ }
31
+ export interface PromoteOptions {
32
+ /** When true, runs the heuristic + dedup but writes nothing. */
33
+ dryRun?: boolean;
34
+ }
35
+ /**
36
+ * Decide whether a staged exchange carries a durable signal.
37
+ *
38
+ * Returns TRUE on any of:
39
+ * 1. Explicit marker (`decision`, `bug`, `gotcha`, …).
40
+ * 2. Correction shape (`no … wrong`, `actually`, `correction`, `the right way`).
41
+ * 3. Contains a wikilink or fenced code block (durable artifact).
42
+ * 4. Length ≥ 120 chars AND has both an "important word" and an action verb
43
+ * (noun-verb proxy — catches substantive sentences).
44
+ *
45
+ * Returns FALSE for chitchat ("ok", "thanks"), single-sentence questions,
46
+ * and tool-only turns (no user/assistant text).
47
+ */
48
+ export declare function isDurableSignal(e: StagedExchange): boolean;
49
+ /**
50
+ * Promote durable staged exchanges into the journal.
51
+ *
52
+ * Idempotent within a session: once promoted, the corresponding staging
53
+ * entries are cleared (only the newest-promoted timestamp is used as the
54
+ * `beforeTs` cursor, so any post-promote stage() calls survive).
55
+ *
56
+ * `opts.dryRun = true` runs the full pipeline (incl. dedup) but writes
57
+ * nothing and clears nothing. Useful for tests and dashboard previews.
58
+ */
59
+ export declare function promote(project: string, opts?: PromoteOptions): Promise<PromoteResult>;
60
+ //# sourceMappingURL=promoter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promoter.d.ts","sourceRoot":"","sources":["../../src/relevance/promoter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAoC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAmB1D;AAqFD;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,CAAC,CA+JxB"}