@ultimat3/cli 22.15.0 → 24.0.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/CLAUDE.md +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
package/src/verify-run.ts
CHANGED
|
@@ -3,15 +3,27 @@
|
|
|
3
3
|
// command: what a run means — never bail early, count what actually ran — belongs to neither.
|
|
4
4
|
|
|
5
5
|
import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
|
|
6
|
+
import type { CoverageMap } from './coverage-lcov';
|
|
7
|
+
import { encodeCoverage } from './coverage-lcov';
|
|
6
8
|
import { msg } from './messages';
|
|
7
|
-
import type { CommandResult, Finding, StepResult } from './output';
|
|
9
|
+
import type { CommandResult, Finding, JsonValue, StepResult } from './output';
|
|
10
|
+
import type { StepGuard } from './verify-deadline';
|
|
11
|
+
import { guardStep, raceDeadline, stepTimeoutMs } from './verify-deadline';
|
|
8
12
|
import {
|
|
9
13
|
floorRequires,
|
|
10
14
|
readVerifyFloor,
|
|
11
15
|
skippedSuiteFinding,
|
|
12
16
|
vanishedSuiteFinding,
|
|
13
17
|
} from './verify-floor';
|
|
14
|
-
import
|
|
18
|
+
import { stalledOutput, stepTimeoutFinding } from './verify-stalled';
|
|
19
|
+
import type {
|
|
20
|
+
CoverageNumbers,
|
|
21
|
+
StepOutcome,
|
|
22
|
+
VerifyContext,
|
|
23
|
+
VerifyStep,
|
|
24
|
+
VerifyStepName,
|
|
25
|
+
} from './verify-step';
|
|
26
|
+
import { GATE_COMMAND } from './verify-step';
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* Run every step, never bailing early: an agent fixing three things at once needs all
|
|
@@ -30,6 +42,30 @@ export async function runVerify(
|
|
|
30
42
|
const only = onlyList(ctx.only);
|
|
31
43
|
const selected = only === undefined ? steps : steps.filter((step) => only.includes(step.name));
|
|
32
44
|
const byName = new Map<string, StepResult>();
|
|
45
|
+
// Per step: the numbers a judged run measured, or — under `--shard`, which cannot judge — the
|
|
46
|
+
// `facts` its slice covered, for `x verify merge` to fold.
|
|
47
|
+
const coverage = new Map<string, JsonValue>();
|
|
48
|
+
// One per run: a step's tag is `<step>@<run>`, so two gates on one machine never kill each
|
|
49
|
+
// other's children.
|
|
50
|
+
const runId = crypto.randomUUID().slice(0, 8);
|
|
51
|
+
const run = async (step: VerifyStep, stepCtx: VerifyContext): Promise<void> => {
|
|
52
|
+
const ran = await runStep(step, stepCtx, floor, runId);
|
|
53
|
+
byName.set(step.name, ran.result);
|
|
54
|
+
if (ran.coverage !== undefined) {
|
|
55
|
+
coverage.set(step.name, { facts: encodeCoverage(ran.coverage) });
|
|
56
|
+
} else if (ran.measured !== undefined) {
|
|
57
|
+
const { lines, funcs, files } = ran.measured;
|
|
58
|
+
coverage.set(step.name, { lines, funcs, files });
|
|
59
|
+
}
|
|
60
|
+
// Told as it finishes, never at the end: the caller streams it, so a cancelled job's log
|
|
61
|
+
// ends on the last step that finished (#589). A progress line is not the verdict: a listener
|
|
62
|
+
// that throws — stderr closed under it — must not take the gate down with it.
|
|
63
|
+
try {
|
|
64
|
+
ctx.onStep?.(ran.result);
|
|
65
|
+
} catch {
|
|
66
|
+
// Nothing to report it on; the document on stdout still carries the step.
|
|
67
|
+
}
|
|
68
|
+
};
|
|
33
69
|
const began = performance.now();
|
|
34
70
|
// The static steps wait for the serial suites and then run BESIDE them — only when `live` is in
|
|
35
71
|
// the list, so a one-step run (`--only`) and a list with no serial suite keep today's order.
|
|
@@ -51,16 +87,12 @@ export async function runVerify(
|
|
|
51
87
|
for (const step of selected) {
|
|
52
88
|
if (beside.includes(step)) continue;
|
|
53
89
|
if (step.name === SERIAL_SUITES[0]) {
|
|
54
|
-
pending = Promise.all(
|
|
55
|
-
beside.map(async (other) => {
|
|
56
|
-
byName.set(other.name, await runStep(other, base, floor));
|
|
57
|
-
}),
|
|
58
|
-
).then(() => undefined);
|
|
90
|
+
pending = Promise.all(beside.map((other) => run(other, base))).then(() => undefined);
|
|
59
91
|
} else if (!SERIAL_SUITES.includes(step.name)) {
|
|
60
92
|
await join();
|
|
61
93
|
}
|
|
62
94
|
const inWindow = pending !== undefined && SERIAL_SUITES.includes(step.name);
|
|
63
|
-
|
|
95
|
+
await run(step, inWindow ? shared : base);
|
|
64
96
|
}
|
|
65
97
|
await join();
|
|
66
98
|
// Reported in the declared order, whatever order the steps finished in: the table, `--json` and
|
|
@@ -98,6 +130,7 @@ export async function runVerify(
|
|
|
98
130
|
// summary line to learn that this run checked one thing.
|
|
99
131
|
// `only` is always the LIST, in declared order — one name is a list of one.
|
|
100
132
|
...(only === undefined ? {} : { notAGateRun: true, only: [...only] }),
|
|
133
|
+
...(coverage.size === 0 ? {} : { coverage: Object.fromEntries(coverage) }),
|
|
101
134
|
},
|
|
102
135
|
// The step's own status: one step, so `failedSteps` is that step and nothing else.
|
|
103
136
|
exitCode: failedSteps.length === 0 ? 0 : 1,
|
|
@@ -147,7 +180,7 @@ export function verifySummary(input: {
|
|
|
147
180
|
* `manifest` joined them 2026-09-23: it compares committed files against the code and writes
|
|
148
181
|
* nothing, and at the repo root its host checks are ~20 whole-tree reads that sat alone at the end.
|
|
149
182
|
*/
|
|
150
|
-
export const BESIDE_SERIAL_SUITES: ReadonlySet<
|
|
183
|
+
export const BESIDE_SERIAL_SUITES: ReadonlySet<VerifyStepName> = new Set<VerifyStepName>([
|
|
151
184
|
'lint',
|
|
152
185
|
'boundaries',
|
|
153
186
|
'filesize',
|
|
@@ -157,20 +190,33 @@ export const BESIDE_SERIAL_SUITES: ReadonlySet<string> = new Set([
|
|
|
157
190
|
]);
|
|
158
191
|
|
|
159
192
|
/** The consecutive run of steps the static group overlaps, in the order `VERIFY_STEP_NAMES` holds. */
|
|
160
|
-
export const SERIAL_SUITES: readonly
|
|
193
|
+
export const SERIAL_SUITES: readonly VerifyStepName[] = ['live', 'job', 'e2e', 'eval'];
|
|
161
194
|
|
|
162
195
|
async function runStep(
|
|
163
196
|
step: VerifyStep,
|
|
164
197
|
ctx: VerifyContext,
|
|
165
198
|
floor: Awaited<ReturnType<typeof readVerifyFloor>>,
|
|
166
|
-
|
|
199
|
+
runId: string,
|
|
200
|
+
): Promise<{
|
|
201
|
+
readonly result: StepResult;
|
|
202
|
+
readonly coverage?: CoverageMap;
|
|
203
|
+
readonly measured?: CoverageNumbers;
|
|
204
|
+
}> {
|
|
205
|
+
const command = ctx.command ?? GATE_COMMAND;
|
|
167
206
|
// Inside the step's own failure, like a throwing `run`: an `applies` that threw escaped every
|
|
168
207
|
// catch and aborted the whole gate, which is the one outcome `runVerify` exists to prevent.
|
|
169
208
|
let applies: boolean;
|
|
170
209
|
try {
|
|
171
210
|
applies = step.applies === undefined ? true : await step.applies(ctx);
|
|
172
211
|
} catch (error) {
|
|
173
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
result: {
|
|
214
|
+
name: step.name,
|
|
215
|
+
ok: false,
|
|
216
|
+
durationMs: 0,
|
|
217
|
+
findings: [findingOf(error, step.name, command)],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
174
220
|
}
|
|
175
221
|
if (!applies) {
|
|
176
222
|
// A skip this repo already ruled out is not a skip. The step ran here before — the floor is
|
|
@@ -180,20 +226,33 @@ async function runStep(
|
|
|
180
226
|
// the summary, `data.failed`, and the reference-app gate's own red list.
|
|
181
227
|
const required = floorRequires(floor, step.name);
|
|
182
228
|
return {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
229
|
+
result: {
|
|
230
|
+
name: step.name,
|
|
231
|
+
ok: !required,
|
|
232
|
+
durationMs: 0,
|
|
233
|
+
skipped: !required,
|
|
234
|
+
findings: required ? [vanishedSuiteFinding(step.name, command)] : [],
|
|
235
|
+
},
|
|
188
236
|
};
|
|
189
237
|
}
|
|
190
238
|
const started = performance.now();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
239
|
+
// The deadline: a step that hangs fails BY NAME and its processes are killed, instead of eating
|
|
240
|
+
// the CI job's whole timeout and leaving a log that says nothing (#589). The step runs on a
|
|
241
|
+
// guarded runner, so everything it starts carries its tag — that is what the kill finds.
|
|
242
|
+
const limit = stepTimeoutMs(step.name, ctx.stepTimeoutMs, floor?.stepTimeoutMs);
|
|
243
|
+
const guard = guardStep(ctx.runner, `${step.name}@${runId}`);
|
|
244
|
+
const raced = await raceDeadline(
|
|
245
|
+
step.run({ ...ctx, runner: guard.runner }).catch(
|
|
246
|
+
(error: unknown): StepOutcome => ({
|
|
247
|
+
ok: false,
|
|
248
|
+
findings: [findingOf(error, step.name, command)],
|
|
249
|
+
}),
|
|
250
|
+
),
|
|
251
|
+
limit,
|
|
196
252
|
);
|
|
253
|
+
const outcome: StepOutcome = raced.timedOut
|
|
254
|
+
? await expired(step.name, limit, guard, command)
|
|
255
|
+
: raced.value;
|
|
197
256
|
// A suite that executed nothing did not run, whatever its exit code says: `bun test` exits 0
|
|
198
257
|
// over an all-skipped file, so the counts are the only channel that can tell the two apart.
|
|
199
258
|
// ONE definition of "nothing ran", read twice, because the floor decides which of the two
|
|
@@ -208,23 +267,43 @@ async function runStep(
|
|
|
208
267
|
// step's own findings so `data.failed`, the counts and every gate reading this table carry it.
|
|
209
268
|
const vanished = nothingRan && required ? [skippedSuiteFinding(step.name, tests.skipped)] : [];
|
|
210
269
|
return {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
270
|
+
result: {
|
|
271
|
+
name: step.name,
|
|
272
|
+
ok: outcome.ok && vanished.length === 0,
|
|
273
|
+
durationMs: Math.round(performance.now() - started),
|
|
274
|
+
// Without a floor to require it, a suite that ran nothing is a SKIP and not a pass (#434):
|
|
275
|
+
// the `e2e` step printed `✓ e2e 46ms` over its one skipped test, which is the one thing a
|
|
276
|
+
// step table may never do — a reader cannot tell a lane that ran from a lane that did not.
|
|
277
|
+
skipped: nothingRan && !required,
|
|
278
|
+
findings: [...outcome.findings, ...vanished],
|
|
279
|
+
...(outcome.output === undefined ? {} : { output: outcome.output }),
|
|
280
|
+
...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
|
|
281
|
+
...(outcome.widthReason === undefined ? {} : { widthReason: outcome.widthReason }),
|
|
282
|
+
...(outcome.shard === undefined ? {} : { shard: outcome.shard }),
|
|
283
|
+
...(tests === undefined ? {} : { tests }),
|
|
284
|
+
},
|
|
285
|
+
...(outcome.coverage === undefined ? {} : { coverage: outcome.coverage }),
|
|
286
|
+
...(outcome.measured === undefined ? {} : { measured: outcome.measured }),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The timed-out step's outcome: the finding naming what was in flight, over what it last printed. */
|
|
291
|
+
async function expired(
|
|
292
|
+
step: VerifyStepName,
|
|
293
|
+
limit: number,
|
|
294
|
+
guard: StepGuard,
|
|
295
|
+
command: string,
|
|
296
|
+
): Promise<StepOutcome> {
|
|
297
|
+
const expiry = await guard.expire();
|
|
298
|
+
const output = stalledOutput(expiry);
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
findings: [stepTimeoutFinding(step, limit, expiry, command)],
|
|
302
|
+
...(output === undefined ? {} : { output }),
|
|
224
303
|
};
|
|
225
304
|
}
|
|
226
305
|
|
|
227
|
-
function findingOf(error: unknown, step: string): Finding {
|
|
306
|
+
function findingOf(error: unknown, step: string, command: string): Finding {
|
|
228
307
|
// A step may throw anything, including an Error that fights being read: `instanceof` runs a
|
|
229
308
|
// Proxy's `getPrototypeOf` trap and `.message` runs a getter, so a hostile throw would take the
|
|
230
309
|
// gate's own report down with it — the one message that may never be lost.
|
|
@@ -232,7 +311,7 @@ function findingOf(error: unknown, step: string): Finding {
|
|
|
232
311
|
return {
|
|
233
312
|
code: 'X_VERIFY_FAILED',
|
|
234
313
|
cause: `step "${step}" threw: ${cause}`,
|
|
235
|
-
fix:
|
|
314
|
+
fix: `${command} --json`,
|
|
236
315
|
docs: ERROR_DOCS_URL,
|
|
237
316
|
};
|
|
238
317
|
}
|
package/src/verify-shard.ts
CHANGED
|
@@ -5,11 +5,23 @@
|
|
|
5
5
|
// of the machine, the clock or the order a glob yielded — so job 3 of 8 is the same files on every
|
|
6
6
|
// runner, and a failing shard reproduces locally with the same flags.
|
|
7
7
|
|
|
8
|
-
import { renderFixShellArg } from '@ultimat3/core';
|
|
8
|
+
import { renderFixShellArg, UltimateError } from '@ultimat3/core';
|
|
9
9
|
import { CryptoHasher } from 'bun';
|
|
10
10
|
import type { ShardFacts } from './output';
|
|
11
|
-
import { VerifyShardInvalidError } from './verify-errors';
|
|
12
11
|
import type { VerifyStepName } from './verify-step';
|
|
12
|
+
import { GATE_COMMAND } from './verify-step';
|
|
13
|
+
|
|
14
|
+
/** `x verify --shard` asked for a split the gate cannot make. */
|
|
15
|
+
export class VerifyShardInvalidError extends UltimateError {
|
|
16
|
+
constructor(input: { reason: string; fix?: string; command?: string }) {
|
|
17
|
+
const command = input.command ?? GATE_COMMAND;
|
|
18
|
+
super({
|
|
19
|
+
code: 'X_VERIFY_SHARD_INVALID',
|
|
20
|
+
cause: `${command} --shard: ${input.reason}`,
|
|
21
|
+
fix: input.fix ?? `${command} --only unit --shard 1/4 --json`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
13
25
|
|
|
14
26
|
/** The steps a shard may split: the parallel suites. `live` and `e2e` are serial by design. */
|
|
15
27
|
export const SHARDABLE_STEPS: readonly VerifyStepName[] = ['unit', 'contract', 'job'];
|
|
@@ -20,18 +32,22 @@ export interface ShardSpec {
|
|
|
20
32
|
readonly total: number;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `3/8` → `{ index: 3, total: 8 }`, refused unless `1 <= i <= n`. `command` is the gate as the
|
|
37
|
+
* caller was invoked, so the refusal's fix runs where it was raised.
|
|
38
|
+
*/
|
|
39
|
+
export function parseShard(raw: string, command: string = GATE_COMMAND): ShardSpec {
|
|
25
40
|
const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(raw);
|
|
26
41
|
const index = match === null ? Number.NaN : Number(match[1]);
|
|
27
42
|
const total = match === null ? Number.NaN : Number(match[2]);
|
|
28
43
|
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || total < 1 || index < 1) {
|
|
29
|
-
throw new VerifyShardInvalidError({ reason: `"${raw}" is not i/n (e.g. 2/4)
|
|
44
|
+
throw new VerifyShardInvalidError({ reason: `"${raw}" is not i/n (e.g. 2/4)`, command });
|
|
30
45
|
}
|
|
31
46
|
if (index > total) {
|
|
32
47
|
throw new VerifyShardInvalidError({
|
|
33
48
|
reason: `shard ${String(index)} does not exist in a ${String(total)}-way split (1..${String(total)})`,
|
|
34
|
-
fix:
|
|
49
|
+
fix: `${command} --only unit --shard ${renderFixShellArg(`${String(total)}/${String(total)}`, '<i/n>')} --json`,
|
|
50
|
+
command,
|
|
35
51
|
});
|
|
36
52
|
}
|
|
37
53
|
return { index, total };
|
|
@@ -41,11 +57,14 @@ export function parseShard(raw: string): ShardSpec {
|
|
|
41
57
|
* The steps a shard run may name: every one must be shardable. Refused with the step named, and a
|
|
42
58
|
* fix that keeps the shardable ones — `live`/`e2e` belong in a job of their own.
|
|
43
59
|
*/
|
|
44
|
-
export function assertShardable(
|
|
60
|
+
export function assertShardable(
|
|
61
|
+
only: readonly VerifyStepName[] | undefined,
|
|
62
|
+
command: string = GATE_COMMAND,
|
|
63
|
+
): void {
|
|
45
64
|
if (only === undefined || only.length === 0) {
|
|
46
65
|
throw new VerifyShardInvalidError({
|
|
47
66
|
reason: 'a shard is one slice of ONE parallel step and needs --only to name it',
|
|
48
|
-
|
|
67
|
+
command,
|
|
49
68
|
});
|
|
50
69
|
}
|
|
51
70
|
const refused = only.filter((step) => !SHARDABLE_STEPS.includes(step));
|
|
@@ -57,8 +76,9 @@ export function assertShardable(only: readonly VerifyStepName[] | undefined): vo
|
|
|
57
76
|
} in a job of its own without --shard`,
|
|
58
77
|
fix:
|
|
59
78
|
kept.length === 0
|
|
60
|
-
?
|
|
61
|
-
:
|
|
79
|
+
? `${command} --only ${renderFixShellArg(refused.join(','), '<step,step>')} --json`
|
|
80
|
+
: `${command} --only ${renderFixShellArg(kept.join(','), '<step,step>')} --shard 1/4 --json`,
|
|
81
|
+
command,
|
|
62
82
|
});
|
|
63
83
|
}
|
|
64
84
|
}
|
|
@@ -116,12 +136,13 @@ export function shardFiles(
|
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
/** A timings file, or a refusal naming it — a CI job must not silently fall back to round-robin. */
|
|
119
|
-
export async function readTimings(path: string): Promise<Timings> {
|
|
139
|
+
export async function readTimings(path: string, command: string = GATE_COMMAND): Promise<Timings> {
|
|
120
140
|
const file = Bun.file(path);
|
|
121
141
|
if (!(await file.exists())) {
|
|
122
142
|
throw new VerifyShardInvalidError({
|
|
123
143
|
reason: `--timings ${path} does not exist`,
|
|
124
144
|
fix: 'bun test --timings=timings.json --update-timings # writes it, then commit or cache it',
|
|
145
|
+
command,
|
|
125
146
|
});
|
|
126
147
|
}
|
|
127
148
|
let parsed: unknown;
|
|
@@ -139,6 +160,7 @@ export async function readTimings(path: string): Promise<Timings> {
|
|
|
139
160
|
throw new VerifyShardInvalidError({
|
|
140
161
|
reason: `--timings ${path} is not a JSON object of { "path": ms }`,
|
|
141
162
|
fix: 'bun test --timings=timings.json --update-timings',
|
|
163
|
+
command,
|
|
142
164
|
});
|
|
143
165
|
}
|
|
144
166
|
return parsed as Timings;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// What a step was doing when its deadline passed, as the `X_VERIFY_STEP_TIMEOUT` finding. A hung
|
|
2
|
+
// test file used to cost eight minutes and report "2 process(es) were killed" — nothing a reader
|
|
3
|
+
// could run. The finding now names the file bun says a killed worker held, and its `fix:` runs it.
|
|
4
|
+
|
|
5
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
|
+
import type { Finding } from './output';
|
|
7
|
+
import { quoteArg } from './shell-quote';
|
|
8
|
+
import type { InFlightRun, StepExpiry } from './verify-deadline';
|
|
9
|
+
import { STEP_TIMEOUT_FIELD } from './verify-deadline';
|
|
10
|
+
import type { VerifyStepName } from './verify-step';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One `bun test` the step was waiting on, read against what it printed as it was stopped. A `type`,
|
|
14
|
+
* like the meta that carries it: an interface is not assignable to `Finding.meta`'s JSON record.
|
|
15
|
+
*/
|
|
16
|
+
export type StalledRun = {
|
|
17
|
+
/** The argv without its file list: `bun test --parallel=3`. */
|
|
18
|
+
readonly command: string;
|
|
19
|
+
/** Files the run was handed; 0 for a serial run, which selects by filter. */
|
|
20
|
+
readonly files: number;
|
|
21
|
+
/** `--parallel`'s width, and 1 for a serial run. */
|
|
22
|
+
readonly workers: number;
|
|
23
|
+
/** The files that were still running, as bun named them. Empty when it named none. */
|
|
24
|
+
readonly stuck: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** `X_VERIFY_STEP_TIMEOUT`'s `meta`. A `type`: an interface is not assignable to `Finding.meta`. */
|
|
28
|
+
export type StepTimeoutMeta = {
|
|
29
|
+
readonly step: string;
|
|
30
|
+
readonly deadlineMs: number;
|
|
31
|
+
readonly killed: readonly { readonly pid: number; readonly command: string }[];
|
|
32
|
+
readonly inFlight: readonly StalledRun[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What a `--parallel` coordinator prints for the file a killed worker was holding. The guard kills
|
|
37
|
+
* the workers first for exactly this line (`verify-deadline.ts`); it is bun's own statement of
|
|
38
|
+
* what was in flight, and it is printed in every reporter mode.
|
|
39
|
+
*/
|
|
40
|
+
const CRASHED = /^\s*✗ (.+) \(worker crashed: SIG[A-Z0-9]+\)$/;
|
|
41
|
+
|
|
42
|
+
/** Bun's per-file header — `::group::`-prefixed on GitHub Actions. A serial run prints it at START. */
|
|
43
|
+
const FILE_LINE = /^(?:::group::)?(\S.*\.test\.[cm]?[jt]sx?):$/;
|
|
44
|
+
|
|
45
|
+
const PARALLEL = '--parallel=';
|
|
46
|
+
|
|
47
|
+
const bare = (path: string): string => (path.startsWith('./') ? path.slice(2) : path);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A parallel run names its stuck files itself, one crash line per killed worker. A serial run has
|
|
51
|
+
* no worker to kill: it prints a file's header when the file STARTS, so the stuck file is the last
|
|
52
|
+
* one printed — unless a `(fail)` follows it, which is also all a failures-only reporter ever
|
|
53
|
+
* prints, and then nothing is claimed.
|
|
54
|
+
*/
|
|
55
|
+
export function stalledRun(run: InFlightRun): StalledRun | undefined {
|
|
56
|
+
const [program, verb, ...rest] = run.command;
|
|
57
|
+
if (program !== 'bun' || verb !== 'test' || run.output === undefined) return undefined;
|
|
58
|
+
const lines = run.output.split('\n').map((line) => line.trimEnd());
|
|
59
|
+
const width = rest.find((arg) => arg.startsWith(PARALLEL));
|
|
60
|
+
const flags = rest.filter((arg) => arg.startsWith('--') && !arg.startsWith('--timings'));
|
|
61
|
+
const command = ['bun', 'test', ...flags].join(' ');
|
|
62
|
+
if (width === undefined) {
|
|
63
|
+
const last = lines.findLastIndex((line) => FILE_LINE.test(line));
|
|
64
|
+
const started = last < 0 ? undefined : FILE_LINE.exec(lines[last] ?? '')?.[1];
|
|
65
|
+
const failed = lines.slice(last + 1).some((line) => line.startsWith('(fail)'));
|
|
66
|
+
return {
|
|
67
|
+
command,
|
|
68
|
+
files: 0,
|
|
69
|
+
workers: 1,
|
|
70
|
+
stuck: started === undefined || failed ? [] : [started],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const files = rest.filter((arg) => !arg.startsWith('--'));
|
|
74
|
+
const crashed = lines.flatMap((line) => {
|
|
75
|
+
const path = CRASHED.exec(line)?.[1];
|
|
76
|
+
// As the caller handed it in, so the `fix:` reruns the very argument the run was given.
|
|
77
|
+
return path === undefined ? [] : [files.find((file) => bare(file) === bare(path)) ?? path];
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
command,
|
|
81
|
+
files: files.length,
|
|
82
|
+
workers: Math.max(1, Number(width.slice(PARALLEL.length)) || 1),
|
|
83
|
+
stuck: [...new Set(crashed)],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** How many paths a `cause:` spells out — the rest are in `meta`. */
|
|
88
|
+
const NAMED_IN_CAUSE = 5;
|
|
89
|
+
|
|
90
|
+
const OUTPUT_TAIL_CHARS = 4_000;
|
|
91
|
+
|
|
92
|
+
/** The last thing each killed run printed: what a reader of a red step looks at first. */
|
|
93
|
+
export function stalledOutput(expiry: StepExpiry): string | undefined {
|
|
94
|
+
const text = expiry.inFlight
|
|
95
|
+
.flatMap((run) => (run.output === undefined || run.output === '' ? [] : [run.output]))
|
|
96
|
+
.join('\n');
|
|
97
|
+
if (text === '') return undefined;
|
|
98
|
+
return text.length > OUTPUT_TAIL_CHARS ? `…${text.slice(-OUTPUT_TAIL_CHARS)}` : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const inFlightClause = (runs: readonly StalledRun[], expiry: StepExpiry): string => {
|
|
102
|
+
const files = runs.flatMap((run) => run.stuck);
|
|
103
|
+
const named = runs.find((run) => run.stuck.length > 0);
|
|
104
|
+
if (named !== undefined) {
|
|
105
|
+
const shown = files.slice(0, NAMED_IN_CAUSE).join(', ');
|
|
106
|
+
const more = files.length > NAMED_IN_CAUSE ? `, +${String(files.length - NAMED_IN_CAUSE)}` : '';
|
|
107
|
+
return `; still running when it was stopped: ${shown}${more} (${named.command})`;
|
|
108
|
+
}
|
|
109
|
+
const waiting = expiry.inFlight[0];
|
|
110
|
+
if (waiting === undefined) return '';
|
|
111
|
+
return runs[0] === undefined
|
|
112
|
+
? `; it was waiting on: ${waiting.command.slice(0, 4).join(' ')}`
|
|
113
|
+
: `; its running "${runs[0].command}" named no file still in flight, so the run itself did not exit`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A step that ran past its deadline, on that step's own line. `command` is the gate as it was
|
|
118
|
+
* invoked here (`x verify`, or `bun run verify` at the framework root), so the fix runs where the
|
|
119
|
+
* finding was raised. With a stuck file named, the fix runs THAT file; the step's own rerun and
|
|
120
|
+
* the `stepTimeoutMs` edit ride behind it.
|
|
121
|
+
*/
|
|
122
|
+
export const stepTimeoutFinding = (
|
|
123
|
+
step: VerifyStepName,
|
|
124
|
+
ms: number,
|
|
125
|
+
expiry: StepExpiry,
|
|
126
|
+
command: string,
|
|
127
|
+
): Finding => {
|
|
128
|
+
const runs = expiry.inFlight.flatMap((run) => stalledRun(run) ?? []);
|
|
129
|
+
const stuck = runs.flatMap((run) => run.stuck);
|
|
130
|
+
const rerun = `${command} --only ${step} --json`;
|
|
131
|
+
const longer = `if it legitimately needs longer, edit x.verify.json — add "${STEP_TIMEOUT_FIELD}": { "${step}": <milliseconds> }`;
|
|
132
|
+
// Each path through `quoteArg`, one argv word apiece: a test file's name is data from the disk,
|
|
133
|
+
// and this line is pasted into a shell.
|
|
134
|
+
const alone = ['bun', 'test', ...stuck.map(quoteArg)].join(' ');
|
|
135
|
+
const meta: StepTimeoutMeta = {
|
|
136
|
+
step,
|
|
137
|
+
deadlineMs: ms,
|
|
138
|
+
killed: expiry.killed.map((one) => ({ pid: one.pid, command: one.command })),
|
|
139
|
+
inFlight: runs,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
code: 'X_VERIFY_STEP_TIMEOUT',
|
|
143
|
+
cause: `step "${step}" did not finish within its ${String(ms)} ms deadline, so it was stopped and ${String(expiry.killed.length)} process(es) it had started were killed${inFlightClause(runs, expiry)}`,
|
|
144
|
+
fix:
|
|
145
|
+
stuck.length > 0
|
|
146
|
+
? `${alone} # what had not finished, alone; the whole step: ${rerun}; ${longer}`
|
|
147
|
+
: `${rerun} # reproduce it alone; ${longer}`,
|
|
148
|
+
docs: ERROR_DOCS_URL,
|
|
149
|
+
...(stuck[0] === undefined ? {} : { at: stuck[0] }),
|
|
150
|
+
meta,
|
|
151
|
+
};
|
|
152
|
+
};
|
package/src/verify-step.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// implementation can live beside the code it checks without importing the list.
|
|
4
4
|
|
|
5
5
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
|
+
import type { CoverageMap } from './coverage-lcov';
|
|
6
7
|
import type { ExecResult, Runner } from './exec';
|
|
7
8
|
import { execOutput } from './exec';
|
|
8
|
-
import type { Finding, ShardFacts } from './output';
|
|
9
|
+
import type { Finding, ShardFacts, StepResult } from './output';
|
|
9
10
|
import type { TestCounts } from './test-counts';
|
|
11
|
+
import type { StepTimeouts } from './verify-deadline';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Every step of the gate, in cost order — cheapest and most informative first, and never a check
|
|
@@ -106,8 +108,29 @@ export interface VerifyContext {
|
|
|
106
108
|
* name is accepted for the callers that narrow to one (`scripts/verify.ts`).
|
|
107
109
|
*/
|
|
108
110
|
readonly only?: VerifyStepName | readonly VerifyStepName[];
|
|
111
|
+
/**
|
|
112
|
+
* Per-step deadlines in milliseconds that beat `x.verify.json`'s `stepTimeoutMs`, which beat the
|
|
113
|
+
* step class's declared number (`verify-deadline.ts`). A step past its deadline fails by name
|
|
114
|
+
* with `X_VERIFY_STEP_TIMEOUT` and every process it started is killed.
|
|
115
|
+
*/
|
|
116
|
+
readonly stepTimeoutMs?: StepTimeouts;
|
|
117
|
+
/**
|
|
118
|
+
* Called as each step finishes, in the order they finish. `--json` prints nothing until the run
|
|
119
|
+
* ends, so a job cancelled mid-run left no trace of where it hung (#589): the command streams
|
|
120
|
+
* one line per step to stderr through this, and stdout stays the single document.
|
|
121
|
+
*/
|
|
122
|
+
readonly onStep?: (step: StepResult) => void;
|
|
123
|
+
/**
|
|
124
|
+
* The gate as it was invoked HERE, for a `fix:` that has to run where it was raised: `x verify`
|
|
125
|
+
* when absent, `bun run verify` from the framework root's own entry — where `x verify` answers
|
|
126
|
+
* `X_NOT_IN_APP`.
|
|
127
|
+
*/
|
|
128
|
+
readonly command?: string;
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
/** `ctx.command`'s default: the binary and the command, as an app types it. */
|
|
132
|
+
export const GATE_COMMAND = 'x verify';
|
|
133
|
+
|
|
111
134
|
export interface StepOutcome {
|
|
112
135
|
readonly ok: boolean;
|
|
113
136
|
readonly findings: readonly Finding[];
|
|
@@ -128,6 +151,24 @@ export interface StepOutcome {
|
|
|
128
151
|
* NOT the same as a suite that ran nothing, which is a number the ratchet acts on.
|
|
129
152
|
*/
|
|
130
153
|
readonly tests?: TestCounts;
|
|
154
|
+
/**
|
|
155
|
+
* What the `unit` suite covered of the app's own tree, when this run did NOT judge it: a
|
|
156
|
+
* `--shard` slice cannot hold a floor, so it hands its facts to `x verify merge`, which folds
|
|
157
|
+
* every shard's and judges once.
|
|
158
|
+
*/
|
|
159
|
+
readonly coverage?: CoverageMap;
|
|
160
|
+
/**
|
|
161
|
+
* What the `unit` suite covered of the app's own tree when this run DID judge it. `--json` drops
|
|
162
|
+
* a green step's `output`, so the numbers ride in `data.coverage` where a reader can find them.
|
|
163
|
+
*/
|
|
164
|
+
readonly measured?: CoverageNumbers;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The two percentages a judged run measured, and the source files they are over. */
|
|
168
|
+
export interface CoverageNumbers {
|
|
169
|
+
readonly lines: number;
|
|
170
|
+
readonly funcs: number;
|
|
171
|
+
readonly files: number;
|
|
131
172
|
}
|
|
132
173
|
|
|
133
174
|
export interface VerifyStep {
|