pi-ghost-autocomplete 0.3.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.
Files changed (74) hide show
  1. package/README.md +236 -0
  2. package/dist/bench-cli.d.ts +3 -0
  3. package/dist/bench-cli.d.ts.map +1 -0
  4. package/dist/bench-cli.js +48 -0
  5. package/dist/bench-cli.js.map +1 -0
  6. package/dist/bench.d.ts +93 -0
  7. package/dist/bench.d.ts.map +1 -0
  8. package/dist/bench.js +241 -0
  9. package/dist/bench.js.map +1 -0
  10. package/dist/config.d.ts +55 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +187 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/context-aggregator.d.ts +14 -0
  15. package/dist/context-aggregator.d.ts.map +1 -0
  16. package/dist/context-aggregator.js +58 -0
  17. package/dist/context-aggregator.js.map +1 -0
  18. package/dist/fuzzy-matcher.d.ts +50 -0
  19. package/dist/fuzzy-matcher.d.ts.map +1 -0
  20. package/dist/fuzzy-matcher.js +160 -0
  21. package/dist/fuzzy-matcher.js.map +1 -0
  22. package/dist/ghost-editor.d.ts +65 -0
  23. package/dist/ghost-editor.d.ts.map +1 -0
  24. package/dist/ghost-editor.js +445 -0
  25. package/dist/ghost-editor.js.map +1 -0
  26. package/dist/index.d.ts +25 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +210 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/metrics.d.ts +56 -0
  31. package/dist/metrics.d.ts.map +1 -0
  32. package/dist/metrics.js +98 -0
  33. package/dist/metrics.js.map +1 -0
  34. package/dist/path-index.d.ts +20 -0
  35. package/dist/path-index.d.ts.map +1 -0
  36. package/dist/path-index.js +148 -0
  37. package/dist/path-index.js.map +1 -0
  38. package/dist/path-matcher.d.ts +32 -0
  39. package/dist/path-matcher.d.ts.map +1 -0
  40. package/dist/path-matcher.js +85 -0
  41. package/dist/path-matcher.js.map +1 -0
  42. package/dist/providers/cloud-provider.d.ts +10 -0
  43. package/dist/providers/cloud-provider.d.ts.map +1 -0
  44. package/dist/providers/cloud-provider.js +52 -0
  45. package/dist/providers/cloud-provider.js.map +1 -0
  46. package/dist/providers/local-provider.d.ts +11 -0
  47. package/dist/providers/local-provider.d.ts.map +1 -0
  48. package/dist/providers/local-provider.js +36 -0
  49. package/dist/providers/local-provider.js.map +1 -0
  50. package/dist/providers/openai-compat-provider.d.ts +29 -0
  51. package/dist/providers/openai-compat-provider.d.ts.map +1 -0
  52. package/dist/providers/openai-compat-provider.js +128 -0
  53. package/dist/providers/openai-compat-provider.js.map +1 -0
  54. package/dist/providers/openai-completions-runner.d.ts +28 -0
  55. package/dist/providers/openai-completions-runner.d.ts.map +1 -0
  56. package/dist/providers/openai-completions-runner.js +46 -0
  57. package/dist/providers/openai-completions-runner.js.map +1 -0
  58. package/dist/providers/race.d.ts +40 -0
  59. package/dist/providers/race.d.ts.map +1 -0
  60. package/dist/providers/race.js +128 -0
  61. package/dist/providers/race.js.map +1 -0
  62. package/dist/providers/truncate.d.ts +7 -0
  63. package/dist/providers/truncate.d.ts.map +1 -0
  64. package/dist/providers/truncate.js +39 -0
  65. package/dist/providers/truncate.js.map +1 -0
  66. package/dist/providers/types.d.ts +18 -0
  67. package/dist/providers/types.d.ts.map +1 -0
  68. package/dist/providers/types.js +2 -0
  69. package/dist/providers/types.js.map +1 -0
  70. package/dist/recent-paths.d.ts +25 -0
  71. package/dist/recent-paths.d.ts.map +1 -0
  72. package/dist/recent-paths.js +114 -0
  73. package/dist/recent-paths.js.map +1 -0
  74. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # pi-ghost-autocomplete
2
+
3
+ Inline ghost-text autocomplete for the [Pi coding agent](https://pi.dev/) (`@earendil-works/pi-coding-agent`).
4
+
5
+ While you type at the Pi prompt, an LLM predicts the most likely continuation
6
+ of the current line and renders it as dim grey "ghost text" right of the
7
+ cursor. Press **Right Arrow** to accept. The cursor never moves until you do,
8
+ the existing slash-command popup keeps **Tab**, and a slow or unreachable
9
+ backend silently disables itself rather than spamming the chat.
10
+
11
+ Four provider modes ship in the same package:
12
+
13
+ | Mode | Backend | Default debounce | Default model |
14
+ | --- | --- | --- | --- |
15
+ | `cloud` | Groq / Cerebras / OpenRouter via `@earendil-works/pi-ai` | 150 ms | `groq/llama-3.1-8b-instant` |
16
+ | `local` | Ollama / vLLM (OpenAI-compatible) | 400 ms | `qwen2.5-coder:1.5b` on `http://localhost:11434` |
17
+ | `openai-compat` | Any OpenAI-compatible REST API (e.g. Mercury Edit) — available as a race member only | 150 ms | provider-specific |
18
+ | `race` | Multiple providers in parallel; first valid response wins | 150 ms | per-member defaults |
19
+
20
+ ## Getting started
21
+
22
+ ### 1. Get a Mercury Edit 2 API key
23
+
24
+ The default provider is **Mercury Edit 2** by Inception Labs — a diffusion-based model optimised for low-latency code completions. Sign up at [platform.inceptionlabs.ai](https://platform.inceptionlabs.ai) to get an API key.
25
+
26
+ Set the `INCEPTION_API_KEY` environment variable so the extension can reach the API. Add it to your shell profile (e.g. `~/.bashrc`, `~/.zshrc`, or PowerShell `$PROFILE`) for persistence:
27
+
28
+ ```bash
29
+ # ~/.bashrc or ~/.zshrc
30
+ export INCEPTION_API_KEY="your-key"
31
+ ```
32
+
33
+ ```powershell
34
+ # PowerShell $PROFILE
35
+ $env:INCEPTION_API_KEY = "your-key"
36
+ ```
37
+
38
+ Or pass it inline for a single session:
39
+
40
+ ```bash
41
+ INCEPTION_API_KEY="your-key" pi
42
+ ```
43
+
44
+ ```powershell
45
+ $env:INCEPTION_API_KEY = "your-key"; pi
46
+ ```
47
+
48
+ To use a different provider instead (Groq, Cerebras, a local model, …), see [Configuration](#configuration).
49
+
50
+ ### 2. Install the extension
51
+
52
+ ```bash
53
+ pi install npm:pi-ghost-autocomplete
54
+ ```
55
+
56
+ This registers the extension globally. Pi picks it up automatically on the next launch. To install it only for a single project, add `-l`:
57
+
58
+ ```bash
59
+ pi install npm:pi-ghost-autocomplete -l # writes to .pi/ in the current directory
60
+ ```
61
+
62
+ ### 3. Launch Pi and try it
63
+
64
+ ```bash
65
+ pi
66
+ ```
67
+
68
+ Start typing at the prompt. A dim ghost text should appear to the right of your cursor within ~150 ms. Press **Right Arrow** to accept it, or keep typing to dismiss it.
69
+
70
+ Run `/ghost` to confirm the extension loaded and see the active configuration:
71
+
72
+ ```
73
+ Pi Ghost: enabled · openai-compat=mercury-edit/mercury-edit-2 debounce=150ms
74
+ ```
75
+
76
+ ### 4. (Optional) Load the extension without installing
77
+
78
+ Pass `-e` to load the package for a single session without a permanent install:
79
+
80
+ ```bash
81
+ pi -e npm:pi-ghost-autocomplete
82
+ ```
83
+
84
+ ## Install
85
+
86
+ ```bash
87
+ npm install pi-ghost-autocomplete
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ All knobs are environment variables — no extra config file needed:
93
+
94
+ | Variable | Default | Effect |
95
+ | --- | --- | --- |
96
+ | `PI_GHOST_DISABLED` | unset | Set to `1` to disable on launch |
97
+ | `PI_GHOST_PROVIDER` | unset (Mercury Edit 2) | `cloud` or `local` — set explicitly to switch away from Mercury Edit 2 (ignored when `PI_GHOST_RACE_PROVIDERS` is set) |
98
+ | `PI_GHOST_PROVIDER_NAME` | `groq` | Cloud only (`PI_GHOST_PROVIDER=cloud`): any `KnownProvider` from `pi-ai` (`groq`, `cerebras`, `openrouter`, …) |
99
+ | `PI_GHOST_MODEL` | provider-specific | Model id, e.g. `llama-3.1-8b-instant`, `qwen2.5-coder:1.5b` |
100
+ | `PI_GHOST_API_KEY` | env var lookup | Cloud only: explicit API key (else `pi-ai` env lookup is used) |
101
+ | `PI_GHOST_BASE_URL` | `http://localhost:11434` | Local only: Ollama / vLLM base URL |
102
+ | `PI_GHOST_DEBOUNCE_MS` | 150 (cloud) / 400 (local) | Inputs are debounced this long before a request is sent |
103
+ | `PI_GHOST_MAX_TOKENS` | 64 (cloud) / 48 (local) | Cap on completion tokens |
104
+ | `PI_GHOST_RECENT` | 4 (cloud) / 2 (local) | Number of recent user/assistant messages bundled as context |
105
+ | `PI_GHOST_MIN_CHARS` | 3 | Minimum draft length before a prediction fires |
106
+ | `PI_GHOST_MAX_LINE` | 240 | Predictions skipped if the current line exceeds this length |
107
+ | `PI_GHOST_METRICS` | unset | Set to `1` to write metrics to `.pi/ghost-autocomplete/metrics.jsonl` |
108
+ | `PI_GHOST_DEBUG_LOG` | unset | Set to `1` to write debug records to `.pi/ghost-autocomplete/debug.jsonl` |
109
+
110
+ Run `/ghost` inside Pi to see the effective config; `/ghost on` and
111
+ `/ghost off` toggle the feature for the current session.
112
+
113
+ ## Race mode
114
+
115
+ Race mode fires requests to multiple providers simultaneously and uses the
116
+ first valid response. This reduces perceived latency when one provider is slow
117
+ or rate-limited.
118
+
119
+ ```bash
120
+ # Race Groq against Cerebras
121
+ PI_GHOST_RACE_PROVIDERS=groq,cerebras pi ...
122
+
123
+ # Race Groq, Cerebras, and Mercury Edit (Inception Labs)
124
+ PI_GHOST_RACE_PROVIDERS=groq,cerebras,mercury-edit \
125
+ INCEPTION_API_KEY=your-key pi ...
126
+ ```
127
+
128
+ Race-specific environment variables:
129
+
130
+ | Variable | Default | Effect |
131
+ | --- | --- | --- |
132
+ | `PI_GHOST_RACE_PROVIDERS` | unset | Comma-separated list of providers to race (`groq`, `cerebras`, `local`, `mercury-edit`, `mercury-edit-2`, or any `KnownProvider`) |
133
+ | `PI_GHOST_RACE_PAUSE_MS` | 400 | Milliseconds before the second+ provider is unblocked |
134
+ | `PI_GHOST_RACE_COOLDOWN_MS` | 1500 | Quiet period after a winner before the next race starts |
135
+ | `PI_GHOST_RACE_CUTOFF_MS` | 700 | Maximum time to wait for any provider before giving up |
136
+ | `PI_GHOST_RACE_<NAME>_MODEL` | provider default | Override the model for a specific race member (e.g. `PI_GHOST_RACE_GROQ_MODEL`) |
137
+ | `PI_GHOST_RACE_<NAME>_API_KEY` | env var lookup | Override the API key for a specific race member |
138
+ | `PI_GHOST_RACE_<NAME>_BASE_URL` | provider default | Override the base URL for a specific race member |
139
+ | `INCEPTION_API_KEY` | unset | API key for Mercury Edit; used when `mercury-edit` or `mercury-edit-2` appears in the race list |
140
+ | `INCEPTION_BASE_URL` | `https://api.inceptionlabs.ai/v1` | Base URL override for Mercury Edit |
141
+
142
+ `<NAME>` in the per-member variables is the provider name uppercased with
143
+ non-alphanumeric characters replaced by `_` (e.g. `mercury-edit` →
144
+ `MERCURY_EDIT`).
145
+
146
+ ## Metrics and debug logging
147
+
148
+ Enable structured JSONL logs for acceptance tracking and performance analysis:
149
+
150
+ ```bash
151
+ PI_GHOST_METRICS=1 pi ... # writes .pi/ghost-autocomplete/metrics.jsonl
152
+ PI_GHOST_DEBUG_LOG=1 pi ... # writes .pi/ghost-autocomplete/debug.jsonl
153
+ ```
154
+
155
+ Metrics records include: timestamp, request id, provider latencies, completion
156
+ mode (`deterministic` or `llm`), result (`produced`, `empty`, `rejected`,
157
+ `aborted`, `error`), rejection reason, completion length, whether the ghost was
158
+ shown, and acceptance outcome (`accepted`, `dismissed`, `stale`, `expired`).
159
+ Raw prompt text and LLM output are never written to the metrics file. Both
160
+ files rotate at 10 MB.
161
+
162
+ ## Benchmark CLI
163
+
164
+ The `pi-ghost-bench` binary reads a metrics JSONL file and reports p50/p95
165
+ latency, acceptance rates, and whether PRD thresholds pass:
166
+
167
+ ```bash
168
+ pi-ghost-bench # reads .pi/ghost-autocomplete/metrics.jsonl
169
+ pi-ghost-bench path/to/metrics.jsonl # explicit path
170
+ pi-ghost-bench --json # machine-readable output
171
+ ```
172
+
173
+ Exit code is `0` when all thresholds pass, `1` when any fail, `2` on I/O error or when the file contains no records.
174
+
175
+ ## How it works
176
+
177
+ - The extension registers a `GhostEditor` via `ctx.ui.setEditorComponent`.
178
+ `GhostEditor` extends Pi's `CustomEditor` so all built-in app keybindings
179
+ (escape, ctrl+d, model switching, slash-command popup) keep working.
180
+ - After every text change, a debounced request is sent through `pi-ai`'s
181
+ `complete()` with an `AbortSignal` that is canceled on the next keystroke.
182
+ - Before calling the LLM, the editor checks the path index for a
183
+ deterministic match. If the current token is a file path prefix or a fuzzy
184
+ filename with a strong enough score, the deterministic result is shown
185
+ immediately and the LLM call is suppressed.
186
+ - The path index is built asynchronously at session start via `git ls-files`
187
+ from the repository root, capped at 20 000 files. High-confidence
188
+ basename-prefix matches (score ≥85) preempt the LLM unconditionally; lower-
189
+ confidence fuzzy matches only activate when an action word (open, edit,
190
+ read, …) precedes the token.
191
+ - Recent agent messages come from `ctx.sessionManager.getBranch()` — only
192
+ user/assistant text, no tool calls. Both providers truncate to a small,
193
+ fixed payload so cloud requests don't ship the whole file.
194
+ - `render()` searches for `CURSOR_MARKER` in the lines returned by
195
+ `super.render()` and injects ANSI dim text directly after it. Pi's
196
+ differential renderer redraws only the changed cells — no flicker, no
197
+ full repaint.
198
+ - Right Arrow is intercepted only if a ghost is shown, the popup
199
+ autocomplete is **not** open, and the cursor sits at the end of the text.
200
+ We use `matchesKey(data, "right")` from `@earendil-works/pi-tui`, which
201
+ handles legacy CSI/SS3 sequences and the Kitty keyboard protocol uniformly,
202
+ so kitty / WezTerm / iTerm2 (Kitty-mode) all work. Otherwise the input
203
+ passes through to `super.handleInput`.
204
+ - Visible-column math uses `string-width` + `strip-ansi`. Ghost text is
205
+ clipped grapheme-aware to the remaining columns on the current visual line
206
+ so wide CJK characters and emoji never overflow the row.
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ npm install
212
+ npm run typecheck # tsc --noEmit against pi-tui / pi-coding-agent v0.74
213
+ npm test # vitest, ~30 unit tests
214
+ npm run build # emits dist/
215
+ ```
216
+
217
+ CI runs all three on Node 20 and 22 (`.github/workflows/ci.yml`).
218
+
219
+ ## Limitations
220
+
221
+ - No native Ollama provider in `pi-ai`; we configure an `openai-compat`
222
+ model pointed at Ollama's `/v1` endpoint. Any OpenAI-compatible local
223
+ server works the same way.
224
+ - Pin to `^0.74` of `pi-tui` / `pi-coding-agent` / `pi-ai` /
225
+ `pi-agent-core` (all under `@earendil-works`). The relevant interfaces are
226
+ pre-1.0; expect occasional follow-up bumps.
227
+ - Predictions are shown only at the end of the buffer to avoid splitting
228
+ lines mid-stream.
229
+ - The path index covers only tracked Git files. Untracked files are not
230
+ indexed in v1.
231
+ - Three-provider racing may create elevated cost or rate-limit pressure at
232
+ high typing speeds.
233
+
234
+ ## License
235
+
236
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bench-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench-cli.d.ts","sourceRoot":"","sources":["../src/bench-cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { formatReport, runBenchmark } from "./bench.js";
4
+ const DEFAULT_PATH = ".pi/ghost-autocomplete/metrics.jsonl";
5
+ async function main() {
6
+ const args = process.argv.slice(2);
7
+ if (args.includes("-h") || args.includes("--help")) {
8
+ process.stdout.write(usage());
9
+ return;
10
+ }
11
+ const wantJson = args.includes("--json");
12
+ const positional = args.filter((a) => !a.startsWith("-"));
13
+ const target = resolve(process.cwd(), positional[0] ?? DEFAULT_PATH);
14
+ try {
15
+ const report = await runBenchmark(target);
16
+ if (wantJson) {
17
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
18
+ }
19
+ else {
20
+ process.stdout.write(`${formatReport(report)}\n`);
21
+ }
22
+ if (report.records === 0) {
23
+ process.stderr.write(`pi-ghost-bench: no metric records in ${target} — nothing to evaluate.\n`);
24
+ process.exit(2);
25
+ }
26
+ const anyFail = report.thresholds.some((t) => !t.pass);
27
+ process.exit(anyFail ? 1 : 0);
28
+ }
29
+ catch (err) {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ process.stderr.write(`pi-ghost-bench: ${msg}\n`);
32
+ process.exit(2);
33
+ }
34
+ }
35
+ function usage() {
36
+ return [
37
+ "pi-ghost-bench [path] [--json]",
38
+ "",
39
+ " path JSONL metrics file (default: .pi/ghost-autocomplete/metrics.jsonl)",
40
+ " --json emit JSON instead of formatted text",
41
+ " -h show this help",
42
+ "",
43
+ "Exit code: 0 if all PRD thresholds pass, 1 if any fail, 2 on I/O error.",
44
+ "",
45
+ ].join("\n");
46
+ }
47
+ void main();
48
+ //# sourceMappingURL=bench-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench-cli.js","sourceRoot":"","sources":["../src/bench-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,YAAY,GAAG,sCAAsC,CAAC;AAE5D,KAAK,UAAU,IAAI;IAClB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QAC9B,OAAO;IACR,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC;IAErE,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9D,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CACnB,wCAAwC,MAAM,2BAA2B,CACzE,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;AACF,CAAC;AAED,SAAS,KAAK;IACb,OAAO;QACN,gCAAgC;QAChC,EAAE;QACF,+EAA+E;QAC/E,gDAAgD;QAChD,2BAA2B;QAC3B,EAAE;QACF,yEAAyE;QACzE,EAAE;KACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,KAAK,IAAI,EAAE,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { MetricsRecord } from "./metrics.js";
2
+ /**
3
+ * PRD §Success Metrics initial candidate thresholds. These are red/green
4
+ * targets, not gates — the harness reports compliance but never errors out.
5
+ */
6
+ export declare const THRESHOLDS: {
7
+ readonly llmP50Ms: 300;
8
+ readonly llmP95Ms: 700;
9
+ readonly deterministicP50Ms: 30;
10
+ readonly validOutputRate: 0.85;
11
+ readonly badOutputRate: 0.15;
12
+ };
13
+ export interface ProviderStats {
14
+ provider: string;
15
+ count: number;
16
+ produced: number;
17
+ empty: number;
18
+ rejected: number;
19
+ aborted: number;
20
+ error: number;
21
+ latencyMs: {
22
+ p50: number;
23
+ p95: number;
24
+ p99: number;
25
+ };
26
+ /** produced / count */
27
+ validRate: number;
28
+ /** (empty + rejected + error) / count */
29
+ badRate: number;
30
+ }
31
+ export interface ModeStats {
32
+ mode: "deterministic" | "llm";
33
+ count: number;
34
+ shown: number;
35
+ accepted: number;
36
+ dismissed: number;
37
+ stale: number;
38
+ expired: number;
39
+ acceptanceRate: number;
40
+ providers: ProviderStats[];
41
+ }
42
+ export interface BenchmarkReport {
43
+ source: string;
44
+ records: number;
45
+ parseErrors: number;
46
+ deterministic: ModeStats;
47
+ llm: ModeStats;
48
+ thresholds: Array<{
49
+ name: string;
50
+ target: string;
51
+ observed: string;
52
+ pass: boolean;
53
+ }>;
54
+ }
55
+ /**
56
+ * Read JSONL metrics records from `path`. Skips malformed lines and counts
57
+ * them in `parseErrors`. Never throws on malformed input — a benchmark harness
58
+ * should degrade gracefully so partial logs still produce a report.
59
+ */
60
+ export declare function loadMetrics(path: string): Promise<{
61
+ records: MetricsRecord[];
62
+ parseErrors: number;
63
+ }>;
64
+ export declare function parseMetricsText(text: string): {
65
+ records: MetricsRecord[];
66
+ parseErrors: number;
67
+ };
68
+ /**
69
+ * Quantile via nearest-rank on a sorted ascending array. Returns 0 on empty.
70
+ * `q` in [0, 1]. We don't interpolate — sample sizes here are tens-to-low-
71
+ * thousands and integer ms values, interpolation buys nothing.
72
+ */
73
+ export declare function quantile(sorted: number[], q: number): number;
74
+ /**
75
+ * Build a {@link BenchmarkReport} from parsed records. Pure: no I/O.
76
+ *
77
+ * For LLM mode, providers are aggregated from each record's `providers[]`
78
+ * trace so race members are tracked individually. For deterministic mode the
79
+ * single-element providers[] is used; provider name corresponds to the band
80
+ * (path-prefix / basename-prefix / fuzzy-basename / fuzzy-path).
81
+ */
82
+ export declare function summarize(records: MetricsRecord[], source?: string, parseErrors?: number): BenchmarkReport;
83
+ /**
84
+ * Render a {@link BenchmarkReport} as a fixed-width plain-text table.
85
+ * Output is line-based and ASCII-safe so it pipes cleanly through `less`,
86
+ * email, or PR descriptions.
87
+ */
88
+ export declare function formatReport(report: BenchmarkReport): string;
89
+ /**
90
+ * One-shot helper: load a JSONL file and produce a report. Used by the CLI.
91
+ */
92
+ export declare function runBenchmark(path: string): Promise<BenchmarkReport>;
93
+ //# sourceMappingURL=bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../src/bench.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,aAAa,EAAkC,MAAM,cAAc,CAAC;AAErG;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,SAAS,CAAC;IACzB,GAAG,EAAE,SAAS,CAAC;IACf,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACrF;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAChC,IAAI,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAG5D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC/C,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACpB,CAcA;AAaD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,aAAa,EAAE,EACxB,MAAM,SAAa,EACnB,WAAW,SAAI,GACb,eAAe,CAajB;AAqHD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CA8B5D;AAMD;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAGzE"}
package/dist/bench.js ADDED
@@ -0,0 +1,241 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /**
3
+ * PRD §Success Metrics initial candidate thresholds. These are red/green
4
+ * targets, not gates — the harness reports compliance but never errors out.
5
+ */
6
+ export const THRESHOLDS = {
7
+ llmP50Ms: 300,
8
+ llmP95Ms: 700,
9
+ deterministicP50Ms: 30,
10
+ validOutputRate: 0.85,
11
+ badOutputRate: 0.15,
12
+ };
13
+ /**
14
+ * Read JSONL metrics records from `path`. Skips malformed lines and counts
15
+ * them in `parseErrors`. Never throws on malformed input — a benchmark harness
16
+ * should degrade gracefully so partial logs still produce a report.
17
+ */
18
+ export async function loadMetrics(path) {
19
+ const text = await readFile(path, "utf8");
20
+ return parseMetricsText(text);
21
+ }
22
+ export function parseMetricsText(text) {
23
+ const records = [];
24
+ let parseErrors = 0;
25
+ for (const line of text.split(/\r?\n/)) {
26
+ if (!line.trim())
27
+ continue;
28
+ try {
29
+ const rec = JSON.parse(line);
30
+ if (isMetricsRecord(rec))
31
+ records.push(rec);
32
+ else
33
+ parseErrors++;
34
+ }
35
+ catch {
36
+ parseErrors++;
37
+ }
38
+ }
39
+ return { records, parseErrors };
40
+ }
41
+ function isMetricsRecord(v) {
42
+ if (!v || typeof v !== "object")
43
+ return false;
44
+ const r = v;
45
+ return (typeof r.ts === "number" &&
46
+ typeof r.requestId === "string" &&
47
+ (r.mode === "deterministic" || r.mode === "llm") &&
48
+ typeof r.result === "string");
49
+ }
50
+ /**
51
+ * Quantile via nearest-rank on a sorted ascending array. Returns 0 on empty.
52
+ * `q` in [0, 1]. We don't interpolate — sample sizes here are tens-to-low-
53
+ * thousands and integer ms values, interpolation buys nothing.
54
+ */
55
+ export function quantile(sorted, q) {
56
+ if (sorted.length === 0)
57
+ return 0;
58
+ if (q <= 0)
59
+ return sorted[0] ?? 0;
60
+ if (q >= 1)
61
+ return sorted[sorted.length - 1] ?? 0;
62
+ const idx = Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1);
63
+ return sorted[idx] ?? 0;
64
+ }
65
+ /**
66
+ * Build a {@link BenchmarkReport} from parsed records. Pure: no I/O.
67
+ *
68
+ * For LLM mode, providers are aggregated from each record's `providers[]`
69
+ * trace so race members are tracked individually. For deterministic mode the
70
+ * single-element providers[] is used; provider name corresponds to the band
71
+ * (path-prefix / basename-prefix / fuzzy-basename / fuzzy-path).
72
+ */
73
+ export function summarize(records, source = "<inline>", parseErrors = 0) {
74
+ const det = records.filter((r) => r.mode === "deterministic");
75
+ const llm = records.filter((r) => r.mode === "llm");
76
+ const deterministic = summarizeMode("deterministic", det);
77
+ const llmStats = summarizeMode("llm", llm);
78
+ return {
79
+ source,
80
+ records: records.length,
81
+ parseErrors,
82
+ deterministic,
83
+ llm: llmStats,
84
+ thresholds: checkThresholds(deterministic, llmStats),
85
+ };
86
+ }
87
+ function summarizeMode(mode, records) {
88
+ const byProvider = new Map();
89
+ for (const r of records) {
90
+ if (!r.providers)
91
+ continue;
92
+ for (const p of r.providers) {
93
+ let arr = byProvider.get(p.provider);
94
+ if (!arr) {
95
+ arr = [];
96
+ byProvider.set(p.provider, arr);
97
+ }
98
+ arr.push(p);
99
+ }
100
+ }
101
+ const providers = [];
102
+ for (const [provider, entries] of byProvider) {
103
+ providers.push(buildProviderStats(provider, entries));
104
+ }
105
+ providers.sort((a, b) => a.provider.localeCompare(b.provider));
106
+ const shown = records.filter((r) => r.shown).length;
107
+ const accepted = countAcceptance(records, "accepted");
108
+ const dismissed = countAcceptance(records, "dismissed");
109
+ const stale = countAcceptance(records, "stale");
110
+ const expired = countAcceptance(records, "expired");
111
+ return {
112
+ mode,
113
+ count: records.length,
114
+ shown,
115
+ accepted,
116
+ dismissed,
117
+ stale,
118
+ expired,
119
+ acceptanceRate: shown > 0 ? accepted / shown : 0,
120
+ providers,
121
+ };
122
+ }
123
+ function countAcceptance(records, outcome) {
124
+ let n = 0;
125
+ for (const r of records)
126
+ if (r.acceptance === outcome)
127
+ n++;
128
+ return n;
129
+ }
130
+ function buildProviderStats(provider, entries) {
131
+ const latencies = entries.map((e) => e.latencyMs).sort((a, b) => a - b);
132
+ const counts = {
133
+ produced: 0,
134
+ empty: 0,
135
+ rejected: 0,
136
+ aborted: 0,
137
+ error: 0,
138
+ };
139
+ for (const e of entries)
140
+ counts[e.result]++;
141
+ const valid = counts.produced;
142
+ const bad = counts.empty + counts.rejected + counts.error;
143
+ return {
144
+ provider,
145
+ count: entries.length,
146
+ produced: counts.produced,
147
+ empty: counts.empty,
148
+ rejected: counts.rejected,
149
+ aborted: counts.aborted,
150
+ error: counts.error,
151
+ latencyMs: {
152
+ p50: quantile(latencies, 0.5),
153
+ p95: quantile(latencies, 0.95),
154
+ p99: quantile(latencies, 0.99),
155
+ },
156
+ validRate: entries.length > 0 ? valid / entries.length : 0,
157
+ badRate: entries.length > 0 ? bad / entries.length : 0,
158
+ };
159
+ }
160
+ function checkThresholds(det, llm) {
161
+ const out = [];
162
+ for (const p of det.providers) {
163
+ out.push({
164
+ name: `deterministic[${p.provider}] p50 latency`,
165
+ target: `< ${THRESHOLDS.deterministicP50Ms}ms`,
166
+ observed: `${p.latencyMs.p50}ms`,
167
+ pass: p.latencyMs.p50 < THRESHOLDS.deterministicP50Ms,
168
+ });
169
+ }
170
+ for (const p of llm.providers) {
171
+ out.push({
172
+ name: `llm[${p.provider}] p50 latency`,
173
+ target: `< ${THRESHOLDS.llmP50Ms}ms`,
174
+ observed: `${p.latencyMs.p50}ms`,
175
+ pass: p.latencyMs.p50 < THRESHOLDS.llmP50Ms,
176
+ });
177
+ out.push({
178
+ name: `llm[${p.provider}] p95 latency`,
179
+ target: `< ${THRESHOLDS.llmP95Ms}ms`,
180
+ observed: `${p.latencyMs.p95}ms`,
181
+ pass: p.latencyMs.p95 < THRESHOLDS.llmP95Ms,
182
+ });
183
+ out.push({
184
+ name: `llm[${p.provider}] valid-output rate`,
185
+ target: `>= ${(THRESHOLDS.validOutputRate * 100).toFixed(0)}%`,
186
+ observed: `${(p.validRate * 100).toFixed(1)}%`,
187
+ pass: p.validRate >= THRESHOLDS.validOutputRate,
188
+ });
189
+ out.push({
190
+ name: `llm[${p.provider}] bad-output rate`,
191
+ target: `<= ${(THRESHOLDS.badOutputRate * 100).toFixed(0)}%`,
192
+ observed: `${(p.badRate * 100).toFixed(1)}%`,
193
+ pass: p.badRate <= THRESHOLDS.badOutputRate,
194
+ });
195
+ }
196
+ return out;
197
+ }
198
+ /**
199
+ * Render a {@link BenchmarkReport} as a fixed-width plain-text table.
200
+ * Output is line-based and ASCII-safe so it pipes cleanly through `less`,
201
+ * email, or PR descriptions.
202
+ */
203
+ export function formatReport(report) {
204
+ const lines = [];
205
+ lines.push("Pi Ghost Benchmark Report");
206
+ lines.push("=========================");
207
+ lines.push(`source: ${report.source}`);
208
+ lines.push(`records: ${report.records}`);
209
+ if (report.parseErrors > 0)
210
+ lines.push(`parse errors: ${report.parseErrors}`);
211
+ lines.push("");
212
+ lines.push(`Deterministic (n=${report.deterministic.count})`);
213
+ lines.push(` shown=${report.deterministic.shown} accepted=${report.deterministic.accepted} dismissed=${report.deterministic.dismissed} stale=${report.deterministic.stale} expired=${report.deterministic.expired} accept-rate=${(report.deterministic.acceptanceRate * 100).toFixed(1)}%`);
214
+ for (const p of report.deterministic.providers) {
215
+ lines.push(` - ${p.provider.padEnd(18)} ${formatProviderLatency(p)}`);
216
+ }
217
+ lines.push("");
218
+ lines.push(`LLM (n=${report.llm.count})`);
219
+ lines.push(` shown=${report.llm.shown} accepted=${report.llm.accepted} dismissed=${report.llm.dismissed} stale=${report.llm.stale} expired=${report.llm.expired} accept-rate=${(report.llm.acceptanceRate * 100).toFixed(1)}%`);
220
+ for (const p of report.llm.providers) {
221
+ lines.push(` - ${p.provider.padEnd(18)} ${formatProviderLatency(p)}`);
222
+ }
223
+ lines.push("");
224
+ lines.push("Thresholds:");
225
+ for (const t of report.thresholds) {
226
+ const mark = t.pass ? "[PASS]" : "[FAIL]";
227
+ lines.push(` ${mark} ${t.name.padEnd(40)} target ${t.target.padEnd(10)} got ${t.observed}`);
228
+ }
229
+ return lines.join("\n");
230
+ }
231
+ function formatProviderLatency(p) {
232
+ return `n=${p.count} p50=${p.latencyMs.p50}ms p95=${p.latencyMs.p95}ms p99=${p.latencyMs.p99}ms valid=${(p.validRate * 100).toFixed(1)}% bad=${(p.badRate * 100).toFixed(1)}%`;
233
+ }
234
+ /**
235
+ * One-shot helper: load a JSONL file and produce a report. Used by the CLI.
236
+ */
237
+ export async function runBenchmark(path) {
238
+ const { records, parseErrors } = await loadMetrics(path);
239
+ return summarize(records, path, parseErrors);
240
+ }
241
+ //# sourceMappingURL=bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.js","sourceRoot":"","sources":["../src/bench.ts"],"names":[],"mappings":"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"}