@patronage/factory-ci 1.0.0-alpha.7 → 1.0.0-alpha.9

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 CHANGED
@@ -20,7 +20,10 @@ The admission rule is **upstream on repetition**: nothing enters this package un
20
20
 
21
21
  ```ts
22
22
  import {
23
+ FACTORY_CANDIDATE_PULL_REQUEST_TYPES,
24
+ factoryProductionImpactWorkflow,
23
25
  factoryWorkflow,
26
+ factoryCandidateOrPushCondition,
24
27
  NODE_PNPM_ACTION_FAMILY_NODE24,
25
28
  } from "@patronage/factory-ci";
26
29
  ```
@@ -47,6 +50,7 @@ const generated = factoryWorkflow({
47
50
  regenerate: "pnpm workflows:generate",
48
51
  },
49
52
  setup: {
53
+ checkout: { fetchDepth: 0, ref: "${{ github.sha }}" },
50
54
  setupNode: { cacheDependencyPath: "pnpm-lock.yaml" },
51
55
  },
52
56
  });
@@ -59,6 +63,45 @@ workflow({/* caller-owned jobs and topology */}).writeOrLint({
59
63
 
60
64
  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
65
 
66
+ Candidate lifecycle support follows the same boundary. Use `FACTORY_CANDIDATE_PULL_REQUEST_TYPES` for the pull-request trigger matrix and `factoryCandidateOrPushCondition()` on each substantive job. With an optional caller-owned path condition, the helper emits the GitHub expression that runs merge-target pushes unconditionally and runs pull-request work only when GitHub's draft boolean says the pull request is a Candidate:
67
+
68
+ ```ts
69
+ const changes = job("changes", {
70
+ if: factoryCandidateOrPushCondition(),
71
+ // caller-owned runner, permissions, and steps
72
+ });
73
+
74
+ const core = job("core", {
75
+ if: factoryCandidateOrPushCondition("needs.changes.outputs.core == 'true'"),
76
+ // caller-owned topology
77
+ });
78
+ ```
79
+
80
+ Migration: upgrade `@patronage/factory-ci`, apply the shared trigger types and job condition in the TypeScript workflow source, then regenerate and commit the emitted YAML. Draft `opened` and `synchronize` events will stop running substantive Verify work. Promotion through `ready_for_review`, later non-draft Candidate events, and merge-target pushes continue to run their applicable battery. A skipped draft run is presentation, not proof.
81
+
82
+ Production impact support follows the same ownership line. A generated deploy workflow declares only its target names and consumes the returned decision step and fail-open `demandedIf(target)` expressions:
83
+
84
+ ```ts
85
+ const impact = factoryProductionImpactWorkflow({
86
+ targets: profile.impact?.targets.map(({ name }) => name) ?? [],
87
+ });
88
+
89
+ const decide = job("impact", {
90
+ outputs: impact.decisionJobOutputs,
91
+ steps: [...setupSteps, impact.decisionStep],
92
+ });
93
+
94
+ const deployWebsite = job("deploy-website", {
95
+ if: impact.demandedIf("website", "impact"),
96
+ needs: [decide],
97
+ // consumer-owned runner, credentials, deploy commands, and convergence
98
+ });
99
+ ```
100
+
101
+ The step calls `psf production:impact` with the merge push's exact `before` and `after` commits. It is `continue-on-error`, and every generated target condition keeps work demanded unless the command succeeded, reported a usable decision, and explicitly withdrew that target. The artifact with an empty target list generates no deploy jobs because job creation remains with the consumer. This package does not own target declarations, deploy topology, credentials, commands, or convergence/no-op proof.
102
+
103
+ The decision checkout must make both push identities reachable. Use `factoryWorkflow({ setup: { checkout: { fetchDepth: 0, ref: "${{ github.sha }}" } } })`; a shallow checkout is safe but deliberately refuses withdrawal because the `before` commit is unreadable.
104
+
62
105
  ### Generated shell
63
106
 
64
107
  ```ts
@@ -90,6 +133,8 @@ import {
90
133
  assertProofReuseCoverage,
91
134
  FACTORY_PROOF_GATE_GUARD,
92
135
  factoryProofGateStep,
136
+ factoryProofReuseSummaryStep,
137
+ factoryProofTimingStartStep,
93
138
  } from "@patronage/factory-ci";
94
139
 
95
140
  import profile from "../../software-factory.profile.json" with { type: "json" };
@@ -102,14 +147,16 @@ const core = job("core", {
102
147
  permissions: { checks: "read", contents: "read" },
103
148
  steps: [
104
149
  step(factoryProofGateStep({ commands: coreCommands, surface: "core" })),
150
+ step(factoryProofTimingStartStep()),
105
151
  ...guardedSteps.map((s) => ({ ...s, if: FACTORY_PROOF_GATE_GUARD })),
152
+ step(factoryProofReuseSummaryStep({ surface: "core" })),
106
153
  ],
107
154
  });
108
155
  ```
109
156
 
110
157
  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.
111
158
 
112
- 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.
159
+ 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. Coverage is the union of `executedCommands` and commands released by `notRequiredCommands` only after the gate validates each release against that same proof binding's command-to-target map and identity-bound impact stamp. Missing, malformed, duplicated, affected, unknown, or differently bound release data refuses reuse and runs the hosted surface. `mode` is reported as diagnostic metadata, never authorized on: a reduced-mode proof whose executed and stamp-authorized released commands cover a surface is reusable.
113
160
 
114
161
  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.
115
162
 
@@ -121,6 +168,8 @@ The gate is a **step, not a job**, marked `continue-on-error`, with no `set -e`.
121
168
 
122
169
  `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. Coverage is exact executable coverage. The deprecated `equivalents` input remains only for patch-release source compatibility and is ignored; prose cannot authorize a skip. 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.
123
170
 
171
+ `factoryProofTimingStartStep()` and `factoryProofReuseSummaryStep()` replace consumer-local proof timing summaries without taking over workflow topology. Put the start step immediately after the gate and the summary step after the guarded work. The helpers keep the established `Start CI timing` / `Record proof-reuse timing` names and distinguish pull-request reuse, pull-request full fallback, and merge-target full execution where the PR-only gate is explicitly not applicable. Reuse links the exact source check and bound head; every path emits a cheap Actions notice. Both steps are `continue-on-error`: missing timing support or an unwritable presentation destination cannot change a required job's conclusion. These helpers change no trust predicate or guard.
172
+
124
173
  ### Alchemy entries
125
174
 
126
175
  ```ts
@@ -205,7 +254,7 @@ The credential env-block helper that the audit found repeated was deliberately c
205
254
 
206
255
  ## Releases
207
256
 
208
- Attended and hand-cut, on the same terms as `@patronage/alchemy-d1-state`: bump the version, run the workspace checks, read the `npm pack --dry-run` file list, publish, tag `factory-ci@<version>`. There is no changesets setup and no automatic release trigger, by design. Consumers pin exact versions.
257
+ Attended and hand-cut, on the same terms as `@patronage/alchemy-d1-state`: bump the version, run the workspace checks, read the `npm pack --dry-run` file list, build and approve one pnpm tarball, publish that exact tarball with an explicit channel tag, then tag `factory-ci@<version>`. Publishing the package directory is not allowed because its `prepack` build would replace the reviewed bytes. There is no changesets setup and no automatic release trigger, by design. Consumers pin exact versions.
209
258
 
210
259
  ## License
211
260
 
package/dist/index.d.ts CHANGED
@@ -54,6 +54,21 @@ declare const NODE_PNPM_ACTION_FAMILY_NODE24: {
54
54
  };
55
55
  };
56
56
  //#endregion
57
+ //#region src/candidate-lifecycle.d.ts
58
+ /**
59
+ * Pull-request events that can create or refresh a Factory candidate run.
60
+ *
61
+ * GitHub's draft boolean remains the lifecycle authority: these triggers make
62
+ * a run visible, while `factoryCandidateOrPushCondition` keeps substantive
63
+ * jobs idle until the pull request is a candidate.
64
+ */
65
+ declare const FACTORY_CANDIDATE_PULL_REQUEST_TYPES: readonly ["opened", "synchronize", "reopened", "ready_for_review"];
66
+ /**
67
+ * Build a GitHub Actions job condition for candidate PRs and merge-target
68
+ * pushes. The caller owns triggers, jobs, runners, permissions, and topology.
69
+ */
70
+ declare const factoryCandidateOrPushCondition: (candidateCondition?: string) => string;
71
+ //#endregion
57
72
  //#region src/bundle-alchemy-entry.d.ts
58
73
  interface BundleAlchemyEntryOptions {
59
74
  /** The Alchemy entry to bundle, e.g. `alchemy.run.ts`. */
@@ -148,12 +163,16 @@ declare const localPreviewStage: (options: LocalPreviewStageOptions) => LocalPre
148
163
  //#endregion
149
164
  //#region src/factory-workflow.d.ts
150
165
  interface WorkflowStep {
166
+ readonly continueOnError?: boolean;
167
+ readonly env?: Readonly<Record<string, string>>;
168
+ readonly id?: string;
151
169
  readonly name: string;
152
170
  readonly uses?: string;
153
171
  readonly with?: Readonly<Record<string, string>>;
154
172
  readonly run?: string;
155
173
  }
156
174
  interface CheckoutStepOptions {
175
+ readonly fetchDepth?: number;
157
176
  readonly name?: string;
158
177
  readonly ref?: string;
159
178
  }
@@ -324,6 +343,13 @@ declare const FACTORY_PROOF_GATE_CHECK_NAME = "patronage-factory/pr-verify";
324
343
  declare const FACTORY_PROOF_GATE_APP_ID = "4314840";
325
344
  /** Step id the guard condition refers to. */
326
345
  declare const FACTORY_PROOF_GATE_STEP_ID = "factory-proof";
346
+ /**
347
+ * Human-visible name of the gate step as the Actions jobs API serves it. A
348
+ * read-only run analyzer (`psf ci:analyze`, #647) matches on this name to
349
+ * classify a run as proof-reuse versus full fallback, so it is exported from
350
+ * exactly the module that writes it — matching on a re-typed copy would drift.
351
+ */
352
+ declare const FACTORY_PROOF_GATE_STEP_NAME = "Check for factory proof of this head";
327
353
  /**
328
354
  * The shell the gate runs under, and it is a correctness requirement rather
329
355
  * than a preference.
@@ -361,6 +387,8 @@ declare const FACTORY_PROOF_GATE_REASON_OUTPUT = "reason";
361
387
  * proof that executed every command the surface requires is reusable.
362
388
  */
363
389
  declare const FACTORY_PROOF_GATE_MODE_OUTPUT = "mode";
390
+ /** Exact Checks API URL of the proof generation selected by the gate. */
391
+ declare const FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT = "source-check-url";
364
392
  /**
365
393
  * Guard for every step the gate protects. Deliberately `!= 'true'` and not
366
394
  * `== 'false'`: an unset, empty, or garbled output must run the suite.
@@ -381,7 +409,8 @@ declare const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request'";
381
409
  * - `pending` the newest generation had not completed when this job read it
382
410
  * - `failed` the newest generation records no pass
383
411
  * - `unreadable` it passed but carries no binding for this repository and head
384
- * - `incomplete` it passed but did not execute every required command
412
+ * - `incomplete` its executed plus stamp-authorized released commands do not
413
+ * cover every required command
385
414
  * - `ambiguous` two newest generations share the greatest start time
386
415
  * - `error` the gate could not reach a decision (fail open)
387
416
  *
@@ -540,6 +569,64 @@ declare const proofReuseCoverage: ({
540
569
  /** `proofReuseCoverage`, as a build failure. */
541
570
  declare const assertProofReuseCoverage: (input: ProofReuseCoverageInput) => ProofReuseCoverageReport;
542
571
  //#endregion
572
+ //#region src/proof-reuse-presentation.d.ts
573
+ /** Generic timing and presentation steps around the proof-reuse gate (#652). */
574
+ declare const FACTORY_PROOF_TIMING_STEP_ID = "ci-timing";
575
+ declare const FACTORY_PROOF_TIMING_START_STEP_NAME = "Start CI timing";
576
+ declare const FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME = "Record proof-reuse timing";
577
+ interface FactoryProofReusePresentationOptions {
578
+ /** Human-readable name of the guarded suite. */
579
+ readonly surface: string;
580
+ }
581
+ interface FactoryProofTimingStartStep {
582
+ readonly continueOnError: true;
583
+ readonly id: typeof FACTORY_PROOF_TIMING_STEP_ID;
584
+ readonly name: typeof FACTORY_PROOF_TIMING_START_STEP_NAME;
585
+ readonly run: string;
586
+ }
587
+ declare const factoryProofTimingStartStep: () => FactoryProofTimingStartStep;
588
+ declare const factoryProofReuseSummaryScript: ({
589
+ surface
590
+ }: FactoryProofReusePresentationOptions) => string;
591
+ interface FactoryProofReuseSummaryStep {
592
+ readonly continueOnError: true;
593
+ readonly env: Readonly<Record<string, string>>;
594
+ readonly if: "always()";
595
+ readonly name: typeof FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME;
596
+ readonly run: string;
597
+ }
598
+ declare const factoryProofReuseSummaryStep: (options: FactoryProofReusePresentationOptions) => FactoryProofReuseSummaryStep;
599
+ //#endregion
600
+ //#region src/production-impact-workflow.d.ts
601
+ declare const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
602
+ declare const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
603
+ declare const FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT = "basis";
604
+ declare const FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT = "unsubscribed_paths";
605
+ /** Stable GitHub-output key for one declared target. */
606
+ declare const productionImpactTargetOutput: (targetName: string) => string;
607
+ interface FactoryProductionImpactWorkflowOptions {
608
+ readonly after?: string;
609
+ readonly before?: string;
610
+ readonly cli?: string;
611
+ readonly profilePath?: string;
612
+ readonly targets: readonly string[];
613
+ }
614
+ interface FactoryProductionImpactWorkflow {
615
+ /** Outputs for a caller-owned decision job that subsequent jobs may consume. */
616
+ readonly decisionJobOutputs: Readonly<Record<string, string>>;
617
+ readonly decisionStep: WorkflowStep;
618
+ /** A fail-open condition: only an explicit usable withdrawal skips work. */
619
+ readonly demandedIf: (targetName: string, decisionJob?: string) => string;
620
+ readonly targetOutputs: Readonly<Record<string, string>>;
621
+ }
622
+ /**
623
+ * Generate the small factory-owned decision seam for a consumer production
624
+ * workflow. Consumers retain jobs, deploy commands, credentials, topology,
625
+ * and convergence checks; this artifact supplies only the decision step and
626
+ * per-target withdrawal conditions.
627
+ */
628
+ declare const factoryProductionImpactWorkflow: (options: FactoryProductionImpactWorkflowOptions) => FactoryProductionImpactWorkflow;
629
+ //#endregion
543
630
  //#region src/vitest-profile.d.ts
544
631
  /** Schema version of the emitted profile document. */
545
632
  declare const VITEST_PROFILE_SCHEMA_VERSION = 1;
@@ -750,6 +837,26 @@ declare const writeVitestProfile: (outputPath: string, profile: VitestProfile) =
750
837
  */
751
838
  declare const runVitestProfile: (options: VitestProfileOptions, dependencies?: VitestProfileDependencies) => Promise<VitestProfile>;
752
839
  //#endregion
840
+ //#region src/vitest-profile-reader.d.ts
841
+ /**
842
+ * Outcome of reading one candidate document. `unrecognized` carries the first
843
+ * reason the document failed — an analyzer reports it verbatim rather than
844
+ * treating an unreadable profile as an empty one.
845
+ */
846
+ type VitestProfileReadResult = {
847
+ kind: "profile";
848
+ profile: VitestProfile;
849
+ } | {
850
+ kind: "unrecognized";
851
+ reason: string;
852
+ };
853
+ /**
854
+ * Check one parsed JSON document against the profile contract the writer
855
+ * emits. Returns the typed profile on success and the first mismatch reason
856
+ * otherwise — never a partially-usable value.
857
+ */
858
+ declare const readVitestProfileDocument: (value: unknown) => VitestProfileReadResult;
859
+ //#endregion
753
860
  //#region src/workflow-shell-lint.d.ts
754
861
  /**
755
862
  * Parse-check the shell embedded in generated workflow YAML (#376).
@@ -801,4 +908,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
801
908
  readonly source: string;
802
909
  }) => void;
803
910
  //#endregion
804
- 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, GitHubApiError, type GithubAppCredentials, type GithubAppTokenOptions, 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, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryProofGateScript, factoryProofGateStep, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, proofReuseCoverage, proofReuseRequiredCommands, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
911
+ export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, 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_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PROOF_TIMING_START_STEP_NAME, FACTORY_PROOF_TIMING_STEP_ID, FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME, type FactoryProductionImpactWorkflow, type FactoryProductionImpactWorkflowOptions, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateStep, type FactoryProofReusePresentationOptions, type FactoryProofReuseSummaryStep, type FactoryProofTimingStartStep, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, GitHubApiError, type GithubAppCredentials, type GithubAppTokenOptions, 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, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateOrPushCondition, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryScript, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };