@hypercarrier/rarebit 0.1.0-alpha.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/AGENTS.md +14 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/RUNBOOK.md +36 -0
- package/SECURITY.md +11 -0
- package/VISUAL-LANGUAGE.md +89 -0
- package/bin/rarebit.mjs +20 -0
- package/package.json +96 -0
- package/schemas/rarebit.schema.json +618 -0
- package/scripts/e2e-recall-pi083.mjs +199 -0
- package/scripts/e2e-summary-cache.mjs +257 -0
- package/scripts/verify-package.mjs +81 -0
- package/src/automatic-summary-policy.mjs +210 -0
- package/src/extension.mjs +516 -0
- package/src/herdr-activity.mjs +111 -0
- package/src/index.mjs +13 -0
- package/src/lifecycle.mjs +322 -0
- package/src/rarebit-activity.mjs +48 -0
- package/src/rarebit-artifact-state.mjs +839 -0
- package/src/rarebit-cli.mjs +305 -0
- package/src/rarebit-command.mjs +281 -0
- package/src/rarebit-core.mjs +525 -0
- package/src/rarebit-model.mjs +226 -0
- package/src/rarebit-recall.mjs +182 -0
- package/src/rarebit-service.mjs +579 -0
- package/src/rarebit-session.mjs +227 -0
- package/src/rarebit-settings.mjs +79 -0
- package/src/rarebit-store.mjs +771 -0
- package/src/rarebit-visual-language.mjs +110 -0
- package/src/types.d.ts +601 -0
- package/test/automatic-summary-policy.test.mjs +476 -0
- package/test/extension-activity.test.mjs +73 -0
- package/test/fixtures/branch-mixed.json +117 -0
- package/test/fixtures/conformance-corpus.v1.json +306 -0
- package/test/fixtures/truncated-mixed.jsonl +2 -0
- package/test/herdr-activity.test.mjs +93 -0
- package/test/lifecycle-contract-independent.test.mjs +627 -0
- package/test/rarebit-activity.test.mjs +82 -0
- package/test/rarebit-artifact-state.test.mjs +440 -0
- package/test/rarebit-cli.test.mjs +222 -0
- package/test/rarebit-command.test.mjs +127 -0
- package/test/rarebit-core.test.mjs +461 -0
- package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
- package/test/rarebit-independent-contract.test.mjs +317 -0
- package/test/rarebit-recall.test.mjs +434 -0
- package/test/rarebit-sidecar-protocol.test.mjs +538 -0
- package/test/rarebit-visual-language.test.mjs +75 -0
- package/test/summary-attention-independent.test.mjs +172 -0
- package/test/types-history.ts +13 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getRarebitArgumentCompletions,
|
|
6
|
+
parseRarebitCommand,
|
|
7
|
+
rarebitCommandDescription,
|
|
8
|
+
rarebitCommandUsage,
|
|
9
|
+
} from "../src/rarebit-command.mjs";
|
|
10
|
+
|
|
11
|
+
const values = (prefix) =>
|
|
12
|
+
getRarebitArgumentCompletions(prefix)?.map(({ value }) => value) ?? null;
|
|
13
|
+
|
|
14
|
+
test("one grammar derives command description and usage", () => {
|
|
15
|
+
assert.equal(
|
|
16
|
+
rarebitCommandDescription(),
|
|
17
|
+
"Rarebit status/recall/config/auto-title/title/summarize",
|
|
18
|
+
);
|
|
19
|
+
assert.equal(
|
|
20
|
+
rarebitCommandUsage(),
|
|
21
|
+
"Usage: /rarebit [status|recall|config|auto-title|title|summarize]",
|
|
22
|
+
);
|
|
23
|
+
assert.equal(
|
|
24
|
+
rarebitCommandUsage("config"),
|
|
25
|
+
"Usage: /rarebit config [max_rarebit_ratio <0..1>|min_total_length <nonnegative estimated tokens>]",
|
|
26
|
+
);
|
|
27
|
+
assert.equal(
|
|
28
|
+
rarebitCommandUsage("auto-title"),
|
|
29
|
+
"Usage: /rarebit auto-title [on|off]",
|
|
30
|
+
);
|
|
31
|
+
assert.equal(
|
|
32
|
+
rarebitCommandUsage("title"),
|
|
33
|
+
"Usage: /rarebit title (use Pi's native /name for a literal title)",
|
|
34
|
+
);
|
|
35
|
+
assert.equal(
|
|
36
|
+
rarebitCommandUsage("recall"),
|
|
37
|
+
"Usage: /rarebit recall <prompt...>",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("parser accepts exactly the command grammar", () => {
|
|
42
|
+
assert.deepEqual(parseRarebitCommand(""), {
|
|
43
|
+
ok: true,
|
|
44
|
+
subcommand: "status",
|
|
45
|
+
arguments: [],
|
|
46
|
+
});
|
|
47
|
+
assert.equal(parseRarebitCommand("config").ok, true);
|
|
48
|
+
assert.equal(parseRarebitCommand("config max_rarebit_ratio 0.4").ok, true);
|
|
49
|
+
assert.equal(parseRarebitCommand("config min_total_length 80000").ok, true);
|
|
50
|
+
assert.equal(parseRarebitCommand("auto-title on").ok, true);
|
|
51
|
+
assert.equal(parseRarebitCommand("auto-title off").ok, true);
|
|
52
|
+
assert.equal(parseRarebitCommand("title").ok, true);
|
|
53
|
+
assert.equal(parseRarebitCommand("summarize").ok, true);
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
parseRarebitCommand(
|
|
56
|
+
"recall Review the evidence, then continue exactly. ",
|
|
57
|
+
),
|
|
58
|
+
{
|
|
59
|
+
ok: true,
|
|
60
|
+
subcommand: "recall",
|
|
61
|
+
arguments: ["Review the evidence, then continue exactly. "],
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
assert.equal(parseRarebitCommand("unknown").error, "unknown_subcommand");
|
|
66
|
+
assert.equal(parseRarebitCommand("status now").error, "invalid_arguments");
|
|
67
|
+
assert.equal(
|
|
68
|
+
parseRarebitCommand("config max_rarebit_ratio 2").error,
|
|
69
|
+
"invalid_value",
|
|
70
|
+
);
|
|
71
|
+
assert.equal(
|
|
72
|
+
parseRarebitCommand("config min_total_length -1").error,
|
|
73
|
+
"invalid_value",
|
|
74
|
+
);
|
|
75
|
+
assert.equal(
|
|
76
|
+
parseRarebitCommand("auto-title maybe").error,
|
|
77
|
+
"invalid_arguments",
|
|
78
|
+
);
|
|
79
|
+
assert.equal(parseRarebitCommand("title literal").error, "invalid_arguments");
|
|
80
|
+
assert.equal(
|
|
81
|
+
parseRarebitCommand("summarize again").error,
|
|
82
|
+
"invalid_arguments",
|
|
83
|
+
);
|
|
84
|
+
assert.equal(parseRarebitCommand("recall").error, "invalid_arguments");
|
|
85
|
+
assert.equal(parseRarebitCommand("recall ").error, "invalid_arguments");
|
|
86
|
+
assert.equal(
|
|
87
|
+
parseRarebitCommand("dump messages legacy grammar").error,
|
|
88
|
+
"unknown_subcommand",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("autocomplete covers subcommands and discrete nested arguments", () => {
|
|
93
|
+
assert.deepEqual(values(""), [
|
|
94
|
+
"status",
|
|
95
|
+
"recall",
|
|
96
|
+
"config",
|
|
97
|
+
"auto-title",
|
|
98
|
+
"title",
|
|
99
|
+
"summarize",
|
|
100
|
+
]);
|
|
101
|
+
assert.deepEqual(values("s"), ["status", "summarize"]);
|
|
102
|
+
assert.deepEqual(values("config "), [
|
|
103
|
+
"config max_rarebit_ratio ",
|
|
104
|
+
"config min_total_length ",
|
|
105
|
+
]);
|
|
106
|
+
assert.deepEqual(values("config m"), [
|
|
107
|
+
"config max_rarebit_ratio ",
|
|
108
|
+
"config min_total_length ",
|
|
109
|
+
]);
|
|
110
|
+
assert.deepEqual(values("auto-title "), ["auto-title on", "auto-title off"]);
|
|
111
|
+
assert.deepEqual(values("auto-title o"), ["auto-title on", "auto-title off"]);
|
|
112
|
+
assert.equal(values("recall "), null);
|
|
113
|
+
assert.equal(values("recall p"), null);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("autocomplete stops at free-form and terminal arguments", () => {
|
|
117
|
+
assert.equal(values("config max_rarebit_ratio "), null);
|
|
118
|
+
assert.equal(values("config max_rarebit_ratio 0.4"), null);
|
|
119
|
+
assert.equal(values("config max_rarebit_ratio 0.4 "), null);
|
|
120
|
+
assert.equal(values("auto-title on "), null);
|
|
121
|
+
assert.equal(values("status "), null);
|
|
122
|
+
assert.equal(values("title "), null);
|
|
123
|
+
assert.equal(values("summarize "), null);
|
|
124
|
+
assert.equal(values("unknown "), null);
|
|
125
|
+
assert.equal(values("recall "), null);
|
|
126
|
+
assert.equal(values("recall long free-form prompt"), null);
|
|
127
|
+
});
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import Ajv from "ajv";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
10
|
+
RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
11
|
+
composeRarebitSummaryDerivationInput,
|
|
12
|
+
composeRarebitSummaryPrompt,
|
|
13
|
+
composeRarebitTitlePrompt,
|
|
14
|
+
evaluateRarebitSummaryEligibility,
|
|
15
|
+
measureRarebits,
|
|
16
|
+
normalizeRarebitSummarySynthesis,
|
|
17
|
+
rarebitJobIdentity,
|
|
18
|
+
selectRarebits,
|
|
19
|
+
titleWithDatePrefix,
|
|
20
|
+
} from "../src/rarebit-core.mjs";
|
|
21
|
+
import { createPiRarebitModelClient } from "../src/rarebit-model.mjs";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_RAREBIT_MAX_PROMPT_CHARS,
|
|
24
|
+
processRarebitSummary,
|
|
25
|
+
processRarebitTitle,
|
|
26
|
+
} from "../src/rarebit-service.mjs";
|
|
27
|
+
|
|
28
|
+
const entry = (id, message) => ({
|
|
29
|
+
type: "message",
|
|
30
|
+
id,
|
|
31
|
+
timestamp: "2026-07-16T09:00:00.000Z",
|
|
32
|
+
message,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Rarebit selection remains sparse evidence while measurement covers all readable message prose", () => {
|
|
36
|
+
const branch = [
|
|
37
|
+
entry("u", { role: "user", content: [{ type: "text", text: "goal" }] }),
|
|
38
|
+
entry("tool", {
|
|
39
|
+
role: "toolResult",
|
|
40
|
+
content: [{ type: "text", text: "abcdefgh" }],
|
|
41
|
+
}),
|
|
42
|
+
entry("continuation", {
|
|
43
|
+
role: "assistant",
|
|
44
|
+
stopReason: "toolUse",
|
|
45
|
+
content: [{ type: "text", text: "work" }],
|
|
46
|
+
}),
|
|
47
|
+
entry("final", {
|
|
48
|
+
role: "assistant",
|
|
49
|
+
stopReason: "stop",
|
|
50
|
+
content: [{ type: "text", text: "answer" }],
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
const selection = selectRarebits(branch);
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
selection.occurrences.map(({ sourceEntryId, outcome, text }) => ({
|
|
56
|
+
sourceEntryId,
|
|
57
|
+
outcome,
|
|
58
|
+
text,
|
|
59
|
+
})),
|
|
60
|
+
[
|
|
61
|
+
{ sourceEntryId: "u", outcome: "user", text: "goal" },
|
|
62
|
+
{ sourceEntryId: "continuation", outcome: "continuation", text: "work" },
|
|
63
|
+
{ sourceEntryId: "final", outcome: "stop", text: "answer" },
|
|
64
|
+
],
|
|
65
|
+
);
|
|
66
|
+
const measurement = measureRarebits(branch, selection);
|
|
67
|
+
assert.equal(measurement.totalMessageProseChars, 22);
|
|
68
|
+
assert.equal(measurement.rarebitChars, 14);
|
|
69
|
+
assert.equal(measurement.estimatedTotalTokens, 6);
|
|
70
|
+
assert.equal(measurement.estimatedRarebitTokens, 4);
|
|
71
|
+
assert.equal(measurement.rarebitRatio, 14 / 22);
|
|
72
|
+
assert.equal(measurement.estimateMethod, "ceil(chars_div_4)");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("summary eligibility records a named estimated-token policy and exact threshold behavior", () => {
|
|
76
|
+
const branch = [entry("u", { role: "user", content: "abcd" })];
|
|
77
|
+
const selection = selectRarebits(branch);
|
|
78
|
+
const measurement = measureRarebits(branch, selection);
|
|
79
|
+
const atBoundary = evaluateRarebitSummaryEligibility(measurement, {
|
|
80
|
+
...DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
81
|
+
minTotalLength: 1,
|
|
82
|
+
maxRarebitRatio: 1,
|
|
83
|
+
});
|
|
84
|
+
assert.equal(atBoundary.eligible, true);
|
|
85
|
+
const overShare = evaluateRarebitSummaryEligibility(measurement, {
|
|
86
|
+
...DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
87
|
+
minTotalLength: 1,
|
|
88
|
+
maxRarebitRatio: 0.999,
|
|
89
|
+
});
|
|
90
|
+
assert.deepEqual(overShare.reasons, ["rarebit_ratio_above_maximum"]);
|
|
91
|
+
const below = evaluateRarebitSummaryEligibility(
|
|
92
|
+
measurement,
|
|
93
|
+
DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
94
|
+
);
|
|
95
|
+
assert.ok(below.reasons.includes("total_length_below_minimum"));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("prompts contain semantic prose only and title is a separate date-prefixed proposal", () => {
|
|
99
|
+
const selection = selectRarebits([
|
|
100
|
+
entry("u", {
|
|
101
|
+
role: "user",
|
|
102
|
+
content: "Investigate a strange latency regression",
|
|
103
|
+
}),
|
|
104
|
+
entry("stop", {
|
|
105
|
+
role: "assistant",
|
|
106
|
+
stopReason: "stop",
|
|
107
|
+
content: "Initial inspection complete",
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
const summaryPrompt = composeRarebitSummaryPrompt(selection);
|
|
111
|
+
assert.match(summaryPrompt, /Investigate a strange latency regression/);
|
|
112
|
+
assert.doesNotMatch(summaryPrompt, /sourceEntryId|contentHash|manifestHash/);
|
|
113
|
+
assert.match(
|
|
114
|
+
summaryPrompt,
|
|
115
|
+
/Tool-call inputs, tool results.*deliberately absent/i,
|
|
116
|
+
);
|
|
117
|
+
assert.match(
|
|
118
|
+
summaryPrompt,
|
|
119
|
+
/Absence of a tool transcript.*never.*work was not performed/i,
|
|
120
|
+
);
|
|
121
|
+
assert.match(summaryPrompt, /concise 'done'.*appear accomplished/i);
|
|
122
|
+
assert.match(summaryPrompt, /I will run tests next/i);
|
|
123
|
+
const titlePrompt = composeRarebitTitlePrompt(selection);
|
|
124
|
+
assert.match(titlePrompt, /Investigate a strange latency regression/);
|
|
125
|
+
assert.doesNotMatch(titlePrompt, /Initial inspection complete/);
|
|
126
|
+
assert.equal(
|
|
127
|
+
titleWithDatePrefix("Title: Latency regression investigation", {
|
|
128
|
+
date: "2026-07-16",
|
|
129
|
+
}),
|
|
130
|
+
"20260716-Latency regression investigation",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("bounded Summary input retains newest evidence and declares every omission", () => {
|
|
135
|
+
const selection = selectRarebits([
|
|
136
|
+
entry("old", { role: "user", content: `OLDEST-${"a".repeat(1_200)}` }),
|
|
137
|
+
entry("middle", {
|
|
138
|
+
role: "assistant",
|
|
139
|
+
stopReason: "toolUse",
|
|
140
|
+
content: `MIDDLE-${"b".repeat(1_200)}`,
|
|
141
|
+
}),
|
|
142
|
+
entry("new", {
|
|
143
|
+
role: "assistant",
|
|
144
|
+
stopReason: "stop",
|
|
145
|
+
content: "NEWEST-EVIDENCE",
|
|
146
|
+
}),
|
|
147
|
+
]);
|
|
148
|
+
const input = composeRarebitSummaryDerivationInput(selection, {
|
|
149
|
+
maxPromptChars: 3_200,
|
|
150
|
+
lifecycleBoundary: "agent_settled",
|
|
151
|
+
});
|
|
152
|
+
assert.ok(input.prompt.length <= 3_200);
|
|
153
|
+
assert.equal(input.coverage.totalMessageCount, 3);
|
|
154
|
+
assert.ok(input.coverage.omittedMessageCount > 0);
|
|
155
|
+
assert.equal(input.coverage.complete, false);
|
|
156
|
+
assert.match(input.prompt, /messages before are trimmed/);
|
|
157
|
+
assert.match(input.prompt, /NEWEST-EVIDENCE/);
|
|
158
|
+
assert.doesNotMatch(input.prompt, /OLDEST-/);
|
|
159
|
+
assert.match(input.prompt, /do not infer what was trimmed/i);
|
|
160
|
+
assert.equal(DEFAULT_RAREBIT_MAX_PROMPT_CHARS, 256_000);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("one oversized newest message is tail-bounded with an explicit character omission", () => {
|
|
164
|
+
const selection = selectRarebits([
|
|
165
|
+
entry("huge", {
|
|
166
|
+
role: "user",
|
|
167
|
+
content: `DISCARDED-PREFIX-${"x".repeat(20_000)}-RETAINED-SUFFIX`,
|
|
168
|
+
}),
|
|
169
|
+
]);
|
|
170
|
+
const input = composeRarebitSummaryDerivationInput(selection, {
|
|
171
|
+
maxPromptChars: 4_000,
|
|
172
|
+
});
|
|
173
|
+
assert.ok(input.prompt.length <= 4_000);
|
|
174
|
+
assert.equal(input.coverage.omittedMessageCount, 0);
|
|
175
|
+
assert.ok(input.coverage.omittedTextChars > 0);
|
|
176
|
+
assert.match(input.prompt, /leading characters.*trimmed/i);
|
|
177
|
+
assert.match(input.prompt, /RETAINED-SUFFIX/);
|
|
178
|
+
assert.doesNotMatch(input.prompt, /DISCARDED-PREFIX/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("Summary normalization accepts explicit uncertainty and conflicting evidence", () => {
|
|
182
|
+
for (const statusReason of ["uncertain", "conflicting_evidence"]) {
|
|
183
|
+
assert.deepEqual(
|
|
184
|
+
normalizeRarebitSummarySynthesis(
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
summary: "The supplied evidence does not support a reliable account.",
|
|
187
|
+
sessionStatus: "needs_attention",
|
|
188
|
+
statusReason,
|
|
189
|
+
}),
|
|
190
|
+
),
|
|
191
|
+
{
|
|
192
|
+
summary: "The supplied evidence does not support a reliable account.",
|
|
193
|
+
sessionStatus: "needs_attention",
|
|
194
|
+
statusReason,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("Pi Summary model calls forward the stable Session cache identity", async () => {
|
|
201
|
+
let receivedOptions;
|
|
202
|
+
const client = await createPiRarebitModelClient(
|
|
203
|
+
{
|
|
204
|
+
modelRegistry: {
|
|
205
|
+
find: () => ({ provider: "test", id: "cache-model" }),
|
|
206
|
+
getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "test-key" }),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
model: { provider: "test", id: "cache-model" },
|
|
211
|
+
piAi: {
|
|
212
|
+
complete: async (_model, _context, options) => {
|
|
213
|
+
receivedOptions = options;
|
|
214
|
+
return { text: "ok" };
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
await client.complete({ prompt: "prompt", cacheSessionId: "session-cache-1" });
|
|
220
|
+
assert.equal(receivedOptions.sessionId, "session-cache-1");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("job identity varies with an operation's semantic inputs, not raw source path", () => {
|
|
224
|
+
const selection = selectRarebits([
|
|
225
|
+
entry("u", { role: "user", content: "goal" }),
|
|
226
|
+
]);
|
|
227
|
+
const base = {
|
|
228
|
+
operation: "summary",
|
|
229
|
+
sessionId: "session-1",
|
|
230
|
+
branch: { leafId: "u" },
|
|
231
|
+
selection,
|
|
232
|
+
policy: { ...DEFAULT_RAREBIT_SUMMARY_POLICY, minTotalLength: 2 },
|
|
233
|
+
promptVersion: "v1",
|
|
234
|
+
model: { id: "cheap" },
|
|
235
|
+
};
|
|
236
|
+
assert.equal(rarebitJobIdentity(base), rarebitJobIdentity({ ...base }));
|
|
237
|
+
assert.notEqual(
|
|
238
|
+
rarebitJobIdentity({ ...base, promptVersion: "rarebit-summary-future" }),
|
|
239
|
+
rarebitJobIdentity({
|
|
240
|
+
...base,
|
|
241
|
+
promptVersion: RAREBIT_SUMMARY_PROMPT_VERSION,
|
|
242
|
+
}),
|
|
243
|
+
"a prompt-contract revision must materialize as a distinct job identity",
|
|
244
|
+
);
|
|
245
|
+
assert.notEqual(
|
|
246
|
+
rarebitJobIdentity(base),
|
|
247
|
+
rarebitJobIdentity({ ...base, operation: "title", policy: null }),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("shared imperative summary service persists a derived receipt without raw selected prose", async () => {
|
|
252
|
+
const root = await mkdtemp(join(tmpdir(), "hc-rarebit-service-"));
|
|
253
|
+
const sessionRoot = join(root, "sessions");
|
|
254
|
+
const sessionDir = join(sessionRoot, "project");
|
|
255
|
+
await mkdir(sessionDir, { recursive: true });
|
|
256
|
+
const sessionFile = join(sessionDir, "alpha.jsonl");
|
|
257
|
+
await writeFile(sessionFile, "{}\n");
|
|
258
|
+
const branch = [entry("u", { role: "user", content: "owner context" })];
|
|
259
|
+
const ctx = {
|
|
260
|
+
sessionManager: {
|
|
261
|
+
getHeader: () => ({ id: "alpha" }),
|
|
262
|
+
getSessionFile: () => sessionFile,
|
|
263
|
+
getBranch: () => branch,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
const result = await processRarebitSummary(ctx, {
|
|
267
|
+
sessionRoot,
|
|
268
|
+
rarebitRoot: join(root, "rarebit"),
|
|
269
|
+
forceSynthesis: true,
|
|
270
|
+
summaryPolicy: { minTotalLength: 999_999 },
|
|
271
|
+
model: { provider: "test", id: "cheap" },
|
|
272
|
+
modelClient: {
|
|
273
|
+
complete: async () => ({
|
|
274
|
+
provider: "test",
|
|
275
|
+
model: "cheap",
|
|
276
|
+
usage: { input: 3, output: 2, totalTokens: 5 },
|
|
277
|
+
text: JSON.stringify({
|
|
278
|
+
summary:
|
|
279
|
+
"Progress: inspected | Findings: context | Questions/Requests: None stated | Next step: review",
|
|
280
|
+
sessionStatus: "finished",
|
|
281
|
+
statusReason: "all_requests_accomplished",
|
|
282
|
+
}),
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
assert.equal(result.record.status, "ok");
|
|
287
|
+
assert.equal("eligibility" in result.record, false);
|
|
288
|
+
assert.equal(
|
|
289
|
+
result.record.inputCoveragePolicy.strategy,
|
|
290
|
+
"newest_suffix_with_explicit_omission",
|
|
291
|
+
);
|
|
292
|
+
assert.equal(result.record.inputCoveragePolicy.maxPromptChars, 256_000);
|
|
293
|
+
assert.deepEqual(result.record.inputCoverage, {
|
|
294
|
+
totalMessageCount: 1,
|
|
295
|
+
includedMessageCount: 1,
|
|
296
|
+
omittedMessageCount: 0,
|
|
297
|
+
omittedTextChars: 0,
|
|
298
|
+
promptChars: result.record.inputCoverage.promptChars,
|
|
299
|
+
complete: true,
|
|
300
|
+
});
|
|
301
|
+
assert.equal(result.record.synthesis.usage.inputTokens, 3);
|
|
302
|
+
assert.equal(result.record.synthesis.usage.outputTokens, 2);
|
|
303
|
+
assert.equal(JSON.stringify(result.record).includes("owner context"), false);
|
|
304
|
+
const persisted = await readFile(result.record.path, "utf8");
|
|
305
|
+
assert.equal(persisted.includes("owner context"), false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("new Summary derivation rejects the retired session_start boundary", async () => {
|
|
309
|
+
await assert.rejects(
|
|
310
|
+
processRarebitSummary(undefined, { lifecycleBoundary: "session_start" }),
|
|
311
|
+
/Unsupported Summary lifecycle boundary/,
|
|
312
|
+
);
|
|
313
|
+
assert.throws(
|
|
314
|
+
() =>
|
|
315
|
+
composeRarebitSummaryPrompt(selectRarebits([]), {
|
|
316
|
+
lifecycleBoundary: "session_start",
|
|
317
|
+
}),
|
|
318
|
+
/Unsupported Summary lifecycle boundary/,
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("writer-aligned JSON Schema accepts every persisted Rarebit terminal status", async () => {
|
|
323
|
+
const schema = JSON.parse(
|
|
324
|
+
readFileSync(
|
|
325
|
+
new URL("../schemas/rarebit.schema.json", import.meta.url),
|
|
326
|
+
"utf8",
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
const validate = new Ajv({ allErrors: true }).compile(schema);
|
|
330
|
+
const root = await mkdtemp(join(tmpdir(), "hc-rarebit-schema-"));
|
|
331
|
+
const sessionRoot = join(root, "sessions");
|
|
332
|
+
const rarebitRoot = join(root, "rarebit");
|
|
333
|
+
const branch = [entry("u", { role: "user", content: "schema evidence" })];
|
|
334
|
+
const run = async (name, config) => {
|
|
335
|
+
const dir = join(sessionRoot, name);
|
|
336
|
+
await mkdir(dir, { recursive: true });
|
|
337
|
+
const sessionFile = join(dir, "session.jsonl");
|
|
338
|
+
await writeFile(sessionFile, "{}\n");
|
|
339
|
+
const result = await processRarebitSummary(
|
|
340
|
+
{
|
|
341
|
+
sessionManager: {
|
|
342
|
+
getHeader: () => ({ id: name }),
|
|
343
|
+
getSessionFile: () => sessionFile,
|
|
344
|
+
getBranch: () => branch,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{ sessionRoot, rarebitRoot, ...config },
|
|
348
|
+
);
|
|
349
|
+
const persisted = (await readFile(result.record.path, "utf8"))
|
|
350
|
+
.trim()
|
|
351
|
+
.split("\n")
|
|
352
|
+
.map(JSON.parse)
|
|
353
|
+
.find((record) => record.type === "rarebit_summary");
|
|
354
|
+
assert.equal(
|
|
355
|
+
validate(persisted),
|
|
356
|
+
true,
|
|
357
|
+
`${name}: ${JSON.stringify(validate.errors)}`,
|
|
358
|
+
);
|
|
359
|
+
return persisted;
|
|
360
|
+
};
|
|
361
|
+
const ineligible = await run("ineligible", {});
|
|
362
|
+
assert.equal(ineligible.status, "ineligible");
|
|
363
|
+
const policyObservedAt = new Date();
|
|
364
|
+
const inhibited = await run("inhibited", {
|
|
365
|
+
summaryPolicy: { minTotalLength: 0, maxRarebitRatio: 1 },
|
|
366
|
+
queryAutomaticSummaryPolicy: async () => ({
|
|
367
|
+
contractVersion: "rarebit-automatic-summary-policy/1",
|
|
368
|
+
queryId: "schema-query",
|
|
369
|
+
decision: "inhibit",
|
|
370
|
+
queryStatus: "inhibited",
|
|
371
|
+
provider: "test-policy",
|
|
372
|
+
reason: "exact_binding",
|
|
373
|
+
observedAt: policyObservedAt.toISOString(),
|
|
374
|
+
validUntil: new Date(policyObservedAt.getTime() + 1_000).toISOString(),
|
|
375
|
+
queriedAt: policyObservedAt.toISOString(),
|
|
376
|
+
provenance: {
|
|
377
|
+
identity: "opaque-identity",
|
|
378
|
+
generation: "opaque-generation",
|
|
379
|
+
association: "opaque-association",
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
assert.equal(inhibited.status, "inhibited");
|
|
384
|
+
const failure = await run("failure", { forceSynthesis: true });
|
|
385
|
+
assert.equal(failure.status, "failure");
|
|
386
|
+
const overflow = await run("overflow", {
|
|
387
|
+
forceSynthesis: true,
|
|
388
|
+
model: { provider: "test", id: "cheap" },
|
|
389
|
+
maxPromptChars: 1,
|
|
390
|
+
});
|
|
391
|
+
assert.equal(overflow.status, "unavailable_overflow");
|
|
392
|
+
const ok = await run("ok", {
|
|
393
|
+
forceSynthesis: true,
|
|
394
|
+
model: { provider: "test", id: "cheap" },
|
|
395
|
+
modelClient: {
|
|
396
|
+
complete: async () => ({
|
|
397
|
+
text: JSON.stringify({
|
|
398
|
+
summary:
|
|
399
|
+
"Progress: p | Findings: f | Questions/Requests: None stated | Next step: n",
|
|
400
|
+
sessionStatus: "finished",
|
|
401
|
+
statusReason: "all_requests_accomplished",
|
|
402
|
+
}),
|
|
403
|
+
}),
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
assert.equal(ok.status, "ok");
|
|
407
|
+
|
|
408
|
+
for (const status of [
|
|
409
|
+
"proposal",
|
|
410
|
+
"applied",
|
|
411
|
+
"skipped_session_changed",
|
|
412
|
+
"skipped_title_changed",
|
|
413
|
+
"skipped_existing_title",
|
|
414
|
+
"failure",
|
|
415
|
+
]) {
|
|
416
|
+
const name = `title-${status}`;
|
|
417
|
+
const dir = join(sessionRoot, name);
|
|
418
|
+
await mkdir(dir, { recursive: true });
|
|
419
|
+
const sessionFile = join(dir, "session.jsonl");
|
|
420
|
+
await writeFile(sessionFile, "{}\n");
|
|
421
|
+
const title = await processRarebitTitle(
|
|
422
|
+
{
|
|
423
|
+
sessionManager: {
|
|
424
|
+
getHeader: () => ({ id: name }),
|
|
425
|
+
getSessionFile: () => sessionFile,
|
|
426
|
+
getBranch: () => branch,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
sessionRoot,
|
|
431
|
+
rarebitRoot,
|
|
432
|
+
sourceEntryId: "u",
|
|
433
|
+
titleDate: "2026-07-16",
|
|
434
|
+
model: { provider: "test", id: "cheap" },
|
|
435
|
+
requestIdentity: status,
|
|
436
|
+
...(status === "failure"
|
|
437
|
+
? {
|
|
438
|
+
modelClient: {
|
|
439
|
+
complete: async () => {
|
|
440
|
+
throw new Error("test");
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
}
|
|
444
|
+
: {
|
|
445
|
+
modelClient: { complete: async () => ({ text: "schema title" }) },
|
|
446
|
+
...(status === "proposal"
|
|
447
|
+
? {}
|
|
448
|
+
: { applyTitle: async () => ({ status }) }),
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
const record = JSON.parse(
|
|
453
|
+
(await readFile(title.record.path, "utf8")).trim().split("\n").at(-1),
|
|
454
|
+
);
|
|
455
|
+
assert.equal(
|
|
456
|
+
validate(record),
|
|
457
|
+
true,
|
|
458
|
+
`title ${status}: ${JSON.stringify(validate.errors)}`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
});
|