promptwheel 0.7.4 → 0.7.6

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.
@@ -0,0 +1,306 @@
1
+ // Type declarations for bin/promptwheel.mjs. Hand-written: there is no build step and no
2
+ // TypeScript dependency in this package. Every runtime export of the engine is declared here
3
+ // (test/promptwheel.test.mjs checks the list stays complete). The report shape is the
4
+ // Verified-Outcome Contract's Section 2 verdict object; see INTEGRATE.md for the integration
5
+ // surface and CONTRACT.md for the protocol.
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Section 2 vocabularies
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** The verdict of a check. */
12
+ export type Verdict = 'pass' | 'fail' | 'gamed' | 'inconclusive';
13
+
14
+ /** The status of one metric. A delta inside the measured noise band is `inconclusive`, never a regression. */
15
+ export type MetricStatus = 'improved' | 'regressed' | 'unchanged' | 'inconclusive' | 'unmeasurable';
16
+
17
+ /** The direction that counts as better. `pass` is a 0/1 gate (1 = the command exited 0). */
18
+ export type Direction = 'up' | 'down' | 'pass';
19
+
20
+ /** How much a delta can be trusted. `none` only on an unmeasurable metric; `unverified` is a single read with no noise sample. */
21
+ export type Confidence = 'none' | 'unverified' | 'low' | 'medium' | 'high';
22
+
23
+ /**
24
+ * How a metric's value is read from its command: the last number in stdout (`number`, the
25
+ * default), the count of non-empty lines (`lines`), the exit code as 1/0 (`exit`), or the
26
+ * first capture group of a regex. A command that exits nonzero reads `null` (unmeasurable)
27
+ * unless the metric sets `allowNonzero`; `exit` mode consumes the exit code by design.
28
+ */
29
+ export type ExtractMode = 'number' | 'lines' | 'exit' | { regex: string };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Config (promptwheel.config.json, or the `config` option of check())
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export interface MetricConfig {
36
+ name: string;
37
+ /** Shell command run in a throwaway worktree at each ref. */
38
+ cmd: string;
39
+ /** Default `'number'`. */
40
+ extract?: ExtractMode;
41
+ /** Default `'up'`. */
42
+ direction?: Direction;
43
+ /** A guarded metric fails the gate on a trusted regression; an unguarded one is tracked only. */
44
+ guard?: boolean;
45
+ /** Read a value even when `cmd` exits nonzero (never needed for `extract: 'exit'`). */
46
+ allowNonzero?: boolean;
47
+ /** Per-command timeout, default 300. */
48
+ timeoutSec?: number;
49
+ /** `false` skips the source-only re-proof for this metric (tripwires whose gain legitimately lives in test files). */
50
+ gamingCheck?: boolean;
51
+ /** Per-metric override of `Config.gamingThreshold`. */
52
+ gamingThreshold?: number;
53
+ }
54
+
55
+ /** A named hard rule: any change touching one of `paths` fails the gate before any metric is read. */
56
+ export interface Mandate {
57
+ name: string;
58
+ /** Globs: a bare name matches that basename at any depth, a pattern with `/` is anchored at the repo root, `**` crosses directories. */
59
+ paths: string[];
60
+ reason?: string;
61
+ }
62
+
63
+ export interface Config {
64
+ /** Path (or paths) to a shared base config whose metrics and mandates this repo inherits; resolved relative to the config file. */
65
+ extends?: string | string[];
66
+ metrics: MetricConfig[];
67
+ mandates?: Mandate[];
68
+ /** Measurement repeats per metric, for the noise band. Default 1. */
69
+ repeat?: number;
70
+ /** Back-compat for `linkDirs`: `false` links nothing. */
71
+ linkNodeModules?: boolean;
72
+ /** Directories outside git to symlink into each worktree (default `['node_modules']`). */
73
+ linkDirs?: string[];
74
+ /** Extra environment for the metric commands; `{wt}` expands to the worktree path. */
75
+ env?: Record<string, string>;
76
+ /** A per-ref build or install, run in the worktree before measuring (a failed setup leaves the metric inert, never a false green). */
77
+ setup?: string;
78
+ /** `false` never appends to `.promptwheel/outcomes.jsonl`. */
79
+ record?: boolean;
80
+ /** Fraction of a gain that must survive the source-only re-run; below it the win is `gamed`. Default 0.5. */
81
+ gamingThreshold?: number;
82
+ /** Extra files that define the gate (a threshold, a budget); a change to one alongside a win is gamed by declaration. Same glob rules as `Mandate.paths`. */
83
+ evaluatorPaths?: string[];
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // check()
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export interface CheckOptions {
91
+ /** Repo path. Default: discovered from `process.cwd()`. */
92
+ repo?: string;
93
+ /** Base ref. Default: the merge-base with `origin/main` (then `origin/master`, `main`, `master`); on the trunk itself, `HEAD~1`. */
94
+ base?: string;
95
+ /** Head ref. Default `HEAD`. */
96
+ head?: string;
97
+ /** Gate uncommitted changes (tracked and untracked) instead of `base..head`. Default `false`. */
98
+ working?: boolean;
99
+ /**
100
+ * A resolved config supplied in process, used as given (an `extends` chain is resolved only
101
+ * when the config is loaded from `promptwheel.config.json`). Omit to load the file, or to
102
+ * auto-detect the test command when there is none.
103
+ */
104
+ config?: Config;
105
+ /** Allow the zero-config auto-detect fallback when there is no config file and no `config` option. Default `true`; `false` throws instead. */
106
+ auto?: boolean;
107
+ /** Measurement repeats for the noise band. Default `config.repeat ?? 1`. */
108
+ repeat?: number;
109
+ /** Run the Section 3 source-only re-proof. Default `true`. */
110
+ detectGaming?: boolean;
111
+ /** A declared override reason (the same as `--allow` or a `Promptwheel-Allow:` commit trailer): a would-be `gamed` reads `pass`, recorded in `report.allowed`. */
112
+ allow?: string;
113
+ /** Append the run to `.promptwheel/outcomes.jsonl`. Default `false`. */
114
+ record?: boolean;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // The report (Section 2 verdict object)
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /** What `evaluate()` returns for one metric: the before/after medians and the trust call on their delta. */
122
+ export interface Evaluation {
123
+ before: number | null;
124
+ after: number | null;
125
+ /** `null` when either side is unmeasurable. */
126
+ delta: number | null;
127
+ status: MetricStatus;
128
+ /** `false` only for a guarded metric with a trusted regression. */
129
+ ok: boolean;
130
+ confidence: Confidence;
131
+ /** The measured jitter band (max spread across repeats); `null` when unmeasurable. */
132
+ noise: number | null;
133
+ /** A guarded pass/fail metric stuck at 0 at both refs: it protects nothing. */
134
+ inert?: true;
135
+ }
136
+
137
+ export interface MetricResult extends Evaluation {
138
+ name: string;
139
+ direction: Direction;
140
+ guard: boolean;
141
+ /** `true` when the win did not survive the source-only re-run; `null` when the source-only patch did not apply. Absent without gaming detection or when the metric did not improve. */
142
+ gamed?: boolean | null;
143
+ /** The metric's value with only the source edits applied. */
144
+ sourceOnly?: number | null;
145
+ /** The fraction of the gain that survived source-only (0..1). */
146
+ retained?: number | null;
147
+ gamingReason?: string;
148
+ /** Set when a declared override cleared a `gamed` on this metric. */
149
+ allowed?: true;
150
+ }
151
+
152
+ export interface MandateViolation {
153
+ name: string;
154
+ reason: string;
155
+ /** The protected paths this change touched (at most 20). */
156
+ files: string[];
157
+ /** The mandate was deleted in this same change; it stays in force for the change that removes it. */
158
+ removed?: true;
159
+ }
160
+
161
+ export interface GateWeakening {
162
+ name: string;
163
+ change: 'guard-removed' | 'guard-disabled';
164
+ /** What the dropped guard would have read at head under its base definition. */
165
+ status: MetricStatus;
166
+ }
167
+
168
+ export interface EvaluatorChange {
169
+ path: string;
170
+ /** Truncated SHA-256 of the file at each ref; `null` where the file is absent. */
171
+ base: string | null;
172
+ head: string | null;
173
+ }
174
+
175
+ /** The gate-integrity record, present when gaming detection ran. */
176
+ export interface GateRecord {
177
+ /** Truncated SHA-256 of the gate definition (metrics, guards, evaluatorPaths, gamingThreshold, mandates) at each ref; `base` is `null` when there was no config at base. */
178
+ fingerprint: { base: string | null; head: string };
179
+ /** The gate definition differs between base and head. */
180
+ changed?: true;
181
+ /** Guards the change dropped that would have failed at head; non-empty forces `gamed` unless allowed. */
182
+ weakened?: GateWeakening[];
183
+ /** Declared `evaluatorPaths` files whose content changed in this diff. */
184
+ evaluatorChanged?: EvaluatorChange[];
185
+ /** A declared override cleared the weakening. */
186
+ allowed?: true;
187
+ }
188
+
189
+ export interface AllowedRecord {
190
+ reason: string;
191
+ /** The metric names (and `'gate'`) the override cleared. */
192
+ cleared: string[];
193
+ }
194
+
195
+ /** Tamper-evident receipt: re-run `(base, head, config)` and `verdictHash` must match. Additive; never changes the verdict. */
196
+ export interface Provenance {
197
+ tool: 'promptwheel';
198
+ toolVersion: string | null;
199
+ baseSha: string | null;
200
+ headSha: string | null;
201
+ configHash: string;
202
+ verdictHash: string;
203
+ }
204
+
205
+ export interface Report {
206
+ /** Short SHA (or the ref as given when it cannot be resolved). */
207
+ base: string;
208
+ head: string;
209
+ mode: 'refs' | 'working';
210
+ repeat: number;
211
+ verdict: Verdict;
212
+ metrics: MetricResult[];
213
+ /** Present only when a mandate was violated (the verdict is then `fail`). */
214
+ mandates?: MandateViolation[];
215
+ gate?: GateRecord;
216
+ allowed?: AllowedRecord;
217
+ provenance: Provenance;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Public library API (stable across 0.x minors)
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /** The Contract revision this build implements; bumps only when the verdict schema or the exit codes change. */
225
+ export declare const CONTRACT_VERSION: '1';
226
+ export declare const VERDICTS: readonly ['pass', 'fail', 'gamed', 'inconclusive'];
227
+ export declare const METRIC_STATUSES: readonly ['improved', 'regressed', 'unchanged', 'inconclusive', 'unmeasurable'];
228
+ export declare const DIRECTIONS: readonly ['up', 'down', 'pass'];
229
+
230
+ /** Section 1: `pass` 0, `fail` 1, `gamed` 2, `inconclusive` 3 (any other string maps to 1). The CLI exits with this. */
231
+ export declare function verdictExitCode(verdict: Verdict | string): 0 | 1 | 2 | 3;
232
+
233
+ /**
234
+ * Run the gate in process and return the Section 2 verdict object (identical to `run --json`).
235
+ * Measures only in throwaway git worktrees, never the caller's working tree. Never prints and
236
+ * never calls `process.exit`; throws a plain `Error` on a precondition failure (not a git
237
+ * repository, `auto: false` with no config, a `config` without a non-empty `metrics` array).
238
+ */
239
+ export declare function check(options?: CheckOptions): Report;
240
+
241
+ /**
242
+ * Structurally validate any verdict object against Section 2 (shapes, the fixed vocabularies,
243
+ * and the honest-verdict rules). Zero dependency, so a third-party implementation can self-check
244
+ * the artifact it emits. `errors` is empty exactly when `ok` is `true`.
245
+ */
246
+ export declare function conformsToContract(report: unknown): { ok: boolean; errors: string[] };
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Pure helpers (exported for unit tests and for consumers that want the arithmetic)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ /** Read a metric value from a command's stdout and exit code. */
253
+ export declare function extract(stdout: string, code: number, mode?: ExtractMode, allowNonzero?: boolean): number | null;
254
+
255
+ /** Median of the non-null samples; `null` when there are none. */
256
+ export declare function median(samples: ReadonlyArray<number | null | undefined>): number | null;
257
+
258
+ /** `max - min` of the non-null samples; `0` when there are none. */
259
+ export declare function spread(samples: ReadonlyArray<number | null | undefined>): number;
260
+
261
+ /** The credibility core: compare before/after samples and trust the delta only if it clears the observed noise band. */
262
+ export declare function evaluate(
263
+ metric: Pick<MetricConfig, 'direction' | 'guard' | 'extract'>,
264
+ beforeSamples: ReadonlyArray<number | null>,
265
+ afterSamples: ReadonlyArray<number | null>,
266
+ repeat: number,
267
+ ): Evaluation;
268
+
269
+ /** Render a report as the PR-comment markdown the GitHub Action posts. */
270
+ export declare function renderMarkdown(report: Report): string;
271
+
272
+ /** The built-in classifier: is this repo-relative path test / config / grader / golden (not production source)? */
273
+ export declare function isNonSource(path: string): boolean;
274
+
275
+ /** Compile a config's `evaluatorPaths` globs into a matcher over repo-relative paths (always `false` when none are declared). */
276
+ export declare function evaluatorMatcher(config?: { evaluatorPaths?: string[] } | null): (path: string) => boolean;
277
+
278
+ /** Directional improvement of `value` over `before` (positive = better); `null` when either is missing. */
279
+ export declare function gain(metric: { direction?: Direction }, before: number | null, value: number | null): number | null;
280
+
281
+ /** The samples measured with only the source slice of the change applied. */
282
+ export interface SourceOnlyResult {
283
+ samples: ReadonlyArray<number | null>;
284
+ /** `false` when the change touched zero production-source files. */
285
+ hadSourceChange: boolean;
286
+ /** The source-only patch did not apply cleanly, so the judgement is inconclusive. */
287
+ applyFailed?: boolean;
288
+ }
289
+
290
+ export interface GamingJudgement {
291
+ /** `null` when the source-only patch did not apply. */
292
+ gamed: boolean | null;
293
+ sourceOnly: number | null;
294
+ retained: number | null;
295
+ reason: string;
296
+ }
297
+
298
+ /** Did a metric's full-diff win survive on the source edits alone? `gamed` when less than `threshold` (default 0.5) of the gain is retained. */
299
+ export declare function judgeGaming(
300
+ metric: { direction?: Direction; before: number | null; after: number | null },
301
+ sourceOnly: SourceOnlyResult,
302
+ threshold?: number,
303
+ ): GamingJudgement;
304
+
305
+ /** Truncated SHA-256 of the verdict content (everything but `provenance`), the reproducible half of the receipt. */
306
+ export declare function verdictFingerprint(report: Pick<Report, 'verdict'> & Partial<Pick<Report, 'metrics' | 'gate'>>): string;