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.
- package/README.md +366 -0
- package/dist/bin/specguard-mcp.d.ts +2 -0
- package/dist/bin/specguard-mcp.js +35 -0
- package/dist/bin/specguard-mcp.js.map +1 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +172 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/errors.d.ts +60 -0
- package/dist/src/errors.js +64 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server.d.ts +28 -0
- package/dist/src/server.js +113 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/support/run-command.d.ts +86 -0
- package/dist/src/support/run-command.js +322 -0
- package/dist/src/support/run-command.js.map +1 -0
- package/dist/src/support/specguard-api.d.ts +11 -0
- package/dist/src/support/specguard-api.js +157 -0
- package/dist/src/support/specguard-api.js.map +1 -0
- package/dist/src/tools/args.d.ts +48 -0
- package/dist/src/tools/args.js +66 -0
- package/dist/src/tools/args.js.map +1 -0
- package/dist/src/tools/index.d.ts +33 -0
- package/dist/src/tools/index.js +34 -0
- package/dist/src/tools/index.js.map +1 -0
- package/dist/src/tools/lint-intent-annotations.d.ts +45 -0
- package/dist/src/tools/lint-intent-annotations.js +342 -0
- package/dist/src/tools/lint-intent-annotations.js.map +1 -0
- package/dist/src/tools/repository-overview.d.ts +424 -0
- package/dist/src/tools/repository-overview.js +797 -0
- package/dist/src/tools/repository-overview.js.map +1 -0
- package/dist/src/tools/types.d.ts +111 -0
- package/dist/src/tools/types.js +2 -0
- package/dist/src/tools/types.js.map +1 -0
- 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
|