@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +10 -0
  3. package/VISUAL-LANGUAGE.md +5 -5
  4. package/package.json +5 -1
  5. package/src/rarebit-visual-language.mjs +6 -10
  6. package/src/types.d.ts +0 -2
  7. package/test/automatic-summary-policy.test.mjs +1 -0
  8. package/test/extension-activity.test.mjs +1 -0
  9. package/test/helpers/selection-monitor.mjs +10 -0
  10. package/test/herdr-activity.test.mjs +1 -0
  11. package/test/home-guard.test.mjs +26 -0
  12. package/test/home.mjs +16 -0
  13. package/test/lifecycle-contract-independent.test.mjs +1 -0
  14. package/test/package-artifact-identity.test.mjs +1 -0
  15. package/test/piq.test.mjs +1 -0
  16. package/test/rarebit-activity.test.mjs +1 -0
  17. package/test/rarebit-artifact-state.test.mjs +1 -0
  18. package/test/rarebit-btw.test.mjs +1 -0
  19. package/test/rarebit-cli.test.mjs +1 -0
  20. package/test/rarebit-command.test.mjs +1 -0
  21. package/test/rarebit-core.test.mjs +1 -0
  22. package/test/rarebit-cross-consumer-conformance.test.mjs +1 -0
  23. package/test/rarebit-fork.test.mjs +1 -0
  24. package/test/rarebit-independent-contract.test.mjs +1 -0
  25. package/test/rarebit-recall.test.mjs +1 -0
  26. package/test/rarebit-recap.test.mjs +1 -0
  27. package/test/rarebit-settings-ui.test.mjs +2 -1
  28. package/test/rarebit-sidecar-protocol.test.mjs +1 -0
  29. package/test/rarebit-v020.test.mjs +1 -0
  30. package/test/rarebit-view.test.mjs +1 -0
  31. package/test/rarebit-visual-language.test.mjs +7 -6
  32. package/test/recap-read-checkpoint.test.mjs +1 -0
  33. package/test/simulator-selection.test.mjs +32 -0
  34. 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
@@ -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: #9a3412; background: #fff7ed">◆!</span> needs you</td>
65
- <td>◆! needs you</td>
66
- <td>attention</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. Attention is the only Summary result with a high-salience mark or optional sound. 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 and never produces the attention sound.
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, and withhold the attention mark and sound until readable native evidence confirms a current result. Never publish `syncing` as a Summary status. Consumers support only the current package projection and don't translate unsupported receipt shapes into visual meaning.
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.5.1",
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 attention gating.
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: "needs you",
48
- tone: "attention",
49
- salience: "attention",
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
  };
@@ -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";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
 
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
 
@@ -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.`);
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import { access, mkdtemp } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -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 { execFileSync, spawnSync } from "node:child_process";
package/test/piq.test.mjs CHANGED
@@ -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 { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
 
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
 
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -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 { mkdtemp, mkdir, writeFile } from "node:fs/promises";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
 
@@ -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 { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import { mkdtemp, readFile, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { mkdtemp } from "node:fs/promises";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import {
3
4
  mkdir,
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import { mkdir, mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -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, /^(?:◆!)?Recap·needsyou·01\/01 08:00\(GMT\+8\)/);
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 assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
  import {
@@ -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";
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
  import { createRequire } from "node:module";
@@ -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 ordinary outcomes neutral and attention singular", () => {
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: "needs you",
50
- tone: "attention",
51
- salience: "attention",
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: "needs you · source pending",
58
+ label: "summarized · source pending",
58
59
  tone: "neutral",
59
60
  salience: "ordinary",
60
61
  },
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import test from "node:test";
3
4
  import { mkdtemp, rm } from "node:fs/promises";
@@ -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
+ });
@@ -1,3 +1,4 @@
1
+ import "./home.mjs";
1
2
  import assert from "node:assert/strict";
2
3
  import Ajv from "ajv";
3
4
  import { readFileSync } from "node:fs";