@hypercarrier/rarebit 0.1.0-alpha.6 → 0.2.0
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/CHANGELOG.md +13 -3
- package/README.md +72 -19
- package/RUNBOOK.md +16 -9
- package/docs/examples/rarebit.settings.json +19 -0
- package/package.json +9 -2
- package/scripts/e2e-recap-pi.mjs +456 -0
- package/scripts/e2e-settings-pi.mjs +267 -0
- package/scripts/e2e-v020-pi.mjs +269 -0
- package/scripts/verify-package.mjs +14 -8
- package/src/extension.mjs +183 -13
- package/src/lifecycle.mjs +32 -7
- package/src/rarebit-cli.mjs +2 -0
- package/src/rarebit-command.mjs +21 -1
- package/src/rarebit-core.mjs +87 -9
- package/src/rarebit-recap.mjs +422 -0
- package/src/rarebit-service.mjs +60 -7
- package/src/rarebit-settings-ui.mjs +479 -0
- package/src/rarebit-settings.mjs +90 -7
- package/src/types.d.ts +60 -3
- package/test/lifecycle-contract-independent.test.mjs +2 -0
- package/test/package-artifact-identity.test.mjs +54 -0
- package/test/rarebit-command.test.mjs +8 -4
- package/test/rarebit-recap.test.mjs +513 -0
- package/test/rarebit-settings-ui.test.mjs +388 -0
- package/test/rarebit-v020.test.mjs +294 -0
- package/test/summary-attention-independent.test.mjs +1 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { exactSelectionApplies } from "./rarebit-artifact-state.mjs";
|
|
2
|
+
import { selectRarebits } from "./rarebit-core.mjs";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
5
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
6
|
+
normalizeRarebitRecapTimezone,
|
|
7
|
+
} from "./rarebit-settings.mjs";
|
|
8
|
+
import { readRarebitCurrent } from "./rarebit-store.mjs";
|
|
9
|
+
import { rarebitSummaryPresentation } from "./rarebit-visual-language.mjs";
|
|
10
|
+
|
|
11
|
+
export const RAREBIT_RECAP_WIDGET_KEY = "rarebit-recap";
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
14
|
+
DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
15
|
+
} from "./rarebit-settings.mjs";
|
|
16
|
+
|
|
17
|
+
function sessionIdentity(ctx) {
|
|
18
|
+
return {
|
|
19
|
+
sessionId: String(
|
|
20
|
+
ctx?.sessionManager?.getHeader?.()?.id ?? ctx?.sessionId ?? "",
|
|
21
|
+
),
|
|
22
|
+
sessionFile: ctx?.sessionManager?.getSessionFile?.() ?? null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function branchSnapshot(ctx) {
|
|
27
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
28
|
+
return Array.isArray(branch) ? branch.slice() : [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function receiptAppliesToContext(receipt, ctx) {
|
|
32
|
+
if (receipt?.status !== "ok") return false;
|
|
33
|
+
const identity = sessionIdentity(ctx);
|
|
34
|
+
if (!identity.sessionId || !identity.sessionFile) return false;
|
|
35
|
+
if (receipt.sessionId !== identity.sessionId) return false;
|
|
36
|
+
const branch = branchSnapshot(ctx);
|
|
37
|
+
return exactSelectionApplies(receipt.selection, selectRarebits(branch));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function observedAtLabel(observedAt, timezone) {
|
|
41
|
+
const date = new Date(observedAt);
|
|
42
|
+
if (Number.isNaN(date.valueOf())) return String(observedAt ?? "unknown");
|
|
43
|
+
const usesHostTimezone = timezone === DEFAULT_RAREBIT_RECAP_TIMEZONE;
|
|
44
|
+
const dateOptions = {
|
|
45
|
+
dateStyle: "medium",
|
|
46
|
+
timeStyle: "short",
|
|
47
|
+
...(usesHostTimezone ? {} : { timeZone: timezone }),
|
|
48
|
+
};
|
|
49
|
+
let dateText;
|
|
50
|
+
try {
|
|
51
|
+
dateText = new Intl.DateTimeFormat(undefined, dateOptions).format(date);
|
|
52
|
+
} catch {
|
|
53
|
+
dateText = new Intl.DateTimeFormat(undefined, {
|
|
54
|
+
dateStyle: "medium",
|
|
55
|
+
timeStyle: "short",
|
|
56
|
+
}).format(date);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let zoneText;
|
|
60
|
+
try {
|
|
61
|
+
const zoneOptions = {
|
|
62
|
+
year: "numeric",
|
|
63
|
+
month: "numeric",
|
|
64
|
+
day: "numeric",
|
|
65
|
+
timeZoneName: "shortOffset",
|
|
66
|
+
...(usesHostTimezone ? {} : { timeZone: timezone }),
|
|
67
|
+
};
|
|
68
|
+
zoneText = new Intl.DateTimeFormat(undefined, zoneOptions)
|
|
69
|
+
.formatToParts(date)
|
|
70
|
+
.find((part) => part.type === "timeZoneName")?.value;
|
|
71
|
+
} catch {
|
|
72
|
+
zoneText = undefined;
|
|
73
|
+
}
|
|
74
|
+
let resolvedTimezone;
|
|
75
|
+
try {
|
|
76
|
+
resolvedTimezone = new Intl.DateTimeFormat(
|
|
77
|
+
undefined,
|
|
78
|
+
usesHostTimezone ? {} : { timeZone: timezone },
|
|
79
|
+
).resolvedOptions().timeZone;
|
|
80
|
+
} catch {
|
|
81
|
+
resolvedTimezone = undefined;
|
|
82
|
+
}
|
|
83
|
+
const zoneLabel = usesHostTimezone
|
|
84
|
+
? `host${resolvedTimezone ? `: ${resolvedTimezone}` : ""}`
|
|
85
|
+
: resolvedTimezone ?? timezone;
|
|
86
|
+
return `${dateText} (${zoneLabel}${zoneText ? `, ${zoneText}` : ""})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let recapWidgetComponents;
|
|
90
|
+
|
|
91
|
+
async function loadRecapWidgetComponents() {
|
|
92
|
+
if (!recapWidgetComponents) {
|
|
93
|
+
recapWidgetComponents = Promise.all([
|
|
94
|
+
import("@earendil-works/pi-tui"),
|
|
95
|
+
import("@earendil-works/pi-coding-agent"),
|
|
96
|
+
]).then(([tui, agent]) => ({
|
|
97
|
+
Container: tui.Container,
|
|
98
|
+
Text: tui.Text,
|
|
99
|
+
DynamicBorder: agent.DynamicBorder,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
return recapWidgetComponents;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function summaryContent(
|
|
106
|
+
receipt,
|
|
107
|
+
{ timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE } = {},
|
|
108
|
+
) {
|
|
109
|
+
const presentation = rarebitSummaryPresentation(receipt.sessionStatus);
|
|
110
|
+
const mark = presentation.mark ? `${presentation.mark} ` : "";
|
|
111
|
+
const heading = `${mark}Recap · ${presentation.label} · as of ${observedAtLabel(receipt.observedAt, timezone)}`;
|
|
112
|
+
const summary = String(receipt.summary ?? "").trim();
|
|
113
|
+
return { heading, summary };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function currentContextOptions(ctx, options) {
|
|
117
|
+
return {
|
|
118
|
+
sessionFile: sessionIdentity(ctx).sessionFile,
|
|
119
|
+
...(options?.sessionRoot ? { sessionRoot: options.sessionRoot } : {}),
|
|
120
|
+
...(options?.rarebitRoot ? { rarebitRoot: options.rarebitRoot } : {}),
|
|
121
|
+
...(options?.allowExternalSession === true
|
|
122
|
+
? { allowExternalSession: true }
|
|
123
|
+
: {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sameIdentity(left, right) {
|
|
128
|
+
return (
|
|
129
|
+
left?.sessionId === right?.sessionId &&
|
|
130
|
+
left?.sessionFile === right?.sessionFile
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Private Pi-only presentation controller. It consumes existing receipts and
|
|
136
|
+
* writes only to the interactive TUI widget surface. It never appends to a
|
|
137
|
+
* Session and never invokes a model.
|
|
138
|
+
*/
|
|
139
|
+
export function createRarebitRecapController({
|
|
140
|
+
delayMs = DEFAULT_RAREBIT_RECAP_DELAY_MS,
|
|
141
|
+
timezone = DEFAULT_RAREBIT_RECAP_TIMEZONE,
|
|
142
|
+
readCurrent = readRarebitCurrent,
|
|
143
|
+
setTimeoutFn = setTimeout,
|
|
144
|
+
clearTimeoutFn = clearTimeout,
|
|
145
|
+
} = {}) {
|
|
146
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 0)
|
|
147
|
+
throw new RangeError("Rarebit recap delay must be a nonnegative integer");
|
|
148
|
+
const recapTimezone = normalizeRarebitRecapTimezone(timezone);
|
|
149
|
+
|
|
150
|
+
let liveContext;
|
|
151
|
+
let liveLifecycleGeneration = 0;
|
|
152
|
+
let controllerGeneration = 0;
|
|
153
|
+
let timer;
|
|
154
|
+
let pending;
|
|
155
|
+
let offerEpoch = 0;
|
|
156
|
+
const displayedReceipts = new Set();
|
|
157
|
+
|
|
158
|
+
const tuiContext = (ctx = liveContext) =>
|
|
159
|
+
ctx?.mode === "tui" && typeof ctx?.ui?.setWidget === "function"
|
|
160
|
+
? ctx
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
const clearWidget = (ctx = liveContext) => {
|
|
164
|
+
const target = tuiContext(ctx);
|
|
165
|
+
if (!target) return;
|
|
166
|
+
try {
|
|
167
|
+
target.ui.setWidget(RAREBIT_RECAP_WIDGET_KEY, undefined, {
|
|
168
|
+
placement: "aboveEditor",
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
// TUI rendering is best-effort and must not affect Summary materialization.
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const cancelTimer = () => {
|
|
176
|
+
if (timer !== undefined) clearTimeoutFn(timer);
|
|
177
|
+
timer = undefined;
|
|
178
|
+
pending = undefined;
|
|
179
|
+
offerEpoch += 1;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const updateContext = (ctx, lifecycleGeneration) => {
|
|
183
|
+
if (ctx) liveContext = ctx;
|
|
184
|
+
if (Number.isSafeInteger(lifecycleGeneration))
|
|
185
|
+
liveLifecycleGeneration = lifecycleGeneration;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const invalidate = (ctx, lifecycleGeneration) => {
|
|
189
|
+
const previousContext = liveContext;
|
|
190
|
+
clearWidget(previousContext);
|
|
191
|
+
updateContext(ctx, lifecycleGeneration);
|
|
192
|
+
controllerGeneration += 1;
|
|
193
|
+
cancelTimer();
|
|
194
|
+
clearWidget(ctx);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const captureMaterialization = (ctx) => {
|
|
198
|
+
const snapshotGeneration = ctx?.rarebitLifecycleGeneration;
|
|
199
|
+
if (
|
|
200
|
+
Number.isSafeInteger(snapshotGeneration) &&
|
|
201
|
+
snapshotGeneration !== liveLifecycleGeneration
|
|
202
|
+
)
|
|
203
|
+
return null;
|
|
204
|
+
return {
|
|
205
|
+
controllerGeneration,
|
|
206
|
+
lifecycleGeneration: Number.isSafeInteger(snapshotGeneration)
|
|
207
|
+
? snapshotGeneration
|
|
208
|
+
: liveLifecycleGeneration,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const tokenIsCurrent = (token) =>
|
|
213
|
+
Boolean(
|
|
214
|
+
token &&
|
|
215
|
+
token.controllerGeneration === controllerGeneration &&
|
|
216
|
+
token.lifecycleGeneration === liveLifecycleGeneration,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const readReceipt = async (ctx, options) => {
|
|
220
|
+
const identity = sessionIdentity(ctx);
|
|
221
|
+
if (!identity.sessionFile) return null;
|
|
222
|
+
const current = await readCurrent({
|
|
223
|
+
...currentContextOptions(ctx, options),
|
|
224
|
+
});
|
|
225
|
+
const receipt = current?.receipt;
|
|
226
|
+
if (!receiptAppliesToContext(receipt, ctx)) return null;
|
|
227
|
+
const artifactState = current?.artifactState;
|
|
228
|
+
if (
|
|
229
|
+
!["request_current", "assessment_current"].includes(
|
|
230
|
+
artifactState?.syncState,
|
|
231
|
+
) ||
|
|
232
|
+
!["request_generation", "exact_selection"].includes(
|
|
233
|
+
artifactState?.applicability,
|
|
234
|
+
) ||
|
|
235
|
+
artifactState?.receiptRef?.jobId !== receipt.jobId ||
|
|
236
|
+
artifactState?.projection?.status !== receipt.sessionStatus
|
|
237
|
+
)
|
|
238
|
+
return null;
|
|
239
|
+
return receipt;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const display = async (
|
|
243
|
+
ctx,
|
|
244
|
+
receipt,
|
|
245
|
+
{ timezone = recapTimezone, token, expectedOfferEpoch } = {},
|
|
246
|
+
) => {
|
|
247
|
+
const target = tuiContext(ctx);
|
|
248
|
+
if (!target || !receiptAppliesToContext(receipt, ctx)) return false;
|
|
249
|
+
const observedControllerGeneration = controllerGeneration;
|
|
250
|
+
const observedLifecycleGeneration = liveLifecycleGeneration;
|
|
251
|
+
const observedOfferEpoch = expectedOfferEpoch ?? offerEpoch;
|
|
252
|
+
const observedIdentity = sessionIdentity(ctx);
|
|
253
|
+
try {
|
|
254
|
+
const components = await loadRecapWidgetComponents();
|
|
255
|
+
if (
|
|
256
|
+
observedControllerGeneration !== controllerGeneration ||
|
|
257
|
+
observedLifecycleGeneration !== liveLifecycleGeneration ||
|
|
258
|
+
observedOfferEpoch !== offerEpoch ||
|
|
259
|
+
(token && !tokenIsCurrent(token)) ||
|
|
260
|
+
!sameIdentity(observedIdentity, sessionIdentity(liveContext)) ||
|
|
261
|
+
!receiptAppliesToContext(receipt, liveContext)
|
|
262
|
+
)
|
|
263
|
+
return false;
|
|
264
|
+
const content = summaryContent(receipt, {
|
|
265
|
+
timezone: normalizeRarebitRecapTimezone(timezone),
|
|
266
|
+
});
|
|
267
|
+
target.ui.setWidget(
|
|
268
|
+
RAREBIT_RECAP_WIDGET_KEY,
|
|
269
|
+
(_tui, theme) => {
|
|
270
|
+
const border = (text) => theme.fg("borderMuted", text);
|
|
271
|
+
const widget = new components.Container();
|
|
272
|
+
const heading = new components.Text("", 1, 0);
|
|
273
|
+
const summary = new components.Text("", 1, 0);
|
|
274
|
+
let previousHeading;
|
|
275
|
+
let previousSummary;
|
|
276
|
+
const themed = (text, color, component, previous) => {
|
|
277
|
+
const rendered = theme.fg(color, text);
|
|
278
|
+
if (rendered !== previous.value) {
|
|
279
|
+
previous.value = rendered;
|
|
280
|
+
component.setText(rendered);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const themedHeading = {
|
|
284
|
+
render: (width) => {
|
|
285
|
+
themed(content.heading, "dim", heading, previousHeading ??= {});
|
|
286
|
+
return heading.render(width);
|
|
287
|
+
},
|
|
288
|
+
invalidate: () => {
|
|
289
|
+
previousHeading = undefined;
|
|
290
|
+
heading.invalidate();
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
const themedSummary = {
|
|
294
|
+
render: (width) => {
|
|
295
|
+
themed(content.summary, "muted", summary, previousSummary ??= {});
|
|
296
|
+
return summary.render(width);
|
|
297
|
+
},
|
|
298
|
+
invalidate: () => {
|
|
299
|
+
previousSummary = undefined;
|
|
300
|
+
summary.invalidate();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
widget.addChild(new components.DynamicBorder(border));
|
|
304
|
+
widget.addChild(themedHeading);
|
|
305
|
+
widget.addChild(themedSummary);
|
|
306
|
+
widget.addChild(new components.DynamicBorder(border));
|
|
307
|
+
return widget;
|
|
308
|
+
},
|
|
309
|
+
{ placement: "aboveEditor" },
|
|
310
|
+
);
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const showExisting = async (ctx, options = {}) => {
|
|
318
|
+
controllerGeneration += 1;
|
|
319
|
+
cancelTimer();
|
|
320
|
+
updateContext(ctx);
|
|
321
|
+
const observedGeneration = controllerGeneration;
|
|
322
|
+
const observedOfferEpoch = offerEpoch;
|
|
323
|
+
const identity = sessionIdentity(ctx);
|
|
324
|
+
const receipt = await readReceipt(ctx, options).catch(() => null);
|
|
325
|
+
if (
|
|
326
|
+
observedGeneration !== controllerGeneration ||
|
|
327
|
+
observedOfferEpoch !== offerEpoch ||
|
|
328
|
+
!sameIdentity(identity, sessionIdentity(liveContext))
|
|
329
|
+
)
|
|
330
|
+
return { shown: false, reason: "stale" };
|
|
331
|
+
if (!receipt) {
|
|
332
|
+
clearWidget(ctx);
|
|
333
|
+
return { shown: false, reason: "no_current_summary" };
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
shown: await display(ctx, receipt, {
|
|
337
|
+
timezone: options.timezone ?? options.recapTimezone ?? recapTimezone,
|
|
338
|
+
expectedOfferEpoch: observedOfferEpoch,
|
|
339
|
+
}),
|
|
340
|
+
reason: "current_summary",
|
|
341
|
+
receipt,
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const offer = ({ ctx, result, token, ...options }) => {
|
|
346
|
+
const record = result?.record;
|
|
347
|
+
if (
|
|
348
|
+
!tuiContext() ||
|
|
349
|
+
!tokenIsCurrent(token) ||
|
|
350
|
+
record?.status !== "ok" ||
|
|
351
|
+
typeof record.summary !== "string" ||
|
|
352
|
+
!record.summary.trim() ||
|
|
353
|
+
options.recapEnabled === false
|
|
354
|
+
)
|
|
355
|
+
return false;
|
|
356
|
+
const key = `${record.sessionId}:${record.branch?.pathHash ?? ""}:${record.jobId}`;
|
|
357
|
+
if (displayedReceipts.has(key) || pending?.key === key) return false;
|
|
358
|
+
cancelTimer();
|
|
359
|
+
const offeredIdentity = sessionIdentity(ctx);
|
|
360
|
+
pending = {
|
|
361
|
+
token,
|
|
362
|
+
record,
|
|
363
|
+
identity: offeredIdentity,
|
|
364
|
+
options,
|
|
365
|
+
key,
|
|
366
|
+
offerEpoch: offerEpoch + 1,
|
|
367
|
+
};
|
|
368
|
+
offerEpoch = pending.offerEpoch;
|
|
369
|
+
const fire = async () => {
|
|
370
|
+
timer = undefined;
|
|
371
|
+
const offerState = pending;
|
|
372
|
+
pending = undefined;
|
|
373
|
+
if (
|
|
374
|
+
!offerState ||
|
|
375
|
+
offerState.offerEpoch !== offerEpoch ||
|
|
376
|
+
!tokenIsCurrent(offerState.token)
|
|
377
|
+
)
|
|
378
|
+
return;
|
|
379
|
+
const current = liveContext;
|
|
380
|
+
const currentIdentity = sessionIdentity(current);
|
|
381
|
+
if (!sameIdentity(offerState.identity, currentIdentity)) return;
|
|
382
|
+
const receipt = await readReceipt(current, offerState.options).catch(
|
|
383
|
+
() => null,
|
|
384
|
+
);
|
|
385
|
+
if (
|
|
386
|
+
offerState.offerEpoch !== offerEpoch ||
|
|
387
|
+
!tokenIsCurrent(offerState.token)
|
|
388
|
+
)
|
|
389
|
+
return;
|
|
390
|
+
if (!receipt || receipt.jobId !== offerState.record.jobId) return;
|
|
391
|
+
if (await display(current, receipt, {
|
|
392
|
+
token: offerState.token,
|
|
393
|
+
expectedOfferEpoch: offerState.offerEpoch,
|
|
394
|
+
timezone:
|
|
395
|
+
offerState.options.timezone ??
|
|
396
|
+
offerState.options.recapTimezone ??
|
|
397
|
+
recapTimezone,
|
|
398
|
+
})) {
|
|
399
|
+
displayedReceipts.add(offerState.key);
|
|
400
|
+
if (displayedReceipts.size > 128)
|
|
401
|
+
displayedReceipts.delete(displayedReceipts.values().next().value);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
timer = setTimeoutFn(fire, options.recapDelayMs ?? delayMs);
|
|
405
|
+
timer?.unref?.();
|
|
406
|
+
return true;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const dispose = () => {
|
|
410
|
+
invalidate(undefined, undefined);
|
|
411
|
+
liveContext = undefined;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
captureMaterialization,
|
|
416
|
+
offer,
|
|
417
|
+
showExisting,
|
|
418
|
+
updateContext,
|
|
419
|
+
invalidate,
|
|
420
|
+
dispose,
|
|
421
|
+
};
|
|
422
|
+
}
|
package/src/rarebit-service.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
3
|
+
DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE,
|
|
3
4
|
RAREBIT_SUMMARY_WRITABLE_LIFECYCLE_BOUNDARIES,
|
|
5
|
+
RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
4
6
|
RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
5
7
|
RAREBIT_TITLE_PROMPT_VERSION,
|
|
6
8
|
composeRarebitSummaryDerivationInput,
|
|
7
9
|
composeRarebitTitlePrompt,
|
|
8
10
|
evaluateRarebitSummaryEligibility,
|
|
9
11
|
measureRarebits,
|
|
12
|
+
normalizeRarebitSummaryPrompt,
|
|
10
13
|
normalizeRarebitSummaryPolicy,
|
|
11
14
|
normalizeRarebitSummarySynthesis,
|
|
12
15
|
normalizeRarebitTitle,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
extractRarebitSynthesisReceipt,
|
|
26
29
|
resolveRarebitModelConfiguration,
|
|
27
30
|
} from "./rarebit-model.mjs";
|
|
31
|
+
import { DEFAULT_RAREBIT_MAX_INPUT_TOKENS } from "./rarebit-settings.mjs";
|
|
28
32
|
import {
|
|
29
33
|
RAREBIT_AUTOMATIC_SUMMARY_POLICY_CONTRACT,
|
|
30
34
|
automaticSummaryInhibitionIdentity,
|
|
@@ -33,7 +37,25 @@ import {
|
|
|
33
37
|
export const RAREBIT_TITLE_IMPLEMENTATION_VERSION = "hc-rarebit-title-v4";
|
|
34
38
|
export const RAREBIT_SUMMARY_IMPLEMENTATION_VERSION = "hc-rarebit-summary-v6";
|
|
35
39
|
export const RAREBIT_SUMMARY_SCHEMA_VERSION = 4;
|
|
36
|
-
export const
|
|
40
|
+
export const RAREBIT_INPUT_CHARS_PER_TOKEN = 4;
|
|
41
|
+
export const DEFAULT_RAREBIT_MAX_PROMPT_CHARS =
|
|
42
|
+
DEFAULT_RAREBIT_MAX_INPUT_TOKENS * RAREBIT_INPUT_CHARS_PER_TOKEN;
|
|
43
|
+
|
|
44
|
+
function maxPromptCharsFromConfig(config) {
|
|
45
|
+
if (config.maxPromptChars !== undefined) {
|
|
46
|
+
if (!Number.isSafeInteger(config.maxPromptChars) || config.maxPromptChars < 1)
|
|
47
|
+
throw new RangeError("maxPromptChars must be a positive safe integer");
|
|
48
|
+
return config.maxPromptChars;
|
|
49
|
+
}
|
|
50
|
+
const maxInputTokens =
|
|
51
|
+
config.maxInputTokens ?? DEFAULT_RAREBIT_MAX_INPUT_TOKENS;
|
|
52
|
+
if (!Number.isSafeInteger(maxInputTokens) || maxInputTokens < 1)
|
|
53
|
+
throw new RangeError("maxInputTokens must be a positive safe integer");
|
|
54
|
+
const maxPromptChars = maxInputTokens * RAREBIT_INPUT_CHARS_PER_TOKEN;
|
|
55
|
+
if (!Number.isSafeInteger(maxPromptChars))
|
|
56
|
+
throw new RangeError("maxInputTokens exceeds the supported prompt limit");
|
|
57
|
+
return maxPromptChars;
|
|
58
|
+
}
|
|
37
59
|
|
|
38
60
|
function sessionFileFrom(ctx) {
|
|
39
61
|
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
@@ -97,12 +119,34 @@ function machineModelProvenance(value) {
|
|
|
97
119
|
};
|
|
98
120
|
}
|
|
99
121
|
|
|
122
|
+
function summaryCacheSessionId(sessionId, promptVersion, summaryPrompt) {
|
|
123
|
+
const usesBuiltinContract =
|
|
124
|
+
promptVersion === RAREBIT_SUMMARY_PROMPT_VERSION &&
|
|
125
|
+
summaryPrompt.guidance === DEFAULT_RAREBIT_SUMMARY_PROMPT_GUIDANCE;
|
|
126
|
+
if (usesBuiltinContract) return sessionId;
|
|
127
|
+
const promptIdentity =
|
|
128
|
+
summaryPrompt.promptIdentity ??
|
|
129
|
+
sha256({
|
|
130
|
+
version: RAREBIT_SUMMARY_PROMPT_IDENTITY_VERSION,
|
|
131
|
+
guidance: summaryPrompt.guidance,
|
|
132
|
+
});
|
|
133
|
+
return sha256({
|
|
134
|
+
version: "rarebit-summary-cache-v1",
|
|
135
|
+
sessionId,
|
|
136
|
+
promptVersion,
|
|
137
|
+
promptIdentity,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
100
141
|
/**
|
|
101
142
|
* Shared imperative summary shell for Pi and CLI adapters. The functional core
|
|
102
143
|
* decides selection, measurement, policy, prompts, and identity; this shell
|
|
103
144
|
* owns only model invocation and append-only derived-record persistence.
|
|
104
145
|
*/
|
|
105
146
|
export async function processRarebitSummary(ctx, config = {}) {
|
|
147
|
+
// Resolve before model selection or job reservation. Invalid prompt config
|
|
148
|
+
// must fail closed without a provider call or a misleading receipt.
|
|
149
|
+
const summaryPrompt = normalizeRarebitSummaryPrompt(config.summaryPrompt);
|
|
106
150
|
const branch = ctx?.sessionManager?.getBranch?.() ?? config.branch ?? [];
|
|
107
151
|
const sessionFile = config.sessionFile ?? sessionFileFrom(ctx);
|
|
108
152
|
const sessionId = config.sessionId ?? sessionIdFrom(ctx);
|
|
@@ -165,10 +209,13 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
165
209
|
provenance: { source: "not_required", status: "not_required" },
|
|
166
210
|
};
|
|
167
211
|
const promptVersion = config.promptVersion ?? RAREBIT_SUMMARY_PROMPT_VERSION;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
212
|
+
// Keep the receipt self-describing without persisting the user guidance.
|
|
213
|
+
// The digest is already part of the job identity and is safe to expose as
|
|
214
|
+
// provenance beside the stable prompt contract version.
|
|
215
|
+
const receiptPromptVersion = summaryPrompt.promptIdentity
|
|
216
|
+
? `${promptVersion}:${summaryPrompt.promptIdentity}`
|
|
217
|
+
: promptVersion;
|
|
218
|
+
const maxPromptChars = maxPromptCharsFromConfig(config);
|
|
172
219
|
const inputCoveragePolicy = {
|
|
173
220
|
strategy: "newest_suffix_with_explicit_omission",
|
|
174
221
|
maxPromptChars,
|
|
@@ -183,6 +230,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
183
230
|
inputPolicy: inputCoveragePolicy,
|
|
184
231
|
lifecycleBoundary,
|
|
185
232
|
promptVersion,
|
|
233
|
+
promptIdentity: summaryPrompt.promptIdentity,
|
|
186
234
|
model: modelResolution.model,
|
|
187
235
|
});
|
|
188
236
|
const jobId = inhibited
|
|
@@ -212,7 +260,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
212
260
|
selection: machineSelection(selection),
|
|
213
261
|
model: modelResolution.model,
|
|
214
262
|
modelProvenance: machineModelProvenance(modelResolution.provenance),
|
|
215
|
-
promptVersion,
|
|
263
|
+
promptVersion: receiptPromptVersion,
|
|
216
264
|
...(inhibited ? { automaticSummaryPolicy } : {}),
|
|
217
265
|
};
|
|
218
266
|
if (!sessionFile) {
|
|
@@ -273,6 +321,7 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
273
321
|
}
|
|
274
322
|
const derivationInput = composeRarebitSummaryDerivationInput(selection, {
|
|
275
323
|
promptVersion,
|
|
324
|
+
summaryPrompt: config.summaryPrompt,
|
|
276
325
|
lifecycleBoundary,
|
|
277
326
|
maxPromptChars,
|
|
278
327
|
});
|
|
@@ -312,7 +361,11 @@ export async function processRarebitSummary(ctx, config = {}) {
|
|
|
312
361
|
const response = await client.complete({
|
|
313
362
|
prompt,
|
|
314
363
|
model: modelResolution.model,
|
|
315
|
-
cacheSessionId:
|
|
364
|
+
cacheSessionId: summaryCacheSessionId(
|
|
365
|
+
sessionId,
|
|
366
|
+
promptVersion,
|
|
367
|
+
summaryPrompt,
|
|
368
|
+
),
|
|
316
369
|
});
|
|
317
370
|
const synthesisResult = normalizeRarebitSummarySynthesis(
|
|
318
371
|
extractModelText(response),
|