@patronage/factory-ci 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -17
- package/dist/index.d.ts +222 -25
- package/dist/index.js +515 -132
- package/package.json +1 -1
- package/src/index.ts +23 -5
- package/src/proof-reuse-gate.ts +669 -0
- package/src/hosted-docs-only-detector.ts +0 -192
package/README.md
CHANGED
|
@@ -59,38 +59,41 @@ workflow({/* caller-owned jobs and topology */}).writeOrLint({
|
|
|
59
59
|
|
|
60
60
|
The **runner is not this package's business**. Jobs, runners, permissions, workflow topology, and deploy policy remain with the caller. The returned values are plain structural objects; this package does not depend on gagen.
|
|
61
61
|
|
|
62
|
-
###
|
|
62
|
+
### Proof reuse
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
65
|
import {
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
assertProofReuseCoverage,
|
|
67
|
+
FACTORY_PROOF_GATE_GUARD,
|
|
68
|
+
factoryProofGateStep,
|
|
68
69
|
} from "@patronage/factory-ci";
|
|
69
|
-
import { job, step } from "gagen";
|
|
70
70
|
|
|
71
71
|
import profile from "../../software-factory.profile.json" with { type: "json" };
|
|
72
72
|
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const coreCommands = profile.verification.commands.filter(
|
|
74
|
+
({ name }) => !name.startsWith("docs:")
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const core = job("core", {
|
|
78
|
+
permissions: { checks: "read", contents: "read" },
|
|
79
79
|
steps: [
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
uses: NODE_PNPM_ACTION_FAMILY_NODE24.checkout.uses,
|
|
83
|
-
with: { "fetch-depth": 0 },
|
|
84
|
-
},
|
|
85
|
-
detector,
|
|
80
|
+
step(factoryProofGateStep({ commands: coreCommands, surface: "core" })),
|
|
81
|
+
...guardedSteps.map((s) => ({ ...s, if: FACTORY_PROOF_GATE_GUARD })),
|
|
86
82
|
],
|
|
87
|
-
outputs: { docs_only: detector.outputs.docs_only },
|
|
88
83
|
});
|
|
89
84
|
```
|
|
90
85
|
|
|
91
|
-
|
|
86
|
+
The **proof-reuse gate** decides whether a hosted job may reuse the local verification the factory already published for this exact head (ADR 0022). Refusing runs hosted CI; it never fails the candidate. The App identity, check name, step id, output names, guard, and every trust predicate are fixed here rather than consumer-configurable — three repositories had grown three answers to the same question and had already drifted.
|
|
87
|
+
|
|
88
|
+
A proof is reusable only when the complete Checks API result (`filter=all`, every page) establishes one unambiguous newest generation by greatest `started_at`, produced by the pinned App for the exact repository and head, completed successfully with `outcome: passed`, and covering every command identity the guarded surface requires. `mode` is reported as diagnostic metadata, never authorized on: a reduced-mode proof that ran everything a surface requires is reusable.
|
|
89
|
+
|
|
90
|
+
The only thing a consumer chooses is **what its surface requires**, expressed as the plain profile command objects that surface selects — a repository with distinct core and docs jobs selects distinct sets and gets distinct required coverage. `proofReuseRequiredCommands()` is the single derivation both the gate and the assertion go through. Identities are baked into the emitted script, so they are held to a plain `[A-Za-z0-9_][\w.:@/-]*` allow-list and shell-quoted at the interpolation site; a selection carrying anything else is unusable and degrades to a gate that always refuses.
|
|
91
|
+
|
|
92
|
+
The gate is a **step, not a job**, marked `continue-on-error`, with no `set -e`. A gate job that errored would leave the guarded job `skipped`, which a summary job that only fails on `failure` / `cancelled` reports as green. As a step it fails open by construction: the step errors, the output is never written, the `!= 'true'` guard reads empty, and every command runs. It writes a machine-readable `reason` output — `proven | none | pending | failed | unreadable | incomplete | ambiguous | error` — so a reuse-collapses-to-never regression is visible instead of hidden behind fail-open.
|
|
93
|
+
|
|
94
|
+
**Emit the step's `shell` verbatim.** `factoryProofGateStep()` sets `shell: bash --noprofile --norc {0}` (`FACTORY_PROOF_GATE_SHELL`) and a generator that drops it breaks the gate. GitHub's default for `run:` is `bash -e {0}` — errexit arrives from the invocation, not the script, so omitting `set -e` does not achieve it. Under the default the gate dies at the first non-zero command before its single `GITHUB_OUTPUT` write: no verdict, no `reason`, reuse silently collapsed to never while the job looks like healthy full CI. `shell: bash` is not a substitute; it expands to `bash --noprofile --norc -eo pipefail {0}`. The script is written to survive errexit as well, and its tests execute every case under both shells — a harness that runs this script under friendlier flags than the runner does is worse than no harness.
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
`assertProofReuseCoverage({ commands, skipped, surface })` is the compile-time guard in front of the runtime `incomplete` refusal: hand it the same selection and the command strings the workflow would skip, and it fails the consumer's build when the two drift apart. Extracting the skipped strings stays with the consumer — this package never parses workflow source, because establishing trust that way is what killed an earlier attempt.
|
|
94
97
|
|
|
95
98
|
### Alchemy entries
|
|
96
99
|
|
package/dist/index.d.ts
CHANGED
|
@@ -194,30 +194,6 @@ interface FactoryWorkflowArtifact<Additional extends Readonly<Record<string, Pin
|
|
|
194
194
|
*/
|
|
195
195
|
declare const factoryWorkflow: <const Additional extends Readonly<Record<string, PinnedAction>> = Record<never, never>>(options: FactoryWorkflowOptions<Additional>) => FactoryWorkflowArtifact<Additional>;
|
|
196
196
|
//#endregion
|
|
197
|
-
//#region src/hosted-docs-only-detector.d.ts
|
|
198
|
-
interface HostedDocsOnlyClassificationPolicy {
|
|
199
|
-
readonly docsOnlyFiles?: readonly string[];
|
|
200
|
-
readonly docsOnlyMarkdownPrefixes?: readonly string[];
|
|
201
|
-
readonly docsOnlyPrefixes?: readonly string[];
|
|
202
|
-
}
|
|
203
|
-
interface HostedDocsOnlyDetectorStep {
|
|
204
|
-
readonly continueOnError: true;
|
|
205
|
-
readonly env: Readonly<{
|
|
206
|
-
BASE_SHA: string;
|
|
207
|
-
HEAD_SHA: string;
|
|
208
|
-
}>;
|
|
209
|
-
readonly id: "docs-only";
|
|
210
|
-
readonly name: "Detect docs-only changes";
|
|
211
|
-
readonly outputs: readonly ["docs_only"];
|
|
212
|
-
readonly run: string;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Generate the pre-install hosted detector from a consumer's canonical
|
|
216
|
-
* classification policy. The returned object is structurally accepted by
|
|
217
|
-
* gagen's `step()` without adding a gagen runtime dependency.
|
|
218
|
-
*/
|
|
219
|
-
declare const hostedDocsOnlyDetectorStep: (policy: HostedDocsOnlyClassificationPolicy) => HostedDocsOnlyDetectorStep;
|
|
220
|
-
//#endregion
|
|
221
197
|
//#region src/execute-alchemy-entry.d.ts
|
|
222
198
|
interface ExecuteAlchemyEntryOptions {
|
|
223
199
|
/**
|
|
@@ -249,4 +225,225 @@ interface ExecuteAlchemyEntryResult {
|
|
|
249
225
|
*/
|
|
250
226
|
declare const executeAlchemyEntry: (options: ExecuteAlchemyEntryOptions) => Promise<ExecuteAlchemyEntryResult>;
|
|
251
227
|
//#endregion
|
|
252
|
-
|
|
228
|
+
//#region src/proof-reuse-gate.d.ts
|
|
229
|
+
/**
|
|
230
|
+
* The GitHub-side **proof-reuse gate** (ADR 0022, #309, #317).
|
|
231
|
+
*
|
|
232
|
+
* It answers one question, before checkout and before install: may this job
|
|
233
|
+
* reuse the local verification the factory already published for *this exact
|
|
234
|
+
* head*? Refusing runs hosted CI; it never fails the candidate. Three
|
|
235
|
+
* repositories had independently grown their own answer to that question and
|
|
236
|
+
* had already drifted apart, so the answer lives here once, with the App
|
|
237
|
+
* identity, check name, step identity, output names, guard, and every trust
|
|
238
|
+
* predicate fixed rather than consumer-configurable.
|
|
239
|
+
*
|
|
240
|
+
* What a consumer chooses is only *what its guarded surface requires*: the
|
|
241
|
+
* plain profile command objects that surface selects. Everything else below is
|
|
242
|
+
* the same for every consumer and every surface.
|
|
243
|
+
*/
|
|
244
|
+
/**
|
|
245
|
+
* The check run the gate reads. Fixed: `patronage-factory/pr-verify` is also
|
|
246
|
+
* postable as a *commit status* by any token with write access, so only the
|
|
247
|
+
* Checks API is ever consulted and only under this exact name.
|
|
248
|
+
*/
|
|
249
|
+
declare const FACTORY_PROOF_GATE_CHECK_NAME = "patronage-factory/pr-verify";
|
|
250
|
+
/**
|
|
251
|
+
* The Patronage Factory GitHub App, pinned (ADR 0022). A runner has no
|
|
252
|
+
* operator user config to read the producing App identity from, and the
|
|
253
|
+
* identity is a trust predicate, so it is not a repository variable a consumer
|
|
254
|
+
* can point somewhere else. Neither the id nor the slug is a secret: GitHub
|
|
255
|
+
* returns `app.id` on every check run it serves.
|
|
256
|
+
*/
|
|
257
|
+
declare const FACTORY_PROOF_GATE_APP_ID = "4314840";
|
|
258
|
+
/** Step id the guard condition refers to. */
|
|
259
|
+
declare const FACTORY_PROOF_GATE_STEP_ID = "factory-proof";
|
|
260
|
+
/**
|
|
261
|
+
* The shell the gate runs under, and it is a correctness requirement rather
|
|
262
|
+
* than a preference.
|
|
263
|
+
*
|
|
264
|
+
* GitHub's default for a `run:` step is `bash -e {0}`: **errexit comes from
|
|
265
|
+
* the invocation, not from the script**, and no `set +e` in the script body
|
|
266
|
+
* can be relied on to express the intent because the abort happens at the
|
|
267
|
+
* failing command. The whole fail-open design below — every path reaching one
|
|
268
|
+
* final `GITHUB_OUTPUT` write, no `set -e` — is void under that default. It
|
|
269
|
+
* was: the gate died at a `read` returning 1 at EOF, two seconds in, with no
|
|
270
|
+
* stdout, no verdict and no `reason`, so reuse collapsed to never *and* the
|
|
271
|
+
* signal built to reveal that could not fire (#319).
|
|
272
|
+
*
|
|
273
|
+
* A `shell:` value containing `{0}` is used verbatim by the runner, so this
|
|
274
|
+
* form supplies no `-e`. The shorthand `shell: bash` must never be used here:
|
|
275
|
+
* it expands to `bash --noprofile --norc -eo pipefail {0}`. `pipefail` is set
|
|
276
|
+
* by the script itself.
|
|
277
|
+
*
|
|
278
|
+
* The script is additionally written to survive errexit anyway. Both layers
|
|
279
|
+
* are deliberate: this one states the contract, that one means a runner which
|
|
280
|
+
* ignores it still gets a decision.
|
|
281
|
+
*/
|
|
282
|
+
declare const FACTORY_PROOF_GATE_SHELL = "bash --noprofile --norc {0}";
|
|
283
|
+
/** Output the verdict is written to. `true` — and only `true` — skips. */
|
|
284
|
+
declare const FACTORY_PROOF_GATE_OUTPUT = "reuse-proof";
|
|
285
|
+
/**
|
|
286
|
+
* Machine-readable refusal category. Fleet-status reads this: it is what makes
|
|
287
|
+
* a reuse-collapses-to-never regression visible instead of hidden behind
|
|
288
|
+
* fail-open, which by construction looks exactly like healthy full CI.
|
|
289
|
+
*/
|
|
290
|
+
declare const FACTORY_PROOF_GATE_REASON_OUTPUT = "reason";
|
|
291
|
+
/**
|
|
292
|
+
* Recorded verification mode, when the gate could read one. Diagnostic
|
|
293
|
+
* metadata only — never an authorization condition (ADR 0022). A reduced-mode
|
|
294
|
+
* proof that executed every command the surface requires is reusable.
|
|
295
|
+
*/
|
|
296
|
+
declare const FACTORY_PROOF_GATE_MODE_OUTPUT = "mode";
|
|
297
|
+
/**
|
|
298
|
+
* Guard for every step the gate protects. Deliberately `!= 'true'` and not
|
|
299
|
+
* `== 'false'`: an unset, empty, or garbled output must run the suite.
|
|
300
|
+
*/
|
|
301
|
+
declare const FACTORY_PROOF_GATE_GUARD = "steps.factory-proof.outputs.reuse-proof != 'true'";
|
|
302
|
+
/**
|
|
303
|
+
* Pull requests only. A push to a protected branch must never skip: deploy
|
|
304
|
+
* gates wait for a green required check at the *merged* SHA, and no local
|
|
305
|
+
* proof was ever written for that tree.
|
|
306
|
+
*/
|
|
307
|
+
declare const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request'";
|
|
308
|
+
/**
|
|
309
|
+
* The complete refusal vocabulary. Deliberately few, because these are the
|
|
310
|
+
* only distinctions the gate can honestly make from one Checks API read.
|
|
311
|
+
*
|
|
312
|
+
* - `proven` a covering, passing, unambiguous proof for this exact head
|
|
313
|
+
* - `none` no App-verified factory check run at this commit
|
|
314
|
+
* - `pending` the newest generation had not completed when this job read it
|
|
315
|
+
* - `failed` the newest generation records no pass
|
|
316
|
+
* - `unreadable` it passed but carries no binding for this repository and head
|
|
317
|
+
* - `incomplete` it passed but did not execute every required command
|
|
318
|
+
* - `ambiguous` two newest generations share the greatest start time
|
|
319
|
+
* - `error` the gate could not reach a decision (fail open)
|
|
320
|
+
*
|
|
321
|
+
* There is no `reduced`: mode stopped being an authorization condition in
|
|
322
|
+
* ADR 0022, so a narrower run that still executed every required command is
|
|
323
|
+
* indistinguishable from a full one *for this surface*, which is the point.
|
|
324
|
+
*/
|
|
325
|
+
declare const FACTORY_PROOF_GATE_REASONS: readonly ["proven", "none", "pending", "failed", "unreadable", "incomplete", "ambiguous", "error"];
|
|
326
|
+
type FactoryProofGateReason = (typeof FACTORY_PROOF_GATE_REASONS)[number];
|
|
327
|
+
/**
|
|
328
|
+
* One entry of a consumer's `verification.commands` (ADR 0021). Only the two
|
|
329
|
+
* load-bearing fields are declared: `name` is the command *identity* the proof
|
|
330
|
+
* records in `executedCommands`, `command` is the string the static coverage
|
|
331
|
+
* assertion matches against the work a reused proof would skip.
|
|
332
|
+
*/
|
|
333
|
+
interface ProofReuseCommand {
|
|
334
|
+
readonly command: string;
|
|
335
|
+
readonly name: string;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The command identities a guarded surface requires, derived from the profile
|
|
339
|
+
* command objects that surface selected. This is the single derivation: the
|
|
340
|
+
* gate script and the static coverage assertion both go through it, so a
|
|
341
|
+
* consumer can never have one notion of "required" at generation time and
|
|
342
|
+
* another at assertion time.
|
|
343
|
+
*
|
|
344
|
+
* `undefined` means the selection is unusable — not an array, empty, or
|
|
345
|
+
* carrying an entry whose `command` is blank or whose `name` is not a plain
|
|
346
|
+
* command identity. An empty required set would make *every* passing proof
|
|
347
|
+
* trivially covering, so it is never silently treated as "requires nothing";
|
|
348
|
+
* callers must refuse instead.
|
|
349
|
+
*/
|
|
350
|
+
declare const proofReuseRequiredCommands: (commands: readonly ProofReuseCommand[]) => readonly string[] | undefined;
|
|
351
|
+
interface FactoryProofGateOptions {
|
|
352
|
+
/**
|
|
353
|
+
* The profile command objects this guarded surface selected. A consumer with
|
|
354
|
+
* distinct core and docs jobs selects distinct sets, and each job's gate
|
|
355
|
+
* therefore requires a different coverage.
|
|
356
|
+
*/
|
|
357
|
+
readonly commands: readonly ProofReuseCommand[];
|
|
358
|
+
/** Names the suite in the job summary. Changes no trust decision. */
|
|
359
|
+
readonly surface: string;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The gate script. Exported so it can be executed directly under test against
|
|
363
|
+
* a stubbed `gh`, rather than only asserted against as workflow text — the
|
|
364
|
+
* script is the security boundary, and asserting on generated YAML would leave
|
|
365
|
+
* its actual decisions untested.
|
|
366
|
+
*
|
|
367
|
+
* Note the deliberate absence of `set -e`: every failure path has to reach the
|
|
368
|
+
* final write, and that write must emit `false`. Omitting it is necessary but
|
|
369
|
+
* not sufficient — the runner supplies `-e` unless the step declares
|
|
370
|
+
* `FACTORY_PROOF_GATE_SHELL` — so the script is also written to reach that
|
|
371
|
+
* write under errexit, and the tests execute it both ways.
|
|
372
|
+
*/
|
|
373
|
+
declare const factoryProofGateScript: ({
|
|
374
|
+
commands,
|
|
375
|
+
surface
|
|
376
|
+
}: FactoryProofGateOptions) => string;
|
|
377
|
+
interface FactoryProofGateStep {
|
|
378
|
+
readonly continueOnError: true;
|
|
379
|
+
readonly env: Readonly<{
|
|
380
|
+
GH_TOKEN: string;
|
|
381
|
+
HEAD_SHA: string;
|
|
382
|
+
}>;
|
|
383
|
+
readonly id: typeof FACTORY_PROOF_GATE_STEP_ID;
|
|
384
|
+
readonly if: typeof FACTORY_PROOF_GATE_IF;
|
|
385
|
+
readonly name: string;
|
|
386
|
+
readonly run: string;
|
|
387
|
+
readonly shell: typeof FACTORY_PROOF_GATE_SHELL;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* The step itself, structurally accepted by gagen's `step()` without adding a
|
|
391
|
+
* gagen runtime dependency. It belongs first in the job it guards: one Checks
|
|
392
|
+
* API read with the default `GITHUB_TOKEN` (`checks: read`), no checkout, no
|
|
393
|
+
* install, so a proven head costs a runner nothing beyond job startup.
|
|
394
|
+
*
|
|
395
|
+
* A **step, not a job**, and that is not a style preference. A separate gate
|
|
396
|
+
* job that errored would leave the guarded job `skipped`, and a summary job
|
|
397
|
+
* that only fails on `failure`/`cancelled` would still report green — a Checks
|
|
398
|
+
* API outage would silently skip verification. As a step it fails open by
|
|
399
|
+
* construction: the step errors, the output is never written, the guard reads
|
|
400
|
+
* empty, and every command runs.
|
|
401
|
+
*/
|
|
402
|
+
declare const factoryProofGateStep: (options: FactoryProofGateOptions) => FactoryProofGateStep;
|
|
403
|
+
interface ProofReuseCoverageInput {
|
|
404
|
+
/** The same selection handed to the gate for this surface. */
|
|
405
|
+
readonly commands: readonly ProofReuseCommand[];
|
|
406
|
+
/**
|
|
407
|
+
* Skipped work covered under a different command string, keyed by the
|
|
408
|
+
* skipped string with the reason as its value. Deliberately explicit: a
|
|
409
|
+
* prose rationale for why some narrower command is "equivalent enough" is
|
|
410
|
+
* exactly what this assertion exists to force into the open.
|
|
411
|
+
*/
|
|
412
|
+
readonly equivalents?: Readonly<Record<string, string>>;
|
|
413
|
+
/**
|
|
414
|
+
* The command strings the consumer's workflow would skip when the gate says
|
|
415
|
+
* `true`, already resolved to the leaves that actually run. Extracting them
|
|
416
|
+
* is the consumer's job: this package never parses workflow source, because
|
|
417
|
+
* doing so is what killed #288.
|
|
418
|
+
*/
|
|
419
|
+
readonly skipped: readonly string[];
|
|
420
|
+
/** Names the surface in the failure message. */
|
|
421
|
+
readonly surface?: string;
|
|
422
|
+
}
|
|
423
|
+
interface ProofReuseCoverageReport {
|
|
424
|
+
/** True only when the selection is usable *and* nothing is uncovered. */
|
|
425
|
+
readonly covered: boolean;
|
|
426
|
+
/** Command identities derived from the selection; empty when unusable. */
|
|
427
|
+
readonly requiredCommands: readonly string[];
|
|
428
|
+
/** Skipped command strings no selected profile command accounts for. */
|
|
429
|
+
readonly uncovered: readonly string[];
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* The static coverage assertion, as a report.
|
|
433
|
+
*
|
|
434
|
+
* Runtime command coverage is authoritative — the gate's `incomplete` refusal
|
|
435
|
+
* is the backstop that cannot be bypassed. This is the compile-time guard in
|
|
436
|
+
* front of it: it fails a consumer's build the moment a workflow skips work
|
|
437
|
+
* that no selected profile command runs, rather than letting the drift show up
|
|
438
|
+
* as CI quietly verifying nothing. The drift is real history: #227 wired a
|
|
439
|
+
* workspace member into CI while the profile never learned about it.
|
|
440
|
+
*/
|
|
441
|
+
declare const proofReuseCoverage: ({
|
|
442
|
+
commands,
|
|
443
|
+
equivalents,
|
|
444
|
+
skipped
|
|
445
|
+
}: ProofReuseCoverageInput) => ProofReuseCoverageReport;
|
|
446
|
+
/** `proofReuseCoverage`, as a build failure. */
|
|
447
|
+
declare const assertProofReuseCoverage: (input: ProofReuseCoverageInput) => ProofReuseCoverageReport;
|
|
448
|
+
//#endregion
|
|
449
|
+
export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_STEP_ID, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateStep, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, type WorkflowStep, assertProofReuseCoverage, bundleAlchemyEntry, executeAlchemyEntry, factoryProofGateScript, factoryProofGateStep, factoryWorkflow, isLocalPreviewStage, localPreviewStage, parseLocalPreviewStage, proofReuseCoverage, proofReuseRequiredCommands };
|