@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.
@@ -1,509 +1,40 @@
1
- // The RUN OUTCOME summary: the non-code answer to "what did this run change, and what backs
2
- // that up".
1
+ // The RUN OUTCOME summary, re-exported from `@cat-factory/contracts`.
3
2
  //
4
- // Reading a finished run used to mean reading a pull request: a branch name, a title, and a
5
- // diff. Everything a person who does not read diffs needs was already captured (the tester's
6
- // structured report, the screenshots it took, the visual-confirmation pairs a human reviewed,
7
- // the per-requirement verdicts it returned) and each of those sat behind its own window, keyed
8
- // by the STEP that produced it, so nobody who had not already learned the pipeline could find
9
- // any of it. This module is the reduction that puts them in one place, keyed by the RUN.
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
- // Three rules shape it, and they are the reason it is a pure module rather than computation
12
- // inside the window:
13
- //
14
- // 1. **Nothing here is asserted.** Every field is read off state a producer already recorded,
15
- // or COUNTED from it. The one derived judgement (a regression: an `established` requirement
16
- // the tester observed to fail) is computed in code from the spec's state and the tester's
17
- // verdict, exactly as the PR verification report computes its own, so a reader can
18
- // re-derive it from the rows. No model is asked for a headline.
19
- // 2. **Absent and zero never render the same.** Every section is a discriminated union whose
20
- // `absent` arm carries a `gap` CODE (mapped to translated copy at the render site, never
21
- // prose from here), because "no tester ran" and "the tester found nothing wrong" are
22
- // opposite facts that a blank section states identically.
23
- // 3. **The join to the spec is optional and says when it did not happen.** Requirement
24
- // verdicts are keyed by the spec's own requirement id; without the service spec loaded
25
- // there is no title to show, so `spec: 'unavailable'` says the ids are all there is rather
26
- // than letting an id read as the requirement's name.
27
- import type { Block, RequirementVerdictStatus, TestConcernSeverity } from '~/types/domain'
28
- import type { ExecutionInstance, PipelineStep } from '~/types/execution'
29
- import type { RequirementState, ServiceSpecView } from '~/types/spec'
30
- import type { ReproductionStatus } from '~/types/reproduction'
31
- import type { PullRequestRef, TestEnvironment } from '@cat-factory/contracts'
32
- import { allPullRequests } from '@cat-factory/contracts'
33
- import { isTesterKind } from '~/utils/catalog'
34
-
35
- /**
36
- * Where the run stands, in the terms the person reading the outcome cares about. Derived from
37
- * the BLOCK's status first (it is what the merge lifecycle writes) and from the run only for
38
- * the states a block cannot distinguish.
39
- */
40
- export type OutcomeDisposition =
41
- | 'merged'
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'
@@ -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 stehen hier die vom Tester gemeldeten Anforderungs-IDs ohne ihre Titel.",
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",
@@ -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 are the requirement ids the tester reported, without their titles.",
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",
@@ -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 son los identificadores de requisito que informó el probador, sin sus títulos.",
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ó",
@@ -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 : voici donc les identifiants d'exigence signalés par le testeur, sans leurs titres.",
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é",