@cat-factory/app 0.241.2 → 0.242.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/app/components/outcome/OutcomeSummaryWindow.vue +59 -45
- package/app/composables/api/spec.ts +14 -4
- package/app/docs/consumer-extensions.md +8 -0
- package/app/stores/serviceSpec.spec.ts +81 -0
- package/app/stores/serviceSpec.ts +55 -25
- package/app/utils/catalog.ts +7 -6
- package/app/utils/runOutcome.ts +38 -507
- package/i18n/locales/de.json +5 -6
- package/i18n/locales/en.json +5 -6
- package/i18n/locales/es.json +5 -6
- package/i18n/locales/fr.json +5 -6
- package/i18n/locales/he.json +5 -6
- package/i18n/locales/it.json +5 -6
- package/i18n/locales/ja.json +5 -6
- package/i18n/locales/pl.json +5 -6
- package/i18n/locales/tr.json +5 -6
- package/i18n/locales/uk.json +5 -6
- package/package.json +2 -2
- package/app/utils/runOutcome.spec.ts +0 -492
package/app/utils/runOutcome.ts
CHANGED
|
@@ -1,509 +1,40 @@
|
|
|
1
|
-
// The RUN OUTCOME summary
|
|
2
|
-
// that up".
|
|
1
|
+
// The RUN OUTCOME summary, re-exported from `@cat-factory/contracts`.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// The reduction itself moved into the contracts package when the summary gained a second
|
|
4
|
+
// consumer: `GET /api/v1/runs/:runId/outcome` serves it to anything holding a workspace key, and
|
|
5
|
+
// the engine's PR verification report reduces the same run evidence for a reviewer. Three
|
|
6
|
+
// documents composed from one run cannot each own their own rules. The SPA's copy and the
|
|
7
|
+
// report's had already drifted on which tester steps count and on what `not covered` counts,
|
|
8
|
+
// so the same run produced different totals depending on where you read it.
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| 'awaiting_merge'
|
|
43
|
-
| 'in_flight'
|
|
44
|
-
| 'needs_attention'
|
|
45
|
-
| 'not_run'
|
|
46
|
-
/**
|
|
47
|
-
* The block names a run nobody resolved (see {@link RunUnavailableGap}), and its status is
|
|
48
|
-
* not one the merge lifecycle writes. What the run did is exactly the fact a block alone
|
|
49
|
-
* cannot carry, and `not_run` would be this card's most visible lie.
|
|
50
|
-
*/
|
|
51
|
-
| 'unknown'
|
|
52
|
-
|
|
53
|
-
/** One pull request the run opened: the own-service PR, plus a peer PR per connected repo. */
|
|
54
|
-
export interface OutcomePullRequest {
|
|
55
|
-
url: string
|
|
56
|
-
number: number | null
|
|
57
|
-
branch: string | null
|
|
58
|
-
/** `owner/name` for a PEER repo's PR; null for the task's own service. */
|
|
59
|
-
repo: string | null
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---- Requirement coverage --------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The gap EVERY evidence section shares: the block names a run (`block.executionId`) the
|
|
66
|
-
* caller could not resolve, so nothing any step recorded is knowable here.
|
|
67
|
-
*
|
|
68
|
-
* It is kept apart from every other gap in this module, which report what a RESOLVED run did
|
|
69
|
-
* or did not produce. "The store does not have this run" and "this pipeline has no tester
|
|
70
|
-
* step" are opposite facts about opposite things, and a card that reported the second for the
|
|
71
|
-
* first would blame the pipeline for a read that never happened, on the one surface whose
|
|
72
|
-
* whole job is to say what is known and what is not.
|
|
73
|
-
*/
|
|
74
|
-
export type RunUnavailableGap = 'run_unavailable'
|
|
75
|
-
|
|
76
|
-
/** Why there is no requirement coverage to show. Each needs a different reaction. */
|
|
77
|
-
export type RequirementsGap =
|
|
78
|
-
| RunUnavailableGap
|
|
79
|
-
| 'no_tester_step'
|
|
80
|
-
| 'tester_not_reported'
|
|
81
|
-
| 'no_verdicts'
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Whether the requirement rows carry the spec's titles and, when they do not, WHY. The two
|
|
85
|
-
* causes leave IDENTICAL rows behind and need different fixes, so they are never merged:
|
|
86
|
-
* `not_read` is a spec this card never got (there was nothing to join against), `unmatched` is
|
|
87
|
-
* a spec it DID read that holds none of the ids the tester reported (a spec rewritten since,
|
|
88
|
-
* or a tester keying its verdicts by something else). Reporting the second as the first would
|
|
89
|
-
* send a reader to fix a read that worked.
|
|
90
|
-
*/
|
|
91
|
-
export type OutcomeSpecJoin = 'joined' | 'not_read' | 'unmatched'
|
|
92
|
-
|
|
93
|
-
/** One requirement the tester ruled on, joined to the spec when the spec could be read. */
|
|
94
|
-
export interface OutcomeRequirement {
|
|
95
|
-
/** The spec requirement id: the join key, and all there is when the join did not land. */
|
|
96
|
-
id: string
|
|
97
|
-
/**
|
|
98
|
-
* The requirement's headline from `spec/`, or null when this id is not in the spec that was
|
|
99
|
-
* read. Null on a row of an otherwise JOINED section is the partial-miss case the render
|
|
100
|
-
* site marks per row: the id is all there is for THIS requirement, and left unmarked beside
|
|
101
|
-
* its titled neighbours it reads as a requirement someone named after a slug.
|
|
102
|
-
*/
|
|
103
|
-
title: string | null
|
|
104
|
-
verdict: RequirementVerdictStatus
|
|
105
|
-
/** What the tester observed, when it said. */
|
|
106
|
-
detail: string | null
|
|
107
|
-
/** Implementation state as `spec/` recorded it, or null when unjoined. */
|
|
108
|
-
state: RequirementState | null
|
|
109
|
-
/**
|
|
110
|
-
* An `established` requirement the tester observed to FAIL: behaviour the platform had
|
|
111
|
-
* previously seen hold and no longer does. Computed here, never read off the report, and the
|
|
112
|
-
* one reading of this section that says the change BROKE something rather than merely not
|
|
113
|
-
* finishing it. An `aspirational` requirement failing is in-flight work, not a regression.
|
|
114
|
-
*/
|
|
115
|
-
regression: boolean
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export type OutcomeRequirements =
|
|
119
|
-
| { status: 'absent'; gap: RequirementsGap }
|
|
120
|
-
| {
|
|
121
|
-
status: 'reported'
|
|
122
|
-
/** Whether the rows carry spec titles, or only the ids the tester keyed them by. */
|
|
123
|
-
spec: OutcomeSpecJoin
|
|
124
|
-
met: number
|
|
125
|
-
notMet: number
|
|
126
|
-
notCovered: number
|
|
127
|
-
regressions: number
|
|
128
|
-
/** Regressions first, then failures, then what was met, then what nobody checked. */
|
|
129
|
-
entries: OutcomeRequirement[]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ---- The tester's report ---------------------------------------------------
|
|
133
|
-
|
|
134
|
-
export type TestsGap = RunUnavailableGap | 'no_tester_step' | 'tester_not_reported'
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The tester's disposition. `could_not_run` is kept apart from `concerns` because they call
|
|
138
|
-
* for opposite reactions: one is a change with bugs in it, the other is a change nobody
|
|
139
|
-
* managed to exercise at all, and a report that collapses them reads as tested either way.
|
|
140
|
-
*/
|
|
141
|
-
export type TestsVerdict = 'greenlit' | 'concerns' | 'could_not_run'
|
|
142
|
-
|
|
143
|
-
export interface OutcomeConcern {
|
|
144
|
-
title: string
|
|
145
|
-
severity: TestConcernSeverity
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export type OutcomeTests =
|
|
149
|
-
| { status: 'absent'; gap: TestsGap }
|
|
150
|
-
| {
|
|
151
|
-
status: 'reported'
|
|
152
|
-
verdict: TestsVerdict
|
|
153
|
-
/** The tester's own prose about the session, attributed as such at the render site. */
|
|
154
|
-
summary: string | null
|
|
155
|
-
/** Verbatim reason the tester could not run at all; null unless `could_not_run`. */
|
|
156
|
-
abortReason: string | null
|
|
157
|
-
/** What it exercised, by name. */
|
|
158
|
-
areas: string[]
|
|
159
|
-
passed: number
|
|
160
|
-
failed: number
|
|
161
|
-
skipped: number
|
|
162
|
-
concerns: OutcomeConcern[]
|
|
163
|
-
environment: TestEnvironment | null
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---- What it looked like ---------------------------------------------------
|
|
167
|
-
|
|
168
|
-
export type VisualsGap = RunUnavailableGap | 'no_visual_step' | 'none_captured'
|
|
169
|
-
|
|
170
|
-
/** One captured view, paired with the reference design it was reviewed against when there is one. */
|
|
171
|
-
export interface OutcomeVisual {
|
|
172
|
-
view: string
|
|
173
|
-
artifactId: string | null
|
|
174
|
-
referenceArtifactId: string | null
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export type OutcomeVisuals =
|
|
178
|
-
| {
|
|
179
|
-
status: 'absent'
|
|
180
|
-
gap: VisualsGap
|
|
181
|
-
/** The gate's own verbatim explanation, when it recorded one. Detail, never the headline. */
|
|
182
|
-
detail: string | null
|
|
183
|
-
}
|
|
184
|
-
| {
|
|
185
|
-
status: 'reported'
|
|
186
|
-
/**
|
|
187
|
-
* Which producer the views came from. `visual_confirm` pairs were put in front of a
|
|
188
|
-
* human and carry a verdict; `tester` shots are captures nobody was asked about, and the
|
|
189
|
-
* card must not let the second read as the first.
|
|
190
|
-
*/
|
|
191
|
-
source: 'visual_confirm' | 'tester'
|
|
192
|
-
/** The gate's phase when the views came from it: awaiting a human, fixing, or approved. */
|
|
193
|
-
phase: 'awaiting_human' | 'fixing' | 'approved' | null
|
|
194
|
-
views: OutcomeVisual[]
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ---- The machine checks ----------------------------------------------------
|
|
198
|
-
|
|
199
|
-
/** The three recorded machine verdicts a non-code reader still needs: did it build, does it work. */
|
|
200
|
-
export type OutcomeCheckKind = 'ci' | 'validation' | 'reproduction'
|
|
201
|
-
export type OutcomeCheckState = 'pass' | 'fail' | 'pending' | 'inconclusive'
|
|
202
|
-
|
|
203
|
-
export interface OutcomeCheck {
|
|
204
|
-
kind: OutcomeCheckKind
|
|
205
|
-
state: OutcomeCheckState
|
|
206
|
-
/**
|
|
207
|
-
* The producer's own qualifier, when the state alone would under-report it: the reproduction
|
|
208
|
-
* verdict that earned an `inconclusive`. Rendered through an exhaustive map, never as prose.
|
|
209
|
-
*/
|
|
210
|
-
reproduction: ReproductionStatus | null
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ---- The whole summary -----------------------------------------------------
|
|
214
|
-
|
|
215
|
-
export interface RunOutcome {
|
|
216
|
-
disposition: OutcomeDisposition
|
|
217
|
-
/** The task's title: the product-language name of what was asked for. */
|
|
218
|
-
title: string
|
|
219
|
-
/** The requester's own description of the ask, trimmed; null when the task carried none. */
|
|
220
|
-
ask: string | null
|
|
221
|
-
/** Every PR the run opened, so the diff stays exactly one click from the summary. */
|
|
222
|
-
pullRequests: OutcomePullRequest[]
|
|
223
|
-
requirements: OutcomeRequirements
|
|
224
|
-
tests: OutcomeTests
|
|
225
|
-
visuals: OutcomeVisuals
|
|
226
|
-
/** Only the checks that actually ran: an absent check is omitted, never rendered as passing. */
|
|
227
|
-
checks: OutcomeCheck[]
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export interface ComposeRunOutcomeInput {
|
|
231
|
-
block: Block
|
|
232
|
-
/**
|
|
233
|
-
* The run, or null when the caller has none. Null is TWO facts, and the block tells them
|
|
234
|
-
* apart: a task with no `executionId` never ran, while a task that names one the caller
|
|
235
|
-
* could not resolve has run and this card simply cannot see it (see
|
|
236
|
-
* {@link RunUnavailableGap}). Callers pass what their store holds and never substitute one
|
|
237
|
-
* for the other.
|
|
238
|
-
*/
|
|
239
|
-
instance: ExecutionInstance | null
|
|
240
|
-
/** The enclosing service's spec, when it has been loaded. Absent ⇒ ids without titles. */
|
|
241
|
-
spec?: ServiceSpecView | null
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* The tester step whose report describes the PR as it stands: a pipeline may carry more than
|
|
246
|
-
* one, and a later one supersedes an earlier one. Falls back to the first tester step so the
|
|
247
|
-
* caller can tell "no tester in this pipeline" from "one is there and has not reported".
|
|
248
|
-
*/
|
|
249
|
-
function testerStep(steps: readonly PipelineStep[]): PipelineStep | null {
|
|
250
|
-
const candidates = steps.filter((s) => isTesterKind(s.agentKind))
|
|
251
|
-
const reported = candidates.filter((s) => s.test?.lastReport)
|
|
252
|
-
return reported.at(-1) ?? candidates[0] ?? null
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Index the service spec's requirements by id, for the verdict join. */
|
|
256
|
-
function specIndex(spec: ServiceSpecView | null | undefined) {
|
|
257
|
-
const byId = new Map<string, { title: string; state: RequirementState }>()
|
|
258
|
-
for (const module of spec?.spec?.modules ?? []) {
|
|
259
|
-
for (const group of module.groups ?? []) {
|
|
260
|
-
for (const req of group.requirements ?? []) {
|
|
261
|
-
byId.set(req.id, { title: req.title, state: req.state ?? 'aspirational' })
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return byId
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Regressions first, then failures, then what held, then what nobody checked. */
|
|
269
|
-
const VERDICT_ORDER: Record<RequirementVerdictStatus, number> = {
|
|
270
|
-
not_met: 1,
|
|
271
|
-
met: 2,
|
|
272
|
-
not_covered: 3,
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* How the rows joined to the spec. Asked of the index rather than of the rows alone, because
|
|
277
|
-
* "no titles" has two causes and only the index can tell them apart: an index with entries
|
|
278
|
-
* that matched nothing is a spec that WAS read (see {@link OutcomeSpecJoin}).
|
|
279
|
-
*/
|
|
280
|
-
function specJoin(entries: readonly OutcomeRequirement[], specRead: boolean): OutcomeSpecJoin {
|
|
281
|
-
if (entries.some((e) => e.title !== null)) return 'joined'
|
|
282
|
-
return specRead ? 'unmatched' : 'not_read'
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function composeRequirements(
|
|
286
|
-
step: PipelineStep | null,
|
|
287
|
-
spec: ServiceSpecView | null | undefined,
|
|
288
|
-
): OutcomeRequirements {
|
|
289
|
-
if (!step) return { status: 'absent', gap: 'no_tester_step' }
|
|
290
|
-
const report = step.test?.lastReport
|
|
291
|
-
if (!report) return { status: 'absent', gap: 'tester_not_reported' }
|
|
292
|
-
const verdicts = report.requirementVerdicts ?? []
|
|
293
|
-
if (verdicts.length === 0) return { status: 'absent', gap: 'no_verdicts' }
|
|
294
|
-
|
|
295
|
-
const index = specIndex(spec)
|
|
296
|
-
const entries: OutcomeRequirement[] = verdicts.map((verdict) => {
|
|
297
|
-
const known = index.get(verdict.requirementId)
|
|
298
|
-
return {
|
|
299
|
-
id: verdict.requirementId,
|
|
300
|
-
title: known?.title ?? null,
|
|
301
|
-
verdict: verdict.status,
|
|
302
|
-
detail: verdict.detail?.trim() || null,
|
|
303
|
-
state: known?.state ?? null,
|
|
304
|
-
regression: known?.state === 'established' && verdict.status === 'not_met',
|
|
305
|
-
}
|
|
306
|
-
})
|
|
307
|
-
entries.sort((a, b) => {
|
|
308
|
-
if (a.regression !== b.regression) return a.regression ? -1 : 1
|
|
309
|
-
const order = VERDICT_ORDER[a.verdict] - VERDICT_ORDER[b.verdict]
|
|
310
|
-
return order !== 0 ? order : (a.title ?? a.id).localeCompare(b.title ?? b.id)
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
status: 'reported',
|
|
315
|
-
// A spec that resolved NO id says WHICH of the two reasons applies rather than reading as a
|
|
316
|
-
// joined spec full of blank titles: the rows look the same either way and mean opposite
|
|
317
|
-
// things about whether the reader is seeing everything.
|
|
318
|
-
spec: specJoin(entries, spec?.spec != null),
|
|
319
|
-
met: entries.filter((e) => e.verdict === 'met').length,
|
|
320
|
-
notMet: entries.filter((e) => e.verdict === 'not_met').length,
|
|
321
|
-
notCovered: entries.filter((e) => e.verdict === 'not_covered').length,
|
|
322
|
-
regressions: entries.filter((e) => e.regression).length,
|
|
323
|
-
entries,
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function composeTests(step: PipelineStep | null): OutcomeTests {
|
|
328
|
-
if (!step) return { status: 'absent', gap: 'no_tester_step' }
|
|
329
|
-
const report = step.test?.lastReport
|
|
330
|
-
if (!report) return { status: 'absent', gap: 'tester_not_reported' }
|
|
331
|
-
|
|
332
|
-
const abortReason = report.abort?.reason?.trim() || null
|
|
333
|
-
const tally = { passed: 0, failed: 0, skipped: 0 }
|
|
334
|
-
for (const outcome of report.outcomes) tally[outcome.status] += 1
|
|
335
|
-
return {
|
|
336
|
-
status: 'reported',
|
|
337
|
-
verdict: abortReason ? 'could_not_run' : report.greenlight ? 'greenlit' : 'concerns',
|
|
338
|
-
summary: report.summary?.trim() || null,
|
|
339
|
-
abortReason,
|
|
340
|
-
areas: report.tested,
|
|
341
|
-
...tally,
|
|
342
|
-
concerns: report.concerns.map((c) => ({ title: c.title, severity: c.severity })),
|
|
343
|
-
environment: report.environment ?? null,
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function composeVisuals(
|
|
348
|
-
steps: readonly PipelineStep[],
|
|
349
|
-
tester: PipelineStep | null,
|
|
350
|
-
): OutcomeVisuals {
|
|
351
|
-
// The visual-confirmation gate is preferred over the tester's raw captures: its pairs were
|
|
352
|
-
// put in FRONT of a human and carry the reference they were judged against.
|
|
353
|
-
const gate = steps.filter((s) => s.visualConfirm).at(-1)?.visualConfirm ?? null
|
|
354
|
-
const pairs = (gate?.pairs ?? []).filter((p) => p.actualArtifactId || p.referenceArtifactId)
|
|
355
|
-
if (pairs.length > 0) {
|
|
356
|
-
return {
|
|
357
|
-
status: 'reported',
|
|
358
|
-
source: 'visual_confirm',
|
|
359
|
-
phase: gate?.phase ?? null,
|
|
360
|
-
views: pairs.map((p) => ({
|
|
361
|
-
view: p.view,
|
|
362
|
-
artifactId: p.actualArtifactId ?? null,
|
|
363
|
-
referenceArtifactId: p.referenceArtifactId ?? null,
|
|
364
|
-
})),
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const shots = tester?.test?.lastReport?.screenshots ?? []
|
|
369
|
-
if (shots.length > 0) {
|
|
370
|
-
return {
|
|
371
|
-
status: 'reported',
|
|
372
|
-
source: 'tester',
|
|
373
|
-
phase: null,
|
|
374
|
-
views: shots.map((s) => ({
|
|
375
|
-
view: s.view,
|
|
376
|
-
artifactId: s.artifactId,
|
|
377
|
-
referenceArtifactId: s.referenceArtifactId ?? null,
|
|
378
|
-
})),
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Nothing to show: say whether anything was ever meant to capture a view. A gate that ran
|
|
383
|
-
// and gathered nothing recorded WHY, and that reason is the whole answer for the reader.
|
|
384
|
-
//
|
|
385
|
-
// Asked of EVERY step, not of the tester whose report was selected: a pipeline can carry a
|
|
386
|
-
// `tester-ui` that has not reported beside a `tester-api` that has, and the selected step is
|
|
387
|
-
// then the api one. Reading the producer off it would tell a reader looking at a UI pipeline
|
|
388
|
-
// that nothing in it captures the interface.
|
|
389
|
-
const degraded = gate?.degradedReason?.trim() || null
|
|
390
|
-
const hadProducer = Boolean(gate) || steps.some((s) => s.agentKind === 'tester-ui')
|
|
391
|
-
return {
|
|
392
|
-
status: 'absent',
|
|
393
|
-
gap: hadProducer ? 'none_captured' : 'no_visual_step',
|
|
394
|
-
detail: degraded,
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function composeChecks(steps: readonly PipelineStep[]): OutcomeCheck[] {
|
|
399
|
-
const checks: OutcomeCheck[] = []
|
|
400
|
-
|
|
401
|
-
const ci = steps.filter((s) => s.agentKind === 'ci' && s.gate).at(-1)?.gate ?? null
|
|
402
|
-
// A CI gate that has not probed yet has no verdict to report; `pending` is what the gate
|
|
403
|
-
// itself records for "the checks are still running", so an unprobed gate is not folded onto it.
|
|
404
|
-
if (ci?.lastVerdict) checks.push({ kind: 'ci', state: ci.lastVerdict, reproduction: null })
|
|
405
|
-
|
|
406
|
-
const validation = steps.filter((s) => s.validation).at(-1)?.validation ?? null
|
|
407
|
-
if (validation) {
|
|
408
|
-
checks.push({
|
|
409
|
-
kind: 'validation',
|
|
410
|
-
state: validation.passed ? 'pass' : 'fail',
|
|
411
|
-
reproduction: null,
|
|
412
|
-
})
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const reproduction = steps.filter((s) => s.reproduction).at(-1)?.reproduction ?? null
|
|
416
|
-
if (reproduction) {
|
|
417
|
-
// Only red-on-the-pre-fix-tree then green-on-the-final-tree is proof; every other verdict
|
|
418
|
-
// is the absence of proof rather than a failure, which is why it is not a `fail`.
|
|
419
|
-
checks.push({
|
|
420
|
-
kind: 'reproduction',
|
|
421
|
-
state: reproduction.status === 'reproduced' ? 'pass' : 'inconclusive',
|
|
422
|
-
reproduction: reproduction.status,
|
|
423
|
-
})
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return checks
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function composeDisposition(
|
|
430
|
-
block: Block,
|
|
431
|
-
instance: ExecutionInstance | null,
|
|
432
|
-
unresolvedRun: boolean,
|
|
433
|
-
): OutcomeDisposition {
|
|
434
|
-
if (block.status === 'done') return 'merged'
|
|
435
|
-
if (block.status === 'pr_ready') return 'awaiting_merge'
|
|
436
|
-
if (instance?.status === 'failed' || block.status === 'blocked') return 'needs_attention'
|
|
437
|
-
// `in_progress` is the block's OWN word for a live run, so it stands whether or not the run
|
|
438
|
-
// itself resolved — the one in-flight reading that needs nothing from the instance.
|
|
439
|
-
if (instance || block.status === 'in_progress') return 'in_flight'
|
|
440
|
-
return unresolvedRun ? 'unknown' : 'not_run'
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Compose a run's outcome summary from what the run already carries. Pure: every input is a
|
|
445
|
-
* value the caller read off its store, so the whole reduction unit-tests without mounting the
|
|
446
|
-
* window that renders it.
|
|
447
|
-
*/
|
|
448
|
-
export function composeRunOutcome({ block, instance, spec }: ComposeRunOutcomeInput): RunOutcome {
|
|
449
|
-
const steps = instance?.steps ?? []
|
|
450
|
-
const tester = testerStep(steps)
|
|
451
|
-
// The block names a run the caller could not resolve. Everything below is read off that run's
|
|
452
|
-
// steps, so composing from the empty list would report a pipeline that ran and produced
|
|
453
|
-
// nothing — the exact misreading this card exists to prevent (see `RunUnavailableGap`).
|
|
454
|
-
const unresolvedRun = !instance && Boolean(block.executionId)
|
|
455
|
-
const asked = {
|
|
456
|
-
disposition: composeDisposition(block, instance, unresolvedRun),
|
|
457
|
-
title: block.title,
|
|
458
|
-
ask: block.description?.trim() || null,
|
|
459
|
-
// Read off the BLOCK, so they survive a run this card cannot see: the pull request is what
|
|
460
|
-
// a merged task is usually reopened for, long after its run left the store.
|
|
461
|
-
pullRequests: allPullRequests(block).map(({ repo, ref }) => toOutcomePr(ref, repo)),
|
|
462
|
-
}
|
|
463
|
-
if (unresolvedRun) {
|
|
464
|
-
return {
|
|
465
|
-
...asked,
|
|
466
|
-
requirements: { status: 'absent', gap: 'run_unavailable' },
|
|
467
|
-
tests: { status: 'absent', gap: 'run_unavailable' },
|
|
468
|
-
visuals: { status: 'absent', gap: 'run_unavailable', detail: null },
|
|
469
|
-
checks: [],
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return {
|
|
473
|
-
...asked,
|
|
474
|
-
requirements: composeRequirements(tester, spec),
|
|
475
|
-
tests: composeTests(tester),
|
|
476
|
-
visuals: composeVisuals(steps, tester),
|
|
477
|
-
checks: composeChecks(steps),
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function toOutcomePr(ref: PullRequestRef, repo: string | undefined): OutcomePullRequest {
|
|
482
|
-
return {
|
|
483
|
-
url: ref.url,
|
|
484
|
-
number: ref.number ?? null,
|
|
485
|
-
branch: ref.branch ?? null,
|
|
486
|
-
repo: repo ?? null,
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Whether a run has anything an outcome summary could show beyond the task's own title: a PR to
|
|
492
|
-
* open, or a step that recorded evidence. EVERY entry point asks this (the board card and the
|
|
493
|
-
* inspector alike, off the one reduction, so they can never disagree) so the affordance appears
|
|
494
|
-
* on a run that produced something and stays absent on one that has not yet, rather than
|
|
495
|
-
* offering a card whose every section reads "nothing here".
|
|
496
|
-
*
|
|
497
|
-
* A run this card could not resolve answers false unless the block still carries a pull
|
|
498
|
-
* request: there is nothing to show, and an affordance that opened onto four "not loaded"
|
|
499
|
-
* notices would be the same empty card by another route.
|
|
500
|
-
*/
|
|
501
|
-
export function hasOutcomeToShow(outcome: RunOutcome): boolean {
|
|
502
|
-
return (
|
|
503
|
-
outcome.pullRequests.length > 0 ||
|
|
504
|
-
outcome.requirements.status === 'reported' ||
|
|
505
|
-
outcome.tests.status === 'reported' ||
|
|
506
|
-
outcome.visuals.status === 'reported' ||
|
|
507
|
-
outcome.checks.length > 0
|
|
508
|
-
)
|
|
509
|
-
}
|
|
10
|
+
// What lives on this side is PRESENTATION: the gap codes below map to translated copy in
|
|
11
|
+
// `OutcomeSummaryWindow.vue`, which is exactly the half the backend must not own (the backend
|
|
12
|
+
// does not localize prose; it emits machine-readable codes the SPA maps). The import path stays
|
|
13
|
+
// so the components that read it are unchanged.
|
|
14
|
+
export {
|
|
15
|
+
composeRunOutcome,
|
|
16
|
+
hasOutcomeToShow,
|
|
17
|
+
RUN_OUTCOME_VERSION,
|
|
18
|
+
parseRunOutcome,
|
|
19
|
+
} from '@cat-factory/contracts'
|
|
20
|
+
export type {
|
|
21
|
+
ComposeRunOutcomeInput,
|
|
22
|
+
OutcomeCheck,
|
|
23
|
+
OutcomeCheckKind,
|
|
24
|
+
OutcomeCheckState,
|
|
25
|
+
OutcomeConcern,
|
|
26
|
+
OutcomeDisposition,
|
|
27
|
+
OutcomePullRequest,
|
|
28
|
+
OutcomeRequirement,
|
|
29
|
+
OutcomeRequirements,
|
|
30
|
+
OutcomeSpecJoin,
|
|
31
|
+
OutcomeTests,
|
|
32
|
+
OutcomeVisual,
|
|
33
|
+
OutcomeVisuals,
|
|
34
|
+
RequirementsGap,
|
|
35
|
+
RunOutcome,
|
|
36
|
+
RunUnavailableGap,
|
|
37
|
+
TestsGap,
|
|
38
|
+
TestsVerdict,
|
|
39
|
+
VisualsGap,
|
|
40
|
+
} from '@cat-factory/contracts'
|
package/i18n/locales/de.json
CHANGED
|
@@ -6131,11 +6131,8 @@
|
|
|
6131
6131
|
"notCovered": "Nicht geprüft: {count}",
|
|
6132
6132
|
"regressions": "Regressionen: {count}",
|
|
6133
6133
|
"regressionTag": "Regression",
|
|
6134
|
-
"idOnly": "nur ID",
|
|
6135
|
-
"idOnlyHint": "Diese ID steht nicht in der Service-Spezifikation, deshalb gibt es dafür keinen Titel.",
|
|
6136
6134
|
"spec": {
|
|
6137
|
-
"not_read": "Die Service-Spezifikation wurde nicht gelesen, deshalb
|
|
6138
|
-
"unmatched": "Keine dieser Anforderungs-IDs steht in der gelesenen Service-Spezifikation, deshalb werden nur die vom Tester gemeldeten IDs gezeigt."
|
|
6135
|
+
"not_read": "Die Service-Spezifikation wurde nicht gelesen, deshalb decken diese Zahlen nur das ab, worüber der Tester entschieden hat, nach ID."
|
|
6139
6136
|
},
|
|
6140
6137
|
"verdict": {
|
|
6141
6138
|
"met": "Bestätigt",
|
|
@@ -6145,8 +6142,10 @@
|
|
|
6145
6142
|
"gap": {
|
|
6146
6143
|
"no_tester_step": "Diese Pipeline hat keinen Test-Schritt, daher wurde keine Anforderung geprüft.",
|
|
6147
6144
|
"tester_not_reported": "Der Tester hat noch nicht berichtet, daher wurde über keine Anforderung entschieden.",
|
|
6148
|
-
"no_verdicts": "Der Tester hat berichtet, ohne über eine Anforderung der Spezifikation zu entscheiden."
|
|
6149
|
-
|
|
6145
|
+
"no_verdicts": "Der Tester hat berichtet, ohne über eine Anforderung der Spezifikation zu entscheiden.",
|
|
6146
|
+
"no_requirements": "Die Service-Spezifikation enthält keine Anforderungen, daher gab es für den Tester nichts zu entscheiden."
|
|
6147
|
+
},
|
|
6148
|
+
"unmatchedVerdicts": "Der Tester hat zusätzlich über {count} ID(s) entschieden, die in dieser Spezifikation nicht vorkommen; seine eigene Zählung liegt deshalb höher als die Zahlen oben."
|
|
6150
6149
|
},
|
|
6151
6150
|
"tests": {
|
|
6152
6151
|
"title": "Wie getestet wurde",
|
package/i18n/locales/en.json
CHANGED
|
@@ -5844,11 +5844,8 @@
|
|
|
5844
5844
|
"notCovered": "Not checked: {count}",
|
|
5845
5845
|
"regressions": "Regressions: {count}",
|
|
5846
5846
|
"regressionTag": "Regression",
|
|
5847
|
-
"idOnly": "id only",
|
|
5848
|
-
"idOnlyHint": "This id is not in the service specification, so there is no title to show for it.",
|
|
5849
5847
|
"spec": {
|
|
5850
|
-
"not_read": "The service specification was not read, so these
|
|
5851
|
-
"unmatched": "None of these requirement ids appear in the service specification that was read, so only the ids the tester reported are shown."
|
|
5848
|
+
"not_read": "The service specification was not read, so these counts cover only what the tester ruled on, by id."
|
|
5852
5849
|
},
|
|
5853
5850
|
"verdict": {
|
|
5854
5851
|
"met": "Verified",
|
|
@@ -5858,8 +5855,10 @@
|
|
|
5858
5855
|
"gap": {
|
|
5859
5856
|
"no_tester_step": "This pipeline has no tester step, so no requirement was checked.",
|
|
5860
5857
|
"tester_not_reported": "The tester has not reported yet, so no requirement has been ruled on.",
|
|
5861
|
-
"no_verdicts": "The tester reported without ruling on any requirement of the service specification."
|
|
5862
|
-
|
|
5858
|
+
"no_verdicts": "The tester reported without ruling on any requirement of the service specification.",
|
|
5859
|
+
"no_requirements": "The service specification records no requirements, so there was nothing for the tester to rule on."
|
|
5860
|
+
},
|
|
5861
|
+
"unmatchedVerdicts": "The tester also ruled on {count} id(s) this service's specification does not carry, so its own tally is higher than the counts above."
|
|
5863
5862
|
},
|
|
5864
5863
|
"tests": {
|
|
5865
5864
|
"title": "How it was tested",
|
package/i18n/locales/es.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "Sin comprobar: {count}",
|
|
5576
5576
|
"regressions": "Regresiones: {count}",
|
|
5577
5577
|
"regressionTag": "Regresión",
|
|
5578
|
-
"idOnly": "solo id",
|
|
5579
|
-
"idOnlyHint": "Este identificador no está en la especificación del servicio, así que no hay ningún título que mostrar.",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "No se leyó la especificación del servicio, así que estos
|
|
5582
|
-
"unmatched": "Ninguno de estos identificadores de requisito aparece en la especificación del servicio que se leyó, así que solo se muestran los identificadores que informó el probador."
|
|
5579
|
+
"not_read": "No se leyó la especificación del servicio, así que estos recuentos solo cubren aquello sobre lo que se pronunció el tester, por identificador."
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "Verificado",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "Esta canalización no tiene paso de pruebas, así que no se comprobó ningún requisito.",
|
|
5591
5588
|
"tester_not_reported": "El tester aún no ha informado, así que no se ha resuelto ningún requisito.",
|
|
5592
|
-
"no_verdicts": "El tester informó sin pronunciarse sobre ningún requisito de la especificación del servicio."
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "El tester informó sin pronunciarse sobre ningún requisito de la especificación del servicio.",
|
|
5590
|
+
"no_requirements": "La especificación del servicio no registra ningún requisito, así que no había nada sobre lo que el tester pudiera pronunciarse."
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "El tester también se pronunció sobre {count} identificador(es) que esta especificación no contiene, así que su propio recuento es mayor que los de arriba."
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "Cómo se probó",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "Non vérifiées : {count}",
|
|
5576
5576
|
"regressions": "Régressions : {count}",
|
|
5577
5577
|
"regressionTag": "Régression",
|
|
5578
|
-
"idOnly": "identifiant seul",
|
|
5579
|
-
"idOnlyHint": "Cet identifiant ne figure pas dans la spécification du service, il n'y a donc aucun titre à afficher.",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "La spécification du service n'a pas été lue :
|
|
5582
|
-
"unmatched": "Aucun de ces identifiants d'exigence ne figure dans la spécification du service qui a été lue ; seuls les identifiants signalés par le testeur sont affichés."
|
|
5579
|
+
"not_read": "La spécification du service n'a pas été lue : ces décomptes ne couvrent donc que ce sur quoi le testeur s'est prononcé, par identifiant."
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "Vérifiée",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "Ce pipeline ne comporte aucune étape de test, aucune exigence n'a donc été vérifiée.",
|
|
5591
5588
|
"tester_not_reported": "Le testeur n'a pas encore rendu son rapport, aucune exigence n'a donc été tranchée.",
|
|
5592
|
-
"no_verdicts": "Le testeur a rendu son rapport sans se prononcer sur la moindre exigence de la spécification."
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "Le testeur a rendu son rapport sans se prononcer sur la moindre exigence de la spécification.",
|
|
5590
|
+
"no_requirements": "La spécification du service n'enregistre aucune exigence : le testeur n'avait donc rien à trancher."
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "Le testeur s'est aussi prononcé sur {count} identifiant(s) absent(s) de cette spécification ; son propre décompte est donc supérieur à ceux ci-dessus."
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "Comment cela a été testé",
|