specguard-mcp 0.1.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 (38) hide show
  1. package/README.md +366 -0
  2. package/dist/bin/specguard-mcp.d.ts +2 -0
  3. package/dist/bin/specguard-mcp.js +35 -0
  4. package/dist/bin/specguard-mcp.js.map +1 -0
  5. package/dist/src/config.d.ts +108 -0
  6. package/dist/src/config.js +172 -0
  7. package/dist/src/config.js.map +1 -0
  8. package/dist/src/errors.d.ts +60 -0
  9. package/dist/src/errors.js +64 -0
  10. package/dist/src/errors.js.map +1 -0
  11. package/dist/src/index.d.ts +5 -0
  12. package/dist/src/index.js +5 -0
  13. package/dist/src/index.js.map +1 -0
  14. package/dist/src/server.d.ts +28 -0
  15. package/dist/src/server.js +113 -0
  16. package/dist/src/server.js.map +1 -0
  17. package/dist/src/support/run-command.d.ts +86 -0
  18. package/dist/src/support/run-command.js +322 -0
  19. package/dist/src/support/run-command.js.map +1 -0
  20. package/dist/src/support/specguard-api.d.ts +11 -0
  21. package/dist/src/support/specguard-api.js +157 -0
  22. package/dist/src/support/specguard-api.js.map +1 -0
  23. package/dist/src/tools/args.d.ts +48 -0
  24. package/dist/src/tools/args.js +66 -0
  25. package/dist/src/tools/args.js.map +1 -0
  26. package/dist/src/tools/index.d.ts +33 -0
  27. package/dist/src/tools/index.js +34 -0
  28. package/dist/src/tools/index.js.map +1 -0
  29. package/dist/src/tools/lint-intent-annotations.d.ts +45 -0
  30. package/dist/src/tools/lint-intent-annotations.js +342 -0
  31. package/dist/src/tools/lint-intent-annotations.js.map +1 -0
  32. package/dist/src/tools/repository-overview.d.ts +424 -0
  33. package/dist/src/tools/repository-overview.js +797 -0
  34. package/dist/src/tools/repository-overview.js.map +1 -0
  35. package/dist/src/tools/types.d.ts +111 -0
  36. package/dist/src/tools/types.js +2 -0
  37. package/dist/src/tools/types.js.map +1 -0
  38. package/package.json +44 -0
@@ -0,0 +1,797 @@
1
+ import { requireApiConfig } from "../config.js";
2
+ import { ApiError } from "../errors.js";
3
+ import { getJson } from "../support/specguard-api.js";
4
+ import { optionalBoolean, optionalString } from "./args.js";
5
+ /**
6
+ * `GET /api/v1/repository` as a tool — shipped today in the platform
7
+ * (`specguard/config/routes.rb`, `Api::V1::RepositoriesController`).
8
+ *
9
+ * == Why this endpoint is the right second tool
10
+ *
11
+ * It is the agent-readable half of the repository dashboard, and its controller
12
+ * says why it exists in the first line of its own comment: *without* it "an
13
+ * agent can learn the suite's size only by running the suite and POSTing it — it
14
+ * cannot ask". That is the cold-start requirement in Project Goals (SPGD-1) and
15
+ * it is already met server-side; what was missing is a way for an agent to reach
16
+ * it without writing HTTP and Bearer plumbing into its prompt. This tool is
17
+ * exactly that gap and nothing more.
18
+ *
19
+ * One request answers what the suite is, what the last CI run cost, where the
20
+ * time went (by file, by directory, by individual example), and how the suite
21
+ * has grown — so the tool is described in those terms rather than as "get
22
+ * repository", which is not a question anybody asks.
23
+ *
24
+ * == The response is passed through, not re-modelled
25
+ *
26
+ * Every figure in that body is annotated in the controller with the reason for
27
+ * its exact shape, and several of those reasons are about honesty rather than
28
+ * convenience: `null` where a value was not measured (never a zero that would
29
+ * read as a measurement), counts served beside the figures they are the
30
+ * denominator of, `tie_break_served: false` admitting the array's order is not
31
+ * reproducible from the fields served. Any reshaping here — flattening,
32
+ * defaulting a null to 0, re-sorting a list — would discard the distinction the
33
+ * controller spent that care preserving. So the body goes back as it arrived.
34
+ *
35
+ * == `branch` narrows the history, and only the history
36
+ *
37
+ * That asymmetry is a documented property of the endpoint rather than a
38
+ * surprise, and it is stated in the schema because an agent that has not read
39
+ * the controller would otherwise read `latest_run` as belonging to the branch it
40
+ * asked for.
41
+ *
42
+ * == `spec_directory` opens an area the ranking only names
43
+ *
44
+ * `latest_run.spec_directories` ranks the heaviest directories and is served
45
+ * unconditionally, so an agent can already see WHERE the time went — but the
46
+ * ranking is at the area grain and cannot say which files inside the area spent
47
+ * it. `?spec_directory=` is the endpoint's answer to that, and the server has
48
+ * served it since the controller took `RequestedSpecDirectoryParam`: the key
49
+ * `latest_run.spec_directory_files` opens from `null` to a populated object the
50
+ * moment the parameter is sent. This bridge withheld it by not offering the
51
+ * parameter, which made the ranking a dead end for every agent that reached it
52
+ * through MCP.
53
+ *
54
+ * The parameter is forwarded and nothing about it is interpreted here. The
55
+ * server owns the whole meaning of the answer — `null` for "you did not ask",
56
+ * `rows: []` for "asked, matched nothing" (a renamed or deleted directory is an
57
+ * ordinary way to arrive, not an error), and a non-String shape read as no ask
58
+ * at all. A blank one sends no parameter, exactly as `branch` does and for the
59
+ * same reason: `getJson` omits an `undefined` value, so `optionalString` is the
60
+ * whole of the blank-handling in both cases.
61
+ *
62
+ * == `spec_file` and `repeated_description` are the same argument, twice more
63
+ *
64
+ * At that point the endpoint served a FOUR-rung drill-down ladder and this
65
+ * bridge forwarded two of them. The sentence above — "this bridge withheld it
66
+ * by not offering the parameter, which made the ranking a dead end for every
67
+ * agent that reached it through MCP" — was true verbatim of two further rungs,
68
+ * and the block was doubled: `additionalProperties: false` REJECTED the
69
+ * argument before the call, and `run()` would have dropped it anyway. Both
70
+ * rankings are served unconditionally to every caller, so the agent was shown
71
+ * the door and denied the handle:
72
+ *
73
+ * `latest_run.spec_files` → `spec_file_examples` (`spec_file`)
74
+ * `latest_run.repeated_descriptions` → `repeated_description_examples` (`repeated_description`)
75
+ *
76
+ * Both follow `spec_directory` exactly — one schema property, one
77
+ * `optionalString` call, one query key — because the pass-through above means
78
+ * the drilled keys populate the moment the parameter is forwarded, with no
79
+ * serializer or rendering work anywhere on this side.
80
+ *
81
+ * The fourth rung is worth naming separately: `repeated_description_examples`
82
+ * is reachable from NO other key. `slowest_examples` is the run-wide top ten
83
+ * and a group's members are usually absent from it entirely, and walking
84
+ * `spec_file_examples` over each path in the group's `files_seen` is N
85
+ * unrelated lists each cut at 50 BY DURATION, with no guarantee any of the
86
+ * group's members survive the cut in any of them. Withholding this one
87
+ * parameter withheld the capability, not merely a shortcut to it.
88
+ *
89
+ * == `unstable_test` is the fifth rung, and the same argument once more
90
+ *
91
+ * `unstable_tests.rows` → `unstable_tests.unstable_test_runs` (`unstable_test`)
92
+ *
93
+ * A flakiness row says `run_count: 30`, `failed_run_count: 4`,
94
+ * `outcome_words: ["failed", "passed"]`. Those three figures are IDENTICAL for
95
+ * two windows that call for opposite work: four failures in runs 27–30 is a
96
+ * REGRESSION, and the work is to find the commit between run 26 and run 27;
97
+ * four failures in runs 3, 11, 19 and 26 is genuine FLAKINESS, where there is
98
+ * no culprit commit and the work is quarantine or shared state. The RUN
99
+ * SEQUENCE is the only thing that separates them, and it is derivable from
100
+ * nothing else this bridge returns — `history` has no per-test grain, and both
101
+ * example drill-ins carry `outcome` for the LATEST RUN alone. So this is the
102
+ * fourth rung's argument again: withholding the parameter withheld the
103
+ * capability rather than a shortcut to it, and an agent told to "fix the flaky
104
+ * tests" hunts nondeterminism in tests that fail deterministically.
105
+ *
106
+ * TWO THINGS DIFFER FROM ITS FOUR SIBLINGS, and both are stated in the schema
107
+ * because neither is guessable from the ladder. The answer lands INSIDE the
108
+ * flakiness block — `unstable_tests.unstable_test_runs`, not under
109
+ * `latest_run.*` where the two example drill-ins live — and `branch` is a hard
110
+ * PREREQUISITE rather than a suggestion: `unstable_tests` is served only for a
111
+ * branch-narrowed window, so this parameter sent alone yields no block at all
112
+ * to drill into. Every sibling works on a plain call; this one does not.
113
+ *
114
+ * THE SEQUENCE RUNS NEWEST RUN FIRST, and that is stated in the schema too,
115
+ * because this is the one list on this tool where the direction IS the payload.
116
+ * The window is `Repository#recent_test_runs`, ordered `created_at: :desc`, and
117
+ * `SpecObservation.outcome_sequence_in` PRESERVES that order rather than
118
+ * re-sorting it. Read front-to-back as run 1 → run N, the regression above
119
+ * reads as four failures at the START of the window that have passed since —
120
+ * a fixed flake, the exact inversion of the truth, with no error anywhere to
121
+ * signal it. The consequence worth stating outright: the run a failure STARTED
122
+ * at is the LAST row of the leading failed block, not the first. The 200 cap
123
+ * takes rows off the OLD end for the same reason, so a truncated sequence is
124
+ * still the recent runs.
125
+ *
126
+ * The run a row belongs to is read off its `commit_sha` / `test_run_id` and
127
+ * NEVER off its index. `rows.length` is not the window's `run_count`: a run
128
+ * that recorded nothing under the description contributes no row, and a
129
+ * description carried by two examples in one run contributes two. Same
130
+ * direction as `history` is not same index into it.
131
+ *
132
+ * == `commit_sha` is not a sixth rung — it moves the ladder
133
+ *
134
+ * The five parameters above narrow what is served ABOUT a run that
135
+ * `Api::V1::RepositoriesController#latest_test_run` had already chosen. This
136
+ * one CHOOSES THAT RUN, which the controller states in those terms: `?branch=`
137
+ * asks about a SERIES, this asks WHICH RUN. It is read once, in that memo, so
138
+ * every run-grain block moves together — `latest_run` and its rollups, the
139
+ * four RUN-GRAIN drill-ins (`spec_directory_files`, `spec_file_examples`,
140
+ * `repeated_description_examples` and `unannotated_examples`, the flag-style
141
+ * rung documented below), `shards`, both growth windows and
142
+ * `previous_test_run`.
143
+ *
144
+ * That is four of the FIVE drill-ins on this tool, and the excluded one is
145
+ * worth naming because it is the composition an agent will actually try:
146
+ * `unstable_test_runs` is read over the BRANCH WINDOW (`history_runs`), not off
147
+ * the anchored run, so it does not move with this parameter. Sent together,
148
+ * `?commit_sha=` and `?unstable_test=` answer about different things on
149
+ * purpose — one run, and the window that run sits in.
150
+ *
151
+ * Withholding it here withheld a capability that the tool was ALREADY
152
+ * DISCLOSING THE NEED FOR. `branch`'s own description names the failure:
153
+ * "`latest_run` always names the repository's newest run, which on a busy repo
154
+ * may be on another branch". And `unstable_test`'s teaches `commit_sha` as the
155
+ * canonical run handle — read the run off each row's `commit_sha`, never off
156
+ * its index — while every `unstable_test_runs` row carries one. The bridge
157
+ * handed shas out and accepted none back, with `additionalProperties: false`
158
+ * refusing the argument before a request was made, so the agent that most needs
159
+ * it — one that edits tests, pushes, waits for CI and re-reads SpecGuard to
160
+ * check its own work — could not work around it. On a repository where anything
161
+ * else pushed in between, it was silently answered about another commit.
162
+ *
163
+ * `renderText` is the whole body verbatim, so this bridge has been SERVING the
164
+ * `run_anchor` block since the API shipped it — with its only informative state
165
+ * structurally unreachable. `requested_commit_sha` was always `nil` through the
166
+ * bridge, so every MCP call read `source: "default"`, `resolved: true`. A
167
+ * disclosure block cannot disclose a fallback to a client that cannot make the
168
+ * ask that falls back.
169
+ *
170
+ * TWO THINGS ARE STATED IN THE SCHEMA because neither is guessable from the
171
+ * ladder. `history` is NOT re-anchored — it stays the recent runs, narrowed
172
+ * only by `branch` — so the `history[0] == latest_run` identity holds on a
173
+ * default call and is NOT expected to hold under an explicit ask; that is the
174
+ * contract, and a client needing the identity back omits the parameter. And an
175
+ * unknown sha DOES NOT 404: a stale bookmark, a pruned run and a commit whose
176
+ * CI never reported are ordinary ways to arrive, so the endpoint falls back to
177
+ * the newest run and SAYS SO (`source: "requested"`, `resolved: false`, the raw
178
+ * ask kept in `requested_commit_sha`, `commit_sha`/`branch` naming what was
179
+ * actually served). Nothing else about the response looks unusual, which is why
180
+ * the schema tells the agent to read `run_anchor.resolved`.
181
+ *
182
+ * == `unannotated_examples` is the one that is a FLAG rather than a name
183
+ *
184
+ * `latest_run.total_specs` − `annotated_specs` → `latest_run.unannotated_examples`
185
+ *
186
+ * The argument for forwarding it is the ladder's again — the parameter was
187
+ * withheld by not being offered, `additionalProperties: false` refused it before
188
+ * a request was made, and `renderText` has therefore been serving
189
+ * `unannotated_examples: null` (the server's "you did not ask" spelling) to a
190
+ * client structurally incapable of asking. What is new is WHOSE question it
191
+ * answers. This is the adoption metric of Project Goals (SPGD-1): an agent told
192
+ * to raise annotation coverage was served `annotated_ratio` and a `null`, so it
193
+ * learned how far it had to go and could not name a single test to annotate. A
194
+ * plain `curl` user could.
195
+ *
196
+ * ONE THING DIFFERS FROM ALL SIX SIBLINGS, and it is the reason this forward is
197
+ * not a copy of the previous five. Every parameter above names a WHICH — which
198
+ * branch, which commit, which area, which file, which description, which test —
199
+ * because each opens the rows behind a LINE of a ranking the client had already
200
+ * read. This one opens a POPULATION rather than a pick: the figure it drills out
201
+ * of is a SUBTRACTION on the run itself, and a subtraction has no rows to have
202
+ * keys, so there is nothing for the ask itself to NAME. So the server reads only
203
+ * whether the parameter was NAMED, which `RequestedUnannotatedExamplesParam`
204
+ * states outright — the value is not read, and THAT INCLUDES `false`:
205
+ * `?unannotated_examples=false` opens the block exactly as `=true` does.
206
+ *
207
+ * That is a hazard on this side rather than a curiosity, and it is why the
208
+ * argument is a BOOLEAN coerced with `optionalBoolean` and the query key is
209
+ * built rather than stringified. `getJson` omits only `undefined`, so
210
+ * `String(false)` would put `unannotated_examples=false` on the URL and open a
211
+ * hundred-row block for the one caller who asked explicitly for it NOT to be
212
+ * opened — the exact misreading the server's guard file exists to prevent, made
213
+ * on the other side of the wire. The key is sent as `"true"` on an affirmative
214
+ * ask and is `undefined` otherwise, so declining and omitting are the same wire
215
+ * request. That matches how every other parameter here is declined: none of them
216
+ * has an "off" value either.
217
+ *
218
+ * WHICH POPULATION IT OPENS IS NOT FIXED, and that is the half this file first
219
+ * got wrong. `specguard` `55e3a09` made `?spec_file=` and `?spec_directory=`
220
+ * narrow this block when either rides along with the flag — the same two
221
+ * parameters that open their own drill-ins beside it — so the ask has FOUR
222
+ * shapes rather than one: the whole run, one file, one area, or the AND of a
223
+ * file and an area. The flag still names nothing, because the narrowing is named
224
+ * by those two parameters and not by this one; what changed is that "the
225
+ * population" is no longer a definite article. `SpecObservation.unannotated_in`
226
+ * appends both predicates to the WHERE that the `COUNT(*) OVER ()` window of
227
+ * `UNANNOTATED_POPULATION_COUNTS` rides, so `recorded_count` counts the NARROWED
228
+ * population rather than the run's — and the controller echoes `spec_file` and
229
+ * `spec_directory` back INSIDE the block, as the server read them and `null`
230
+ * when not sent, for exactly that reason: `recorded_count` is the one figure
231
+ * here a client reconciles against `total_specs - annotated_specs`, and a
232
+ * silently narrowed count breaks that reconciliation. The echo is what makes the
233
+ * count's population readable.
234
+ *
235
+ * ⭐ AND THE ONE ASK NOW OPENS TWO BLOCKS, THE SECOND OF WHICH IS A RANKING.
236
+ * `specguard` `9df1b3d` added `latest_run.unannotated_directories` under this
237
+ * SAME flag — no new parameter, no new value, nothing extra for a client to send
238
+ * — so every call that already asks for the worklist is already being served the
239
+ * map beside it. It answers what the worklist cannot: the worklist is WHICH
240
+ * TESTS to go and annotate, and the map is WHERE THE DEBT IS, rolled up by area,
241
+ * which is what a reader picks the next `?spec_directory=` narrowing FROM. That
242
+ * is the same shape `spec_directory`'s own description states one parameter
243
+ * over — one ask, several blocks, each in its own grain — so it is said here in
244
+ * that form rather than in a new one.
245
+ *
246
+ * TWO CAPS UNDER ONE ASK, and the difference is the KIND of list rather than the
247
+ * grain. `UNANNOTATED_EXAMPLES_LIMIT` is 100 and `UNANNOTATED_DIRECTORIES_LIMIT`
248
+ * is 10, and the server's constant says why: the hundred caps a WORKLIST, sized
249
+ * for a batch somebody opens, annotates and re-delivers in one sitting; the ten
250
+ * caps a RANKING, which exists only to name where the debt is concentrated, and
251
+ * a reader who cannot pick from ten areas is not helped by eighty. The ORDERS
252
+ * differ for the same reason — the worklist is file-navigable, and the map is
253
+ * `unannotated_count DESC, path ASC`: ranked by debt, with path as a tiebreak
254
+ * only. A fully-annotated area is a REAL ROW here with `unannotated_count: 0`
255
+ * against its real `recorded_count`, never an omission. Those rows sort last
256
+ * COLLECTIVELY, so on a run with more areas than the cap they are cut and never
257
+ * seen, but on a run inside the cap they ARE LISTED and listed is correct. So
258
+ * `rows.size` is not a count of areas WITH debt — read each row's
259
+ * `unannotated_count` for that; and `directory_count` counts EVERY area the run
260
+ * touched, not every area with debt, and never `rows.size` either.
261
+ *
262
+ * ⭐ THE TWO KEYS OF THIS ONE BLOCK DISAGREE IN TWO PLACES, ON PURPOSE, and both
263
+ * are counting traps rather than curiosities. `serialized_unannotated_directories`
264
+ * discloses both at unusual length precisely because the machine-readable
265
+ * consumer is the one that would otherwise discover them by arithmetic — and
266
+ * this bridge IS that consumer.
267
+ *
268
+ * (a) SCOPE. `unannotated_examples.recorded_count` NARROWS with `?spec_file=` /
269
+ * `?spec_directory=`; `unannotated_directories` stays WHOLE-RUN under both. So
270
+ * under a narrowing the worklist's `recorded_count` is NOT the sum of the map's
271
+ * `unannotated_count`s, AND NEITHER FIGURE IS WRONG: the first counts the one
272
+ * area or file you named, the second ranks the whole run. The map is whole-run
273
+ * BY DESIGN, because it is the thing a client picks a narrowing FROM and a map
274
+ * that narrowed to the area you had already picked would answer nothing — one
275
+ * row, echoing the parameter back. The sum is short of the run's total whenever
276
+ * `directory_count > rows.size` besides, narrowing or no narrowing. This is why
277
+ * the reconciliation rule above is scoped to the WORKLIST's count and to that
278
+ * count alone.
279
+ *
280
+ * (b) NULL VERSUS EMPTY. On a run that recorded no per-example rows at all, with
281
+ * the flag sent, `unannotated_examples` is a PRESENT block with `rows: []` and
282
+ * `recorded_count: 0`, while `unannotated_directories` is `null`. That is not an
283
+ * inconsistency to iron out. The sibling's zero is ambiguous by construction —
284
+ * "fully annotated" and "recorded nothing at all" reach the same
285
+ * `recorded_count: 0` there — and this key is how a client tells them apart: a
286
+ * PRESENT map beside that zero means the run has a per-area grain and the zero
287
+ * is the SUCCESS state; a `null` map means the run recorded nothing and the zero
288
+ * is an ABSENCE of data. Serving `rows: []` here instead would spend a
289
+ * distinction a client has no other way to make.
290
+ *
291
+ * THE `commit_sha` ROSTERS above and in README.md CORRECTLY STAY AT FOUR, and
292
+ * the reason is the roster's UNIT, not anything about this block's shape. That
293
+ * roster carries ONE REPRESENTATIVE KEY PER DRILL-IN PARAMETER, not one entry
294
+ * per response key: `spec_directory` opens THREE blocks (see its own
295
+ * description below), yet only `spec_directory_files` is on the roster —
296
+ * `directory_run_file_growth` and `directory_runtime_file_growth` are absent
297
+ * from it for exactly this reason, and the guard in
298
+ * `test/tools/repository-overview.test.ts` enforces it that way, deriving the
299
+ * obligation from the schema's PARAMETERS and mapping each to the single key it
300
+ * represents. `unannotated_directories` is a SECOND BLOCK OF AN EXISTING
301
+ * PARAMETER'S ASK and adds no parameter, so it is not a roster entry. It is at
302
+ * run grain and does re-anchor, and is covered there by "`latest_run` and its
303
+ * rollups".
304
+ *
305
+ * FOUR THINGS ARE STATED IN THE SCHEMA. It is at RUN GRAIN, so it moves with
306
+ * `commit_sha` like everything else under `latest_run` — unlike `unstable_test`,
307
+ * which does not. A FULLY-ANNOTATED run answers `rows: []` /
308
+ * `recorded_count: 0` with 200, never a 404 and never the no-ask `null`: that is
309
+ * the state the metric exists to reach, so an agent walking a repository to
310
+ * completion must see the block go empty rather than watch it vanish at the
311
+ * moment it succeeded and be unable to tell that from its own parameter having
312
+ * been dropped. The pair above — that `spec_file`/`spec_directory` narrow this
313
+ * population when they ride along, and are echoed back so the client can tell
314
+ * which population `recorded_count` is of. And the second block this one ask
315
+ * opens, with its own cap, its own ranking order and both of the disagreements
316
+ * above, because a pass-through `renderText` puts that key in front of every
317
+ * agent whether or not anything here has named it.
318
+ *
319
+ * == `delivery_health` and `credential_health` are that rule applied to the
320
+ * == blocks that say WHETHER TO BELIEVE THE REST
321
+ *
322
+ * The sentence directly above is the whole argument, and until now it was
323
+ * unapplied at the top level of the very same body. `Api::V1::RepositoriesController`
324
+ * serves both blocks UNCONDITIONALLY — it says "SERVED ON EVERY RESPONSE" in
325
+ * capitals at both sites — so `renderText` has been handing them to every MCP
326
+ * agent since they shipped, while this description enumerated the response in
327
+ * exhaustive detail and named neither.
328
+ *
329
+ * NOTHING HERE OPENS THEM, which is exactly why nothing here caught the
330
+ * omission. Every other block this file discusses arrived attached to a
331
+ * parameter, and the roster guard in `test/tools/repository-overview.test.ts`
332
+ * derives its obligation from `inputSchema.properties` — so a block that adds no
333
+ * property is structurally invisible to it, as `unannotated_directories` was one
334
+ * section up. The only other check on this string is a `length >= 80` floor. The
335
+ * schema is UNTOUCHED by this change for that reason: the two are response
336
+ * blocks, not asks, and a reader must not be able to infer a flag that does not
337
+ * exist.
338
+ *
339
+ * WHAT THEY ANSWER IS "WHY IS THIS DATA LYING TO ME", which is the one question
340
+ * an agent cannot answer from any other key here. `delivery_health` is the
341
+ * staleness verdict — `refusing?`, `last_rejection_at`, and the endpoint's own
342
+ * refusal reasons per retained delivery — and without it a `latest_run` that is
343
+ * days old reads as a suite nobody ran rather than a suite the platform stopped
344
+ * accepting. `credential_health` covers the one failure `delivery_health`
345
+ * structurally cannot: a 401 resolves no repository and writes no
346
+ * `IngestRejection` row, so an auth-broken pipeline leaves every rejection
347
+ * figure at zero. It reports the state anyway because it need not observe the
348
+ * 401 — it owns the key row and stamped the instant the token was retired.
349
+ *
350
+ * A QUIET ANSWER IS A POSITIVE FINDING, and that is stated outright rather than
351
+ * left to be inferred, on the controller's own reasoning at both sites: an agent
352
+ * that is served `refusing: false` must be able to tell "nothing was refused"
353
+ * from "SpecGuard does not track that", and the difference is not visible in the
354
+ * value. A human reads the dashboard panels for this; an agent reads only what
355
+ * this string told it to look for.
356
+ *
357
+ * TWO FURTHER KEYS ARE NAMED HERE FOR THE SAME REASON, both found by taking the
358
+ * membership question as a GREP over the endpoint's top-level keys rather than
359
+ * as a reading of this file. `api_key.last_used_at` is the claim the two health
360
+ * blocks exist to CORRECT — it is stamped on the way in, before the payload is
361
+ * looked at, so a repository whose every delivery is refused serves its freshest
362
+ * timestamp beside its stalest run, and the controller answers that with
363
+ * `acceptance_reported_by` / `rotation_reported_by` naming the keys that answer
364
+ * what it cannot. Naming the correction and not the claim would have been half a
365
+ * sentence. And the truncation contract, which is NOT the uniform family it looks like from the
366
+ * key names: only eight lists have a `*_window` sibling at all, MOST lists under `latest_run`
367
+ * carry an inline `limit` beside `rows` instead, four of those windows serve no bound of their
368
+ * own, `rejections_window` serves a bound and no order, and the lists this census found carrying
369
+ * no bound anywhere are `credential_health.keys` and BOTH `latest_run.shards` lists (`rows`,
370
+ * ranked slowest-first off `TestRun#shard_durations`, and `per_shard`, in delivery order off
371
+ * `#shard_reports`), each complete by construction. So the rule is stated in the direction that
372
+ * stays true as the endpoint grows, and whose correctness does NOT depend on that list being
373
+ * exhaustive: a bound BESIDE a list means a page, and no bound means the whole set. Quantifying
374
+ * over the capped cases instead — "every ranking is capped, except..." — is what put a false
375
+ * universal here twice, because the census that produced it counted lists that HAVE a bound and
376
+ * never asked how many have none. It sends an agent looking for a disclosure that does not exist
377
+ * and leaves it unable to tell "complete by construction" from "silently cut", which is the exact
378
+ * misreading the two blocks above were named to prevent.
379
+ *
380
+ * == `suite_size_measured`, `shard_count` and `timed_shard_count` are that rule
381
+ * == applied a FOURTH time, to the keys that say WHETHER TWO ROWS MAY BE
382
+ * == DIFFERENCED AT ALL
383
+ *
384
+ * The same argument, the same blind spot, the same remedy — and the remaining
385
+ * unapplied case. `serialized_history_row` in `Api::V1::RepositoriesController`
386
+ * puts all three on EVERY `history[]` row, and `suite_size_measured` is served a
387
+ * SECOND time on `latest_run`, deliberately from the same predicate so that a
388
+ * single response body cannot describe one row twice and disagree with itself
389
+ * (in the unfiltered window `history[0]` IS `latest_run`). Until now this string
390
+ * named none of the three, while selling history differencing outright: the
391
+ * `branch` parameter below tells an agent that consecutive all-branch rows "must
392
+ * not be differenced", which teaches the differencing and names only the one
393
+ * hazard that happens to be expressible as a parameter.
394
+ *
395
+ * THEY ARE ONE BLOCK BECAUSE THEY ARE ONE QUESTION. `suite_size_measured` says
396
+ * whether a row is a measurement at all; `shard_count` is the denominator of
397
+ * `total_specs` (a SUM over the shards RECORDED, and what `TestRun#assembled_like?`
398
+ * reads to decide differenceability); `timed_shard_count` is the denominator of
399
+ * `duration_seconds` (a MAX over the shards that REPORTED, whose absence lets a
400
+ * client report the controller's "70% speedup produced entirely by telemetry
401
+ * loss"). An agent that differences two rows without all three gets a number
402
+ * wearing a SHA and a timestamp that make it read as a checked fact.
403
+ *
404
+ * THE LAST SENTENCE OF THE DESCRIPTION WAS ALSO WRONG IN ITS REACH, not merely
405
+ * silent. "A null is 'not measured', never zero" routes a reader to NULLNESS as
406
+ * the measured/not-measured signal, but `TestRun#suite_size_measured?` is
407
+ * `total_specs_count.to_i.positive?` — so a run that reported zero tests serves a
408
+ * NON-NULL `total_specs: 0` beside `suite_size_measured: false`. A reader obeying
409
+ * the string's own stated rule reads that row as "measured, 0 tests" where the
410
+ * server says "not a measurement". The sentence's true content about nulls is
411
+ * kept; what is added is the bound, that the rule does not run backwards. The
412
+ * controller serialized the boolean rather than leaving the client to re-derive
413
+ * it from `total_specs` precisely so the two could not drift — and this bridge's
414
+ * silence was forcing every MCP agent into exactly that re-derivation.
415
+ *
416
+ * THE SCHEMA IS UNTOUCHED, for the reason stated one section up: these are
417
+ * RESPONSE keys, not asks, and a reader must not be able to infer a flag that
418
+ * does not exist. And NO GUARD CAN CATCH A REGRESSION OF THIS CHANGE — every
419
+ * roster guard in `test/tools/repository-overview.test.ts` opens with
420
+ * `inputSchema.properties` and derives its obligation from a PARAMETER, so a
421
+ * block that adds none is invisible to them by construction, and the only other
422
+ * check on this string is a `length >= 80` floor. That is why the reasoning is
423
+ * recorded here at this length: this comment is the only thing standing between
424
+ * these three keys and a silent re-wording that drops them again.
425
+ */
426
+ const getRepositoryOverview = {
427
+ name: "get_repository_overview",
428
+ title: "Get SpecGuard repository overview",
429
+ description: "Ask SpecGuard what a repository's test suite looks like, WITHOUT running it. Returns the " +
430
+ "repository the configured API key resolves to, its latest CI run (total and annotated spec " +
431
+ "counts, annotated ratio, wall-clock and per-shard cost), where that run spent its time " +
432
+ "(heaviest spec files, heaviest directories, slowest individual examples with file and line), " +
433
+ "which descriptions are repeated across the suite (the overcoverage ranking: one description " +
434
+ "carried by many examples, and which files it is spread over), " +
435
+ "which areas of the suite grew or shrank and which got slower or faster since the previous run " +
436
+ "on the same branch (the per-area comparisons, at BOTH the example-count grain and the " +
437
+ "runtime grain — an area where an existing spec was made slow gains no examples and appears " +
438
+ "only in the runtime one), " +
439
+ "the recent run history for growth over time, and the branches that have runs. " +
440
+ "Pass `branch` for two more: which tests fail intermittently rather than consistently (the " +
441
+ "cross-run flakiness ranking) and how the areas moved across the whole branch window rather " +
442
+ "than between the last two runs. " +
443
+ "Four of those rankings open: pass `spec_directory` to see the spec files inside one of the " +
444
+ "heaviest directories — and, in the same answer, which of those files grew and which got " +
445
+ "slower — `spec_file` to see the individual examples inside one of the heaviest files, " +
446
+ "`repeated_description` to see the examples that all share one repeated description, or " +
447
+ "`unstable_test` (alongside `branch`) to see one flaky test's outcome run by run, which is " +
448
+ "the only way to tell a regression from genuine flakiness. " +
449
+ "All of those describe the repository's NEWEST run, which on a busy repository may be another " +
450
+ "branch's: pass `commit_sha` to be answered about ONE named run instead — after pushing a " +
451
+ "commit and waiting for CI, say — then read `run_anchor` to confirm which run you were served, " +
452
+ "because an unknown sha falls back to the newest rather than erroring. " +
453
+ "Pass `unannotated_examples: true` to list the individual tests SpecGuard CANNOT see — the " +
454
+ "examples behind the annotated ratio, which is otherwise a percentage with nothing to act on — " +
455
+ "and, in the same answer, which AREAS of the suite carry the most of them. " +
456
+ "TWO BLOCKS COME BACK ON EVERY RESPONSE — no parameter to pass, no flag to set — and they " +
457
+ "answer what everything above silently depends on: is SpecGuard still being fed? " +
458
+ "`delivery_health` is why the figures may be STALE: `refusing` is a comparison of stamps, not a " +
459
+ "live wire — true when the newest refusal is newer than the newest ACCEPTED run, and true when " +
460
+ "nothing has ever been accepted — so read it with `last_rejection_at` and judge recency " +
461
+ "yourself, because a repository refused once and quiet since still answers true. Each retained " +
462
+ "rejection carries the endpoint's own reasons and, where the client reported one, the client " +
463
+ "version that sent it. A `latest_run` from days ago beside a live rejection stream is a suite " +
464
+ "SpecGuard STOPPED ACCEPTING, not a suite nobody ran. " +
465
+ "`credential_health` covers the break that one structurally CANNOT see — a rejected key " +
466
+ "resolves no repository and writes no rejection row, so an authentication-broken pipeline is " +
467
+ "invisible to every rejection figure — by naming any key that was ROTATED and has not " +
468
+ "authenticated since: a secret some pipeline has not picked up. " +
469
+ "Read a quiet answer as a FINDING rather than a gap: `refusing: false` is 'nothing was refused' " +
470
+ "and `rotated_and_unused: false` is 'no key is stranded', and neither is 'SpecGuard does not " +
471
+ "track that'. " +
472
+ "Do NOT read `api_key.last_used_at` as evidence anything was ACCEPTED — it is stamped on the " +
473
+ "way in, before the payload is looked at, so a repository having every run thrown away still " +
474
+ "reports it seconds ago; `delivery_health` answers acceptance and `credential_health` answers " +
475
+ "rotation. " +
476
+ "Where a bound sits beside a list, the list is a PAGE and not the set: `limit` next to " +
477
+ "`rows`, or on that list's `*_window` block, which is also where the ORDER the cut was made " +
478
+ "in is named when the list has one. What announces the cut varies too — `truncated`, " +
479
+ "`bounded`, `returned` short of `limit`, or a `recorded_count` larger than the rows you were " +
480
+ "served — so read the bound beside the list in front of you and never take a full-looking " +
481
+ "ranking for the whole set. Where NO bound sits beside a list, it is everything there was: " +
482
+ "`credential_health.keys` and both `latest_run.shards` lists are complete by construction, " +
483
+ "which is a FINDING and not a disclosure someone forgot. " +
484
+ "THREE MORE KEYS RIDE THE RESPONSE with no parameter to pass, and together they decide " +
485
+ "WHETHER TWO RUNS MAY BE DIFFERENCED AT ALL — which is what the history is for. " +
486
+ "`suite_size_measured` is served on `latest_run` AND on every `history[]` row, from the same " +
487
+ "predicate at both sites so one run cannot disagree with itself: `false` means that run " +
488
+ "reported NO tests, so its `total_specs` is a REPORT AND NOT A MEASUREMENT and a difference " +
489
+ "taken against it describes the report rather than the suite. Never difference across a row " +
490
+ "where it is false. " +
491
+ "`shard_count` and `timed_shard_count` ride every `history[]` row and are the two " +
492
+ "DENOMINATORS: difference `total_specs` only across rows of EQUAL `shard_count`, because that " +
493
+ "count is a SUM over the shards RECORDED, and difference `duration_seconds` only across rows " +
494
+ "of EQUAL `timed_shard_count`, because that figure is a MAX over the shards that REPORTED. " +
495
+ "Ignore the second and a run whose four shards all reported, differenced against a run whose " +
496
+ "two slowest were cancelled — identical `shard_count`, identical `suite_size_measured`, only " +
497
+ "`timed_shard_count` differing — reads as a 70% speedup produced entirely by telemetry loss. " +
498
+ "A half-reported run is the ORDINARY state, not an exotic one: every sharded run passes " +
499
+ "through it while its shards are still arriving, and a job cancelled after two of four shards " +
500
+ "leaves a half-sized row in the history permanently. " +
501
+ "Use it to orient in an unfamiliar suite, to find what is slow before optimising, to find " +
502
+ "what got slower or bigger since last time, to find which tests are flaky, to find " +
503
+ "duplicated coverage before refactoring, to see annotation coverage, or to check that what " +
504
+ "SpecGuard holds is still being delivered before trusting any of it. " +
505
+ "Needs SPECGUARD_ENDPOINT and SPECGUARD_API_KEY. " +
506
+ "Figures are null where CI did not report them — a null is 'not measured', never zero. That " +
507
+ "rule is about NULLS and does not run backwards: a non-null figure is not thereby a " +
508
+ "measurement. A run that reported zero tests serves a real `total_specs: 0` beside " +
509
+ "`suite_size_measured: false`, so it is that boolean — never the nullness, and never a " +
510
+ "re-derivation of your own from `total_specs` — that says whether the row measured a suite.",
511
+ inputSchema: {
512
+ type: "object",
513
+ properties: {
514
+ branch: {
515
+ type: "string",
516
+ description: "Narrow the run history to one branch, giving a real growth series instead of the " +
517
+ "default all-branches window (whose consecutive rows are routinely different branches " +
518
+ "and must not be differenced). Narrows `history` ONLY: `latest_run` always names the " +
519
+ "repository's newest run, which on a busy repo may be on another branch. Use a name " +
520
+ "from `branches`; an unknown one returns an empty history rather than an error. " +
521
+ "It also UNLOCKS two blocks that read the same window and are `null` without it: " +
522
+ "`unstable_tests` (which tests failed intermittently across the window rather than " +
523
+ "consistently) and `directory_growth` (how each area moved between the two ENDPOINTS of " +
524
+ "that window). The per-area comparisons against the PREVIOUS RUN — `directory_run_growth` " +
525
+ "and `directory_runtime_growth` — need no branch and take none; they scope to the latest " +
526
+ "run's own branch by construction, so a plain call already carries them.",
527
+ },
528
+ spec_directory: {
529
+ type: "string",
530
+ description: "Open ONE area of the `latest_run.spec_directories` ranking, which says where the time " +
531
+ "went by directory but not which files inside it spent it. Use a path exactly as served " +
532
+ "in `latest_run.spec_directories.rows[].path`. Asking populates " +
533
+ "`latest_run.spec_directory_files` — the spec files in that one directory with their " +
534
+ "`total_seconds`/`recorded_count`/`timed_count`, plus the AREA's own `file_count`, " +
535
+ "`recorded_count`, `timed_count` and the `limit` the row list was cut at (the totals " +
536
+ "describe the whole area, not the returned page, so do not re-derive them from `rows`). " +
537
+ "The one ask opens THREE blocks, each in its own grain: `spec_directory_files` for which " +
538
+ "files carry the area's wall clock, `directory_run_file_growth` for which of them changed " +
539
+ "SIZE since the previous run, and `directory_runtime_file_growth` for which of them " +
540
+ "changed TIME. The last two are the answer to the dead end the area-grain comparisons " +
541
+ "leave — `spec/models 412 → 459 (+47)`, but WHICH FILES did that — and they need no " +
542
+ "second parameter. " +
543
+ "SENT TOGETHER WITH `unannotated_examples`, it also narrows THAT worklist to this area — " +
544
+ "its rows and its `recorded_count` both — and that block echoes the path back so you can " +
545
+ "see which population its count is of. That is a narrowing of a block the FLAG opened, " +
546
+ "not a fourth block this parameter opens. It narrows on the SAME equality this parameter " +
547
+ "already uses for its own blocks — the immediate parent directory, never a prefix, so " +
548
+ "`spec/models` does not reach `spec/models/orders`. " +
549
+ "Omit it and all three are `null`, meaning you did not ask — an area the run recorded " +
550
+ "nothing for is `rows: []` instead, not an error. The two growth blocks are additionally " +
551
+ "`null` when there is no previous run to compare this one against, which is the same " +
552
+ "'not measured' the area-grain comparisons report.",
553
+ },
554
+ spec_file: {
555
+ type: "string",
556
+ description: "Open ONE file of the `latest_run.spec_files` ranking, which says which files cost the " +
557
+ "most but not which examples inside them spent it. Use a path exactly as served in " +
558
+ "`latest_run.spec_files.rows[].path`. Asking populates " +
559
+ "`latest_run.spec_file_examples` — up to 50 of that file's individual examples cut by " +
560
+ "DURATION, each with `name`, `file_path`, `line_number`, `spec_file_path`, " +
561
+ "`duration_seconds` and `outcome`, plus the FILE's own `recorded_count` and " +
562
+ "`timed_count` and the `limit` the row list was cut at (the totals describe the whole " +
563
+ "file, not the returned page, so do not re-derive them from `rows`). " +
564
+ "SENT TOGETHER WITH `unannotated_examples`, it also narrows THAT worklist to this file — " +
565
+ "its rows and its `recorded_count` both — and that block echoes the path back so you can " +
566
+ "see which population its count is of. " +
567
+ "Omit it and the key is `null`, meaning you did not ask — a file that matched nothing " +
568
+ "is `rows: []` instead, not an error: a renamed or deleted spec file and a stale " +
569
+ "bookmark are ordinary ways to arrive.",
570
+ },
571
+ repeated_description: {
572
+ type: "string",
573
+ description: "Open ONE group of the `latest_run.repeated_descriptions` ranking — the overcoverage " +
574
+ "ranking, which names descriptions carried by many examples but not WHICH examples say " +
575
+ "the same thing. Use a description exactly as served in " +
576
+ "`latest_run.repeated_descriptions.rows[].name`. Asking populates " +
577
+ "`latest_run.repeated_description_examples` — up to 25 of that group's members, each " +
578
+ "with `name`, `file_path`, `line_number`, `spec_file_path`, `duration_seconds` and " +
579
+ "`outcome`, plus the GROUP's own `recorded_count` and `timed_count` and the `limit` " +
580
+ "the row list was cut at (the totals describe the whole group, not the returned page, " +
581
+ "so do not re-derive them from `rows`). This is the ONLY way to reach a group's " +
582
+ "members: `slowest_examples` is the run-wide top ten and rarely contains them, and " +
583
+ "`spec_file` over each path in `files_seen` cuts each file by duration with no " +
584
+ "guarantee the group's members survive. " +
585
+ "Omit it and the key is `null`, meaning you did not ask — a description that matched " +
586
+ "nothing is `rows: []` instead, not an error: a test renamed since and an edited " +
587
+ "description are ordinary ways to arrive.",
588
+ },
589
+ unstable_test: {
590
+ type: "string",
591
+ description: "Open ONE row of the `unstable_tests` ranking — the cross-run flakiness ranking, which " +
592
+ "counts how often a test failed across the window but not WHEN. Use a description " +
593
+ "exactly as served in `unstable_tests.rows[].name`. Asking populates " +
594
+ "`unstable_tests.unstable_test_runs` — that description's rows run by run in window " +
595
+ "order, NEWEST RUN FIRST, up to 200, each with `test_run_id`, `commit_sha`, `branch`, " +
596
+ "`ingested_at`, `outcome`, `duration_seconds`, `spec_file_path` and `line_number`, " +
597
+ "plus the DESCRIPTION's own `recorded_count`, `reported_outcome_count`, " +
598
+ "`unreported_outcome_count`, the window's `run_count` and the `limit` the row list was " +
599
+ "cut at (the totals describe the whole window, not the returned page, so do not " +
600
+ "re-derive them from `rows`). " +
601
+ "This is the ONLY way to tell a regression from genuine flakiness: `run_count: 30`, " +
602
+ "`failed_run_count: 4`, `outcome_words: [\"failed\", \"passed\"]` are IDENTICAL for " +
603
+ "failures in runs 27–30 — a regression, so find the commit — and failures in runs 3, " +
604
+ "11, 19 and 26 — flakiness, where there is no culprit commit. The sequence is not " +
605
+ "derivable from anything else served: `history` has no per-test grain, and " +
606
+ "`spec_file_examples`/`repeated_description_examples` carry `outcome` for the latest " +
607
+ "run only. " +
608
+ "MIND THE DIRECTION when you read those positions: element 0 is the MOST RECENT run in " +
609
+ "the window, so the run a failure STARTED at is the LAST row of the leading failed " +
610
+ "block, not the first. Read front-to-back as run 1 onwards and the regression above " +
611
+ "looks like a flake that was fixed — the exact inversion, with no error to signal it. " +
612
+ "The 200 cap drops the OLDEST rows for the same reason. Read the run off each row's " +
613
+ "`commit_sha`/`test_run_id`, never off its index: a run that recorded nothing under " +
614
+ "the description contributes no row and a description carried by two examples in one " +
615
+ "run contributes two, so `rows` is not one entry per run. " +
616
+ "`branch` IS REQUIRED WITH IT, unlike every other argument here: `unstable_tests` is " +
617
+ "served only for a branch-narrowed window, so this parameter sent alone leaves the " +
618
+ "whole containing block `null` and there is nothing to drill into — not an empty " +
619
+ "`rows: []`, no block at all. " +
620
+ "Omit it and the key is `null`, meaning you did not ask — a description the window " +
621
+ "recorded nothing for is `rows: []` instead, not an error: identity here is semantic, " +
622
+ "so a renamed test starts a NEW history and a stale bookmark is an ordinary way to " +
623
+ "arrive.",
624
+ },
625
+ commit_sha: {
626
+ type: "string",
627
+ description: "Anchor the whole answer on ONE run, naming it by commit sha. This is a DIFFERENT KIND " +
628
+ "OF ASK from every other argument here: the five above narrow what is served ABOUT a run " +
629
+ "that was already chosen for you, and this one CHOOSES THAT RUN. `branch` asks about a " +
630
+ "SERIES; this asks WHICH RUN. Use a sha exactly as served in " +
631
+ "`latest_run.commit_sha`, `history[].commit_sha` or " +
632
+ "`unstable_tests.unstable_test_runs.rows[].commit_sha`. " +
633
+ "Everything at run grain re-anchors together: `latest_run` and its five rollups, the " +
634
+ "four RUN-GRAIN drill-ins (`spec_directory_files`, `spec_file_examples`, " +
635
+ "`repeated_description_examples` and `unannotated_examples`), `shards`, both per-area " +
636
+ "growth windows and `previous_test_run`. " +
637
+ "`unstable_test_runs` is the one drill-in that does NOT move with " +
638
+ "it: it is read over the branch window rather than off the anchored run, so sending " +
639
+ "this with `unstable_test` still gives you that test across the whole window. " +
640
+ "Use it when you need to be answered about a SPECIFIC run rather than whatever is " +
641
+ "newest — after pushing a commit and waiting for CI, say, on a repository where anything " +
642
+ "else may have pushed in between: `latest_run` otherwise names the repository's newest " +
643
+ "run, which may be another branch's, with no error and no signal that you were answered " +
644
+ "about someone else's commit. " +
645
+ "`history` DOES NOT MOVE WITH IT. It stays the repository's recent runs, newest first, " +
646
+ "narrowed only by `branch` — so the `history[0] == latest_run` identity that holds on a " +
647
+ "default call is NOT expected to hold here: naming an older run makes `latest_run` a row " +
648
+ "from the middle of `history`, or from behind its bound entirely. That is the contract, " +
649
+ "not a bug. A client that needs the identity back omits this parameter. " +
650
+ "AN UNKNOWN SHA DOES NOT ERROR — IT FALLS BACK AND SAYS SO, so read `run_anchor` rather " +
651
+ "than trusting the shape of a successful response. A stale bookmark, a pruned run and a " +
652
+ "commit whose CI never reported are ordinary ways to arrive, and all three are served " +
653
+ "the repository's newest run with `source: \"requested\"`, `resolved: false` and the " +
654
+ "raw ask echoed in `requested_commit_sha` — while `run_anchor.commit_sha`/`branch` name " +
655
+ "the run ACTUALLY SERVED and will not equal what you asked for. THAT INEQUALITY IS THE " +
656
+ "ONLY SIGNAL: check `run_anchor.resolved` before believing the run-grain blocks are " +
657
+ "about your commit. `resolved` is false in exactly one case — you named a sha and are " +
658
+ "not being served it — so it is `true` on a plain call, where there was no ask to fail. " +
659
+ "Omit it and `run_anchor` reads `source: \"default\"`, `requested_commit_sha: null`. " +
660
+ "A blank value — or `null` — is treated as NO ASK AT ALL rather than an error, but a " +
661
+ "value of the WRONG TYPE (a number or an array, say) IS REFUSED BY NAME " +
662
+ "(\"`commit_sha` must be a string.\") before any request is made — so send a " +
663
+ "numeric-looking short sha such as `1234567` as a string, not as a number. No hex or " +
664
+ "length checking is done: `commit_sha` is a plain string column written from whatever " +
665
+ "CI reported, so short and long forms both work — pass the sha back exactly as it was " +
666
+ "served.",
667
+ },
668
+ unannotated_examples: {
669
+ type: "boolean",
670
+ description: "Open a run's UNANNOTATED examples — the individual tests behind `latest_run`'s " +
671
+ "`total_specs` MINUS `annotated_specs`, the subtraction the dashboard renders as " +
672
+ "\"SpecGuard cannot see the other N tests\". Every other population this endpoint reports " +
673
+ "can be walked down to the examples it counts; annotation coverage was the exception, so " +
674
+ "`annotated_ratio` told you how far you had to go and not one test to annotate. " +
675
+ "THIS ONE IS A FLAG, NOT A NAME — the only argument here that takes `true` rather than a " +
676
+ "value. The others open the rows behind a LINE of a ranking and so carry that line's key; " +
677
+ "this opens a POPULATION, which is a subtraction on the run and has no line to name. " +
678
+ "WHICH population is still yours to choose, with parameters you already have: sent ALONE " +
679
+ "the flag opens the WHOLE RUN, and sent TOGETHER WITH `spec_file` or `spec_directory` it " +
680
+ "narrows to that file, that area, or — when both ride along — the AND of the two. Four " +
681
+ "shapes from one flag. Those two keep opening their own blocks as well; narrowing this " +
682
+ "one is additional, not instead. " +
683
+ "Asking populates `latest_run.unannotated_examples` — up to 100 of the unannotated " +
684
+ "examples OF WHATEVER YOU ASKED FOR, each with `name`, `file_path`, `line_number` and " +
685
+ "`spec_file_path` (FOUR fields: no `duration_seconds` and no `outcome`, unlike the " +
686
+ "per-example drill-ins above), plus that same population's own `recorded_count`, the " +
687
+ "`limit` the row list was cut at, and `spec_file`/`spec_directory` ECHOED BACK as the " +
688
+ "server READ them — `null` for each one you did not send. " +
689
+ "READ THE ECHO BEFORE YOU READ THE COUNT. `unannotated_examples.recorded_count` — the " +
690
+ "WORKLIST's count, and only that one; the map below deliberately does not narrow — is " +
691
+ "the one figure here you would reconcile against `total_specs - annotated_specs`, and " +
692
+ "it counts the NARROWED population whenever either echo is non-null — so that " +
693
+ "reconciliation is expected to hold only when both echoes are `null`. The echo is what " +
694
+ "tells the two cases apart, which is why the server sends it. " +
695
+ "Do not re-derive `recorded_count` from `rows` in either case. Un-narrowed, this " +
696
+ "population is routinely the entire run — a repository that has just installed the gem " +
697
+ "has every test in it — so the cap is the normal case rather than the exotic one; a " +
698
+ "narrowed ask is cut at the same 100. " +
699
+ "THE ONE ASK OPENS TWO BLOCKS, each in its own grain: `latest_run.unannotated_examples` " +
700
+ "for WHICH TESTS to go and annotate, and `latest_run.unannotated_directories` for WHERE " +
701
+ "THE DEBT IS — one run's annotation debt rolled up by code AREA, which is what you pick " +
702
+ "the next `spec_directory` narrowing FROM. Both come from this ONE flag: there is no " +
703
+ "second parameter to send and no new value. " +
704
+ "The map's rows carry `path`, `unannotated_count` and the `recorded_count` that area was " +
705
+ "counted against (the operands, never a fraction), plus `directory_count` — EVERY area " +
706
+ "the run touched, not every area with debt, and not `rows.size` — and its OWN `limit`, " +
707
+ "which is 10 and NOT the worklist's 100. Two caps under one ask, and the difference is " +
708
+ "the kind of list: 100 caps a WORKLIST to work through, 10 caps a RANKING to pick from. " +
709
+ "The orders differ for the same reason — the worklist is file-navigable, the map is " +
710
+ "ranked `unannotated_count` DESC with `path` as a tiebreak only. A fully-annotated area " +
711
+ "is a real ROW with `unannotated_count: 0`, never an omission. Those rows sort last " +
712
+ "COLLECTIVELY, so on a run with more areas than the cap they are cut and never seen, " +
713
+ "but on a run inside the cap they ARE LISTED and listed is correct. So `rows.size` is " +
714
+ "not a count of areas WITH debt — read each row's `unannotated_count`. " +
715
+ "THE TWO BLOCKS DISAGREE IN TWO PLACES, ON PURPOSE — do not reconcile them by " +
716
+ "arithmetic. " +
717
+ "(1) SCOPE: `spec_file`/`spec_directory` narrow the WORKLIST and its `recorded_count`, " +
718
+ "and the MAP stays WHOLE-RUN under both. So under a narrowing " +
719
+ "`unannotated_examples.recorded_count` is NOT the sum of " +
720
+ "`unannotated_directories.rows[].unannotated_count`, and NEITHER IS WRONG: one counts " +
721
+ "the area or file you named, the other ranks the whole run. The map is whole-run by " +
722
+ "design because it is what you choose a narrowing FROM — narrowed to the area you had " +
723
+ "already picked it would answer nothing. The sum is short of the run's total whenever " +
724
+ "`directory_count > rows.size` besides, narrowing or not. " +
725
+ "(2) NULL VERSUS EMPTY: on a run that recorded no per-example rows at all, with the flag " +
726
+ "sent, `unannotated_examples` is a PRESENT block with `rows: []` and `recorded_count: 0` " +
727
+ "while `unannotated_directories` is `null`. That is a signal, not an inconsistency — " +
728
+ "`recorded_count: 0` on the worklist means BOTH \"fully annotated\" and \"recorded " +
729
+ "nothing\", and the map is how you tell them apart: a present map beside that zero means " +
730
+ "the zero is the SUCCESS state, a `null` map means the run recorded nothing and the zero " +
731
+ "is an ABSENCE of data. " +
732
+ "BOTH BLOCKS ARE AT RUN GRAIN, so each MOVES WITH `commit_sha` like everything else " +
733
+ "under `latest_run`, unlike `unstable_test` which does not. " +
734
+ "A FULLY-ANNOTATED RUN IS NOT AN ERROR AND NOT A `null`: the worklist answers 200 with " +
735
+ "`rows: []` and `recorded_count: 0`, because that is the state the metric exists to " +
736
+ "reach — so a repository walked to completion shows the block EMPTY rather than gone. " +
737
+ "A narrowing that matched nothing reads the SAME way and is never a 404: an unknown or " +
738
+ "renamed path, a file that is already fully annotated, and a contradictory file-and-area " +
739
+ "pair all answer `rows: []` with both narrowings echoed, which is an empty intersection " +
740
+ "rather than a dropped parameter. " +
741
+ "Omit the flag and BOTH keys are `null`, meaning you did not ask. `false` means the " +
742
+ "same as omitting it and sends nothing at all: declining is not sending, which is how " +
743
+ "every other argument here is declined too — none of them has an \"off\" value.",
744
+ },
745
+ },
746
+ additionalProperties: false,
747
+ },
748
+ async run(args, context) {
749
+ const branch = optionalString(args["branch"], "branch");
750
+ const specDirectory = optionalString(args["spec_directory"], "spec_directory");
751
+ const specFile = optionalString(args["spec_file"], "spec_file");
752
+ const repeatedDescription = optionalString(args["repeated_description"], "repeated_description");
753
+ const unstableTest = optionalString(args["unstable_test"], "unstable_test");
754
+ const commitSha = optionalString(args["commit_sha"], "commit_sha");
755
+ // A BOOLEAN, and deliberately not stringified below. The server reads only
756
+ // whether the key is PRESENT — `?unannotated_examples=false` opens the block
757
+ // exactly as `=true` does — and `getJson` omits only `undefined`, so sending
758
+ // `String(false)` would open a hundred-row block for the one caller who
759
+ // asked explicitly for it not to be. See this file's header.
760
+ const unannotatedExamples = optionalBoolean(args["unannotated_examples"], "unannotated_examples");
761
+ const api = requireApiConfig(context.config);
762
+ const body = await getJson(api, "/api/v1/repository", {
763
+ branch,
764
+ spec_directory: specDirectory,
765
+ spec_file: specFile,
766
+ repeated_description: repeatedDescription,
767
+ unstable_test: unstableTest,
768
+ commit_sha: commitSha,
769
+ unannotated_examples: unannotatedExamples === true ? "true" : undefined,
770
+ }, context.fetch);
771
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
772
+ throw new ApiError("SpecGuard returned a JSON value that was not an object.");
773
+ }
774
+ const overview = body;
775
+ return {
776
+ text: renderText(overview),
777
+ structured: overview,
778
+ };
779
+ },
780
+ };
781
+ export default getRepositoryOverview;
782
+ /**
783
+ * The text rendering is the SAME object serialised — deliberately not a prose
784
+ * summary of it.
785
+ *
786
+ * A summary would have to choose which of the response's figures to keep, and
787
+ * every one of them is there because the controller argued it was not derivable
788
+ * from the others. Worse, wording them would re-introduce exactly what that
789
+ * endpoint refuses to serve: it emits structured counts rather than the
790
+ * dashboard's English captions, on the grounds that a machine-readable client
791
+ * cannot act on a sentence. Turning them back into sentences here would undo
792
+ * that on the last hop.
793
+ */
794
+ function renderText(overview) {
795
+ return JSON.stringify(overview, null, 2);
796
+ }
797
+ //# sourceMappingURL=repository-overview.js.map