@hypercarrier/rarebit 0.5.1 → 0.6.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 +15 -0
- package/README.md +10 -0
- package/VISUAL-LANGUAGE.md +5 -5
- package/package.json +5 -1
- package/src/rarebit-visual-language.mjs +6 -10
- package/src/types.d.ts +0 -2
- package/test/automatic-summary-policy.test.mjs +1 -0
- package/test/extension-activity.test.mjs +1 -0
- package/test/helpers/selection-monitor.mjs +10 -0
- package/test/herdr-activity.test.mjs +1 -0
- package/test/home-guard.test.mjs +26 -0
- package/test/home.mjs +16 -0
- package/test/lifecycle-contract-independent.test.mjs +1 -0
- package/test/package-artifact-identity.test.mjs +1 -0
- package/test/piq.test.mjs +1 -0
- package/test/rarebit-activity.test.mjs +1 -0
- package/test/rarebit-artifact-state.test.mjs +1 -0
- package/test/rarebit-btw.test.mjs +1 -0
- package/test/rarebit-cli.test.mjs +1 -0
- package/test/rarebit-command.test.mjs +1 -0
- package/test/rarebit-core.test.mjs +1 -0
- package/test/rarebit-cross-consumer-conformance.test.mjs +1 -0
- package/test/rarebit-fork.test.mjs +1 -0
- package/test/rarebit-independent-contract.test.mjs +1 -0
- package/test/rarebit-recall.test.mjs +1 -0
- package/test/rarebit-recap.test.mjs +1 -0
- package/test/rarebit-settings-ui.test.mjs +2 -1
- package/test/rarebit-sidecar-protocol.test.mjs +1 -0
- package/test/rarebit-v020.test.mjs +1 -0
- package/test/rarebit-view.test.mjs +1 -0
- package/test/rarebit-visual-language.test.mjs +7 -6
- package/test/recap-read-checkpoint.test.mjs +1 -0
- package/test/simulator-selection.test.mjs +32 -0
- package/test/summary-attention-independent.test.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
- Retire the attention presentation of the `needs_attention` Summary status.
|
|
6
|
+
It now reads as an ordinary Summary: no `◆!` mark, no "needs you" label, and
|
|
7
|
+
no attention tone. The status and its classifier stay unchanged. The Recap
|
|
8
|
+
heading reads `Recap·summarized·…`.
|
|
9
|
+
|
|
10
|
+
## 0.5.2 — 2026-10-15
|
|
11
|
+
|
|
12
|
+
- Add a development-only simulator lane (`npm run test:sim`) that runs real
|
|
13
|
+
Rarebit selection, materialization, Recall, compaction and tree navigation on
|
|
14
|
+
the shared HyperCarrier Pi simulator. It needs the HyperCarrier workspace.
|
|
15
|
+
`sim/` is not in the npm package. The lane found no Rarebit product bug.
|
|
16
|
+
- Move the selection monitor tests into `test/`.
|
|
17
|
+
|
|
3
18
|
## 0.5.1 — 2026-10-12
|
|
4
19
|
|
|
5
20
|
- Document the release privacy gate as the baseline range scan in `RUNBOOK.md`
|
package/README.md
CHANGED
|
@@ -400,6 +400,16 @@ Use [GitHub Issues](https://github.com/deephbz/rarebit/issues) for normal
|
|
|
400
400
|
support. Include no Session prose, credentials, or Recall files in public
|
|
401
401
|
reports.
|
|
402
402
|
|
|
403
|
+
## Development verification
|
|
404
|
+
|
|
405
|
+
`npm ci && npm test` runs standalone unit and selection checks. The private
|
|
406
|
+
R&D simulator lane uses HC's shared `@hypercarrier/pi-sim`; it does not enter
|
|
407
|
+
Rarebit's root install or published artifact. Run `npm run test:sim` in the HC
|
|
408
|
+
workspace or set `HC_PI_SIM_PATH` to an installed simulator package root.
|
|
409
|
+
Run `npm run test:sim:campaign` for the longer seeded campaign.
|
|
410
|
+
[Simulator scope, setup, properties, replay, and results](sim/README.md) remain
|
|
411
|
+
repository-only development artifacts. No real provider credentials are needed.
|
|
412
|
+
|
|
403
413
|
## Release and source history
|
|
404
414
|
|
|
405
415
|
Rarebit is released from an immutable version tag by
|
package/VISUAL-LANGUAGE.md
CHANGED
|
@@ -61,9 +61,9 @@ The scopes are deliberately different: Session-history events are repeatable, ti
|
|
|
61
61
|
</tr>
|
|
62
62
|
<tr>
|
|
63
63
|
<td><code>needs_attention</code></td>
|
|
64
|
-
<td><span style="color: #
|
|
65
|
-
<td
|
|
66
|
-
<td>
|
|
64
|
+
<td><span style="color: #334155">summarized</span></td>
|
|
65
|
+
<td>summarized</td>
|
|
66
|
+
<td>ordinary</td>
|
|
67
67
|
</tr>
|
|
68
68
|
<tr>
|
|
69
69
|
<td><code>ineligible</code></td>
|
|
@@ -82,8 +82,8 @@ The scopes are deliberately different: Session-history events are repeatable, ti
|
|
|
82
82
|
|
|
83
83
|
There is one user-message category, and every selected user message uses the same visual role. The smaller solid continuation dot means work continues. The larger solid stop circle marks a reported response boundary, not verified completion. Supporting intervals paint first, then continuations, stops and errors, and user messages last. Selection adds a halo without replacing the canonical mark.
|
|
84
84
|
|
|
85
|
-
Summary reasons such as `decision`, `blocker`, or `unfinished` are secondary text, never new colors or shapes. Summary prose uses normal body styling.
|
|
85
|
+
Summary reasons such as `decision`, `blocker`, or `unfinished` are secondary text, never new colors or shapes. Summary prose uses normal body styling. No Summary result has a high-salience mark, color, or sound. `needs_attention` is an ordinary result: show the Summary text, not an attention label. Owner attention comes from other sources, such as Arc asks and runtime facts. Finished has no green, checkmark, or success treatment because it is an appearance-level Session assessment, not proof that a Task, Project, tool run, or delivery succeeded. Error is diagnostic.
|
|
86
86
|
|
|
87
|
-
Synchronization and applicability qualify a result; they are not additional statuses. Append `· source pending` when applicable, expose detailed sync/applicability in an inspector
|
|
87
|
+
Synchronization and applicability qualify a result; they are not additional statuses. Append `· source pending` when applicable, expose detailed sync/applicability in an inspector. Never publish `syncing` as a Summary status. Consumers support only the current package projection and don't translate unsupported receipt shapes into visual meaning.
|
|
88
88
|
|
|
89
89
|
Every mark has an accessible text label. Do not rely on color, hover, or sound. A muted monochrome snapshot may use lighter weight or terminal `dim`, but its text must remain readable and meet WCAG 2.2 AA where applicable. Terminal renderers preserve the monochrome mark and label with the closest theme-safe ANSI treatment. Consumers import the package mapping rather than inventing another one, cite this file for perceptual intent, and keep application layout local.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypercarrier/rarebit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Sparse, provenance-bearing conversational evidence recovery for Pi sessions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "HyperCarrier contributors",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"test": "node --test test/*.test.mjs",
|
|
32
|
+
"test:sim": "node --test sim/*.test.mjs",
|
|
33
|
+
"test:sim:campaign": "RAREBIT_SIM_RUNS=100 node --test sim/*.test.mjs",
|
|
34
|
+
"test:sim:replay": "node sim/replay.mjs",
|
|
32
35
|
"test:e2e:tui": "node scripts/e2e-v020-pi.mjs",
|
|
33
36
|
"test:e2e:summary-cache": "node scripts/e2e-summary-cache.mjs",
|
|
34
37
|
"test:e2e:recall": "node scripts/e2e-recall-pi0842.mjs",
|
|
@@ -55,6 +58,7 @@
|
|
|
55
58
|
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
56
59
|
"@earendil-works/pi-tui": "0.84.2",
|
|
57
60
|
"ajv": "^6.15.0",
|
|
61
|
+
"fast-check": "^4.10.2",
|
|
58
62
|
"typescript": "^5.9.3"
|
|
59
63
|
},
|
|
60
64
|
"pi": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Executable projection of the perceptual contract in ../VISUAL-LANGUAGE.md.
|
|
2
2
|
// Consumers own layout and renderer-specific styling; this module owns semantic
|
|
3
|
-
// marks, labels, tones, and source-pending
|
|
3
|
+
// marks, labels, tones, and the source-pending qualifier. No Summary status has an
|
|
4
|
+
// attention presentation: `needs_attention` reads as an ordinary Summary.
|
|
4
5
|
|
|
5
6
|
export const RAREBIT_EVENT_PRESENTATION = Object.freeze({
|
|
6
7
|
user_message: Object.freeze({
|
|
@@ -43,10 +44,10 @@ export const RAREBIT_SUMMARY_PRESENTATION = Object.freeze({
|
|
|
43
44
|
salience: "ordinary",
|
|
44
45
|
}),
|
|
45
46
|
needs_attention: Object.freeze({
|
|
46
|
-
mark:
|
|
47
|
-
label: "
|
|
48
|
-
tone: "
|
|
49
|
-
salience: "
|
|
47
|
+
mark: null,
|
|
48
|
+
label: "summarized",
|
|
49
|
+
tone: "neutral",
|
|
50
|
+
salience: "ordinary",
|
|
50
51
|
}),
|
|
51
52
|
ineligible: Object.freeze({
|
|
52
53
|
mark: null,
|
|
@@ -101,10 +102,5 @@ export function rarebitSummaryPresentation(
|
|
|
101
102
|
...presentation,
|
|
102
103
|
mark: null,
|
|
103
104
|
label: `${presentation.label} · source pending`,
|
|
104
|
-
tone: presentation.tone === "attention" ? "neutral" : presentation.tone,
|
|
105
|
-
salience:
|
|
106
|
-
presentation.salience === "attention"
|
|
107
|
-
? "ordinary"
|
|
108
|
-
: presentation.salience,
|
|
109
105
|
});
|
|
110
106
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -199,7 +199,6 @@ export type RarebitVisualTone =
|
|
|
199
199
|
| "user"
|
|
200
200
|
| "continuation"
|
|
201
201
|
| "boundary"
|
|
202
|
-
| "attention"
|
|
203
202
|
| "diagnostic"
|
|
204
203
|
| "muted";
|
|
205
204
|
export type RarebitEventKind =
|
|
@@ -213,7 +212,6 @@ export type RarebitVisualPresentation = {
|
|
|
213
212
|
| "smaller"
|
|
214
213
|
| "larger"
|
|
215
214
|
| "ordinary"
|
|
216
|
-
| "attention"
|
|
217
215
|
| "muted"
|
|
218
216
|
| "diagnostic";
|
|
219
217
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Independent observation of the prose-selection contract; no simulator needed.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
export function expectedProse(branch) {
|
|
4
|
+
return branch.filter(e => e.type === 'message' && (e.message.role === 'user' || (e.message.role === 'assistant' && ['stop', 'toolUse'].includes(e.message.stopReason))))
|
|
5
|
+
.map(e => ({ id: e.id ?? e.message.id ?? null, text: typeof e.message.content === 'string' ? e.message.content : (e.message.content ?? []).filter(b => b.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n'), readable: typeof e.message.content === 'string' || (e.message.content ?? []).some(b => b.type === 'text' && typeof b.text === 'string') }))
|
|
6
|
+
.filter(e => e.readable).map(({ id, text }) => ({ id, text }));
|
|
7
|
+
}
|
|
8
|
+
export function checkSelection(selection, branch) {
|
|
9
|
+
assert.deepEqual(selection.occurrences.map(o => ({ id: o.sourceEntryId, text: o.text })), expectedProse(branch), 'R1 selection contract');
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "./home.mjs";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { Worker } from "node:worker_threads";
|
|
7
|
+
|
|
8
|
+
const root = join(import.meta.dirname, "..");
|
|
9
|
+
|
|
10
|
+
test("HOME guard: every test file imports test/home.mjs first", () => {
|
|
11
|
+
const missing = [];
|
|
12
|
+
for (const dir of ["test", "sim"]) for (const name of readdirSync(join(root, dir))) {
|
|
13
|
+
if (!name.endsWith(".test.mjs")) continue;
|
|
14
|
+
const first = readFileSync(join(root, dir, name), "utf8").match(/^import\b.*$/m)?.[0];
|
|
15
|
+
if (first !== `import "${dir === "test" ? "./home.mjs" : "../test/home.mjs"}";`) missing.push(`${dir}/${name}`);
|
|
16
|
+
}
|
|
17
|
+
assert.deepEqual(missing, []);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Bun caches os.homedir() at start, so a runtime HOME never reaches it. A worker thread is the same case under
|
|
21
|
+
// Node: its HOME stays in its own process.env copy.
|
|
22
|
+
test("HOME guard: a process whose os.homedir() does not follow HOME stops before any test code runs", async () => {
|
|
23
|
+
const worker = new Worker(new URL("./home.mjs", import.meta.url));
|
|
24
|
+
const error = await new Promise((resolve) => { worker.once("error", resolve); worker.once("exit", () => resolve(undefined)); });
|
|
25
|
+
assert.match(String(error), /HOME isolation failed: .* No test code ran\./);
|
|
26
|
+
});
|
package/test/home.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Test HOME isolation: import this module first in every test file, or load it as a Vitest setup file. It gives the
|
|
2
|
+
// test process a fresh HOME and proves that os.homedir() follows it before any test code runs. Bun caches
|
|
3
|
+
// os.homedir() at start, so a HOME set at runtime does not reach it; on 2026-10-29 a Bun test run wrote the
|
|
4
|
+
// operator's ~/.pi this way. scripts/verify-test-home.mjs keeps every copy of this file identical.
|
|
5
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
6
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const home = mkdtempSync(join(tmpdir(), "hc-test-home-"));
|
|
10
|
+
process.once("exit", () => rmSync(home, { recursive: true, force: true }));
|
|
11
|
+
process.env.HOME = home;
|
|
12
|
+
// Pi paths then default inside the fresh HOME; a test that needs an agent directory sets its own.
|
|
13
|
+
delete process.env.PI_CODING_AGENT_DIR;
|
|
14
|
+
const resolved = realpathSync(homedir()), operator = realpathSync(userInfo().homedir);
|
|
15
|
+
if (resolved !== realpathSync(home) || resolved === operator)
|
|
16
|
+
throw new Error(`HOME isolation failed: os.homedir() is ${resolved}, HOME is ${home}, the operator HOME is ${operator}. This runtime does not follow a HOME set at runtime; run the tests under Node. No test code ran.`);
|
package/test/piq.test.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./home.mjs";
|
|
1
2
|
import assert from "node:assert/strict";
|
|
2
3
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -192,7 +193,7 @@ test("recap renders the human label and converts the receipt timestamp to the co
|
|
|
192
193
|
const result = await controller.showExisting(context);
|
|
193
194
|
assert.equal(result.shown, true);
|
|
194
195
|
const heading = widgets[0]({ requestRender() {} }, { fg: (_name, text) => text }).render(200).find((line) => line.includes("Recap·")).trim();
|
|
195
|
-
assert.match(heading, /^
|
|
196
|
+
assert.match(heading, /^Recap·summarized·01\/01 08:00\(GMT\+8\)/);
|
|
196
197
|
assert.doesNotMatch(heading, /as of|2026|Asia\/Hong_Kong/);
|
|
197
198
|
assert.doesNotMatch(heading, /Rarebit Summary/);
|
|
198
199
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./home.mjs";
|
|
1
2
|
import test from "node:test";
|
|
2
3
|
import assert from "node:assert/strict";
|
|
3
4
|
import {
|
|
@@ -37,7 +38,7 @@ test("event presentation preserves the canonical occurrence grammar", () => {
|
|
|
37
38
|
assert.equal(rarebitEventPresentation("terminal_error").mark, "×");
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
test("Summary presentation keeps
|
|
41
|
+
test("Summary presentation keeps every outcome neutral, needs_attention included", () => {
|
|
41
42
|
assert.deepEqual(rarebitSummaryPresentation("finished"), {
|
|
42
43
|
mark: null,
|
|
43
44
|
label: "appears finished",
|
|
@@ -45,16 +46,16 @@ test("Summary presentation keeps ordinary outcomes neutral and attention singula
|
|
|
45
46
|
salience: "ordinary",
|
|
46
47
|
});
|
|
47
48
|
assert.deepEqual(rarebitSummaryPresentation("needs_attention"), {
|
|
48
|
-
mark:
|
|
49
|
-
label: "
|
|
50
|
-
tone: "
|
|
51
|
-
salience: "
|
|
49
|
+
mark: null,
|
|
50
|
+
label: "summarized",
|
|
51
|
+
tone: "neutral",
|
|
52
|
+
salience: "ordinary",
|
|
52
53
|
});
|
|
53
54
|
assert.deepEqual(
|
|
54
55
|
rarebitSummaryPresentation("needs_attention", { sourcePending: true }),
|
|
55
56
|
{
|
|
56
57
|
mark: null,
|
|
57
|
-
label: "
|
|
58
|
+
label: "summarized · source pending",
|
|
58
59
|
tone: "neutral",
|
|
59
60
|
salience: "ordinary",
|
|
60
61
|
},
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "./home.mjs";
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import fc from 'fast-check';
|
|
5
|
+
import { selectRarebits } from '../src/rarebit-core.mjs';
|
|
6
|
+
import { parseNativeSession, resolveActiveBranch } from '../src/rarebit-session.mjs';
|
|
7
|
+
import { checkSelection } from './helpers/selection-monitor.mjs';
|
|
8
|
+
|
|
9
|
+
test('R1 exhaustive role/outcome/content pairs; R8 old and current native formats', () => {
|
|
10
|
+
const entries = [];
|
|
11
|
+
for (const role of ['user', 'assistant', 'toolResult']) for (const stopReason of ['stop', 'toolUse', 'error', 'aborted', 'length', undefined]) for (const content of ['prose', [{ type: 'text', text: 'prose' }, { type: 'thinking', thinking: 'private reasoning' }], [{ type: 'toolCall', id: 'synthetic', name: 'read', arguments: {} }], []]) entries.push({ type: 'message', message: { role, stopReason, content } });
|
|
12
|
+
let count = 0;
|
|
13
|
+
for (const a of entries) for (const b of entries) {
|
|
14
|
+
const branch = [a, b].map((e, i) => ({ ...e, id: `e${i}`, parentId: i ? 'e0' : null }));
|
|
15
|
+
checkSelection(selectRarebits(branch), branch); count++;
|
|
16
|
+
}
|
|
17
|
+
assert.equal(count, 5184);
|
|
18
|
+
for (const version of [1, 2, 3]) {
|
|
19
|
+
const branch = entries.map((e, i) => ({ ...e, id: `e${i}`, parentId: i ? `e${i-1}` : null, legacyExtra: 'retained' }));
|
|
20
|
+
const original = JSON.stringify([{ type: 'session', id: 'synthetic-old', version }, ...branch]);
|
|
21
|
+
const parsed = parseNativeSession(JSON.parse(original).map(e => JSON.stringify(e)).join('\n'));
|
|
22
|
+
checkSelection(selectRarebits(resolveActiveBranch(parsed)), branch);
|
|
23
|
+
assert.equal(JSON.stringify([parsed.header, ...parsed.entries]), original, 'R8 no read-time migration');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('R1/R8 fast-check mixed native evidence with shrinking', () => {
|
|
28
|
+
fc.assert(fc.property(fc.array(fc.record({ role: fc.constantFrom('user', 'assistant', 'toolResult'), stopReason: fc.constantFrom('stop', 'toolUse', 'error', 'aborted'), text: fc.string({ maxLength: 100 }) }), { maxLength: 30 }), records => {
|
|
29
|
+
const branch = records.map((r, i) => ({ type: 'message', id: `e${i}`, parentId: i ? `e${i-1}` : null, message: { role: r.role, stopReason: r.stopReason, content: [{ type: 'text', text: r.text }] } }));
|
|
30
|
+
checkSelection(selectRarebits(branch), branch);
|
|
31
|
+
}), { seed: 20261015, numRuns: 100 });
|
|
32
|
+
});
|