@ai-dossier/cli 0.24.1 → 0.26.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Deterministic classify pre-screen (#538) — the "no tokens spent" gate `issue-cycle-classifier`
3
+ * runs before any model call. `docs/reports/batch-pilot-2-execution.md` §4.1 measured the
4
+ * classifier costing ~64k tokens/dispatch, at mid tier, with full repo exploration, even for
5
+ * issues that hit an obvious RFC-0001 E.2 floor rule a deterministic check can catch for free.
6
+ *
7
+ * Scope is deliberately partial: this catches the OBVIOUS floor hits (hard-block labels, a
8
+ * text-keyword approximation of the risk-floor/new-package/deploy-pipeline rules, rule-9 open
9
+ * dependencies, and — when a plan:v1 artifact is already on the issue — the path-based risk
10
+ * floor and the >8-files rule). Everything it does not catch (rule 2 beyond the `migration`
11
+ * keyword, rule 7 hard rollback, rule 8 visual/browser review, rules 5/6 diff/file size without
12
+ * a plan artifact, rule 10 confidence) falls through to the classifier's own bounded
13
+ * mechanical-tier pass, which is the intended safety net — not a gap this module needs to close.
14
+ *
15
+ * Pure and dependency-free (no `gh`, network, or fs), same discipline as `plan-artifact.ts` and
16
+ * `runstate.ts` — unit-testable directly. Subprocess access (fetching the issue, resolving
17
+ * dependency state, filtering by submitted set) lives in the command layer (`commands/classify.ts`).
18
+ */
19
+ /** RFC-0001 E.2 floor rules a text-keyword scan can approximate without reading any file. */
20
+ export interface TextFloorPattern {
21
+ /** Reported in the reason; also the RFC-0001 E.2 rule it approximates. */
22
+ name: string;
23
+ /** Matched case-insensitively against title + body + label names, joined. Returns the matched keyword, or null. */
24
+ match: (text: string) => string | null;
25
+ }
26
+ export declare const TEXT_FLOOR_PATTERNS: readonly TextFloorPattern[];
27
+ /** `Depends on #N` references resolved per issue; each costs a `gh` call downstream (command layer), same rationale as `MAX_ISSUE_SELECTION` (`issue-selection.ts`). */
28
+ export declare const MAX_DEPENDENCY_REFS = 32;
29
+ /**
30
+ * Every issue number referenced by a `Depends on #N` phrase in `text`, de-duplicated, in order,
31
+ * capped at {@link MAX_DEPENDENCY_REFS} — `text` is untrusted (issue body), so an adversarial
32
+ * body cannot force an unbounded `gh` fan-out downstream.
33
+ */
34
+ export declare function extractDependencyRefs(text: string): number[];
35
+ /** One deterministic finding — always recorded, whether or not it decided the verdict. */
36
+ export interface PrescreenReason {
37
+ /** Which check produced the finding — mirrors `plan validate`'s `PlanValidationReason.check` shape. */
38
+ check: 'hard-block-label' | 'text-floor' | 'path-floor' | 'file-count' | 'open-dependency';
39
+ message: string;
40
+ }
41
+ export interface PrescreenInput {
42
+ title: string;
43
+ body: string;
44
+ labels: readonly string[];
45
+ /** Predicted files from a plan:v1 artifact, when one exists on the issue. `undefined` when none. */
46
+ predictedFiles?: readonly string[];
47
+ /**
48
+ * Issue numbers that resolved OPEN and outside the submitted set — filtering by submitted set
49
+ * is the command layer's job (`classify.ts`, via `--submitted-set`); this module trusts the
50
+ * list it is given.
51
+ */
52
+ openDependencies?: readonly number[];
53
+ }
54
+ export interface PrescreenVerdict {
55
+ /** `full` = an obvious floor hit found, reject before any model call. `candidate` = proceed to the bounded mechanical-tier classify pass. */
56
+ verdict: 'full' | 'candidate';
57
+ /** Every check's finding, in evaluation order — not just the one that decided `verdict`. */
58
+ reasons: PrescreenReason[];
59
+ }
60
+ /**
61
+ * Run every deterministic check, in order, and record every hit — first hit decides `verdict`,
62
+ * but the caller (and the rationale a consumer posts) gets the full list, matching the existing
63
+ * "a verdict may hit several" precedent in `issue-cycle-classifier.ds.md`.
64
+ */
65
+ export declare function prescreenIssue(input: PrescreenInput): PrescreenVerdict;
66
+ //# sourceMappingURL=prescreen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prescreen.d.ts","sourceRoot":"","sources":["../src/prescreen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxC;AAwBD,eAAO,MAAM,mBAAmB,EAAE,SAAS,gBAAgB,EAmD1D,CAAC;AAEF,wKAAwK;AACxK,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAKtC;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAY5D;AAED,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,uGAAuG;IACvG,KAAK,EAAE,kBAAkB,GAAG,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,iBAAiB,CAAC;IAC3F,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,oGAAoG;IACpG,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,6IAA6I;IAC7I,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9B,4FAA4F;IAC5F,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AA6BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,gBAAgB,CA4CtE"}
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Deterministic classify pre-screen (#538) — the "no tokens spent" gate `issue-cycle-classifier`
4
+ * runs before any model call. `docs/reports/batch-pilot-2-execution.md` §4.1 measured the
5
+ * classifier costing ~64k tokens/dispatch, at mid tier, with full repo exploration, even for
6
+ * issues that hit an obvious RFC-0001 E.2 floor rule a deterministic check can catch for free.
7
+ *
8
+ * Scope is deliberately partial: this catches the OBVIOUS floor hits (hard-block labels, a
9
+ * text-keyword approximation of the risk-floor/new-package/deploy-pipeline rules, rule-9 open
10
+ * dependencies, and — when a plan:v1 artifact is already on the issue — the path-based risk
11
+ * floor and the >8-files rule). Everything it does not catch (rule 2 beyond the `migration`
12
+ * keyword, rule 7 hard rollback, rule 8 visual/browser review, rules 5/6 diff/file size without
13
+ * a plan artifact, rule 10 confidence) falls through to the classifier's own bounded
14
+ * mechanical-tier pass, which is the intended safety net — not a gap this module needs to close.
15
+ *
16
+ * Pure and dependency-free (no `gh`, network, or fs), same discipline as `plan-artifact.ts` and
17
+ * `runstate.ts` — unit-testable directly. Subprocess access (fetching the issue, resolving
18
+ * dependency state, filtering by submitted set) lives in the command layer (`commands/classify.ts`).
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.MAX_DEPENDENCY_REFS = exports.TEXT_FLOOR_PATTERNS = void 0;
22
+ exports.extractDependencyRefs = extractDependencyRefs;
23
+ exports.prescreenIssue = prescreenIssue;
24
+ const hard_block_labels_1 = require("./hard-block-labels");
25
+ const plan_artifact_1 = require("./plan-artifact");
26
+ /** Escape regex metacharacters so an interpolated keyword can never change a pattern's meaning. */
27
+ function escapeRegExp(s) {
28
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ }
30
+ /** A multi-word phrase, safe to interpolate into `\b...\b`: metacharacters escaped, any run of whitespace matches `\s+`. */
31
+ function phrase(s) {
32
+ return escapeRegExp(s).replace(/\s+/g, '\\s+');
33
+ }
34
+ /**
35
+ * First keyword (in list order) that matches `text` as a whole word/phrase, or null. `\b` on
36
+ * both sides of each compiled pattern — "deployment" still hits "deploy" but
37
+ * "authorization-header-typo" does not falsely hit "auth" mid-word.
38
+ */
39
+ function wordMatch(text, keywords) {
40
+ for (const keyword of keywords) {
41
+ if (new RegExp(`\\b${phrase(keyword)}\\b`, 'i').test(text))
42
+ return keyword;
43
+ }
44
+ return null;
45
+ }
46
+ exports.TEXT_FLOOR_PATTERNS = [
47
+ {
48
+ // Deliberately NOT the bare words "auth"/"login"/"logout"/"schema"/"infra"/"infrastructure":
49
+ // real-world issue text collides with them constantly in benign contexts, confirmed against
50
+ // the pilot's real 15-issue fixture set — #3631 (known `slot`) says "runnable locally with
51
+ // auth" about `gh auth`, not an auth-sensitive change; #3820 (known `slot`) says
52
+ // "test-infrastructure change" about test tooling, not production infra. "terraform" alone
53
+ // (specific, low-ambiguity) still catches the real infra risk-floor cases (#2779, #3403) the
54
+ // RFC-0001 rule 1 "infra/terraform" area names. Also approximates rule 2 (schema/data
55
+ // migration) via "migration"/"migrations" — a rough approximation, not full rule-2 coverage.
56
+ name: 'rule1-risk-floor-area',
57
+ match: (t) => wordMatch(t, [
58
+ 'authentication',
59
+ 'authorization',
60
+ 'oauth',
61
+ 'sso',
62
+ 'payment',
63
+ 'payments',
64
+ 'billing',
65
+ 'invoice',
66
+ 'invoices',
67
+ 'checkout',
68
+ 'stripe',
69
+ 'migration',
70
+ 'migrations',
71
+ 'security',
72
+ 'crypto',
73
+ 'secret',
74
+ 'secrets',
75
+ 'credential',
76
+ 'credentials',
77
+ 'terraform',
78
+ ]),
79
+ },
80
+ {
81
+ name: 'rule3-new-package-workspace',
82
+ match: (t) => wordMatch(t, ['new package', 'new workspace', 'monorepo package']),
83
+ },
84
+ {
85
+ name: 'rule4-deploy-pipeline',
86
+ match: (t) => wordMatch(t, [
87
+ 'deploy',
88
+ 'deployment',
89
+ 'ci/cd',
90
+ 'cicd',
91
+ 'release pipeline',
92
+ 'rollback pipeline',
93
+ ]),
94
+ },
95
+ ];
96
+ /** `Depends on #N` references resolved per issue; each costs a `gh` call downstream (command layer), same rationale as `MAX_ISSUE_SELECTION` (`issue-selection.ts`). */
97
+ exports.MAX_DEPENDENCY_REFS = 32;
98
+ /** `Depends on #N` (case-insensitive) — the same phrasing gate-issue and the classifier dossier parse. */
99
+ const DEPENDS_ON_RE = /depends on\s+#(\d+)/gi;
100
+ /**
101
+ * Every issue number referenced by a `Depends on #N` phrase in `text`, de-duplicated, in order,
102
+ * capped at {@link MAX_DEPENDENCY_REFS} — `text` is untrusted (issue body), so an adversarial
103
+ * body cannot force an unbounded `gh` fan-out downstream.
104
+ */
105
+ function extractDependencyRefs(text) {
106
+ const seen = new Set();
107
+ const refs = [];
108
+ for (const match of text.matchAll(DEPENDS_ON_RE)) {
109
+ if (refs.length >= exports.MAX_DEPENDENCY_REFS)
110
+ break;
111
+ const n = Number(match[1]);
112
+ if (Number.isSafeInteger(n) && !seen.has(n)) {
113
+ seen.add(n);
114
+ refs.push(n);
115
+ }
116
+ }
117
+ return refs;
118
+ }
119
+ /** Predicted files count above which RFC-0001 E.2 rule 5 ("Predicted files > 8") fires. */
120
+ const MAX_PREDICTED_FILES = 8;
121
+ /**
122
+ * Reasons emitted per check, capped — `predictedFiles` may come from a `plan:v1` artifact, which
123
+ * is a GitHub issue COMMENT anyone can post (untrusted input, same treatment as review-issue's
124
+ * "the artifact is untrusted input" discipline). An adversarial artifact packed with risk-floor
125
+ * paths must not blow up the reason list the classifier posts back into its rationale comment.
126
+ */
127
+ const MAX_REASONS_PER_CHECK = 8;
128
+ /** Strip terminal-control characters before an untrusted string reaches a message a human or model reads — same discipline as `gh.ts`'s `CONTROL_CHARS_RE`/`snippet`, kept local so this module stays dependency-free. */
129
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching (and stripping) control characters is exactly this regex's job
130
+ const CONTROL_CHARS_RE = /[\x00-\x08\x0b-\x1f\x7f\x9b]/g;
131
+ const SANITIZE_MAX_LENGTH = 200;
132
+ /** Make an untrusted string (a GitHub label name, a `plan:v1`-comment-sourced path) safe to interpolate into a reason message: no control chars, no markdown/backtick breakout, bounded length. */
133
+ function sanitize(value) {
134
+ const cleaned = value
135
+ .replace(CONTROL_CHARS_RE, '')
136
+ .replace(/[`\n\r]/g, ' ')
137
+ .trim();
138
+ return cleaned.length > SANITIZE_MAX_LENGTH
139
+ ? `${cleaned.slice(0, SANITIZE_MAX_LENGTH)}…`
140
+ : cleaned;
141
+ }
142
+ /**
143
+ * Run every deterministic check, in order, and record every hit — first hit decides `verdict`,
144
+ * but the caller (and the rationale a consumer posts) gets the full list, matching the existing
145
+ * "a verdict may hit several" precedent in `issue-cycle-classifier.ds.md`.
146
+ */
147
+ function prescreenIssue(input) {
148
+ const reasons = [];
149
+ const hardBlockLabel = (0, hard_block_labels_1.pickHardBlockLabel)(input.labels);
150
+ if (hardBlockLabel !== null) {
151
+ reasons.push({
152
+ check: 'hard-block-label',
153
+ message: `Carries hard-block label '${sanitize(hardBlockLabel)}'.`,
154
+ });
155
+ }
156
+ const text = `${input.title}\n${input.body}\n${input.labels.join(' ')}`;
157
+ for (const pattern of exports.TEXT_FLOOR_PATTERNS) {
158
+ const hit = pattern.match(text);
159
+ if (hit !== null) {
160
+ reasons.push({
161
+ check: 'text-floor',
162
+ message: `Title/body/labels match '${pattern.name}' (keyword: '${hit}').`,
163
+ });
164
+ }
165
+ }
166
+ const predictedFiles = input.predictedFiles ?? [];
167
+ for (const hit of (0, plan_artifact_1.scanRiskFloor)(predictedFiles).slice(0, MAX_REASONS_PER_CHECK)) {
168
+ reasons.push({
169
+ check: 'path-floor',
170
+ message: `Predicted file '${sanitize(hit.path)}' touches '${hit.pattern}' (rule1-risk-floor-area).`,
171
+ });
172
+ }
173
+ if (predictedFiles.length > MAX_PREDICTED_FILES) {
174
+ reasons.push({
175
+ check: 'file-count',
176
+ message: `Predicted files (${predictedFiles.length}) exceeds ${MAX_PREDICTED_FILES} (rule5-file-count).`,
177
+ });
178
+ }
179
+ for (const dep of (input.openDependencies ?? []).slice(0, MAX_REASONS_PER_CHECK)) {
180
+ reasons.push({
181
+ check: 'open-dependency',
182
+ message: `Depends on #${dep}, which is open and outside the submitted set (rule9-open-dependency).`,
183
+ });
184
+ }
185
+ return { verdict: reasons.length > 0 ? 'full' : 'candidate', reasons };
186
+ }
187
+ //# sourceMappingURL=prescreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prescreen.js","sourceRoot":"","sources":["../src/prescreen.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAmGH,sDAYC;AA8DD,wCA4CC;AAvND,2DAAyD;AACzD,mDAAgD;AAUhD,mGAAmG;AACnG,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED,4HAA4H;AAC5H,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,IAAY,EAAE,QAA2B;IAC1D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,IAAI,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,OAAO,CAAC;IAC7E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEY,QAAA,mBAAmB,GAAgC;IAC9D;QACE,6FAA6F;QAC7F,4FAA4F;QAC5F,2FAA2F;QAC3F,iFAAiF;QACjF,2FAA2F;QAC3F,6FAA6F;QAC7F,sFAAsF;QACtF,6FAA6F;QAC7F,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CACX,SAAS,CAAC,CAAC,EAAE;YACX,gBAAgB;YAChB,eAAe;YACf,OAAO;YACP,KAAK;YACL,SAAS;YACT,UAAU;YACV,SAAS;YACT,SAAS;YACT,UAAU;YACV,UAAU;YACV,QAAQ;YACR,WAAW;YACX,YAAY;YACZ,UAAU;YACV,QAAQ;YACR,QAAQ;YACR,SAAS;YACT,YAAY;YACZ,aAAa;YACb,WAAW;SACZ,CAAC;KACL;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,aAAa,EAAE,eAAe,EAAE,kBAAkB,CAAC,CAAC;KACjF;IACD;QACE,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CACX,SAAS,CAAC,CAAC,EAAE;YACX,QAAQ;YACR,YAAY;YACZ,OAAO;YACP,MAAM;YACN,kBAAkB;YAClB,mBAAmB;SACpB,CAAC;KACL;CACF,CAAC;AAEF,wKAAwK;AAC3J,QAAA,mBAAmB,GAAG,EAAE,CAAC;AAEtC,0GAA0G;AAC1G,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C;;;;GAIG;AACH,SAAgB,qBAAqB,CAAC,IAAY;IAChD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACjD,IAAI,IAAI,CAAC,MAAM,IAAI,2BAAmB;YAAE,MAAM;QAC9C,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACZ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AA8BD,2FAA2F;AAC3F,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEhC,0NAA0N;AAC1N,mIAAmI;AACnI,MAAM,gBAAgB,GAAG,+BAA+B,CAAC;AACzD,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC,mMAAmM;AACnM,SAAS,QAAQ,CAAC,KAAa;IAC7B,MAAM,OAAO,GAAG,KAAK;SAClB,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC;SAC7B,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,IAAI,EAAE,CAAC;IACV,OAAO,OAAO,CAAC,MAAM,GAAG,mBAAmB;QACzC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,GAAG;QAC7C,CAAC,CAAC,OAAO,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAAC,KAAqB;IAClD,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,MAAM,cAAc,GAAG,IAAA,sCAAkB,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACxD,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,kBAAkB;YACzB,OAAO,EAAE,6BAA6B,QAAQ,CAAC,cAAc,CAAC,IAAI;SACnE,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACxE,KAAK,MAAM,OAAO,IAAI,2BAAmB,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK,EAAE,YAAY;gBACnB,OAAO,EAAE,4BAA4B,OAAO,CAAC,IAAI,gBAAgB,GAAG,KAAK;aAC1E,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,IAAI,EAAE,CAAC;IAClD,KAAK,MAAM,GAAG,IAAI,IAAA,6BAAa,EAAC,cAAc,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,EAAE,CAAC;QAChF,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,mBAAmB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,GAAG,CAAC,OAAO,4BAA4B;SACpG,CAAC,CAAC;IACL,CAAC;IACD,IAAI,cAAc,CAAC,MAAM,GAAG,mBAAmB,EAAE,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,oBAAoB,cAAc,CAAC,MAAM,aAAa,mBAAmB,sBAAsB;SACzG,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,EAAE,CAAC;QACjF,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,iBAAiB;YACxB,OAAO,EAAE,eAAe,GAAG,wEAAwE;SACpG,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,CAAC;AACzE,CAAC"}
@@ -33,8 +33,40 @@ export declare const BATCH_MERGE_WAIT_PHASE = "batch-merge-wait";
33
33
  * here too, rather than silently sorting to the end alphabetically.
34
34
  */
35
35
  export declare const STATS_PHASE_ORDER: readonly string[];
36
- /** The bucket a run lands in when no milestone in the run carries a `model=` key. */
37
- export declare const UNKNOWN_MODEL = "unknown";
36
+ /**
37
+ * The bucket a run lands in when no milestone in the run carries a `model=` key.
38
+ *
39
+ * Angle-bracketed so it cannot be forged: {@link canonicalModel} lowercases and peels routing
40
+ * prefixes, and no input it accepts produces a leading `<`. A bare `unknown` was reachable from
41
+ * remote input (`Unknown`, `llmgateway/unknown`, `~~~unknown` all canonicalise to it), which
42
+ * would let anyone who can comment on an issue steer the "we could not attribute this" row.
43
+ */
44
+ export declare const UNKNOWN_MODEL = "<unknown>";
45
+ /**
46
+ * Gateway and provider tokens stripped from a recorded `model=` to find its bucket.
47
+ *
48
+ * Agents record whatever id their CLI was invoked with, and the same model reaches the trail
49
+ * under several spellings depending on how it was routed: `glm-5.3` direct vs
50
+ * `llmgateway/glm-5.3` through a gateway, `z-ai/glm-latest` vs opencode's `~z-ai/glm-latest`
51
+ * alias form, `openrouter-kimi-latest` vs `moonshotai/kimi-latest`. Unbucketed, a single
52
+ * model's runs split across rows and the per-model view — the whole reason the gate records
53
+ * `model=` — answers a question nobody asked (`imboard-ai/ai-dossier#528`).
54
+ *
55
+ * Deliberately an allowlist of *known routing* tokens, never a generic "drop the first
56
+ * segment" rule: merging two genuinely different models is the one error this table cannot
57
+ * survive, so an unrecognised leading segment is always kept.
58
+ */
59
+ export declare const MODEL_ROUTING_PREFIXES: readonly string[];
60
+ /**
61
+ * The bucket key for a recorded `model=` — the model itself, with routing noise removed.
62
+ *
63
+ * Lowercased, any leading `~` (opencode's gateway-alias marker) dropped, then known routing
64
+ * prefixes peeled off. A value that is *only* routing tokens keeps its identity rather than
65
+ * collapsing into every other such id — that is the length guard in {@link stripRoutingPrefix},
66
+ * not the empty-string fallback below, which is reachable only for an id that was empty or
67
+ * whitespace to begin with.
68
+ */
69
+ export declare function canonicalModel(raw: string): string;
38
70
  /** The group a milestone lands in when it carries no `run=` id. */
39
71
  export declare const UNKNOWN_RUN = "unknown-run";
40
72
  /**
@@ -94,9 +126,23 @@ export interface PhaseAggregate {
94
126
  /** Samples below zero, i.e. from clock skew — a median over these is not meaningful. */
95
127
  negative_samples: number;
96
128
  }
97
- /** Per-`model=` view of whole-run totals — the point of recording `model=` at the gate. */
129
+ /**
130
+ * Per-`model=` view of whole-run totals and outcomes — the point of recording `model=`.
131
+ *
132
+ * Duration alone answers "how long", never "which models finish the work". Both are needed
133
+ * to route a tier at a model with any evidence behind it, so the bucket carries the run's
134
+ * terminal state alongside its span.
135
+ */
98
136
  export interface ModelAggregate {
137
+ /** Canonical bucket key — see {@link canonicalModel}. */
99
138
  model: string;
139
+ /**
140
+ * The distinct raw `model=` spellings that folded into this bucket, sorted.
141
+ *
142
+ * Always populated (a bucket with one spelling lists that one), so a fold is visible in
143
+ * the output rather than silently reshaping someone's cohort.
144
+ */
145
+ aliases: string[];
100
146
  runs: number;
101
147
  /** Runs of this model whose total was measurable; the rest are excluded below. */
102
148
  samples: number;
@@ -104,6 +150,24 @@ export interface ModelAggregate {
104
150
  min_total_seconds: number | null;
105
151
  max_total_seconds: number | null;
106
152
  negative_samples: number;
153
+ /**
154
+ * Runs whose last milestone was a delivery phase at `done` — see {@link DELIVERED_PHASES}.
155
+ */
156
+ delivered: number;
157
+ /** Runs whose last milestone was `blocked`, whatever the phase. */
158
+ blocked: number;
159
+ /** Everything else: still in flight, parked awaiting merge, partial, or fenced. */
160
+ unfinished: number;
161
+ /**
162
+ * `delivered / runs`, in [0, 1] — a **floor**, not a rate.
163
+ *
164
+ * Every run is in the denominator, including ones still in flight, parked at
165
+ * `awaiting-merge`, `partial`, or superseded by a takeover fence. A model whose runs are
166
+ * mid-flight therefore reads low until they land, and a fenced generation counts against
167
+ * the same model whose successor delivered the work. Compare buckets over a closed window,
168
+ * or read `unfinished` alongside this.
169
+ */
170
+ delivery_rate: number;
107
171
  }
108
172
  /** The two cross-run rollups: per-phase spread, and whole-run totals bucketed by model. */
109
173
  export interface StatsAggregates {
@@ -149,6 +213,23 @@ export interface IssueTrail {
149
213
  }
150
214
  /** One aggregate row's skew note, or null when every sample was forwards. */
151
215
  export declare function skewNote(negativeSamples: number): string | null;
216
+ /**
217
+ * Most folded spellings named inline on a model row before the rest become a count.
218
+ *
219
+ * An attacker who can comment on the issue can post arbitrarily many distinct `model=`
220
+ * spellings that all canonicalise to one bucket, so an uncapped list is an unbounded cell —
221
+ * the DoS {@link MAX_CELL_LENGTH} exists to stop, reached by another route.
222
+ */
223
+ export declare const MAX_NAMED_ALIASES = 3;
224
+ /**
225
+ * The folded-alias note for a model row, or null when nothing folded.
226
+ *
227
+ * Pure and exported (like {@link skewNote}) so the disclosure guarantee — a merge is always
228
+ * visible, never silent — is testable, rather than living only in a private renderer.
229
+ * Values are milestone-derived and therefore remote: each is passed through
230
+ * {@link renderValue} before it can reach a terminal.
231
+ */
232
+ export declare function modelNote(model: ModelAggregate): string | null;
152
233
  export interface StatsInput {
153
234
  trails: IssueTrail[];
154
235
  /** Issues that could not be read at all. */
@@ -1 +1 @@
1
- {"version":3,"file":"runstate-stats.d.ts","sourceRoot":"","sources":["../src/runstate-stats.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runstate-stats.d.ts","sourceRoot":"","sources":["../src/runstate-stats.ts"],"names":[],"mappings":"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"}