@patronage/factory-ci 1.0.0-alpha.30 → 1.0.0-alpha.32

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
@@ -34,9 +34,34 @@ import {
34
34
 
35
35
  - `actions`: the explicit Node/pnpm action family plus every workflow-specific action pin
36
36
  - `setupSteps`: checkout → pnpm → Node → install, customizable by named role
37
+ - `setupStepsByRole`: the same steps keyed by stable role (`SetupStepRole`: `"checkout"`, `"setupPnpm"`, `"setupNode"`, `"install"`) instead of array position, so a caller never locates the checkout step by display name or asserts a fixed install-step count
37
38
  - `writeOptions`: the provenance banner and `pinDeps: false`, ready to spread into gagen's `writeOrLint`
38
39
 
39
- Every action is validated as `owner/repo@<full sha>`, with its release tag kept separately. A complete `actionFamily` is required; there is no default that could silently move a consumer between action majors. `NODE_PNPM_ACTION_FAMILY_NODE24` is the canonical family whose checkout, Node setup, and pnpm setup actions all declare a Node 24 JavaScript runtime.
40
+ A consumer that needs its own checkout step (for example, one pinned to a pull request's head SHA) followed by the shared pnpm/Node/install sequence selects by role instead of destructuring `setupSteps`:
41
+
42
+ ```ts
43
+ import { setupStepsForRoles } from "@patronage/factory-ci";
44
+
45
+ const afterCheckout = setupStepsForRoles(verifyWorkflow, [
46
+ "setupPnpm",
47
+ "setupNode",
48
+ "install",
49
+ ]);
50
+
51
+ const steps = [
52
+ {
53
+ name: "Checkout (head SHA)",
54
+ uses: verifyWorkflow.actions.checkout.uses,
55
+ with: { ref },
56
+ },
57
+ ...afterCheckout,
58
+ // job-specific steps
59
+ ];
60
+ ```
61
+
62
+ This selection is stable under a renamed step or an added supported role: `setupStepsForRoles` reads named roles from `setupStepsByRole`, never array shape.
63
+
64
+ Every action is validated as `owner/repo@<full sha>`, with its release tag kept separately. A complete `actionFamily` is required; there is no default that could silently move a consumer between action majors. `NODE_PNPM_ACTION_FAMILY_NODE24` is the canonical family whose checkout, Node setup, and pnpm setup actions all declare a Node 24 JavaScript runtime. Generated workflows read Node from `.nvmrc`. This repository pins 24.20.0.
40
65
 
41
66
  ```ts
42
67
  const generated = factoryWorkflow({
@@ -128,11 +153,11 @@ const deployWebsite = job("deploy-website", {
128
153
  });
129
154
  ```
130
155
 
131
- 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.
156
+ The generated decision step runs `psf production:impact --candidate "$FACTORY_CANDIDATE_SHA"` (the `candidate` option, default `github.sha`, plus `--stage` when the consumer passes one). The command resolves each target's demand from that target's last trusted successful deployment receipt to the candidate, never from the push's `before` and `after` commits. The step 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. Each deploy job declares `impact.permissions` and appends `impact.receiptSteps(target)` after its deploy and health steps: a success receipt (`--publish-receipt success --target <name> --candidate "$FACTORY_CANDIDATE_SHA"`), a bind step named for the new deployment id, and a failure receipt. Only a bound success receipt becomes the next baseline. 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, health policy, or convergence/no-op proof.
132
157
 
133
- 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.
158
+ The decision checkout must make the baseline commit reachable. Use `factoryWorkflow({ setup: { checkout: { fetchDepth: 0, ref: "${{ github.sha }}" } } })`; a shallow checkout is safe but deliberately refuses withdrawal because the baseline commit is unreadable.
134
159
 
135
- When deployment begins from a successful `workflow_run` instead of the push event itself, carry that push identity through the typed artifact seam rather than reconstructing `before` from `HEAD^`:
160
+ When deployment begins from a successful `workflow_run` instead of the push event itself, carry that push identity through the typed artifact seam so the decision job checks out the triggering head and passes it as the candidate:
136
161
 
137
162
  ```ts
138
163
  import {
@@ -172,8 +197,7 @@ const identity = factoryPushIdentityConsumer({
172
197
  downloadArtifact: workflowArtifact.actions.downloadArtifact,
173
198
  });
174
199
  const impact = factoryProductionImpactWorkflow({
175
- after: identity.outputs.after,
176
- before: identity.outputs.before,
200
+ candidate: identity.outputs.after,
177
201
  targets: profile.impact.targets.map(({ name }) => name),
178
202
  });
179
203
  const decision = {
@@ -313,7 +337,7 @@ const jobs = [
313
337
  ];
314
338
  ```
315
339
 
316
- A consumer's generated merge-target-push Verify workflow is the only producer of `patronage-factory/merge-freeze` generations (#356, ADR 0016 as amended; #429, #872). A red merge-target Verify completes a generation active, a green one completes it inactive, and `pr:ready` reads the generation on the candidate's own base tip. No command, scheduled job, or second workflow writes that check run; the operator override (`demand:waive --demand merge-freeze`) waives the demand for one candidate and never writes here.
340
+ A consumer's generated merge-target-push Verify workflow is the only producer of `patronage-factory/merge-freeze` generations (#356, ADR 0016; #429, #872). A red merge-target Verify completes a generation active, a green one completes it inactive, and `pr:ready` reads the generation on the candidate's own base tip. No command, scheduled job, or second workflow writes that check run; the operator override (`demand:waive --demand merge-freeze`) waives the demand for one candidate and never writes here.
317
341
 
318
342
  `factoryMergeFreezeJob` owns everything a reader trusts: the check name (`FACTORY_MERGE_FREEZE_CHECK_NAME`), the pinned Patronage Factory App identity, the `actions/create-github-app-token` inputs, the `needs.*.result` fold (`FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION`), the merge-target condition (`FACTORY_MERGE_FREEZE_IF`, built from `FACTORY_MERGE_TARGET_REF_CONDITION`), and the job's least-privilege `permissions` (an empty block: the App installation token carries the check-run write, the job uses no `GITHUB_TOKEN` scope, and it never checks out the repository). The caller owns the runner kind (`app-token` for the Factory App token), the timeout, and the `needs` list.
319
343
 
@@ -405,7 +429,7 @@ The raw summary script builder and the internal `ci-timing` step-id and step-nam
405
429
  import { bundleAlchemyEntry, executeAlchemyEntry } from "@patronage/factory-ci";
406
430
  ```
407
431
 
408
- `bundleAlchemyEntry({ entry, outfile, ... })` flattens a TypeScript Alchemy entry to a single ESM file the Alchemy CLI can run, keeping `alchemy`, `alchemy/*`, `effect`, and `effect/*` external — both packages are identity-sensitive and a second copy breaks them silently. Options: `absWorkingDir`, `alias`, `packages` (`"external"` by default), `sourcemap`, `target`, `tsconfig`. Returns the absolute outfile path.
432
+ `bundleAlchemyEntry({ entry, outfile, ... })` flattens a TypeScript Alchemy entry to a single ESM file the Alchemy CLI can run, keeping `alchemy`, `alchemy/*`, `effect`, and `effect/*` external — both packages are identity-sensitive and a second copy breaks them silently. The bundler runs with `logLevel: "silent"`: a failed build rejects, and writes no resolver path or source excerpt to the inherited stderr on its way out. Options: `absWorkingDir`, `alias`, `packages` (`"external"` by default), `sourcemap`, `target`, `tsconfig`. Returns the absolute outfile path.
409
433
 
410
434
  `alias` is a specifier-to-target map handed to esbuild. Substitution runs before the `packages` and `external` decisions, so an aliased bare specifier is inlined even under `packages: "external"` — that is how a repo points a workspace-only or duplicated package at one file. Give absolute paths or package names; which aliases a repo needs is consumer policy and this package ships no defaults. `executeAlchemyEntry` passes its whole `bundle` option through, so the map is available there too. Because substitution runs first, an alias on `alchemy`, `effect`, or any of their subpaths would silently defeat the externals and bundle a second copy, so those keys are rejected outright. Targets are not string-matched — no rule over how a path is spelled can survive `..` segments or symlinks — so instead the build's metafile is checked afterwards and the bundle is rejected if it carries any input from a reserved package, however that file was reached. A consumer shim that re-exports `effect` by bare specifier stays external naturally and is allowed.
411
435
 
@@ -591,6 +615,45 @@ A protected stage's value is the physical resources it already owns. An Alchemy
591
615
 
592
616
  Two inputs are refused outright rather than passing: an empty marker list, and a marker with neither a `value` nor a `before` snapshot to compare against. Both would resolve green having asserted nothing.
593
617
 
618
+ #### Lifecycle
619
+
620
+ ```ts
621
+ import { runAlchemyLifecycle } from "@patronage/factory-ci/alchemy";
622
+
623
+ const result = await runAlchemyLifecycle({
624
+ request: { operation: "deploy", stack: "hq", stage: "v1" },
625
+ policy: {
626
+ stages: table,
627
+ holder: runId,
628
+ retainedIdentities: markers,
629
+ adopt: true,
630
+ nonConvergentResources: ["hq-worker"],
631
+ },
632
+ state: d1Service,
633
+ entry: { from: import.meta.url, bundledEntry: "alchemy.run.ts", env },
634
+ });
635
+ ```
636
+
637
+ `runAlchemyLifecycle` is the one operation over an Alchemy plan, deploy, or destroy. It owns the order; a consumer cannot get the order wrong because it does not call the steps. Paitronage and HQ each reconstructed this sequence from the guards below and got different parts wrong (paitronage#1917), which is the repetition that admits it here.
638
+
639
+ The consumer passes typed policy and two capabilities. `state` is the store from `@patronage/alchemy-d1-state`: `snapshotStage`, `get`, and `acquireStageOwnership`, whose lease carries `renew`, `release`, `deleteEmptyStage` and `childEnvironment`, matched structurally, so this package learns no table, account, or SQL. `entry` is what `executeAlchemyEntry` needs minus what the lifecycle decides: the arguments are the lifecycle's, stdio is always piped so the plan can be read, and a `bundle` is built once up front, with every child launched in `cwd`, then the bundle root, then this process's directory — the same chain `executeAlchemyEntry` resolves. A bundle that cannot build is `PREPARATION_FAILED` and nothing else: the bundler's own diagnostics are silenced at the source. There is no `redact` and no echo because there is no sink: each child's captured output is parsed and dropped, written nowhere, so nothing in it can reach a job log or an evidence file through this operation. Every child's environment carries the stage lease; see ownership below.
640
+
641
+ The sequence, per operation:
642
+
643
+ - **`plan`** takes the stage lease, reads the persisted state, runs `deploy --dry-run`, reads the state again, and refuses with `STATE_CHANGED` if any resource record or the stack output differs — all under the one lease, so no other writer can reach the stage while the child plans. That refusal wins over the child's own exit status, because a planning child that wrote is the more serious finding. On an empty protected stage with a `firstDeploy` policy the result carries `planHash`, the value an operator authorizes.
644
+ - **`deploy`** starts with the same leased read-only plan, then admits it under the same lease. An empty protected stage is a first deploy: the plan may only `create`, and its hash must be on `firstDeploy.authorizedHashes`, or the refusal is `FIRST_DEPLOY_UNAUTHORIZED` carrying the hash. A protected stage with state refuses `replace` and `delete` (`EFFECT_NOT_PERMITTED`), and every `retainedIdentities` marker must hold against the recorded values before the apply (`RESOURCE_IDENTITY_MISMATCH`); a protected stage with no markers is `INVALID_POLICY`, because a check with nothing in it must not read as a proof. A disposable stage is constrained by neither. Then the apply child (`--yes`, with `--adopt` when the policy says so), under the same lease. Then, still under it: the persisted state again, which must record every resource the plan created (`DEPLOY_INCOMPLETE`; the result's `created` names their FQNs — a planned create resolves to an FQN that was absent before the apply, or was recorded in flight, `creating` or `deleting`, and is exactly the FQN the create row names — a row carries only the rendered logical id, which equals the FQN for a top-level resource and cannot name a nested one, so an interrupted top-level create recovers and a nested one is refused rather than guessed; the record must have left the in-flight state) and, on a protected stage, the same identities as before; then a second `deploy --dry-run`, bracketed by the same two-read check as the first so a convergence child that wrote is `STATE_CHANGED`, which must be a no-op (`NOT_CONVERGED`) except an `update` on a persisted FQN named in `nonConvergentResources`: a Worker carrying a write-only secret binding re-plans as `update` on every deploy because the value can never be read back. Only `update`, only on those FQNs.
645
+ - **`destroy`** is refused on a protected stage. Then, under the stage lease, the destroy child, then the atomic stage view must be empty (`DESTROY_RESIDUE`) and the empty output row is removed in the same lease (`cleanup: "removed" | "absent"`). An unreadable store fails with `STATE_STORE_UNAVAILABLE`; it never reads as empty.
646
+
647
+ Ownership follows #1074 and #1107. Each operation runs under **one** stage lease, held from its first read to its last decision, and every child it launches — the dry runs, the apply, the destroy — runs inside that lease: the lifecycle spreads the lease's `childEnvironment()` into the child's environment, and the adapter in the child adopts it. The stage is never unowned while an operation runs, so no other writer can interleave between the admitted plan and the apply, and a second run on the stage is `STAGE_HELD`. Before anything read under the lease is decided on, the lifecycle **renews** the lease: renewal is the adapter's fenced operation, it changes no rows unless this holder's unexpired row is still there, and it fails with `lost` when the lease expired or was taken over — `OWNERSHIP_LOST`, and the decision is not made. Independently of that, every exit from a leased window passes the same fence check, whether the window returned or threw and wherever it threw from: a lease lost anywhere in the window — during the apply child, during a state read, under a child failure — is `OWNERSHIP_LOST` at the phase the run was in, ahead of whatever else was found there. The check sits outside the window's code, so no early return or new failure path inside it can skip acceptance. A fence that holds lets a genuine store or child failure through unchanged. A successful `release` is not that check: the adapter's release is idempotent by design and succeeds when the row is gone or belongs to a later holder, so it proves nothing, and the lifecycle infers nothing from it. A release that _fails_ is a different fact — the store could not be reached and this holder's row may still be there, blocking the stage until it expires — so after a successful operation it is reported as `LEASE_RELEASE_FAILED`; a failure inside the operation is reported ahead of it.
648
+
649
+ The consumer's entry point passes its environment to the adapter: `d1State({ ..., ownership: { stack, stage, holder, adoptFrom: process.env } })`. That is the whole wiring, the same for a plan, a deploy and a destroy, and the same when an operator runs the entry by hand with no parent (the adapter then acquires as usual). Nothing in the child is trusted to opt in: while the parent holds the stage, a child whose state layer takes its own lease is refused at its first state call, and one that takes none is refused at its first write, by the store's fence. The token crosses the process boundary only through that environment; it is on no result, no failure, and no type this package exports. The store fences its rows, not Cloudflare: a provider call that reached Cloudflare before a lease was lost is not undone, and the refused state write is how the loss surfaces (see the adapter's README). The lifecycle does not reimplement any of this; it consumes it.
650
+
651
+ Every failure is an `AlchemyLifecycleError` with an enumerated `code`, `phase` (`request`, `admission`, `state`, `execution`, `postcondition`), and `mutation` (`not-started`, `read-only-started`, `mutation-may-have-occurred`). The message is the fixed sentence `LIFECYCLE_FAILURE_MESSAGES[code]` and nothing else; no failure carries a `cause`. Nothing a child printed, nothing the store said, and nothing a credential resolver quoted can reach an evidence sink through this error. After a deploy or destroy child was started, no failure reports `not-started`.
652
+
653
+ The plan is read once, by `parsePlanEffects`, from the lines Alchemy's `formatPlanLines` prints: one `Plan:` summary and one `[id] action` row per resource. The id in a row is Alchemy's rendered logical id, not an identity: `old/Worker` and `new/Worker` both print as `[Worker]`. So a plan that prints one id for two resources is refused (`AMBIGUOUS_RESOURCE`), and every check that binds a row to a resource resolves it to exactly one persisted FQN first — a planned create to the one FQN recorded after the apply and not before it, a convergence row to the one FQN recorded for it — and refuses when zero or several match. The adapter is bound to `PLAN_OUTPUT_ALCHEMY_VERSION`, which is `ALCHEMY_BASELINE.alchemy`, and the lifecycle refuses an installed Alchemy of any other version (`UNSUPPORTED_ALCHEMY_VERSION`) before any child runs, so a renderer change cannot be read as a plan with different effects. The version is read from the manifest of the package that owns the CLI the children run — `alchemy/bin/alchemy.js` is resolved from the consumer and the nearest `package.json` named `alchemy` above it is read from disk — because `alchemy/package.json` is not on the package's `exports` map and cannot be resolved as a specifier. The adapter fails on output it cannot account for — no summary, two summaries, an unknown action, a summary it cannot parse, a count that does not match the rows — and reports only the reason, never the output. An effect it could not account for is never reported as zero.
654
+
655
+ What stays with the consumer: the resource graph, the credentials and environment the child runs with, the stage table, the retained-identity markers, the first-deploy authorization list, the state store's configuration, and health checks after the deploy. The Paitronage stack keys, HQ's frozen identities, and every other product value are inputs. There is no `psf` command for the lifecycle: the capabilities are consumer code, not flags, so the consumer calls the function from its own script.
656
+
594
657
  #### Lifecycle guards
595
658
 
596
659
  ```ts
@@ -612,7 +675,7 @@ Four checks that surround an Alchemy run. Paitronage and firedup each built the
612
675
 
613
676
  `secretNamePolicy({ names, values })` builds the `redact` function `executeAlchemyEntry` already takes. It is a policy, not a mechanism. The project passes the names; this package ships no secret-name list and infers none from a name pattern. The policy is fail-closed at construction: a declared name with no value, a value under eight characters, or an empty `names` list handed a populated `values` map, throws before there is any output to redact, rather than quietly covering less than the caller believes. Empty names beside empty values is the one pass-through: a project with no secrets in its Alchemy output declares none. Values are trimmed before replacement, so redaction never swallows the line break after a secret, and longer values are replaced first, so a secret containing another secret leaves no fragment.
614
677
 
615
- Four things paitronage does are deliberately not here. The plan-read-only before/after state capture is dropped: `Plan.make` never mutates, so the assertion re-proved a guarantee. Stage eligibility is dropped: which stages may take a first deploy is project policy, and `firstDeployAuthorization` answers only whether this exact plan was authorized. `assertFirstDeployEffects` the check that a first-deploy plan may only create, never update or delete stays with the consumer: it reads the plan's own effect shape, which is Alchemy's type and the project's grammar, not a hash. The live zone lookup on the hosted path stays with the consumer for the same reason: which zones an account may deploy into is project policy, and this package makes one Cloudflare call, against the token, not against the account's inventory.
678
+ Two things paitronage does are deliberately not here. Stage eligibility is dropped: which stages may take a first deploy is project policy, and `firstDeployAuthorization` answers only whether this exact plan was authorized. The live zone lookup on the hosted path stays with the consumer: which zones an account may deploy into is project policy, and this package makes one Cloudflare call, against the token, not against the account's inventory. Two things that were once left out now live in `runAlchemyLifecycle` above: the plan-read-only before/after state capture, which caught a real write in the shared D1 store (#1074), and the first-deploy rule that a plan may only create, which the plan adapter can now read.
616
679
 
617
680
  #### `evaluateStack`
618
681
 
@@ -688,6 +751,62 @@ The tests pack the actual tarball, extract it into a throwaway external consumer
688
751
 
689
752
  The same consumer proves the ADR 0031 rule from the packed bytes rather than from source: it imports `@patronage/factory-ci/alchemy` with neither `alchemy` nor `effect` installed, and it bundles the packed `dist/index.js` so esbuild's own resolution reports every bare specifier the root graph still needs. Either package appearing in that list fails the build.
690
753
 
754
+ A second suite packs **both** `@patronage/factory-ci` and `@patronage/alchemy-d1-state` and installs the two tarballs side by side in one consumer, over one copy of `alchemy` and one of `effect` — the pairing a real deploy runs. It packs with `pnpm`, not `npm`, because `publishConfig.exports` is applied by pnpm and ignored by npm: packing with npm would install a manifest no consumer ever sees. It then runs a whole `runAlchemyLifecycle` deploy and destroy from the tarballs, with a real child process adopting the parent's stage lease and writing through the packed adapter into local SQLite.
755
+
756
+ Three facts only that shape can establish. The subpath's deep exports run from `dist` — a manifest that lost `./alchemy` once broke every lifecycle call while a source-tree import stayed green. The **adapter** and the consumer resolve one copy of `alchemy`: an error the adapter raises is `instanceof` the `StateStoreError` class the consumer imported, which a class-name comparison would not catch, because a duplicate names its class the same. And both tarballs declare the same peer ranges, read from the installed manifests and asserted against the installed pins and `ALCHEMY_BASELINE` together.
757
+
758
+ That second claim is narrow on purpose, and the suite says so where it is made. The `instanceof` compares exactly two things — the class the adapter raised and the class the consumer imported — so it detects a second copy of `alchemy` resolved by **the adapter tarball**, and only that. Three things it does not cover, none of them asserted anywhere here: a duplicate under this package's own tarball, which leaves the whole suite green because the detector never compares a class this package raised; what a duplicate would go on to break, since Alchemy's services are keyed by string and a faithful second copy still satisfies the same `State` key; and `effect`, where a second copy under the adapter with a distinct `runPromise` also leaves the suite green, because Effect values are plain data and cross copies.
759
+
760
+ ## Adopting the shared lifecycle
761
+
762
+ `runAlchemyLifecycle` owns the order a plan, a deploy or a destroy runs in. A consumer hands it typed policy and two narrow capabilities — a state store and an Alchemy entry — and cannot call the steps in another order, because it does not call the steps.
763
+
764
+ ### What adoption deletes
765
+
766
+ - **The local plan parser.** Plan text is read by `parsePlanEffects` inside the operation. A consumer that still greps plan output is running a second implementation of a contract that moved.
767
+ - **Local sequencing.** Any plan-then-apply-then-converge routine becomes the one call.
768
+ - **Local destructive-plan gates and stage guards** written as ad-hoc conditionals. They become the `stages` table and `retainedIdentities`.
769
+ - **Push-diff production demand.** Demand comes from the last trusted successful deployment receipt, never from a push's before/after commits.
770
+ - **Setup steps located by array position or display name.** They become `setupStepsForRoles`.
771
+ - **`paths:` filters on a deploy trigger.** A path filter asks whether this push touched the project, which is the wrong question once a shared-package change can reach the default branch and fail to deploy.
772
+
773
+ ### What stays with the consumer
774
+
775
+ The resource graph and its physical names; every credential the graph needs; the stage table; the retained-identity markers; the first-deploy authorization list; the state store's own configuration; product health policy; and the lease holder label, which must identify one run rather than be typed by an operator.
776
+
777
+ Convergence proves the control plane agrees with the state store. It cannot prove the product serves, so a consumer that needs that runs its own health gate after the operation and before it records a successful deployment.
778
+
779
+ ### Contract and schema changes
780
+
781
+ | Change | Package | What an adopter does |
782
+ | --- | --- | --- |
783
+ | `runAlchemyLifecycle`, `AlchemyLifecycleError` with enumerated `code`/`phase`/`mutation` | `@patronage/factory-ci/alchemy` | Replace local sequencing with one call; print the enumerated fields, never free text. |
784
+ | `StageStateStore` | `@patronage/factory-ci/alchemy` | Structural. `@patronage/alchemy-d1-state`'s `buildService` already satisfies it. |
785
+ | `acquireStageOwnership`, `StageOwnership`, `snapshotStage`, `ownership.adoptFrom` | `@patronage/alchemy-d1-state` | New surface, no migration. The ownership row is a second reserved sentinel FQN in the existing `(stack, stage, fqn, data)` shape, one row per owned stage. |
786
+ | Deployment receipts | `psf production:impact` | Grant `deployments: write` on the deploying job and take the receipt-binding step name from the generator, never by hand. |
787
+ | `setupStepsForRoles`, `SETUP_STEP_ROLES` | `@patronage/factory-ci` | Select setup steps by role. |
788
+
789
+ Neither package's peer range moved: both accept `alchemy >=2.0.0-beta.63 <3` and `effect >=4.0.0-beta.98 <5`, and the qualified installed pair is the one in `ALCHEMY_BASELINE`.
790
+
791
+ ### What a consumer still owes its own tests
792
+
793
+ HQ, the first production consumer, exercises one path: a `deploy` into a protected stage, with `adopt`, retained identities, an empty non-convergent list, and a lease every child adopts. Everything below is shared behaviour HQ's production path never reaches. None of it is covered by a production run, so a consumer that depends on one owes its own qualification against its own graph.
794
+
795
+ The right-hand column says what this repository proves about each, because "covered by tests" and "covered through the operation" are different claims:
796
+
797
+ | # | Path | Covered here |
798
+ | --- | --- | --- |
799
+ | 1 | `destroy`, including `DESTROY_RESIDUE` and all three cleanup outcomes | Yes, through `runAlchemyLifecycle`. All three outcomes — `removed`, `occupied` and `absent` — are asserted on the operation's result, not on a direct call to the adapter |
800
+ | 2 | `firstDeploy` — the plan-hash authorization for an empty protected stage | Yes, through `runAlchemyLifecycle` |
801
+ | 3 | `STAGE_HELD` contention between two runs on one stage | Yes, through `runAlchemyLifecycle` |
802
+ | 4 | A `deploy` into a disposable stage: the admission branch for an unprotected stage | Yes, through `runAlchemyLifecycle` |
803
+ | 5 | `nonConvergentResources` — the tolerated-`update` branch | Yes, through `runAlchemyLifecycle` |
804
+ | 6 | `policy.profile` | Yes — the child's `--profile` argument at all three insertion sites: the dry-run plan, the apply and the destroy. Added by #1079 |
805
+ | 7 | `policy.ownershipTtlMillis` | Yes — the lease window the database records, at the one acquire site every operation shares. Added by #1079. **Note the adoption behaviour it exposes**: adoption resets `expiresAt` to the database's current time plus the **adopting** process's TTL, without taking the maximum against the stored expiry, so it can shorten a parent's window as easily as lengthen it. A shortened window fails closed — the fenced renewal reports `OWNERSHIP_LOST` rather than proceeding — and exclusivity is unaffected. HQ never notices, because both sides take the ten-minute default. `alchemy-d1-state`'s README carries the full note |
806
+ | 8 | The `bundle` entry mode, and the `alias` map with it | Yes, through `runAlchemyLifecycle`, including the `alias` map: the operation bundles an entry that imports an aliased module, and the test reads the bundle the operation produced and asserts the aliased module's marker is in it. Losing the alias does not fail the build — `bundleAlchemyEntry` defaults an unresolved package to external, so the import survives unresolved and the bundle is still written — it fails that marker assertion. A consumer that hands the operation a `bundledEntry` never bundles |
807
+ | 9 | `LEASE_RELEASE_FAILED` — a store outage at exactly the release | Yes, through `runAlchemyLifecycle` |
808
+ | 10 | More than one stack, or a stack claiming more than one stage | **Only at the stage table.** `stagePolicy` is tested with multi-stack tables; no test drives `runAlchemyLifecycle` with one |
809
+
691
810
  ## No configuration surface
692
811
 
693
812
  Functions take plain typed objects. Proof and preview configuration lives in the canonical `software-factory.profile.json` schema, never in this package (ADR 0021); consumers pass the relevant profile policy directly at generation time.
@@ -1,3 +1,3 @@
1
1
  {
2
- "fingerprint": "cd07c573ff4f7d8a9d2b25ce8e3f9d34b7b806a15087af8367953344fd2adc4e"
2
+ "fingerprint": "ff8d152eaadee6c0b886de4d73f1a1335ef2acbf8d2df3500ce57aea02d8cb0d"
3
3
  }
@@ -1,3 +1,4 @@
1
+ import { n as ExecuteAlchemyEntryOptions } from "../execute-alchemy-entry-DB8fFZTu.js";
1
2
  import * as Alchemy from "alchemy";
2
3
  import * as Effect from "effect/Effect";
3
4
  import * as Layer from "effect/Layer";
@@ -572,6 +573,297 @@ declare const secretNamePolicy: ({
572
573
  values
573
574
  }: SecretNamePolicyOptions) => ((text: string) => string);
574
575
  //#endregion
576
+ //#region src/alchemy/lifecycle-failure.d.ts
577
+ /**
578
+ * The lifecycle failure envelope (#1077).
579
+ *
580
+ * Every failure `runAlchemyLifecycle` raises is one of these. The code, the
581
+ * phase, and the mutation certainty are enumerated; the message is a fixed
582
+ * sentence looked up by code. Nothing a child process printed, nothing a
583
+ * state store said, and nothing a credential resolver quoted ever reaches
584
+ * the message, and no failure carries a `cause`. Evidence sinks (`--json`,
585
+ * the job summary) can print any field of this error verbatim.
586
+ */
587
+ type AlchemyLifecycleOperation = "plan" | "deploy" | "destroy";
588
+ /** Where in the sequence the failure was raised. */
589
+ type AlchemyLifecyclePhase = /** The request itself or the policy is malformed. */"request" /** A pre-mutation check refused the operation. */ | "admission" /** The state store could not be read or owned. */ | "state" /** The Alchemy child failed. */ | "execution" /** A check after the child ran did not hold. */ | "postcondition";
590
+ /**
591
+ * What the failure says about the target stage. `not-started` means no
592
+ * child ran. `read-only-started` means only a planning child ran: no child
593
+ * that can mutate was started. `mutation-may-have-occurred` means a deploy
594
+ * or destroy child was started, so the stage may differ from before.
595
+ */
596
+ type MutationCertainty = "not-started" | "read-only-started" | "mutation-may-have-occurred";
597
+ type AlchemyLifecycleFailureCode = "INVALID_REQUEST" | "INVALID_POLICY" | "UNSUPPORTED_ALCHEMY_VERSION" | "UNKNOWN_STAGE" | "AMBIGUOUS_STAGE" | "PROTECTED_STAGE" | "UNKNOWN_STACK" | "STAGE_NOT_OWNED" | "PREPARATION_FAILED" | "STAGE_HELD" | "OWNERSHIP_LOST" | "LEASE_RELEASE_FAILED" | "STATE_STORE_UNAVAILABLE" | "STATE_STORE_INVALID" | "STATE_CHANGED" | "RESOURCE_IDENTITY_MISMATCH" | "FIRST_DEPLOY_UNAUTHORIZED" | "EFFECT_NOT_PERMITTED" | "UNKNOWN_EFFECT" | "AMBIGUOUS_RESOURCE" | "CHILD_FAILED" | "DEPLOY_INCOMPLETE" | "NOT_CONVERGED" | "DESTROY_RESIDUE";
598
+ /**
599
+ * The one sentence each code is reported with. The table is the whole
600
+ * vocabulary: a message that is not in it cannot be raised.
601
+ */
602
+ declare const LIFECYCLE_FAILURE_MESSAGES: Readonly<Record<AlchemyLifecycleFailureCode, string>>;
603
+ interface AlchemyLifecycleFailureFields {
604
+ readonly code: AlchemyLifecycleFailureCode;
605
+ readonly mutation: MutationCertainty;
606
+ readonly operation: AlchemyLifecycleOperation;
607
+ readonly phase: AlchemyLifecyclePhase;
608
+ /**
609
+ * The reviewed plan hash, on `FIRST_DEPLOY_UNAUTHORIZED` only. It is the
610
+ * value an operator authorizes, and it is a hash, never plan text.
611
+ */
612
+ readonly planHash?: string;
613
+ }
614
+ /** A sanitized lifecycle failure. See the module comment. */
615
+ declare class AlchemyLifecycleError extends Error implements AlchemyLifecycleFailureFields {
616
+ readonly code: AlchemyLifecycleFailureCode;
617
+ readonly mutation: MutationCertainty;
618
+ readonly operation: AlchemyLifecycleOperation;
619
+ readonly phase: AlchemyLifecyclePhase;
620
+ readonly planHash?: string;
621
+ constructor(fields: AlchemyLifecycleFailureFields);
622
+ /** The fields an evidence sink prints. Fixed shape, fixed words. */
623
+ toJSON(): AlchemyLifecycleFailureFields & {
624
+ readonly message: string;
625
+ };
626
+ }
627
+ declare const isAlchemyLifecycleError: (error: unknown) => error is AlchemyLifecycleError;
628
+ //#endregion
629
+ //#region src/alchemy/plan-effects.d.ts
630
+ /**
631
+ * The plan-output adapter (#1077): the one reader of what the installed
632
+ * Alchemy CLI prints for a plan.
633
+ *
634
+ * Alchemy has no structured plan output at the fleet baseline, so the plan
635
+ * is read from the lines `formatPlanLines` in `alchemy/src/Cli/LoggingCli.ts`
636
+ * prints: one `Plan:` summary and one `[id] action` line per resource.
637
+ * Paitronage and HQ each wrote this reader; this is the one copy, and it is
638
+ * bound to the Alchemy version whose renderer it reads. A consumer running a
639
+ * different Alchemy is refused by `runAlchemyLifecycle` before any child
640
+ * runs, so a renderer change cannot be read as a plan with different effects.
641
+ *
642
+ * The adapter fails on anything it does not recognise: no summary, two
643
+ * summaries, an action outside the known set, a summary it cannot parse, or
644
+ * a summary whose counts do not equal the lines under it. An effect it could
645
+ * not account for is never reported as zero.
646
+ */
647
+ /** The Alchemy version whose plan renderer this adapter reads. */
648
+ declare const PLAN_OUTPUT_ALCHEMY_VERSION: string;
649
+ declare const PLAN_ACTIONS: readonly ["create", "update", "replace", "delete", "noop"];
650
+ type PlanAction = (typeof PLAN_ACTIONS)[number];
651
+ /** One top-level resource row of a plan. */
652
+ interface PlanResourceEffect {
653
+ readonly action: PlanAction;
654
+ /** The logical id Alchemy printed in the row's tag. */
655
+ readonly id: string;
656
+ }
657
+ interface PlanEffects {
658
+ /** Top-level resource count per action, as the summary line states. */
659
+ readonly counts: Readonly<Record<PlanAction, number>>;
660
+ /** Every top-level resource row, in the order printed. */
661
+ readonly resources: readonly PlanResourceEffect[];
662
+ }
663
+ type PlanEffectsRejection = "no-summary" | "multiple-summaries" | "unknown-action" | "summary-unrecognized" | "count-mismatch" | "effects-under-no-changes";
664
+ /** The adapter could not read the output. `reason` is the whole detail. */
665
+ declare class PlanEffectsError extends Error {
666
+ readonly reason: PlanEffectsRejection;
667
+ constructor(reason: PlanEffectsRejection);
668
+ }
669
+ /**
670
+ * Read one plan from Alchemy CLI output.
671
+ *
672
+ * Throws `PlanEffectsError` on output the adapter cannot account for. The
673
+ * output itself never appears in the error.
674
+ */
675
+ declare const parsePlanEffects: (output: string) => PlanEffects;
676
+ //#endregion
677
+ //#region src/alchemy/lifecycle.d.ts
678
+ interface AlchemyLifecycleRequest {
679
+ readonly operation: AlchemyLifecycleOperation;
680
+ readonly stack: string;
681
+ readonly stage: string;
682
+ }
683
+ interface StageTarget {
684
+ readonly stack: string;
685
+ readonly stage: string;
686
+ }
687
+ /** Whether a stack output row exists and what it holds. */
688
+ type StackOutputOccupancy = {
689
+ readonly present: false;
690
+ } | {
691
+ readonly present: true;
692
+ readonly empty: boolean;
693
+ readonly value: unknown;
694
+ };
695
+ /**
696
+ * One consistent read of a stage, as `@patronage/alchemy-d1-state` returns
697
+ * it from a single statement. An unreadable stage fails; it is never an
698
+ * empty view.
699
+ */
700
+ interface StageSnapshotView {
701
+ /** Decoded resource FQNs. */
702
+ readonly resources: readonly string[];
703
+ readonly output: StackOutputOccupancy;
704
+ /** No resources and an absent or `{}` output row. */
705
+ readonly empty: boolean;
706
+ }
707
+ type StageCleanupOutcome = "removed" | "absent" | "occupied";
708
+ /**
709
+ * A held stage lease, as `@patronage/alchemy-d1-state` returns it.
710
+ *
711
+ * `release` is idempotent there: it succeeds when the row is gone or belongs
712
+ * to a later holder. So a release that succeeds proves nothing about
713
+ * ownership. `renew` is the fenced operation: it changes zero rows and fails
714
+ * with `lost` unless this holder's unexpired row is still there. The
715
+ * lifecycle renews before it accepts anything it read under the lease.
716
+ *
717
+ * `childEnvironment` is how the lease crosses a process boundary: the
718
+ * entries a child's environment must carry for the adapter in that child to
719
+ * adopt this lease. Every child the lifecycle launches carries them.
720
+ */
721
+ interface StageLease {
722
+ /** Drop the lease. Idempotent; never an ownership signal. */
723
+ readonly release: () => Effect.Effect<void, unknown>;
724
+ /** Extend the lease. Fails with `lost` once the lease is gone. */
725
+ readonly renew: () => Effect.Effect<void, unknown>;
726
+ /**
727
+ * The environment entries a child needs to adopt this lease. They carry
728
+ * the bearer token, so they go into a child's environment and nowhere
729
+ * else: never into a result, a failure, or a log.
730
+ */
731
+ readonly childEnvironment: () => Readonly<Record<string, string>>;
732
+ /**
733
+ * Atomically remove the stack output row if, in the same statement, it is
734
+ * `{}` and no resource rows exist. Never deletes a resource row.
735
+ */
736
+ readonly deleteEmptyStage: () => Effect.Effect<StageCleanupOutcome, unknown>;
737
+ }
738
+ /**
739
+ * The state capability: the subset of `D1StateService` the lifecycle reads
740
+ * through. `@patronage/alchemy-d1-state` satisfies it structurally; this
741
+ * package does not import that package and learns no table or account.
742
+ */
743
+ interface StageStateStore {
744
+ readonly snapshotStage: (target: StageTarget) => Effect.Effect<StageSnapshotView, unknown>;
745
+ readonly get: (request: StageTarget & {
746
+ readonly fqn: string;
747
+ }) => Effect.Effect<unknown, unknown>;
748
+ readonly acquireStageOwnership: (request: StageTarget & {
749
+ readonly holder: string;
750
+ readonly ttlMillis?: number;
751
+ }) => Effect.Effect<StageLease, unknown>;
752
+ }
753
+ interface FirstDeployPolicy {
754
+ /** The plan hashes an operator authorized. Owned by the consumer. */
755
+ readonly authorizedHashes: readonly string[];
756
+ /**
757
+ * The reviewed source commit the plan is bound to, or `null` to bind the
758
+ * plan to no commit. See `firstDeployPlanHash`.
759
+ */
760
+ readonly sourceSha: string | null;
761
+ }
762
+ interface AlchemyLifecyclePolicy {
763
+ /** The consumer's stage table. */
764
+ readonly stages: StagePolicyTable;
765
+ /** The label the stage lease is held under, such as a CI run id. */
766
+ readonly holder: string;
767
+ /** Lease length in milliseconds. The store's default applies when omitted. */
768
+ readonly ownershipTtlMillis?: number;
769
+ /**
770
+ * The physical identities a protected stage retains. Required for a
771
+ * deploy into a protected stage that already holds state; checked before
772
+ * the apply against the recorded values, and after it against the
773
+ * pre-apply snapshot.
774
+ */
775
+ readonly retainedIdentities?: readonly RetainedIdentityMarker[];
776
+ /**
777
+ * Authorizes the one deploy no earlier state constrains. A deploy into an
778
+ * empty stage with no policy here is refused.
779
+ */
780
+ readonly firstDeploy?: FirstDeployPolicy;
781
+ /** Pass `--adopt` to the deploy plan and apply. HQ's `v1` cutover flag. */
782
+ readonly adopt?: boolean;
783
+ /**
784
+ * Persisted FQNs whose `update` the post-deploy plan tolerates. A Worker
785
+ * carrying a write-only secret binding re-plans as `update` on every
786
+ * deploy because the value can never be read back; that is a property of
787
+ * the secret, not drift. Only `update` is tolerated, only on these FQNs.
788
+ * A top-level resource's FQN is its logical id; a nested one is
789
+ * `namespace/id`.
790
+ */
791
+ readonly nonConvergentResources?: readonly string[];
792
+ /** The Alchemy auth profile, passed as `--profile`. */
793
+ readonly profile?: string;
794
+ }
795
+ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
796
+ /**
797
+ * The entry capability: what `executeAlchemyEntry` needs minus what the
798
+ * lifecycle decides. Arguments are the lifecycle's; stdio is always piped so
799
+ * the plan can be read; the capture limit is `EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER`.
800
+ * There is no `redact` because there is no sink: the captured output is
801
+ * parsed and dropped. It is written nowhere, so nothing in it can reach a
802
+ * job log or an evidence file through this operation.
803
+ */
804
+ type AlchemyLifecycleEntry = DistributiveOmit<ExecuteAlchemyEntryOptions, "args" | "stdio" | "maxBuffer" | "redact">;
805
+ interface RunAlchemyLifecycleOptions {
806
+ readonly request: AlchemyLifecycleRequest;
807
+ readonly policy: AlchemyLifecyclePolicy;
808
+ readonly state: StageStateStore;
809
+ readonly entry: AlchemyLifecycleEntry;
810
+ }
811
+ interface AlchemyLifecycleResult {
812
+ readonly operation: AlchemyLifecycleOperation;
813
+ readonly stack: string;
814
+ readonly stage: string;
815
+ /** What the run did to the stage. A plan never mutates. */
816
+ readonly mutation: Exclude<MutationCertainty, "not-started">;
817
+ /** The installed Alchemy version the children ran under. */
818
+ readonly alchemyVersion: string;
819
+ /** A protected stage held no state before the operation. */
820
+ readonly firstDeploy: boolean;
821
+ /** The plan the operation read (plan), applied (deploy), or destroyed. */
822
+ readonly effects: PlanEffects;
823
+ /**
824
+ * The persisted FQN of every resource the deploy's plan created, resolved
825
+ * against the state recorded after the apply. Present on deploy only.
826
+ */
827
+ readonly created?: readonly string[];
828
+ /** The post-deploy plan. Present on deploy only. */
829
+ readonly convergence?: PlanEffects;
830
+ /** The reviewed first-deploy hash. Present on a plan of an empty stage with a first-deploy policy. */
831
+ readonly planHash?: string;
832
+ /** What the empty-stage cleanup did. Present on destroy only. */
833
+ readonly cleanup?: StageCleanupOutcome;
834
+ }
835
+ /**
836
+ * Run one lifecycle operation, in the one order the operation runs in.
837
+ *
838
+ * `plan`: read the persisted state under the stage lease, run the dry-run
839
+ * child, read the state again, and refuse if anything — a resource record
840
+ * or the stack output — changed. Returns the plan's effects and, for an
841
+ * empty stage with a first-deploy policy, the hash an operator authorizes.
842
+ *
843
+ * `deploy`: the same read-only plan, then admission on it. An empty stage is
844
+ * a first deploy: the plan may only create, and its hash must be on the
845
+ * authorized list. A protected stage with state refuses `replace` and
846
+ * `delete`, and its retained identities must hold before the apply. Then the
847
+ * apply child, then the persisted state again: it must record what the plan
848
+ * created and, on a protected stage, the same identities as before. Then a
849
+ * second dry-run plan, which must be a no-op except an `update` on a resource
850
+ * the policy names non-convergent.
851
+ *
852
+ * `destroy`: refused on a protected stage. Then, under the stage lease, the
853
+ * destroy child, then the atomic stage view must be empty and the empty
854
+ * output row is removed. An unreadable store fails; it never reads as empty.
855
+ *
856
+ * Each operation holds one stage lease from its first read to its last
857
+ * decision, and every child adopts it through its environment. The stage is
858
+ * never unowned while an operation runs, so no other writer can interleave,
859
+ * and a child that takes no ownership is refused by the store at its first
860
+ * write.
861
+ *
862
+ * Every failure is an `AlchemyLifecycleError`. After a deploy or destroy
863
+ * child was started, no failure reports `not-started`.
864
+ */
865
+ declare const runAlchemyLifecycle: (options: RunAlchemyLifecycleOptions) => Promise<AlchemyLifecycleResult>;
866
+ //#endregion
575
867
  //#region src/alchemy/index.d.ts
576
868
  /**
577
869
  * The `@patronage/factory-ci/alchemy` subpath: the only place in this package
@@ -592,4 +884,4 @@ declare const secretNamePolicy: ({
592
884
  /** The specifier a consumer imports this surface by. */
593
885
  declare const ALCHEMY_SUBPATH = "@patronage/factory-ci/alchemy";
594
886
  //#endregion
595
- export { ALCHEMY_SUBPATH, type AssertDestroyLeftNothingOptions, type AssertRetainedIdentitiesOptions, type AssertUrlImpliesAuthOptions, type AuthBindingNameMatcher, type CloudflareCredential, CloudflareCredentialUnavailableError, type CredentialPreflightOptions, type CredentialPreflightResult, type CredentialSource, type EvaluateStackInput, type EvaluatedBinding, type EvaluatedGraph, type EvaluatedResource, type FirstDeployAuthorization, type FirstDeployAuthorizationOptions, type FirstDeployPlanHashOptions, HOSTED_CLOUDFLARE_CREDENTIAL_NAMES, LOCAL_PREVIEW_STAGES, MINIMUM_REDACTABLE_SECRET_LENGTH, RetainedIdentityError, type RetainedIdentityMarker, type RetainedIdentitySection, type RetainedIdentityViolation, type RetainedIdentityViolationReason, SECRET_REDACTION_PLACEHOLDER, type SecretNamePolicyOptions, type StackStageOwnership, type StageMatcher, type StagePolicy, StagePolicyError, type StagePolicyErrorCode, type StagePolicyTable, type StageStateReader, type StageStateTarget, type StateSnapshot, type StateSnapshotResource, type StateSnapshotScope, type UrlImpliesAuthAllowance, UrlImpliesAuthError, type UrlImpliesAuthViolation, type UrlImpliesAuthViolationReason, anyStage, assertDestroyLeftNothing, assertRetainedIdentities, assertUrlImpliesAuth, credentialPreflight, evaluateStack, firstDeployAuthorization, firstDeployPlanHash, parseStateSnapshot, secretNamePolicy, stagePolicy };
887
+ export { ALCHEMY_SUBPATH, type AlchemyLifecycleEntry, AlchemyLifecycleError, type AlchemyLifecycleFailureCode, type AlchemyLifecycleFailureFields, type AlchemyLifecycleOperation, type AlchemyLifecyclePhase, type AlchemyLifecyclePolicy, type AlchemyLifecycleRequest, type AlchemyLifecycleResult, type AssertDestroyLeftNothingOptions, type AssertRetainedIdentitiesOptions, type AssertUrlImpliesAuthOptions, type AuthBindingNameMatcher, type CloudflareCredential, CloudflareCredentialUnavailableError, type CredentialPreflightOptions, type CredentialPreflightResult, type CredentialSource, type EvaluateStackInput, type EvaluatedBinding, type EvaluatedGraph, type EvaluatedResource, type FirstDeployAuthorization, type FirstDeployAuthorizationOptions, type FirstDeployPlanHashOptions, type FirstDeployPolicy, HOSTED_CLOUDFLARE_CREDENTIAL_NAMES, LIFECYCLE_FAILURE_MESSAGES, LOCAL_PREVIEW_STAGES, MINIMUM_REDACTABLE_SECRET_LENGTH, type MutationCertainty, PLAN_ACTIONS, PLAN_OUTPUT_ALCHEMY_VERSION, type PlanAction, type PlanEffects, PlanEffectsError, type PlanEffectsRejection, type PlanResourceEffect, RetainedIdentityError, type RetainedIdentityMarker, type RetainedIdentitySection, type RetainedIdentityViolation, type RetainedIdentityViolationReason, type RunAlchemyLifecycleOptions, SECRET_REDACTION_PLACEHOLDER, type SecretNamePolicyOptions, type StackOutputOccupancy, type StackStageOwnership, type StageCleanupOutcome, type StageLease, type StageMatcher, type StagePolicy, StagePolicyError, type StagePolicyErrorCode, type StagePolicyTable, type StageSnapshotView, type StageStateReader, type StageStateStore, type StageStateTarget, type StageTarget, type StateSnapshot, type StateSnapshotResource, type StateSnapshotScope, type UrlImpliesAuthAllowance, UrlImpliesAuthError, type UrlImpliesAuthViolation, type UrlImpliesAuthViolationReason, anyStage, assertDestroyLeftNothing, assertRetainedIdentities, assertUrlImpliesAuth, credentialPreflight, evaluateStack, firstDeployAuthorization, firstDeployPlanHash, isAlchemyLifecycleError, parsePlanEffects, parseStateSnapshot, runAlchemyLifecycle, secretNamePolicy, stagePolicy };