@starci/skills 1.1.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.
Files changed (321) hide show
  1. package/INDEX.md +74 -0
  2. package/INDEX.vi.md +75 -0
  3. package/README.md +44 -0
  4. package/README.vi.md +43 -0
  5. package/SKILL.md +135 -0
  6. package/SKILL.vi.md +128 -0
  7. package/alias/INDEX.md +104 -0
  8. package/alias/INDEX.vi.md +104 -0
  9. package/alias/alias.json +297 -0
  10. package/bin/starci-skills.mjs +219 -0
  11. package/knowledge/INDEX.md +22 -0
  12. package/knowledge/INDEX.vi.md +22 -0
  13. package/knowledge/grammars/starci/DNA.md +146 -0
  14. package/knowledge/grammars/starci/DNA.vi.md +146 -0
  15. package/knowledge/grammars/starci/INDEX.md +25 -0
  16. package/knowledge/grammars/starci/INDEX.vi.md +25 -0
  17. package/knowledge/grammars/starci/family.md +50 -0
  18. package/knowledge/grammars/starci/family.vi.md +50 -0
  19. package/knowledge/grammars/starci/idioms.md +120 -0
  20. package/knowledge/grammars/starci/idioms.vi.md +121 -0
  21. package/knowledge/grammars/starci/playbook.md +36 -0
  22. package/knowledge/grammars/starci/playbook.vi.md +35 -0
  23. package/knowledge/patterns/be/INDEX.md +29 -0
  24. package/knowledge/patterns/be/INDEX.vi.md +29 -0
  25. package/knowledge/patterns/be/comment.md +80 -0
  26. package/knowledge/patterns/be/comment.vi.md +80 -0
  27. package/knowledge/patterns/be/error.md +86 -0
  28. package/knowledge/patterns/be/error.vi.md +86 -0
  29. package/knowledge/patterns/be/folder.md +87 -0
  30. package/knowledge/patterns/be/folder.vi.md +86 -0
  31. package/knowledge/patterns/be/function.md +80 -0
  32. package/knowledge/patterns/be/function.vi.md +80 -0
  33. package/knowledge/patterns/be/imports.md +79 -0
  34. package/knowledge/patterns/be/imports.vi.md +79 -0
  35. package/knowledge/patterns/be/naming.md +87 -0
  36. package/knowledge/patterns/be/naming.vi.md +87 -0
  37. package/knowledge/patterns/be/test.md +79 -0
  38. package/knowledge/patterns/be/test.vi.md +79 -0
  39. package/knowledge/patterns/be/typing.md +73 -0
  40. package/knowledge/patterns/be/typing.vi.md +73 -0
  41. package/knowledge/patterns/fe/INDEX.md +29 -0
  42. package/knowledge/patterns/fe/INDEX.vi.md +29 -0
  43. package/knowledge/patterns/fe/comment.md +70 -0
  44. package/knowledge/patterns/fe/comment.vi.md +70 -0
  45. package/knowledge/patterns/fe/error.md +61 -0
  46. package/knowledge/patterns/fe/error.vi.md +61 -0
  47. package/knowledge/patterns/fe/folder.md +98 -0
  48. package/knowledge/patterns/fe/folder.vi.md +95 -0
  49. package/knowledge/patterns/fe/function.md +70 -0
  50. package/knowledge/patterns/fe/function.vi.md +70 -0
  51. package/knowledge/patterns/fe/imports.md +87 -0
  52. package/knowledge/patterns/fe/imports.vi.md +87 -0
  53. package/knowledge/patterns/fe/naming.md +77 -0
  54. package/knowledge/patterns/fe/naming.vi.md +77 -0
  55. package/knowledge/patterns/fe/test.md +73 -0
  56. package/knowledge/patterns/fe/test.vi.md +73 -0
  57. package/knowledge/patterns/fe/typing.md +67 -0
  58. package/knowledge/patterns/fe/typing.vi.md +67 -0
  59. package/knowledge/ui/INDEX.md +101 -0
  60. package/knowledge/ui/INDEX.vi.md +101 -0
  61. package/knowledge/ui/composition/INDEX.md +60 -0
  62. package/knowledge/ui/composition/INDEX.vi.md +63 -0
  63. package/knowledge/ui/composition/accent.md +73 -0
  64. package/knowledge/ui/composition/accent.vi.md +74 -0
  65. package/knowledge/ui/composition/action.md +55 -0
  66. package/knowledge/ui/composition/action.vi.md +56 -0
  67. package/knowledge/ui/composition/coverage.md +37 -0
  68. package/knowledge/ui/composition/coverage.vi.md +37 -0
  69. package/knowledge/ui/composition/cta.md +79 -0
  70. package/knowledge/ui/composition/cta.vi.md +79 -0
  71. package/knowledge/ui/composition/feedback.md +55 -0
  72. package/knowledge/ui/composition/feedback.vi.md +56 -0
  73. package/knowledge/ui/composition/hierarchy.md +83 -0
  74. package/knowledge/ui/composition/hierarchy.vi.md +82 -0
  75. package/knowledge/ui/composition/layout.md +91 -0
  76. package/knowledge/ui/composition/layout.vi.md +93 -0
  77. package/knowledge/ui/composition/responsive.md +67 -0
  78. package/knowledge/ui/composition/responsive.vi.md +67 -0
  79. package/knowledge/ui/composition/state.md +105 -0
  80. package/knowledge/ui/composition/state.vi.md +106 -0
  81. package/knowledge/ui/presentation/INDEX.md +94 -0
  82. package/knowledge/ui/presentation/INDEX.vi.md +69 -0
  83. package/knowledge/ui/presentation/boundary.md +170 -0
  84. package/knowledge/ui/presentation/boundary.vi.md +169 -0
  85. package/knowledge/ui/presentation/font.md +155 -0
  86. package/knowledge/ui/presentation/font.vi.md +156 -0
  87. package/knowledge/ui/presentation/gap.md +181 -0
  88. package/knowledge/ui/presentation/gap.vi.md +181 -0
  89. package/knowledge/ui/presentation/margin.md +168 -0
  90. package/knowledge/ui/presentation/margin.vi.md +171 -0
  91. package/knowledge/ui/presentation/measure.md +174 -0
  92. package/knowledge/ui/presentation/measure.vi.md +178 -0
  93. package/knowledge/ui/presentation/overflow.md +142 -0
  94. package/knowledge/ui/presentation/overflow.vi.md +142 -0
  95. package/knowledge/ui/presentation/padding.md +293 -0
  96. package/knowledge/ui/presentation/padding.vi.md +292 -0
  97. package/knowledge/ui/presentation/surface.md +166 -0
  98. package/knowledge/ui/presentation/surface.vi.md +166 -0
  99. package/knowledge/ui/presentation/text-flow.md +123 -0
  100. package/knowledge/ui/presentation/text-flow.vi.md +123 -0
  101. package/knowledge/ui/presentation/tone.md +114 -0
  102. package/knowledge/ui/presentation/tone.vi.md +113 -0
  103. package/knowledge/ui/proof/INDEX.md +55 -0
  104. package/knowledge/ui/proof/INDEX.vi.md +55 -0
  105. package/knowledge/ui/proof/accessibility.md +67 -0
  106. package/knowledge/ui/proof/accessibility.vi.md +68 -0
  107. package/knowledge/ui/proof/contrast.md +58 -0
  108. package/knowledge/ui/proof/contrast.vi.md +57 -0
  109. package/knowledge/ui/proof/focus.md +76 -0
  110. package/knowledge/ui/proof/focus.vi.md +76 -0
  111. package/knowledge/ui/proof/motion.md +63 -0
  112. package/knowledge/ui/proof/motion.vi.md +64 -0
  113. package/knowledge/ui/proof/render-truth.md +63 -0
  114. package/knowledge/ui/proof/render-truth.vi.md +63 -0
  115. package/operators/INDEX.md +199 -0
  116. package/operators/INDEX.vi.md +199 -0
  117. package/operators/architecture-decide/errors.json +115 -0
  118. package/operators/architecture-decide/operator.json +20 -0
  119. package/operators/architecture-decide/operator.md +133 -0
  120. package/operators/architecture-decide/operator.vi.md +130 -0
  121. package/operators/architecture-decide/self-test.mjs +221 -0
  122. package/operators/architecture-decide/validate.mjs +112 -0
  123. package/operators/backend-source-apply/errors.json +66 -0
  124. package/operators/backend-source-apply/operator.json +20 -0
  125. package/operators/backend-source-apply/operator.md +189 -0
  126. package/operators/backend-source-apply/operator.vi.md +185 -0
  127. package/operators/backend-source-apply/self-test.mjs +257 -0
  128. package/operators/backend-source-apply/validate.mjs +240 -0
  129. package/operators/business-decide/errors.json +78 -0
  130. package/operators/business-decide/operator.json +20 -0
  131. package/operators/business-decide/operator.md +176 -0
  132. package/operators/business-decide/operator.vi.md +174 -0
  133. package/operators/business-decide/self-test.mjs +225 -0
  134. package/operators/business-decide/validate.mjs +277 -0
  135. package/operators/content-generate/errors.json +106 -0
  136. package/operators/content-generate/operator.json +21 -0
  137. package/operators/content-generate/operator.md +155 -0
  138. package/operators/content-generate/operator.vi.md +155 -0
  139. package/operators/content-generate/self-test.mjs +288 -0
  140. package/operators/content-generate/validate.mjs +196 -0
  141. package/operators/errors.json +178 -0
  142. package/operators/frontend-direction-decide/errors.json +135 -0
  143. package/operators/frontend-direction-decide/operator.json +21 -0
  144. package/operators/frontend-direction-decide/operator.md +167 -0
  145. package/operators/frontend-direction-decide/operator.vi.md +167 -0
  146. package/operators/frontend-direction-decide/self-test.mjs +226 -0
  147. package/operators/frontend-direction-decide/validate.mjs +146 -0
  148. package/operators/frontend-presentation-resolve/errors.json +42 -0
  149. package/operators/frontend-presentation-resolve/operator.json +19 -0
  150. package/operators/frontend-presentation-resolve/operator.md +142 -0
  151. package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
  152. package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
  153. package/operators/frontend-presentation-resolve/validate.mjs +143 -0
  154. package/operators/frontend-source-apply/errors.json +30 -0
  155. package/operators/frontend-source-apply/operator.json +20 -0
  156. package/operators/frontend-source-apply/operator.md +127 -0
  157. package/operators/frontend-source-apply/operator.vi.md +126 -0
  158. package/operators/frontend-source-apply/self-test.mjs +214 -0
  159. package/operators/frontend-source-apply/validate.mjs +133 -0
  160. package/operators/frontend-surface-audit/errors.json +5 -0
  161. package/operators/frontend-surface-audit/operator.json +22 -0
  162. package/operators/frontend-surface-audit/operator.md +121 -0
  163. package/operators/frontend-surface-audit/operator.vi.md +121 -0
  164. package/operators/frontend-surface-audit/self-test.mjs +185 -0
  165. package/operators/frontend-surface-audit/validate.mjs +127 -0
  166. package/operators/git-publish/errors.json +54 -0
  167. package/operators/git-publish/operator.json +20 -0
  168. package/operators/git-publish/operator.md +180 -0
  169. package/operators/git-publish/operator.vi.md +178 -0
  170. package/operators/git-publish/self-test.mjs +177 -0
  171. package/operators/git-publish/validate.mjs +106 -0
  172. package/operators/platform-operate/errors.json +90 -0
  173. package/operators/platform-operate/operator.json +22 -0
  174. package/operators/platform-operate/operator.md +162 -0
  175. package/operators/platform-operate/operator.vi.md +160 -0
  176. package/operators/platform-operate/self-test.mjs +202 -0
  177. package/operators/platform-operate/validate.mjs +193 -0
  178. package/operators/quality-verify/errors.json +54 -0
  179. package/operators/quality-verify/operator.json +20 -0
  180. package/operators/quality-verify/operator.md +185 -0
  181. package/operators/quality-verify/operator.vi.md +177 -0
  182. package/operators/quality-verify/self-test.mjs +210 -0
  183. package/operators/quality-verify/validate.mjs +197 -0
  184. package/operators/release-deploy/errors.json +158 -0
  185. package/operators/release-deploy/operator.json +23 -0
  186. package/operators/release-deploy/operator.md +183 -0
  187. package/operators/release-deploy/operator.vi.md +181 -0
  188. package/operators/release-deploy/self-test.mjs +235 -0
  189. package/operators/release-deploy/validate.mjs +129 -0
  190. package/operators/uat-verify/errors.json +78 -0
  191. package/operators/uat-verify/operator.json +25 -0
  192. package/operators/uat-verify/operator.md +162 -0
  193. package/operators/uat-verify/operator.vi.md +161 -0
  194. package/operators/uat-verify/self-test.mjs +270 -0
  195. package/operators/uat-verify/validate.mjs +202 -0
  196. package/operators/workspace-bind/errors.json +90 -0
  197. package/operators/workspace-bind/operator.json +21 -0
  198. package/operators/workspace-bind/operator.md +148 -0
  199. package/operators/workspace-bind/operator.vi.md +148 -0
  200. package/operators/workspace-bind/self-test.mjs +205 -0
  201. package/operators/workspace-bind/validate.mjs +152 -0
  202. package/package.json +50 -0
  203. package/readiness/initialization/workspaces/commit-policy.json +91 -0
  204. package/readiness/initialization/workspaces/config.schema.json +35 -0
  205. package/readiness/initialization/workspaces/device-state.schema.json +54 -0
  206. package/readiness/initialization/workspaces/local-route.schema.json +206 -0
  207. package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
  208. package/resources/INDEX.md +96 -0
  209. package/resources/INDEX.vi.md +99 -0
  210. package/resources/agents/profiles/claude.json +131 -0
  211. package/resources/agents/profiles/openai.json +131 -0
  212. package/resources/orchestrator.json +71 -0
  213. package/resources/tools.json +85 -0
  214. package/routing.json +272 -0
  215. package/scripts/alias-registry.mjs +31 -0
  216. package/scripts/device-state.mjs +497 -0
  217. package/scripts/device-state.spec.mjs +18 -0
  218. package/scripts/errors-registry.mjs +68 -0
  219. package/scripts/generate-alias-doc.mjs +66 -0
  220. package/scripts/generate-grammar-dna.mjs +387 -0
  221. package/scripts/generate-operators-index.mjs +95 -0
  222. package/scripts/generate-presentation-owned.mjs +681 -0
  223. package/scripts/install-cli.spec.mjs +74 -0
  224. package/scripts/json-schema.mjs +94 -0
  225. package/scripts/operator-md.mjs +96 -0
  226. package/scripts/run-operator-self-tests.mjs +36 -0
  227. package/scripts/validate-alias.mjs +165 -0
  228. package/scripts/validate-defaults.mjs +72 -0
  229. package/scripts/validate-knowledge-citations.mjs +90 -0
  230. package/scripts/validate-operator.mjs +125 -0
  231. package/scripts/validate-request.mjs +80 -0
  232. package/scripts/validate-resources.mjs +117 -0
  233. package/scripts/validate-response.mjs +141 -0
  234. package/scripts/validate-routing.mjs +91 -0
  235. package/scripts/validate-step.mjs +50 -0
  236. package/scripts/validate-templates.mjs +226 -0
  237. package/scripts/validate-templates.spec.mjs +144 -0
  238. package/scripts/validate-workflows.mjs +106 -0
  239. package/scripts/workspace-portable.mjs +389 -0
  240. package/scripts/workspace-portable.spec.mjs +246 -0
  241. package/templates/README.md +47 -0
  242. package/templates/README.vi.md +45 -0
  243. package/templates/changes.example.md +27 -0
  244. package/templates/grammars.template.md +34 -0
  245. package/templates/kinds/architecture-decision.contract.json +14 -0
  246. package/templates/kinds/architecture-decision.skeleton.md +52 -0
  247. package/templates/kinds/backend-source-application.contract.json +10 -0
  248. package/templates/kinds/backend-source-application.skeleton.md +34 -0
  249. package/templates/kinds/business-promise-authority.contract.json +13 -0
  250. package/templates/kinds/business-promise-authority.skeleton.md +67 -0
  251. package/templates/kinds/capture.schema.json +33 -0
  252. package/templates/kinds/changes.contract.json +10 -0
  253. package/templates/kinds/changes.skeleton.md +26 -0
  254. package/templates/kinds/checks.schema.json +44 -0
  255. package/templates/kinds/claims.schema.json +165 -0
  256. package/templates/kinds/conformance.schema.json +15 -0
  257. package/templates/kinds/content-brief.contract.json +12 -0
  258. package/templates/kinds/content-brief.skeleton.md +45 -0
  259. package/templates/kinds/content-generation-receipt.contract.json +11 -0
  260. package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
  261. package/templates/kinds/content-review.contract.json +11 -0
  262. package/templates/kinds/content-review.skeleton.md +45 -0
  263. package/templates/kinds/contract.schema.json +28 -0
  264. package/templates/kinds/coverage-matrix.schema.json +86 -0
  265. package/templates/kinds/coverage.schema.json +62 -0
  266. package/templates/kinds/current-state.schema.json +40 -0
  267. package/templates/kinds/delta.schema.json +95 -0
  268. package/templates/kinds/e2e.schema.json +63 -0
  269. package/templates/kinds/frontend-direction-decision.contract.json +93 -0
  270. package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
  271. package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
  272. package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
  273. package/templates/kinds/frontend-source-application.contract.json +10 -0
  274. package/templates/kinds/frontend-source-application.skeleton.md +30 -0
  275. package/templates/kinds/frontend-surface-audit.contract.json +52 -0
  276. package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
  277. package/templates/kinds/gate-result.schema.json +64 -0
  278. package/templates/kinds/git-publication.contract.json +82 -0
  279. package/templates/kinds/git-publication.skeleton.md +61 -0
  280. package/templates/kinds/independent-critique.contract.json +9 -0
  281. package/templates/kinds/independent-critique.skeleton.md +28 -0
  282. package/templates/kinds/inventory.schema.json +35 -0
  283. package/templates/kinds/model.schema.json +99 -0
  284. package/templates/kinds/mutations.schema.json +65 -0
  285. package/templates/kinds/platform-operation-receipt.contract.json +76 -0
  286. package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
  287. package/templates/kinds/probes.schema.json +130 -0
  288. package/templates/kinds/proof.schema.json +17 -0
  289. package/templates/kinds/quality-verification.contract.json +87 -0
  290. package/templates/kinds/quality-verification.skeleton.md +58 -0
  291. package/templates/kinds/release-deployment.contract.json +85 -0
  292. package/templates/kinds/release-deployment.skeleton.md +67 -0
  293. package/templates/kinds/route.schema.json +293 -0
  294. package/templates/kinds/stack-model.schema.json +90 -0
  295. package/templates/kinds/uat-capture.schema.json +35 -0
  296. package/templates/kinds/uat-flow-verification.contract.json +11 -0
  297. package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
  298. package/templates/kinds/uat-snapshot.schema.json +96 -0
  299. package/templates/kinds/uat-verdicts.schema.json +41 -0
  300. package/templates/kinds/ui-coverage.schema.json +77 -0
  301. package/templates/kinds/verdicts.schema.json +39 -0
  302. package/templates/kinds/workspace-route-binding.contract.json +12 -0
  303. package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
  304. package/templates/kinds/writes.schema.json +37 -0
  305. package/templates/operator.template.md +73 -0
  306. package/templates/patterns.template.md +31 -0
  307. package/templates/step/request.schema.json +42 -0
  308. package/templates/step/response.schema.json +144 -0
  309. package/templates/ui-composition.template.md +36 -0
  310. package/templates/ui-presentation.template.md +57 -0
  311. package/templates/ui-proof.template.md +34 -0
  312. package/workflows/README.md +37 -0
  313. package/workflows/README.vi.md +37 -0
  314. package/workflows/backend-feature.json +59 -0
  315. package/workflows/content-unit.json +19 -0
  316. package/workflows/frontend-new-surface.json +81 -0
  317. package/workflows/frontend-reconstruct.json +67 -0
  318. package/workflows/frontend-refine.json +67 -0
  319. package/workflows/frontend-with-uat.json +78 -0
  320. package/workflows/full-feature.json +104 -0
  321. package/workflows/release.json +29 -0
@@ -0,0 +1,183 @@
1
+ # release.deploy
2
+
3
+ ## Job
4
+
5
+ Deploy one immutable release to one declared target under its declared authorization and prove the
6
+ steady state it reached, taking the recovery or rollback branch inside the same pass rather than
7
+ assuming the rollout succeeded.
8
+
9
+ ## Recovery and rollback are branches of this job
10
+
11
+ A rollout that does not stabilize is still this operator's problem. The run ends on one of three
12
+ terminals and never in the middle: the release is deployed, the previous release is restored, or the
13
+ work is blocked with an exact reason. A restored release is its own terminal outcome; it must never
14
+ be read as successful delivery of the release it rejected.
15
+
16
+ ## The release is immutable and exact
17
+
18
+ A release is identified by its `sha256:` digest, not by a tag, a branch, or a build number. The
19
+ artifact is never rebuilt, retagged, or substituted inside this pass; if the digest cannot be
20
+ resolved the run blocks rather than building a replacement and calling it the same release. The
21
+ manifest must have been validated against exactly this release, because a manifest pinned to another
22
+ release is how an unreviewed image reaches a reviewed target.
23
+
24
+ ## Authorization is declared, never implied
25
+
26
+ Deployment requires its own declared grant covering this project, this environment, this target and
27
+ the `deploy` action, still valid at the moment the target was observed. No ordinary task, no
28
+ precedent from a sibling project and no urgency implies it, and an unauthorized deployment does not
29
+ become authorized by being useful. Destructive loss, a credential rotation, or a new host, domain,
30
+ tenant or project leaves this operator's authority entirely and returns `APPROVAL_REQUIRED`.
31
+
32
+ ## Credentials are names, never values
33
+
34
+ Handles are resolved through existing custody at the moment they are needed. A resolved value never
35
+ enters the plan, the manifest, the receipt, a log line, a command argument, or a message. The receipt
36
+ records which handles were resolved and nothing more, and no field in the contract can hold a value
37
+ even if someone tried: a token written where a `secret-ref://` handle belongs is rejected as
38
+ malformed rather than quietly carried into an argument list.
39
+
40
+ ## Every effect is a compare-and-set
41
+
42
+ A step either mutates a boundary or it does not. The mutating steps are `host-prepare`,
43
+ `artifact-publish`, `migrate`, `domain-reconcile`, `rollout`, `recover` and `rollback`, and each
44
+ records the observed revision of its own boundary before and after. A desired state that already
45
+ matches is a proved idempotent no-op and is recorded as one; claiming an application without moving
46
+ a revision is refused, and a reading step that reports a revision has invented a fact about a
47
+ boundary it never touched. The execution root is ignored and rebuildable.
48
+
49
+ ## Monitoring distinguishes progressing from failing
50
+
51
+ On this project a push to `main` triggers the workflow and boot takes roughly eight to nine minutes,
52
+ so `progressing` is the expected condition for most of the window and is never treated as a failure,
53
+ and a monitoring deadline shorter than the window it must contain produces a guaranteed false
54
+ failure. One transient probe never becomes recovery: a failing condition has to persist across at
55
+ least two observations. A release that is neither this release nor the one it replaces stops the run
56
+ as `CONCURRENT_DRIFT` and forces a replan; it is never recovered or rolled back as though it
57
+ belonged here. Recovery repeats only approved reversible actions, numbers its attempts contiguously
58
+ from one, preserves the same release identity, and cannot end in a deployment once exhausted.
59
+ Rollback is valid only when the exact safe release still exists, the current data and schema state
60
+ remain compatible with it, and the revision actually moved.
61
+
62
+ ## Steady state is proved, not assumed
63
+
64
+ A rollout that returned without an error is not a deployment. Steady means the immutable digest is
65
+ active, every declared target is available, no superseded target remains active unless the strategy
66
+ permits it, the window elapsed in full, and every declared probe passed across the whole of it. That
67
+ is what turns three silent failures into detectable ones: the workflow finished while the old digest
68
+ is still serving traffic; one of two targets never came back and the other absorbed the load; the
69
+ readiness probe passed once, at the one moment it happened to be asked. At least one declared probe
70
+ is public, because a run observing only container health proves nothing a user could see; here the
71
+ GraphQL typename probe returning `200` is the readiness signal.
72
+
73
+ ## The two fallbacks are ordered, and the rest terminate
74
+
75
+ A failed rollout is not the end of the run, it is the entry to the first fallback: `ROLLOUT_FAILED`
76
+ takes the recovery branch, which repeats only approved reversible actions against the same release
77
+ identity. When those run out, `RECOVERY_EXHAUSTED` takes the second fallback: rollback to
78
+ `rollbackIdentity` by its exact digest. Both are recorded under `## Fallbacks taken`, because a
79
+ branch taken silently is a branch nobody can audit. After that there is nothing left to try, so
80
+ `ROLLBACK_IDENTITY_MISSING`, `STEADY_STATE_UNPROVEN` and `CONCURRENT_DRIFT` terminate: a rollback
81
+ without its safe release, a window that never closed, and a foreign release that appeared mid-run are
82
+ each a state this operator must not act further on.
83
+
84
+ ## Deadlines and probes have defaults, approval does not
85
+
86
+ `steadyDeadline` defaults to 600 seconds because that is the boot time this project actually shows,
87
+ and `probes` default to the set the validated manifest declares, so a person who names neither is
88
+ still measured against something real. `approval` has no default at all: changing what production
89
+ serves is always something a person said yes to.
90
+
91
+ ## Boundary
92
+
93
+ Context is read-only apart from the declared mutations. The operator applies only the declared host,
94
+ migration, domain and rollout mutations against the frozen release identity, restores the exact
95
+ declared rollback release when that branch is taken, and writes only `response/` of its own branch:
96
+ `data/probes.json`, `response.md` and `response.json`. It does not log, persist, echo, or return a
97
+ resolved credential value; does not deploy a release the declared authorization does not cover; does
98
+ not rebuild, retag, or otherwise alter the immutable artifact identified by the frozen digest; does
99
+ not edit the intent or revalidate the manifest into something else; does not recover or roll back a
100
+ release that appeared during execution and does not belong to this run; does not report a rolled-back
101
+ run as successful delivery of the rejected release; and does not declare steady state from a single
102
+ probe observation or from an assumed rollout.
103
+
104
+ ## Context
105
+
106
+ | Alias | Bind | Required |
107
+ | --- | --- | --- |
108
+ | `@remote/ghcr/<image>` | the immutable image by digest; a tag is never a binding | yes |
109
+ | `@workspaces/device-state` | credential handles by name and their custody; values never appear | yes |
110
+ | `@remote/github-actions/<runId>` | CI evidence of the build and the rollout, read only | no |
111
+
112
+ ## Inputs
113
+
114
+ | Kind | From | Required |
115
+ | --- | --- | --- |
116
+ | `quality-verification` | `quality.verify`; verification precedes an immutable build, and it is the authorization this run stands on | yes |
117
+
118
+ ## Requirements
119
+
120
+ | Field | Type | Default | Ask |
121
+ | --- | --- | --- | --- |
122
+ | `release` | id | — | The immutable release identity being deployed, with the `sha256:` digest that identifies it |
123
+ | `target` | id | — | The one target this deployment may change, and the environment it sits in |
124
+ | `approval` | id | — | The declared deploy grant covering this project, environment and target; changing what production serves always needs a person |
125
+ | `probes` | list of `{probeId, kind, endpointRef, expectStatus}` | the probes the validated manifest declares | What steady state is measured by; at least one probe is public |
126
+ | `steadyDeadline` | number | 600 | The bounded monitoring deadline in seconds, measured against the boot time this project shows |
127
+ | `rollbackIdentity` | `{releaseId, artifactRef, digest, dataCompatible}` | — | The exact safe release the rollback fallback restores, by digest |
128
+ | `resume` | token | null | The blocked branch's token when re-entering after a stop |
129
+
130
+ ## Steps
131
+
132
+ | # | Step | Params | Reads | Writes | Stops with |
133
+ | --- | --- | --- | --- | --- | --- |
134
+ | 1 | Validate the gate, the authorization the input carries, and the resume | `resume` | `request/request.json`, input `quality-verification` as the authorization this run stands on | — | `INVALID_INPUT`, `AUTHORIZATION_MISSING`, `NO_PROGRESS` |
135
+ | 2 | Bind the release and compile the plan | `release`, `target`, `approval` | @remote/ghcr/<image> at the frozen digest, @remote/github-actions/<runId> for the observed state, @tools/git, @tools/ci | — | `MANIFEST_INVALID`, `APPROVAL_REQUIRED` |
136
+ | 3 | Initialize the execution root and resolve the credentials by name | — | @workspaces/device-state for the declared handles and their custody, @tools/secrets | — | `CREDENTIAL_UNAVAILABLE` |
137
+ | 4 | Prepare the host, publish the artifact by digest, migrate and reconcile the domain | — | @remote/ghcr/<image> for the artifact by digest, @remote/github-actions/<runId> for each boundary's revision before and after | @tools/shell | `HOST_UNAVAILABLE`, `ARTIFACT_MISSING`, `MIGRATION_BLOCKED`, `DOMAIN_UNRECONCILED` |
138
+ | 5 | Roll out | — | @remote/ghcr/<image> for the target revision before and after | @tools/container | `ROLLOUT_FAILED` |
139
+ | 6 | Monitor within the deadline, with backoff | `steadyDeadline`, `probes` | @remote/github-actions/<runId> for the probe observations across the window, @tools/http | `response/data/probes.json` | — |
140
+ | 7 | Detect concurrent drift before acting | — | `response/data/probes.json`, @remote/ghcr/<image> for the active release by digest | — | `CONCURRENT_DRIFT` |
141
+ | 8 | Take the recovery branch when the failure persists | — | `response/data/probes.json`, @remote/ghcr/<image> at the same release identity | @tools/container | `RECOVERY_EXHAUSTED` |
142
+ | 9 | Take the rollback branch when recovery cannot hold | `rollbackIdentity` | @remote/ghcr/<image> at the exact safe digest | @tools/container | `ROLLBACK_IDENTITY_MISSING` |
143
+ | 10 | Prove the steady state, write the receipt and emit | — | everything above | `response/response.md`, `response/response.json` | `STEADY_STATE_UNPROVEN` |
144
+
145
+ Steps 8 and 9 are the two fallbacks in order, not a sequence every run walks: a run enters step 8
146
+ only under `ROLLOUT_FAILED` and step 9 only under `RECOVERY_EXHAUSTED`, and a run that took neither
147
+ records the branch as `none`. A resume begins again at validation, reuses only unchanged fingerprinted
148
+ observations, and keeps the same release identity, because a different release is a different
149
+ deployment; a resume that adds no authorization, manifest, credential or observation change is
150
+ `NO_PROGRESS`.
151
+
152
+ ## Outputs
153
+
154
+ | Kind | File | Type | Required |
155
+ | --- | --- | --- | --- |
156
+ | `release-deployment` | `response/response.md` | md | yes |
157
+ | `probes` | `response/data/probes.json` | data | yes |
158
+
159
+ ## Stops
160
+
161
+ | Code | Disposition |
162
+ | --- | --- |
163
+ | `INVALID_INPUT` | terminate |
164
+ | `NO_PROGRESS` | terminate |
165
+ | `AUTHORIZATION_MISSING` | terminate |
166
+ | `MANIFEST_INVALID` | terminate |
167
+ | `APPROVAL_REQUIRED` | terminate |
168
+ | `CREDENTIAL_UNAVAILABLE` | terminate |
169
+ | `HOST_UNAVAILABLE` | terminate |
170
+ | `ARTIFACT_MISSING` | terminate |
171
+ | `MIGRATION_BLOCKED` | terminate |
172
+ | `DOMAIN_UNRECONCILED` | terminate |
173
+ | `ROLLOUT_FAILED` | fallback |
174
+ | `RECOVERY_EXHAUSTED` | fallback |
175
+ | `CONCURRENT_DRIFT` | terminate |
176
+ | `ROLLBACK_IDENTITY_MISSING` | terminate |
177
+ | `STEADY_STATE_UNPROVEN` | terminate |
178
+
179
+ ## Next
180
+
181
+ | When | Operator |
182
+ | --- | --- |
183
+ | the host, the artifact registry, the credential custody or the safe release needs a shared runtime change | `platform.operate` |
@@ -0,0 +1,181 @@
1
+ # release.deploy
2
+
3
+ ## Việc
4
+
5
+ Deploy một release bất biến lên một target đã khai, dưới đúng thẩm quyền đã khai, rồi chứng minh
6
+ trạng thái ổn định mà nó đạt tới, đi nhánh recovery hay rollback ngay trong cùng lượt chạy thay vì
7
+ giả định rollout đã thành công.
8
+
9
+ ## Recovery và rollback là nhánh của chính việc này
10
+
11
+ Một rollout không ổn định lại vẫn là vấn đề của operator này. Lượt chạy kết thúc ở đúng một trong ba
12
+ điểm cuối và không bao giờ dừng giữa chừng: release được deploy, release trước được khôi phục, hoặc
13
+ công việc bị chặn với một lý do chính xác. Một release được khôi phục là điểm cuối của riêng nó; nó
14
+ không bao giờ được đọc thành việc giao thành công cái release mà nó vừa từ chối.
15
+
16
+ ## Release là bất biến và chính xác
17
+
18
+ Một release được nhận diện bằng digest `sha256:` của nó, không phải bằng tag, nhánh hay số build.
19
+ Artifact không bao giờ được build lại, gắn tag lại hay thay thế bên trong lượt này; nếu digest không
20
+ phân giải được thì lượt chạy dừng, chứ không build một cái thay thế rồi gọi nó là cùng một release.
21
+ Manifest phải đã được kiểm đúng với chính release này, vì manifest ghim vào một release khác chính là
22
+ cách một image chưa duyệt tới được một target đã duyệt.
23
+
24
+ ## Thẩm quyền là được khai, không bao giờ được ngầm hiểu
25
+
26
+ Deploy đòi hỏi một grant khai riêng, phủ đúng project này, môi trường này, target này và hành động
27
+ `deploy`, và còn hiệu lực tại đúng lúc target được quan sát. Không nhiệm vụ thường nào, không tiền lệ
28
+ từ một project anh em nào và không sự gấp gáp nào ngầm cấp nó, và một lần deploy không có thẩm quyền
29
+ không trở thành có thẩm quyền chỉ vì nó hữu ích. Mất mát phá huỷ, xoay vòng credential, hay một host,
30
+ domain, tenant hoặc project mới đều nằm hẳn ngoài thẩm quyền của operator này và trả `APPROVAL_REQUIRED`.
31
+
32
+ ## Credential là tên, không bao giờ là giá trị
33
+
34
+ Handle được phân giải qua custody đang có, đúng lúc cần. Một giá trị đã phân giải không bao giờ đi
35
+ vào plan, manifest, biên bản, một dòng log, một tham số lệnh hay một tin nhắn. Biên bản ghi lại
36
+ những handle nào đã được phân giải và không gì hơn, và không trường nào trong hợp đồng chứa nổi một
37
+ giá trị dù có ai cố: một token viết vào chỗ đáng lẽ là handle `secret-ref://` bị từ chối như đầu vào
38
+ sai dạng, thay vì lặng lẽ trôi vào một danh sách tham số.
39
+
40
+ ## Mọi tác động đều là compare-and-set
41
+
42
+ Một bước hoặc có làm đổi một boundary, hoặc không. Các bước có làm đổi là `host-prepare`,
43
+ `artifact-publish`, `migrate`, `domain-reconcile`, `rollout`, `recover` và `rollback`, và mỗi bước
44
+ ghi lại revision quan sát được của chính boundary mình trước và sau. Trạng thái mong muốn mà đã trùng
45
+ sẵn là một no-op idempotent đã được chứng minh và được ghi đúng như vậy; tuyên bố đã áp dụng mà
46
+ revision không dịch chuyển thì bị từ chối, còn một bước chỉ đọc mà báo revision là bịa ra một sự thật
47
+ về boundary nó chưa hề chạm. Execution root là thứ bỏ đi được và dựng lại được.
48
+
49
+ ## Giám sát phân biệt đang tiến và đang hỏng
50
+
51
+ Ở dự án này, push lên `main` kích hoạt workflow và boot mất khoảng tám tới chín phút, nên
52
+ `progressing` là điều kiện bình thường trong phần lớn cửa sổ và không bao giờ bị coi là lỗi, và một
53
+ deadline giám sát ngắn hơn cửa sổ mà nó phải chứa sẽ sinh ra một lỗi giả chắc chắn. Một lần probe
54
+ chớp nhoáng không bao giờ thành recovery: điều kiện hỏng phải kéo dài qua ít nhất hai lần quan sát.
55
+ Một release không phải release này và cũng không phải release nó thay thế sẽ dừng lượt chạy bằng
56
+ `CONCURRENT_DRIFT` và buộc lập kế hoạch lại; nó không bao giờ được recover hay rollback như thể nó
57
+ thuộc về đây. Recovery chỉ lặp lại những hành động đảo ngược được đã duyệt, đánh số các lần thử liền
58
+ mạch từ một, giữ nguyên danh tính release, và một khi đã cạn thì không thể kết thúc bằng một lần
59
+ deploy. Rollback chỉ hợp lệ khi release an toàn chính xác vẫn còn, trạng thái dữ liệu và schema hiện
60
+ tại vẫn tương thích với nó, và revision thực sự đã dịch chuyển.
61
+
62
+ ## Trạng thái ổn định là được chứng minh, không phải được giả định
63
+
64
+ Một rollout trả về không lỗi chưa phải một lần deploy. Ổn định nghĩa là digest bất biến đang active,
65
+ mọi target đã khai đều sẵn sàng, không target bị thay thế nào còn active trừ khi chiến lược cho phép,
66
+ cửa sổ đã trôi qua trọn vẹn, và mọi probe đã khai đều pass suốt cửa sổ đó. Đó là thứ biến ba lỗi
67
+ im lặng thành ba lỗi phát hiện được: workflow chạy xong trong khi digest cũ vẫn đang phục vụ traffic;
68
+ một trong hai target không bao giờ quay lại còn cái kia gánh hết tải; probe readiness pass đúng một
69
+ lần, đúng khoảnh khắc nó tình cờ được hỏi. Ít nhất một probe đã khai phải là probe công khai, vì một
70
+ lượt chạy chỉ quan sát sức khoẻ container thì chẳng chứng minh được gì mà người dùng thấy được; ở đây
71
+ probe GraphQL typename trả `200` là tín hiệu readiness.
72
+
73
+ ## Hai fallback có thứ tự, phần còn lại thì terminate
74
+
75
+ Một rollout hỏng không phải là hết lượt, nó là cửa vào fallback thứ nhất: `ROLLOUT_FAILED` đi nhánh
76
+ phục hồi, nhánh chỉ lặp lại những hành động đảo ngược được đã duyệt trên cùng danh tính release. Khi
77
+ chúng cạn, `RECOVERY_EXHAUSTED` đi fallback thứ hai: rollback về `rollbackIdentity` theo đúng digest
78
+ của nó. Cả hai đều được ghi dưới `## Fallbacks taken`, vì một nhánh đi âm thầm là một nhánh không ai
79
+ soát lại được. Sau đó không còn gì để thử nữa, nên `ROLLBACK_IDENTITY_MISSING`, `STEADY_STATE_UNPROVEN`
80
+ và `CONCURRENT_DRIFT` terminate: một lần rollback không còn release an toàn, một cửa sổ không bao giờ
81
+ khép, và một release lạ xuất hiện giữa lượt, mỗi thứ đều là trạng thái operator này không được hành
82
+ động tiếp lên.
83
+
84
+ ## Deadline và probe có mặc định, phê duyệt thì không
85
+
86
+ `steadyDeadline` mặc định 600 giây vì đó là thời gian boot mà dự án này thực sự cho thấy, còn `probes`
87
+ mặc định lấy bộ mà manifest đã kiểm khai ra, nên người không nêu cái nào vẫn được đo theo một thứ có
88
+ thật. `approval` thì hoàn toàn không có mặc định: đổi thứ mà production đang phục vụ luôn là việc có
89
+ người nói đồng ý.
90
+
91
+ ## Ranh giới ghi
92
+
93
+ Context chỉ đọc, trừ các mutation đã khai. Operator chỉ áp các mutation host, migration, domain và
94
+ rollout đã khai lên đúng danh tính release đã đóng băng, khôi phục đúng release rollback đã khai khi
95
+ đi nhánh đó, và chỉ ghi `response/` của nhánh mình: `data/probes.json`, `response.md` và
96
+ `response.json`. Nó không log, lưu, in ra hay trả về một giá trị credential đã phân giải; không deploy
97
+ một release mà thẩm quyền đã khai không phủ; không build lại, gắn tag lại hay thay đổi artifact bất
98
+ biến mà digest đóng băng chỉ tới; không sửa intent hay kiểm lại manifest thành một thứ khác; không
99
+ recover hay rollback một release xuất hiện giữa lượt và không thuộc về lượt này; không báo một lượt đã
100
+ rollback là đã giao thành công cái release bị từ chối; và không tuyên bố trạng thái ổn định từ một lần
101
+ quan sát probe duy nhất hay từ một rollout được giả định.
102
+
103
+ ## Context
104
+
105
+ | Alias | Bind | Bắt buộc |
106
+ | --- | --- | --- |
107
+ | `@remote/ghcr/<image>` | image bất biến theo digest; một tag không bao giờ là ràng buộc | có |
108
+ | `@workspaces/device-state` | handle credential theo tên và custody của chúng; giá trị không bao giờ xuất hiện | có |
109
+ | `@remote/github-actions/<runId>` | bằng chứng CI của build và rollout, chỉ đọc | không |
110
+
111
+ ## Đầu vào
112
+
113
+ | Kind | Từ đâu | Bắt buộc |
114
+ | --- | --- | --- |
115
+ | `quality-verification` | `quality.verify`; kiểm định đi trước một bản build bất biến, và nó là thẩm quyền mà lượt chạy này đứng lên | có |
116
+
117
+ ## Yêu cầu
118
+
119
+ | Field | Kiểu | Mặc định | Hỏi |
120
+ | --- | --- | --- | --- |
121
+ | `release` | id | — | Danh tính release bất biến đang được deploy, kèm digest `sha256:` nhận diện nó |
122
+ | `target` | id | — | Đúng một target mà lần deploy này được đổi, cùng môi trường nó nằm trong |
123
+ | `approval` | id | — | Grant deploy đã khai phủ project, môi trường và target này; đổi thứ production đang phục vụ luôn cần một con người |
124
+ | `probes` | list of `{probeId, kind, endpointRef, expectStatus}` | the probes the validated manifest declares | Trạng thái ổn định được đo bằng gì; ít nhất một probe là công khai |
125
+ | `steadyDeadline` | number | 600 | Deadline giám sát có chặn trên, tính bằng giây, đo theo thời gian boot mà dự án này cho thấy |
126
+ | `rollbackIdentity` | `{releaseId, artifactRef, digest, dataCompatible}` | — | Đúng release an toàn mà fallback rollback khôi phục, theo digest |
127
+ | `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
128
+
129
+ ## Các bước
130
+
131
+ | # | Bước | Tham số | Đọc | Ghi | Dừng với |
132
+ | --- | --- | --- | --- | --- | --- |
133
+ | 1 | Kiểm cổng vào, thẩm quyền mà Đầu vào mang, và resume | `resume` | `request/request.json`, Đầu vào `quality-verification` là thẩm quyền lượt chạy này đứng lên | — | `INVALID_INPUT`, `AUTHORIZATION_MISSING`, `NO_PROGRESS` |
134
+ | 2 | Ràng release và biên dịch kế hoạch | `release`, `target`, `approval` | @remote/ghcr/<image> tại digest đã đóng băng, @remote/github-actions/<runId> cho trạng thái quan sát được, @tools/git, @tools/ci | — | `MANIFEST_INVALID`, `APPROVAL_REQUIRED` |
135
+ | 3 | Khởi tạo execution root và phân giải credential theo tên | — | @workspaces/device-state cho các handle đã khai và custody của chúng, @tools/secrets | — | `CREDENTIAL_UNAVAILABLE` |
136
+ | 4 | Chuẩn bị host, publish artifact theo digest, migrate và đối chiếu domain | — | @remote/ghcr/<image> cho artifact theo digest, @remote/github-actions/<runId> cho revision trước và sau của từng boundary | @tools/shell | `HOST_UNAVAILABLE`, `ARTIFACT_MISSING`, `MIGRATION_BLOCKED`, `DOMAIN_UNRECONCILED` |
137
+ | 5 | Roll out | — | @remote/ghcr/<image> cho revision của target trước và sau | @tools/container | `ROLLOUT_FAILED` |
138
+ | 6 | Theo dõi trong deadline, có backoff | `steadyDeadline`, `probes` | @remote/github-actions/<runId> cho các quan sát probe suốt cửa sổ, @tools/http | `response/data/probes.json` | — |
139
+ | 7 | Phát hiện drift đồng thời trước khi hành động | — | `response/data/probes.json`, @remote/ghcr/<image> cho release đang active theo digest | — | `CONCURRENT_DRIFT` |
140
+ | 8 | Đi nhánh phục hồi khi lỗi còn dai dẳng | — | `response/data/probes.json`, @remote/ghcr/<image> tại cùng danh tính release | @tools/container | `RECOVERY_EXHAUSTED` |
141
+ | 9 | Đi nhánh rollback khi phục hồi không giữ được | `rollbackIdentity` | @remote/ghcr/<image> tại đúng digest an toàn | @tools/container | `ROLLBACK_IDENTITY_MISSING` |
142
+ | 10 | Chứng minh trạng thái ổn định, viết biên bản và phát | — | mọi thứ ở trên | `response/response.md`, `response/response.json` | `STEADY_STATE_UNPROVEN` |
143
+
144
+ Bước 8 và 9 là hai fallback theo thứ tự, không phải một chuỗi mà lượt chạy nào cũng đi qua: một lượt
145
+ chỉ vào bước 8 dưới `ROLLOUT_FAILED` và chỉ vào bước 9 dưới `RECOVERY_EXHAUSTED`, còn lượt không đi
146
+ cái nào thì ghi nhánh là `none`. Một lần resume bắt đầu lại từ cổng vào, chỉ dùng lại quan sát có
147
+ fingerprint không đổi, và giữ nguyên danh tính release, vì một release khác là một lần deploy khác;
148
+ một lần resume không thêm thẩm quyền, manifest, credential hay quan sát nào là `NO_PROGRESS`.
149
+
150
+ ## Đầu ra
151
+
152
+ | Kind | File | Kiểu | Bắt buộc |
153
+ | --- | --- | --- | --- |
154
+ | `release-deployment` | `response/response.md` | md | có |
155
+ | `probes` | `response/data/probes.json` | data | có |
156
+
157
+ ## Dừng
158
+
159
+ | Code | Xử lý |
160
+ | --- | --- |
161
+ | `INVALID_INPUT` | terminate |
162
+ | `NO_PROGRESS` | terminate |
163
+ | `AUTHORIZATION_MISSING` | terminate |
164
+ | `MANIFEST_INVALID` | terminate |
165
+ | `APPROVAL_REQUIRED` | terminate |
166
+ | `CREDENTIAL_UNAVAILABLE` | terminate |
167
+ | `HOST_UNAVAILABLE` | terminate |
168
+ | `ARTIFACT_MISSING` | terminate |
169
+ | `MIGRATION_BLOCKED` | terminate |
170
+ | `DOMAIN_UNRECONCILED` | terminate |
171
+ | `ROLLOUT_FAILED` | fallback |
172
+ | `RECOVERY_EXHAUSTED` | fallback |
173
+ | `CONCURRENT_DRIFT` | terminate |
174
+ | `ROLLBACK_IDENTITY_MISSING` | terminate |
175
+ | `STEADY_STATE_UNPROVEN` | terminate |
176
+
177
+ ## Kế tiếp
178
+
179
+ | Khi | Operator |
180
+ | --- | --- |
181
+ | host, registry artifact, custody credential hay release an toàn cần một thay đổi runtime dùng chung | `platform.operate` |
@@ -0,0 +1,235 @@
1
+ // Proves validate.mjs on a synthetic session branch: one steady deployment, one that took the
2
+ // recovery fallback, one that took both fallbacks and ended rolled back, one blocked on an unproven
3
+ // steady state, and one mutation per law, each of which must fail with a line that names the defect.
4
+ import assert from 'node:assert/strict';
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { validateReleaseStep } from './validate.mjs';
9
+
10
+ const RELEASE = 'release:2026.01.10-1';
11
+ const PREVIOUS = 'release:2026.01.03-2';
12
+ const DIGEST = `sha256:${'1'.repeat(64)}`;
13
+ const SAFE_DIGEST = `sha256:${'2'.repeat(64)}`;
14
+ const TARGET = 'production/api';
15
+ const APPROVAL = '@worktrees/businesses/features/release/model.json#deploy-grant';
16
+ const MANIFEST = '.stacks/production/api.manifest.json';
17
+ const ARTIFACT = '@remote/ghcr/starci/academy-api';
18
+ const DEADLINE = 600;
19
+ const PROBES = [
20
+ { probeId: 'public-graphql-typename', kind: 'public', endpointRef: 'https://api.example/graphql', expectStatus: 200 },
21
+ ];
22
+ const ROLLBACK = { releaseId: PREVIOUS, artifactRef: ARTIFACT, digest: SAFE_DIGEST, dataCompatible: true };
23
+
24
+ const observation = (at, condition, status = 'pass') => ({
25
+ observedAt: at, condition, activeReleaseIds: [RELEASE], activeDigest: DIGEST, availableTargets: 1,
26
+ probeResults: [{ probeId: 'public-graphql-typename', status, observedStatus: status === 'pass' ? 200 : 502, observedAt: at }],
27
+ });
28
+
29
+ const probesJson = ({ observations = [observation('2026-01-10T00:01:00.000Z', 'progressing'), observation('2026-01-10T00:07:00.000Z', 'steady')], finalCondition = 'steady', deadlineSeconds = DEADLINE, elapsedSeconds = 420, backoffSeconds = 30 } = {}) => ({
30
+ deadlineSeconds, elapsedSeconds, backoffSeconds, observations, finalCondition,
31
+ });
32
+
33
+ const NL = String.fromCharCode(10);
34
+ const STEPS = [
35
+ ['`authorize`', 'applied', '—', '—', 'the declared grant covers this project, environment and target'],
36
+ ['`rollout`', 'applied', '4', '5', 'the target moved to the immutable digest'],
37
+ ['`monitor`', 'applied', '—', '—', 'the window was observed to its end'],
38
+ ];
39
+
40
+ function responseMd({
41
+ release = RELEASE, digest = DIGEST, target = TARGET, approval = APPROVAL, deadline = DEADLINE,
42
+ rollbackId = PREVIOUS, outcome = 'deployed', branch = 'none', steps = STEPS,
43
+ monitoring = { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'steady' },
44
+ steady = { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
45
+ fallbacksTaken = [],
46
+ } = {}) {
47
+ return `# release-deployment — ${release}
48
+
49
+ The immutable release reached its declared target under the declared grant, and the steady state
50
+ rests on the observed digest, the available targets and every declared probe across the window.
51
+
52
+ ## Binding
53
+
54
+ | Field | Value |
55
+ | --- | --- |
56
+ | Operator | \`release.deploy\` |
57
+ | Step | \`step-1/parallel-1\` |
58
+ | Project | \`starci-academy\` |
59
+ | Release | ${release} |
60
+ | Artifact | \`${ARTIFACT}\` |
61
+ | Digest | \`${digest}\` |
62
+ | Target | ${target} |
63
+ | Environment | production |
64
+ | Replaced release | \`${PREVIOUS}\` |
65
+ | Approval | ${approval} |
66
+ | Manifest | \`${MANIFEST}\` |
67
+ | Steady deadline | ${deadline} |
68
+ | Rollback identity | ${rollbackId} |
69
+
70
+ ## Outcome
71
+
72
+ | Field | Value |
73
+ | --- | --- |
74
+ | Outcome | ${outcome} |
75
+ | Branch | ${branch} |
76
+
77
+ ## Steps
78
+
79
+ | Step | State | Revision before | Revision after | Statement |
80
+ | --- | --- | --- | --- | --- |
81
+ ${steps.map((s) => `| ${s[0]} | ${s[1]} | ${s[2]} | ${s[3]} | ${s[4]} |`).join('\n')}
82
+
83
+ ## Monitoring
84
+
85
+ | Field | Value |
86
+ | --- | --- |
87
+ | Deadline | ${monitoring.Deadline} |
88
+ | Elapsed | ${monitoring.Elapsed} |
89
+ | Backoff | ${monitoring.Backoff} |
90
+ | Final condition | ${monitoring['Final condition']} |
91
+
92
+ ## Steady state
93
+
94
+ | Metric | Value |
95
+ | --- | --- |
96
+ ${Object.entries(steady).map(([k, v]) => `| ${k} | ${/^sha256:/.test(String(v)) ? `\`${v}\`` : v} |`).join('\n')}
97
+
98
+ ## Findings
99
+
100
+ | Code | Step | Statement |
101
+ | --- | --- | --- |
102
+ | \`IDEMPOTENT_NO_OP\` | \`host-prepare\` | the host already matched the declaration |
103
+
104
+ ## Fallbacks taken
105
+
106
+ | Code | Action |
107
+ | --- | --- |
108
+ ${fallbacksTaken.map((c) => `| \`${c}\` | the branch was taken and recorded |`).join(NL)}
109
+ `;
110
+ }
111
+
112
+ const requestJson = ({ release = RELEASE, target = TARGET, approval = APPROVAL, probes = PROBES, deadline = DEADLINE, rollbackIdentity = ROLLBACK, extra = {} } = {}) => ({
113
+ schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, sessionId: 's-test',
114
+ contexts: [{ alias: '@remote/ghcr/starci/academy-api', head: null }],
115
+ requirements: { release, target, approval, probes, steadyDeadline: deadline, rollbackIdentity, resume: null, ...extra },
116
+ inputs: { 'quality-verification': 'step-1/parallel-1/response/quality.md' }, resume: null,
117
+ });
118
+
119
+ const responseJson = ({ status = 'done', stop, fallbacks = [], next = ['platform.operate'] } = {}) => ({
120
+ schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, status, ...(stop ? { stop } : {}),
121
+ fallbacks,
122
+ fields: status === 'blocked' ? {} : { 'release-deployment': 'response/response.md', probes: 'response/data/probes.json' },
123
+ commits: [], next,
124
+ });
125
+
126
+ function writeBranch(files) {
127
+ const session = mkdtempSync(path.join(tmpdir(), 'release-session-'));
128
+ const branch = path.join(session, 'step-1', 'parallel-1');
129
+ for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
130
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'release.deploy' }, current: '1/1', status: 'running' }));
131
+ writeFileSync(path.join(branch, 'response', 'quality.md'), '# quality\n');
132
+ for (const [name, content] of Object.entries(files)) {
133
+ if (content === null) continue;
134
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
135
+ }
136
+ return { branch, session };
137
+ }
138
+
139
+ const baseline = (over = {}) => ({
140
+ 'request/request.json': requestJson(),
141
+ 'response/response.json': responseJson(),
142
+ 'response/response.md': responseMd(),
143
+ 'response/data/probes.json': probesJson(),
144
+ ...over,
145
+ });
146
+
147
+
148
+ // The recovery fallback: two failing observations, then steady again on the same release.
149
+ const recovered = () => baseline({
150
+ 'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED'] }),
151
+ 'response/data/probes.json': probesJson({
152
+ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')],
153
+ }),
154
+ 'response/response.md': responseMd({
155
+ branch: 'recover',
156
+ steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved reversible action restored the target']],
157
+ fallbacksTaken: ['ROLLOUT_FAILED'],
158
+ }),
159
+ });
160
+
161
+ // Both fallbacks in order: the recovery ran out and the safe release was restored by digest.
162
+ const rolledBack = () => baseline({
163
+ 'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
164
+ 'response/data/probes.json': probesJson({
165
+ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:09:00.000Z', 'steady')],
166
+ }),
167
+ 'response/response.md': responseMd({
168
+ outcome: 'rolled-back', branch: 'rollback',
169
+ steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved actions ran out'], ['`rollback`', 'applied', '6', '7', 'the safe release was restored by digest']],
170
+ steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
171
+ fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'],
172
+ }),
173
+ });
174
+
175
+ async function expectValid(files, label) {
176
+ const { branch, session } = writeBranch(files);
177
+ const { errors } = await validateReleaseStep(branch);
178
+ rmSync(session, { recursive: true, force: true });
179
+ assert.deepEqual(errors, [], `${label} should be valid`);
180
+ }
181
+ async function expectError(files, needle, label) {
182
+ const { branch, session } = writeBranch(files);
183
+ const { errors } = await validateReleaseStep(branch);
184
+ rmSync(session, { recursive: true, force: true });
185
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
186
+ }
187
+
188
+ await expectValid(baseline(), 'a steady deployment of the immutable release');
189
+ await expectValid(recovered(), 'the recovery fallback taken after a persistent failure');
190
+ await expectValid(rolledBack(), 'both fallbacks in order, ending in a restored release');
191
+ await expectValid({
192
+ 'request/request.json': requestJson(),
193
+ 'response/response.json': responseJson({ status: 'blocked', stop: 'STEADY_STATE_UNPROVEN', next: [] }),
194
+ 'response/response.md': null, 'response/data/probes.json': null,
195
+ }, 'blocked because the steady window never closed');
196
+
197
+ await expectError(baseline({ 'response/response.json': { ...responseJson(), stop: 'ROLLOUT_FAILED' } }), 'only a blocked response carries a stop', 'done with a stop');
198
+ await expectError(baseline({ 'response/response.json': responseJson({ status: 'blocked', stop: 'ROLLOUT_FAILED', next: [] }) }), 'has disposition fallback under these requirements', 'a fallback code used to block the branch');
199
+ await expectError(baseline({ 'response/response.json': responseJson({ fallbacks: ['STEADY_STATE_UNPROVEN'] }) }), 'cannot be taken as a fallback', 'a terminate code taken as a fallback');
200
+ await expectError(baseline({
201
+ 'response/response.json': responseJson({ fallbacks: ['RECOVERY_EXHAUSTED'] }),
202
+ 'response/response.md': responseMd({ outcome: 'rolled-back', branch: 'rollback', steps: [...STEPS, ['`rollback`', 'applied', '6', '7', 'restored']], steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' }, fallbacksTaken: ['RECOVERY_EXHAUSTED'] }),
203
+ }), 'reached only through an exhausted recovery', 'a rollback that skipped the recovery branch');
204
+ await expectError(baseline({ 'request/request.json': requestJson({ extra: { strategy: 'rolling' } }) }), 'requirements.strategy is not a field', 'a field the operator no longer declares');
205
+ await expectError(baseline({ 'request/request.json': requestJson({ approval: null }) }), 'required field approval has no value', 'a production deploy nobody approved');
206
+ await expectError(baseline({ 'request/request.json': requestJson({ probes: [{ probeId: 'container-health', kind: 'internal', endpointRef: 'unix:///health', expectStatus: 200 }] }), 'response/data/probes.json': probesJson({ observations: [{ ...observation('2026-01-10T00:01:00.000Z', 'progressing'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:01:00.000Z' }] }, { ...observation('2026-01-10T00:07:00.000Z', 'steady'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:07:00.000Z' }] }] }) }), 'at least one declared probe is public', 'a run proving only container health');
207
+ await expectError(baseline({ 'request/request.json': requestJson({ rollbackIdentity: { ...ROLLBACK, digest: 'v1.4.0' } }) }), 'a rollback by tag restores whatever the tag now points at', 'a rollback identity named by tag');
208
+ await expectError(baseline({ 'request/request.json': requestJson({ extra: { release: RELEASE }, target: TARGET, approval: 'ghp_abcdefghijklmnopqrstuvwxyz' }) }), 'looks like a resolved credential value', 'a token where a handle belongs');
209
+ await expectError(baseline({ 'request/request.json': requestJson({ deadline: 120 }) }), 'but the request pinned 120s', 'monitoring measured against another deadline');
210
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ elapsedSeconds: 900 }) }), 'ran past its own bounded deadline', 'monitoring that outran its deadline');
211
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ backoffSeconds: 900 }) }), 'backoff cannot exceed the deadline', 'a backoff wider than the window');
212
+ await expectError({
213
+ ...recovered(),
214
+ 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')] }),
215
+ }, 'one transient probe is not a persistent failure', 'recovery entered on a single failing probe');
216
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ finalCondition: 'deadline-exceeded' }) }), 'which is STEADY_STATE_UNPROVEN, not a deployment', 'a deployment over an exceeded deadline');
217
+ await expectError(baseline({ 'response/response.md': responseMd({ release: 'release:someone-elses' }) }), 'but the request bound', 'a receipt for another release');
218
+ await expectError(baseline({ 'response/response.md': responseMd({ digest: 'latest' }) }), 'identified by its digest, never by a tag', 'a release named by tag');
219
+ await expectError(baseline({ 'response/response.md': responseMd({ approval: '`someone-elses-grant`' }) }), 'names an approval the request did not bind', 'a grant borrowed from another deployment');
220
+ await expectError(baseline({ 'response/response.md': responseMd({ branch: 'recover' }) }), 'but the fallbacks taken say none', 'a branch the fallbacks never recorded');
221
+ await expectError({
222
+ ...rolledBack(),
223
+ 'response/response.md': responseMd({ outcome: 'deployed', branch: 'rollback', steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'x'], ['`rollback`', 'applied', '6', '7', 'y']], fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
224
+ }, 'a restored release is its own terminal', 'a rollback reported as delivery');
225
+ await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '—', '—', 'x'], ['`rollout`', 'applied', '4', '4', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'claims it applied without moving a revision', 'an application that moved nothing');
226
+ await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '2', '3', 'x'], ['`rollout`', 'applied', '4', '5', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'reports a revision for a boundary it never touched', 'a reading step inventing a revision');
227
+ await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'while another digest is active', 'a deployment over the old digest');
228
+ await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 2', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'a partly available target set is not steady state', 'one target absorbing the load for two');
229
+ await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '1', 'Window elapsed': '300' } }) }), 'still serving traffic', 'a superseded target left active');
230
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:07:00.000Z', 'steady')] }) }), 'claimed from a single observation', 'steady state read off one probe');
231
+ await expectError(baseline({ 'response/response.md': responseMd({ monitoring: { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'progressing' } }) }), 'but the series ended steady', 'a receipt disagreeing with its own series');
232
+ await expectError(baseline({ 'response/data/probes.json': { ...probesJson(), finalCondition: 'nope' } }), 'finalCondition', 'probes schema');
233
+ await expectError(baseline({ 'response/response.md': responseMd().replace('## Steady state', '## Stable state') }), 'missing section ^## Steady state$', 'receipt section renamed');
234
+
235
+ process.stdout.write('release.deploy self-test: 4 valid branches, 26 rejected mutations\n');