@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,129 @@
1
+ // release.deploy's own law over one branch, on top of the shared step check: the receipt names the
2
+ // release, target, approval and rollback identity the request bound; no credential value appears
3
+ // anywhere; the two fallbacks are taken in order and recorded, and a branch nobody entered is `none`;
4
+ // monitoring stayed inside the deadline, a failing condition persisted across at least two
5
+ // observations before recovery, and one transient probe never became a branch; a foreign release
6
+ // terminates instead of being recovered; and steady state is proved by the digest, the targets and
7
+ // every declared probe across the whole window, never by a single observation.
8
+ import { existsSync } from 'node:fs';
9
+ import { readFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import process from 'node:process';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { validateStep } from '../../scripts/validate-step.mjs';
14
+ import { tableUnder } from '../../scripts/validate-response.mjs';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
18
+ const SECRET = /(?:ghp_[A-Za-z0-9]{16,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{16,}|AKIA[0-9A-Z]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})/;
19
+ const empty = (v) => v === undefined || v === null || v === '' || v === '—';
20
+ const rows = (text, heading) => tableUnder(text, heading) ?? [];
21
+ const fields = (text, heading) => Object.fromEntries(rows(text, heading).map(([k, v]) => [k, v]));
22
+
23
+ export async function validateReleaseStep(branchDir, root = ROOT) {
24
+ const base = await validateStep(root, branchDir);
25
+ const errors = [...base.errors];
26
+ const { response, request, requirements = {}, present = new Set() } = base;
27
+ if (!response || response.operatorId !== 'release.deploy') return { errors };
28
+ const read = (f) => readFile(path.join(branchDir, f), 'utf8');
29
+ const has = (f) => existsSync(path.join(branchDir, f));
30
+
31
+ if (empty(requirements.approval)) errors.push('request.json: approval has no default; changing what production serves is always something a person said yes to');
32
+ const deadline = Number(requirements.steadyDeadline ?? 600);
33
+ if (!Number.isFinite(deadline) || deadline <= 0) errors.push('request.json: steadyDeadline must be a positive number of seconds');
34
+ const declaredProbes = Array.isArray(requirements.probes) ? requirements.probes : [];
35
+ if (declaredProbes.length && !declaredProbes.some((p) => p.kind === 'public')) errors.push('request.json: at least one declared probe is public, because container health proves nothing a user could see');
36
+ const rollback = requirements.rollbackIdentity ?? null;
37
+ if (rollback && !DIGEST.test(rollback.digest ?? '')) errors.push('request.json: rollbackIdentity names no sha256 digest; a rollback by tag restores whatever the tag now points at');
38
+
39
+ // Credentials are names, never values, in the request as much as in the receipt.
40
+ for (const [key, value] of Object.entries(requirements)) {
41
+ if (typeof value !== 'string') continue;
42
+ if (SECRET.test(value) && !value.startsWith('secret-ref://')) errors.push(`request.json: requirements.${key} looks like a resolved credential value; handles are names, never values`);
43
+ }
44
+
45
+ // The two fallbacks are ordered: rollback is reachable only through an exhausted recovery.
46
+ const fallbacks = response.fallbacks ?? [];
47
+ if (fallbacks.includes('RECOVERY_EXHAUSTED') && !fallbacks.includes('ROLLOUT_FAILED')) errors.push('response/response.json: RECOVERY_EXHAUSTED was taken without ROLLOUT_FAILED; the rollback branch is reached only through an exhausted recovery');
48
+
49
+ let probes = null;
50
+ if (present.has('probes') && has('response/data/probes.json')) { try { probes = JSON.parse(await read('response/data/probes.json')); } catch { probes = null; } }
51
+ if (probes) {
52
+ if (probes.deadlineSeconds !== deadline) errors.push(`response/data/probes.json: monitored to ${probes.deadlineSeconds}s but the request pinned ${deadline}s`);
53
+ if (probes.elapsedSeconds > probes.deadlineSeconds) errors.push('response/data/probes.json: monitoring ran past its own bounded deadline');
54
+ if (probes.backoffSeconds > probes.deadlineSeconds) errors.push('response/data/probes.json: the backoff cannot exceed the deadline it sits inside');
55
+ const declaredIds = new Set(declaredProbes.map((p) => p.probeId));
56
+ for (const o of probes.observations) {
57
+ for (const r of o.probeResults) if (declaredIds.size && !declaredIds.has(r.probeId)) errors.push(`response/data/probes.json: probe ${r.probeId} was observed but the request never declared it`);
58
+ if (declaredIds.size) { const seen = new Set(o.probeResults.map((r) => r.probeId)); for (const id of declaredIds) if (!seen.has(id)) errors.push(`response/data/probes.json: the observation at ${o.observedAt} skipped declared probe ${id}`); }
59
+ }
60
+ // One transient probe never becomes recovery.
61
+ const failing = probes.observations.filter((o) => o.condition === 'failing').length;
62
+ if (fallbacks.includes('ROLLOUT_FAILED') && failing < 2) errors.push('response/data/probes.json: the recovery branch was taken after fewer than two failing observations; one transient probe is not a persistent failure');
63
+ if (probes.finalCondition === 'steady' && probes.observations.at(-1)?.condition !== 'steady') errors.push('response/data/probes.json: the final condition is steady but the last observation is not');
64
+ if (probes.finalCondition === 'deadline-exceeded' && response.status === 'done') errors.push('response/data/probes.json: the deadline was exceeded, which is STEADY_STATE_UNPROVEN, not a deployment');
65
+ } else if (response.status === 'done') errors.push('response/data/probes.json: a done branch needs the monitored probe series');
66
+
67
+ if (!(present.has('release-deployment') && has('response/response.md'))) {
68
+ if (response.status === 'done') errors.push('response/response.md: a done branch needs the deployment receipt');
69
+ return { errors };
70
+ }
71
+ const text = await read('response/response.md');
72
+ if (SECRET.test(text)) errors.push('response/response.md: the receipt carries something shaped like a resolved credential value');
73
+ const binding = fields(text, '## Binding');
74
+ const outcome = fields(text, '## Outcome');
75
+ const steps = rows(text, '## Steps');
76
+ const monitoring = fields(text, '## Monitoring');
77
+ const steady = fields(text, '## Steady state');
78
+
79
+ if (!empty(requirements.release) && binding.Release !== requirements.release) errors.push(`response/response.md: Binding deploys ${binding.Release} but the request bound ${requirements.release}`);
80
+ if (!empty(requirements.target) && binding.Target !== requirements.target) errors.push(`response/response.md: Binding names target ${binding.Target} but the request bound ${requirements.target}`);
81
+ if (!empty(requirements.approval) && binding.Approval !== requirements.approval) errors.push('response/response.md: Binding names an approval the request did not bind');
82
+ if (!DIGEST.test(binding.Digest ?? '')) errors.push('response/response.md: Binding names no sha256 digest; a release is identified by its digest, never by a tag');
83
+ if (rollback && binding['Rollback identity'] !== rollback.releaseId) errors.push('response/response.md: Binding names a rollback identity the request did not declare');
84
+ if (String(binding['Steady deadline']) !== String(deadline)) errors.push(`response/response.md: Binding says the deadline is ${binding['Steady deadline']} but the request pinned ${deadline}`);
85
+
86
+ // The branch the receipt records is the branch the fallbacks say was taken.
87
+ const expectedBranch = fallbacks.includes('RECOVERY_EXHAUSTED') ? 'rollback' : fallbacks.includes('ROLLOUT_FAILED') ? 'recover' : 'none';
88
+ if (outcome.Branch !== expectedBranch) errors.push(`response/response.md: Branch is ${outcome.Branch} but the fallbacks taken say ${expectedBranch}`);
89
+ if (expectedBranch === 'rollback' && outcome.Outcome !== 'rolled-back') errors.push('response/response.md: a rollback branch cannot report a deployed outcome; a restored release is its own terminal');
90
+ if (expectedBranch !== 'rollback' && outcome.Outcome === 'rolled-back') errors.push('response/response.md: the outcome is rolled-back but no rollback branch was taken');
91
+ if (outcome.Outcome === 'rolled-back' && !rollback) errors.push('response/response.md: a rollback happened with no declared rollback identity to restore');
92
+
93
+ // Every effect is a compare-and-set: an applied mutating step moved a revision, a reading step did not.
94
+ const MUTATING = new Set(['host-prepare', 'artifact-publish', 'migrate', 'domain-reconcile', 'rollout', 'recover', 'rollback']);
95
+ for (const [step, state, before, after] of steps) {
96
+ const at = `response/response.md: step ${step}`;
97
+ if (MUTATING.has(step)) {
98
+ if (state === 'applied' && (before === '—' || after === '—' || before === after)) errors.push(`${at} claims it applied without moving a revision`);
99
+ if (state === 'no-op' && before !== after) errors.push(`${at} is a no-op and moved a revision anyway`);
100
+ } else if (before !== '—' || after !== '—') errors.push(`${at} reports a revision for a boundary it never touched`);
101
+ }
102
+ const stepNames = new Set(steps.map(([s]) => s));
103
+ if (expectedBranch === 'recover' && !stepNames.has('recover')) errors.push('response/response.md: the recovery fallback was taken but no recover step is recorded');
104
+ if (expectedBranch === 'rollback' && !stepNames.has('rollback')) errors.push('response/response.md: the rollback fallback was taken but no rollback step is recorded');
105
+ if (expectedBranch === 'none' && (stepNames.has('recover') || stepNames.has('rollback'))) errors.push('response/response.md: a branch step is recorded while the run took neither fallback');
106
+
107
+ if (probes) {
108
+ if (String(monitoring.Deadline) !== String(probes.deadlineSeconds)) errors.push('response/response.md: Monitoring deadline differs from the measured series');
109
+ if (monitoring['Final condition'] !== probes.finalCondition) errors.push(`response/response.md: Monitoring says ${monitoring['Final condition']} but the series ended ${probes.finalCondition}`);
110
+ }
111
+
112
+ // Steady state is proved, not assumed.
113
+ if (outcome.Outcome === 'deployed') {
114
+ if (steady['Active digest'] !== binding.Digest) errors.push('response/response.md: the deployment is reported while another digest is active');
115
+ const [available, of] = (steady['Available targets'] ?? '').split(' of ');
116
+ if (available !== undefined && of !== undefined && Number(available) < Number(of)) errors.push(`response/response.md: ${available} of ${of} targets are available; a partly available target set is not steady state`);
117
+ if (Number(steady['Superseded active'] ?? 0) > 0) errors.push('response/response.md: a superseded target is still active, so the old release is still serving traffic');
118
+ if (probes && probes.finalCondition !== 'steady') errors.push('response/response.md: a deployment is reported while the observed series never reached steady');
119
+ if (probes && probes.observations.length < 2) errors.push('response/response.md: steady state is claimed from a single observation');
120
+ }
121
+ return { errors };
122
+ }
123
+
124
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
125
+ const target = process.argv[2];
126
+ if (!target) { process.stderr.write('usage: node validate.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
127
+ const { errors } = await validateReleaseStep(path.resolve(target));
128
+ if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('valid release.deploy branch\n');
129
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "note": "Stop codes only uat.verify emits. Same entry shape as operators/errors.json, scope implicit. Shared codes (INVALID_INPUT, SOURCE_DRIFT, NO_PROGRESS) come from operators/errors.json. RUNTIME_UNAVAILABLE is emitted here too but is defined in operators/frontend-surface-audit/errors.json; it belongs in operators/errors.json with both operators in scope, and until that move validate-operator reports it as out of scope here. No code asks a person for a credential: the shared UAT password is resolved by name at login, and unavailable provisioning blocks instead of prompting.",
4
+ "codes": {
5
+ "ADMISSION_MISSING": {
6
+ "domain": "quality",
7
+ "disposition": "terminate",
8
+ "meaning": {
9
+ "en": "The surface audit or the quality verification that admits product UAT is absent, or one of them was taken at another commit than the pinned head.",
10
+ "vi": "Lượt soi bề mặt hoặc lượt kiểm chất lượng cho phép UAT sản phẩm đang thiếu, hoặc một trong hai được lấy ở commit khác commit đã ghim."
11
+ },
12
+ "resume": {
13
+ "en": "Re-run the missing admission at the pinned commit.",
14
+ "vi": "Chạy lại admission còn thiếu tại đúng commit đã ghim."
15
+ }
16
+ },
17
+ "PROVISIONING_UNAVAILABLE": {
18
+ "domain": "control-panel",
19
+ "disposition": "terminate",
20
+ "meaning": {
21
+ "en": "The sealed UAT credential cannot be resolved by name, or the flow's dedicated account does not exist; asking a person to sign in is forbidden in every branch.",
22
+ "vi": "Không giải được thông tin đăng nhập UAT niêm phong theo tên, hoặc tài khoản riêng của luồng không tồn tại; nhờ người đăng nhập bị cấm ở mọi nhánh."
23
+ },
24
+ "resume": {
25
+ "en": "Restore the sealed credential or the account; never a person's sign-in.",
26
+ "vi": "Khôi phục thông tin đăng nhập niêm phong hoặc tài khoản; không bao giờ nhờ người đăng nhập."
27
+ }
28
+ },
29
+ "LEASE_INVALID": {
30
+ "domain": "control-panel",
31
+ "disposition": "terminate",
32
+ "meaning": {
33
+ "en": "The exclusive lease on the flow directory is expired, foreign, or bound to another run, generation or origin.",
34
+ "vi": "Lease độc quyền trên thư mục luồng đã hết hạn, thuộc chỗ khác, hoặc gắn vào lượt chạy, generation hay origin khác."
35
+ },
36
+ "resume": {
37
+ "en": "The orchestrator grants the lease again for this run.",
38
+ "vi": "Orchestrator cấp lại lease cho đúng lượt chạy này."
39
+ }
40
+ },
41
+ "EVIDENCE_UNAVAILABLE": {
42
+ "domain": "runtime",
43
+ "disposition": "terminate",
44
+ "meaning": {
45
+ "en": "A case produced no capture, no screenshot, or no screenshot whose login field could be masked, so a lane has nothing to be judged on.",
46
+ "vi": "Một case không sinh ra capture, không sinh ra ảnh chụp, hoặc không ảnh nào che được ô mật khẩu, nên một làn không có gì để xét."
47
+ },
48
+ "resume": {
49
+ "en": "Restore the dependency and run the frozen case again under a new runId.",
50
+ "vi": "Khôi phục phụ thuộc rồi chạy lại case đã đóng băng dưới một runId mới."
51
+ }
52
+ },
53
+ "FIXTURE_VIOLATION": {
54
+ "domain": "caller",
55
+ "disposition": "terminate",
56
+ "meaning": {
57
+ "en": "The seed, the run namespace or the cleanup scope could not be satisfied: a seed would have created the outcome under test, or cleanup would have reached outside the namespace.",
58
+ "vi": "Seed, namespace lượt chạy hay phạm vi dọn dẹp không thoả được: seed sẽ tạo ra chính kết quả cần kiểm, hoặc dọn dẹp sẽ với ra ngoài namespace."
59
+ },
60
+ "resume": {
61
+ "en": "Correct the fixture boundary in seed/records.json.",
62
+ "vi": "Sửa ranh giới fixture trong seed/records.json."
63
+ }
64
+ },
65
+ "CANONICAL_WRITE_DENIED": {
66
+ "domain": "backend",
67
+ "disposition": "terminate",
68
+ "meaning": {
69
+ "en": "The flow directory cannot be written and read back under the exclusive lease, or the write would have rewritten an existing run record.",
70
+ "vi": "Không ghi rồi đọc lại được thư mục luồng dưới lease độc quyền, hoặc lần ghi sẽ đè lên một hồ sơ lượt chạy đã có."
71
+ },
72
+ "resume": {
73
+ "en": "Restore write authority on the flow directory, or publish under a new runId.",
74
+ "vi": "Khôi phục quyền ghi trên thư mục luồng, hoặc phát hành dưới một runId mới."
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "id": "uat.verify",
4
+ "domain": "test",
5
+ "job": "Verify one product flow end to end on the running product at the pinned commit, and publish one append-only run record with three independently judged lanes, or stop at the exact unavailability instead of manufacturing a verdict.",
6
+ "package": "operator.md",
7
+ "errors": "errors.json",
8
+ "validator": "validate.mjs",
9
+ "selfTest": "self-test.mjs",
10
+ "resources": {
11
+ "profile": "sol-fresh",
12
+ "grammarBound": false,
13
+ "tools": {
14
+ "@tools/fileread": "context-aliases",
15
+ "@tools/sourcewrite": "declared-write-set",
16
+ "@tools/git": "read",
17
+ "@tools/websearch": "bounded",
18
+ "@tools/visualize": "html",
19
+ "@tools/browsercontrol": "required",
20
+ "@tools/http": "probe",
21
+ "@tools/secrets": "resolve-by-name",
22
+ "@tools/database": "namespaced-write"
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,162 @@
1
+ # uat.verify
2
+
3
+ ## Job
4
+
5
+ Verify one product flow end to end on the running product at the pinned commit, and publish one
6
+ append-only run record with three independently judged lanes, or stop at the exact unavailability
7
+ instead of manufacturing a verdict.
8
+
9
+ ## UAT runs only when a person asked for it
10
+
11
+ `requestedBy` names the person who asked; without it the operator has nothing to run for and stops at
12
+ the gate. Nothing about this operator is routine: it signs in as a real user, writes real records into
13
+ a shared runtime and leaves a permanent run record behind, so the trigger is a person and never a
14
+ schedule, a chain default or another agent's convenience. `runId` and `lease` are not questions for a
15
+ person either: the orchestrator generates the run identifier and grants the exclusive lease on the
16
+ flow directory before the branch starts, and an invocation that arrives without them is
17
+ `INVALID_INPUT` rather than a prompt.
18
+
19
+ ## The password is a name, never a value
20
+
21
+ Every UAT account shares one password, sealed at `.stacks/<env>/secrets/uat.enc` with the shared
22
+ master identity, and each flow owns its own dedicated username. The operator resolves the credential
23
+ by name through `@workspaces/device-state` at the moment of login and at no other moment; it never
24
+ copies the value into a variable it writes, a fixture, a command it records, or a sentence it
25
+ publishes. The password is never plaintext anywhere this operator writes: not in `response/`, not in
26
+ the run record under `runs/<runId>/`, not in a log. The login field is masked in every screenshot,
27
+ including the ones taken before submission and the ones taken after a failed attempt, because a
28
+ capture is published evidence and a password that reached a picture has already left custody. An
29
+ account record therefore carries a username, a role, a credential name and the sealed file's path,
30
+ and nothing that could hold a secret.
31
+
32
+ ## Freeze precedes execution
33
+
34
+ The snapshot is written before any product action and never edited afterwards. It states the commit,
35
+ the cases in their frozen order with their named assertions, the account record, the seed
36
+ fingerprint and the fixture namespace. That ordering turns three invisible failures into detectable
37
+ ones: a case that was never frozen cannot appear in a result, a run cannot be re-explained after the
38
+ fact by editing what it claimed to test, and an admission cannot be back-dated onto a commit it
39
+ never saw. Both admissions — the `frontend-surface-audit` receipt and the `quality-verification` receipt —
40
+ must name the same commit as the pinned head; either one absent, or either one taken at another
41
+ commit, is `ADMISSION_MISSING`, because a clean surface and a green gate at some other commit say
42
+ nothing about the product this run drives.
43
+
44
+ ## Three lanes, judged apart
45
+
46
+ Behaviour, UX and UI are judged on their own evidence and never borrow each other's conclusions.
47
+ Exactly three lanes are published, each with its own pass or fail and its own evidence references; a
48
+ lane with no evidence is not a fail but `EVIDENCE_UNAVAILABLE`, because charging unavailability as a
49
+ failure blames a product nobody observed. A UI defect on an application-owned node routes to
50
+ presentation, a behaviour defect routes to the backend, and a UX defect routes to a person: nobody
51
+ resolves a question of intent by re-running the flow harder.
52
+
53
+ ## The namespace owns everything this run wrote
54
+
55
+ Every record this run writes carries `is_uat=true` and the `runId` namespace, so what the run created
56
+ is separable from what the product already had. Cleanup deletes exactly that namespace and nothing
57
+ else: not another run's namespace, not a record that merely carries the UAT flag, and never a run
58
+ record. Verification itself reads and does not write, and a seed may never create the outcome under
59
+ test.
60
+
61
+ ## Runs are append-only
62
+
63
+ `runs/<runId>/` is written once, at the end, under the exclusive lease, and `latest` is moved to point
64
+ at it. A run folder that already exists is never rewritten, never trimmed and never corrected: a
65
+ second attempt is a new `runId`, and the old record stays as the evidence of what was observed then.
66
+ History that can be edited is not history.
67
+
68
+ ## Boundary
69
+
70
+ Context is read-only apart from the flow directory. The operator writes the snapshot and the run
71
+ record under `@worktrees/uat/<flow>/<case>` while it holds the exclusive lease, and writes only
72
+ `response/` of its own branch: `data/snapshot.json`, `data/captures/<case>.json`,
73
+ `data/verdicts.json`, the screenshots and the sheet under `response/artifacts/`, `response.md` and
74
+ `response.json`. It does not read or write the password as a value, does not ask a person to sign in
75
+ or paste a credential, does not repair the product to make a case pass, does not edit the frozen
76
+ snapshot after execution begins, does not rewrite or delete a run record, and does not delete
77
+ anything outside its own fixture namespace.
78
+
79
+ ## Context
80
+
81
+ | Alias | Bind | Required |
82
+ | --- | --- | --- |
83
+ | `@worktrees/uat/<flow>/<case>` | the flow directory: `flow.md`, `account.json`, `seed/`, the append-only `runs/<runId>/` history and the `latest` pointer, bound by fingerprint per file and written only under the exclusive lease | yes |
84
+ | `@worktrees/_templates` | the UAT flow template a new flow directory is created from; consumed, never modified | yes |
85
+ | `@worktrees/sessions/central-runtime` | the ready runtime owner, its generation and its exact origins; readiness is proved, not assumed | yes |
86
+ | `@workspaces/device-state` | the sealed credential roster; the shared UAT password is resolved by name here at login and read nowhere else | yes |
87
+ | `@workspaces/be` | the routed backend checkout at the pinned commit, whose behaviour the flow verifies and whose store holds the namespaced records | yes |
88
+
89
+ ## Inputs
90
+
91
+ | Kind | From | Required |
92
+ | --- | --- | --- |
93
+ | `frontend-surface-audit` | the surface audit that found the frontend clean, taken at the pinned commit | yes |
94
+ | `quality-verification` | the quality gate that passed, taken at the same pinned commit | yes |
95
+
96
+ ## Requirements
97
+
98
+ | Field | Type | Default | Ask |
99
+ | --- | --- | --- | --- |
100
+ | `requestedBy` | id | — | Who asked for this UAT run; UAT never starts without a person behind it |
101
+ | `feature` | id | — | The feature key that addresses the flow directory |
102
+ | `flow` | id | — | The one product flow this invocation verifies |
103
+ | `cases` | list of `caseId` | every case of the flow | Which frozen cases to run; the default is every case `flow.md` declares, in its order |
104
+ | `runId` | id | the orchestrator's run id | Not asked of a person: the orchestrator generates it and it namespaces every record this run writes |
105
+ | `lease` | token | the orchestrator's lease | Not asked of a person: the orchestrator grants the exclusive lease on the flow directory before the branch starts |
106
+ | `resume` | token | null | The blocked branch's token when re-entering after a stop |
107
+
108
+ ## Steps
109
+
110
+ | # | Step | Params | Reads | Writes | Stops with |
111
+ | --- | --- | --- | --- | --- | --- |
112
+ | 1 | Validate the gate, the resume, the exclusive lease and the person who asked | `requestedBy`, `lease`, `resume` | `request/request.json`, @worktrees/uat/<flow>/<case> for `latest` and the prior run record, @workspaces/be at the pinned commit, @tools/git | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
113
+ | 2 | Confirm admission: the surface audit is clean and the quality gate is green at the same pinned commit | — | input `frontend-surface-audit`, input `quality-verification` | — | `ADMISSION_MISSING` |
114
+ | 3 | Preflight the runtime: the sealed credential resolves by name, the account exists, the store answers | — | @workspaces/device-state for the credential named by `account.json`, @worktrees/sessions/central-runtime for the generation and origins, @tools/secrets, @tools/http | — | `PROVISIONING_UNAVAILABLE` |
115
+ | 4 | Freeze the snapshot from `flow.md`, `account.json` and `seed/` | `feature`, `flow`, `cases` | @worktrees/uat/<flow>/<case>, @worktrees/_templates for the flow template | @worktrees/uat/<flow>/<case> (snapshot), `response/data/snapshot.json`, @tools/sourcewrite | `CANONICAL_WRITE_DENIED` |
116
+ | 5 | Seed the frozen records into the run namespace | `runId` | `response/data/snapshot.json`, @workspaces/be | @tools/database | `FIXTURE_VIOLATION` |
117
+ | 6 | Execute the frozen cases in order on the session worktree at the pinned commit | — | `response/data/snapshot.json`, @worktrees/sessions/central-runtime for the origin and generation, @workspaces/device-state for the credential at login only, @tools/browsercontrol, @tools/websearch | — | `LEASE_INVALID`, `RUNTIME_UNAVAILABLE` |
118
+ | 7 | Capture at each named assertion with the login field masked, and stitch the sheet | — | `response/data/snapshot.json`, @worktrees/sessions/central-runtime for the most direct runtime evidence | `response/data/captures/<case>.json`, `response/artifacts/<case>.png`, `response/artifacts/sheet.png`, @tools/visualize | `EVIDENCE_UNAVAILABLE` |
119
+ | 8 | Judge the three lanes apart | — | `response/data/captures/<case>.json` | `response/data/verdicts.json` | — |
120
+ | 9 | Verify read-only, then delete the run namespace and nothing else | `runId` | @workspaces/be for the records carrying `is_uat=true` and this namespace, `response/data/verdicts.json` | @tools/database | — |
121
+ | 10 | Append `runs/<runId>/`, move `latest`, and emit | `runId` | everything above | @worktrees/uat/<flow>/<case> (runs/<runId>/ and latest), `response/response.md`, `response/response.json`, @tools/sourcewrite | — |
122
+
123
+ A blocked run publishes no run record at all, because a half-written record is the artifact a later
124
+ reader would mistake for a decision. A resume begins again at validation, reuses only observations
125
+ whose fingerprints are unchanged, and writes under the same lease; a resume that adds no admission,
126
+ lease, evidence or case change is `NO_PROGRESS`. A second attempt after a published run is a new
127
+ `runId`, never an edit of the old one.
128
+
129
+ ## Outputs
130
+
131
+ | Kind | File | Type | Required |
132
+ | --- | --- | --- | --- |
133
+ | `uat-flow-verification` | `response/response.md` | md | yes |
134
+ | `uat-snapshot` | `response/data/snapshot.json` | data | yes |
135
+ | `uat-capture` | `response/data/captures/<case>.json` | data | yes |
136
+ | `uat-verdicts` | `response/data/verdicts.json` | data | yes |
137
+ | `screenshot` | `response/artifacts/<case>.png` | artifact | yes |
138
+ | `sheet` | `response/artifacts/sheet.png` | artifact | yes |
139
+
140
+ ## Stops
141
+
142
+ | Code | Disposition |
143
+ | --- | --- |
144
+ | `INVALID_INPUT` | terminate |
145
+ | `SOURCE_DRIFT` | terminate |
146
+ | `NO_PROGRESS` | terminate |
147
+ | `ADMISSION_MISSING` | terminate |
148
+ | `PROVISIONING_UNAVAILABLE` | terminate |
149
+ | `LEASE_INVALID` | terminate |
150
+ | `RUNTIME_UNAVAILABLE` | terminate |
151
+ | `EVIDENCE_UNAVAILABLE` | terminate |
152
+ | `FIXTURE_VIOLATION` | terminate |
153
+ | `CANONICAL_WRITE_DENIED` | terminate |
154
+
155
+ ## Next
156
+
157
+ | When | Operator |
158
+ | --- | --- |
159
+ | all three lanes pass | `git.publish` |
160
+ | the UI lane fails on an application-owned node | `frontend.presentation.resolve` |
161
+ | the behaviour lane fails | `backend.source.apply` |
162
+ | the UX lane fails: a person decides what the experience should be, and the flow is verified again only after that decision | `user` |
@@ -0,0 +1,161 @@
1
+ # uat.verify
2
+
3
+ ## Việc
4
+
5
+ Kiểm chứng một luồng sản phẩm từ đầu đến cuối trên sản phẩm đang chạy tại commit đã ghim, rồi phát
6
+ một hồ sơ lượt chạy chỉ-thêm với ba làn được xét độc lập, hoặc dừng đúng chỗ không sẵn sàng thay vì
7
+ chế ra một phán quyết.
8
+
9
+ ## UAT chỉ chạy khi có người yêu cầu
10
+
11
+ `requestedBy` nêu tên người đã yêu cầu; thiếu nó thì operator không có ai để chạy cho và dừng ngay ở
12
+ cổng. Không có gì ở operator này là thường lệ: nó đăng nhập như một người dùng thật, ghi bản ghi thật
13
+ vào một runtime dùng chung và để lại một hồ sơ lượt chạy vĩnh viễn, nên thứ khởi động nó là một con
14
+ người, không bao giờ là lịch hẹn, mặc định của chain hay sự tiện tay của một agent khác. `runId` và
15
+ `lease` cũng không phải câu hỏi dành cho người: orchestrator sinh mã lượt chạy và cấp lease độc quyền
16
+ trên thư mục luồng trước khi nhánh bắt đầu, và một lần gọi đến mà thiếu chúng là `INVALID_INPUT` chứ
17
+ không phải một lời hỏi.
18
+
19
+ ## Mật khẩu là một cái tên, không bao giờ là một giá trị
20
+
21
+ Mọi tài khoản UAT dùng chung một mật khẩu, niêm phong tại `.stacks/<env>/secrets/uat.enc` bằng master
22
+ identity dùng chung, còn mỗi luồng giữ username riêng của nó. Operator giải mã thông tin đăng nhập
23
+ theo tên qua `@workspaces/device-state` đúng lúc đăng nhập và không lúc nào khác; nó không bao giờ
24
+ chép giá trị ấy vào một biến nó ghi ra, một fixture, một câu lệnh nó lưu lại hay một câu nó phát
25
+ hành. Mật khẩu không bao giờ nằm ở dạng rõ tại bất kỳ nơi nào operator này ghi: không trong
26
+ `response/`, không trong hồ sơ lượt chạy dưới `runs/<runId>/`, không trong log. Ô mật khẩu bị che
27
+ trong mọi ảnh chụp, kể cả ảnh chụp trước khi gửi và ảnh chụp sau một lần đăng nhập hỏng, vì một ảnh
28
+ chụp là bằng chứng đã phát hành và mật khẩu đã lọt vào ảnh thì đã rời khỏi vòng giữ. Vì thế hồ sơ tài
29
+ khoản chỉ mang username, vai trò, tên thông tin đăng nhập và đường dẫn file niêm phong, không mang
30
+ thứ gì có thể chứa một bí mật.
31
+
32
+ ## Đóng băng đi trước thực thi
33
+
34
+ Snapshot được ghi trước mọi hành động lên sản phẩm và không bao giờ sửa lại sau đó. Nó nêu commit, các
35
+ case theo thứ tự đã đóng băng cùng những khẳng định có tên, hồ sơ tài khoản, fingerprint của seed và
36
+ namespace fixture. Thứ tự ấy biến ba thất bại vô hình thành ba thất bại thấy được: một case chưa từng
37
+ được đóng băng không thể xuất hiện trong kết quả, một lượt chạy không thể được giải thích lại sau khi
38
+ xong bằng cách sửa điều nó nói là mình kiểm, và một admission không thể bị gán ngược vào một commit nó
39
+ chưa từng thấy. Cả hai admission — biên bản `frontend-surface-audit` và biên bản `quality-verification` —
40
+ phải nêu đúng commit đã ghim; thiếu một trong hai, hoặc một trong hai lấy ở commit khác, là
41
+ `ADMISSION_MISSING`, vì một bề mặt sạch và một cổng xanh ở commit khác chẳng nói gì về sản phẩm mà
42
+ lượt chạy này đang lái.
43
+
44
+ ## Ba làn, xét riêng
45
+
46
+ Hành vi, UX và UI được xét trên bằng chứng của riêng chúng và không bao giờ mượn kết luận của nhau.
47
+ Đúng ba làn được phát hành, mỗi làn có pass hoặc fail riêng và tham chiếu bằng chứng riêng; một làn
48
+ không có bằng chứng không phải là fail mà là `EVIDENCE_UNAVAILABLE`, vì tính sự không sẵn sàng thành
49
+ lỗi là đổ tội cho một sản phẩm chưa ai quan sát. Lỗi UI trên một node do ứng dụng sở hữu đi về
50
+ presentation, lỗi hành vi đi về backend, còn lỗi UX đi về người: không ai giải một câu hỏi về ý đồ
51
+ bằng cách chạy lại luồng mạnh tay hơn.
52
+
53
+ ## Namespace sở hữu mọi thứ lượt chạy này ghi ra
54
+
55
+ Mọi bản ghi lượt chạy này ghi đều mang `is_uat=true` và namespace `runId`, nên thứ lượt chạy tạo ra
56
+ tách bạch với thứ sản phẩm vốn đã có. Dọn dẹp xoá đúng namespace ấy và không gì khác: không xoá
57
+ namespace của lượt chạy khác, không xoá bản ghi chỉ vì nó mang cờ UAT, và không bao giờ xoá một hồ sơ
58
+ lượt chạy. Bản thân khâu kiểm chứng chỉ đọc chứ không ghi, và seed không bao giờ được tạo ra chính kết
59
+ quả cần kiểm.
60
+
61
+ ## Hồ sơ lượt chạy chỉ được thêm
62
+
63
+ `runs/<runId>/` được ghi một lần, ở cuối, dưới lease độc quyền, rồi `latest` được dời sang trỏ vào nó.
64
+ Một thư mục lượt chạy đã tồn tại thì không bao giờ bị ghi đè, bị cắt bớt hay bị "sửa cho đúng": lần
65
+ thử thứ hai là một `runId` mới, còn hồ sơ cũ ở lại làm bằng chứng cho điều đã quan sát được lúc ấy.
66
+ Lịch sử mà sửa được thì không còn là lịch sử.
67
+
68
+ ## Ranh giới ghi
69
+
70
+ Context là chỉ-đọc, trừ thư mục luồng. Operator ghi snapshot và hồ sơ lượt chạy dưới
71
+ `@worktrees/uat/<flow>/<case>` trong khi còn giữ lease độc quyền, và chỉ ghi trong `response/` của
72
+ nhánh mình: `data/snapshot.json`, `data/captures/<case>.json`, `data/verdicts.json`, các ảnh chụp và
73
+ tấm ghép dưới `response/artifacts/`, `response.md` và `response.json`. Nó không đọc cũng không ghi mật
74
+ khẩu dưới dạng giá trị, không nhờ người đăng nhập hay đưa thông tin đăng nhập, không sửa sản phẩm cho
75
+ một case đậu, không sửa snapshot đã đóng băng sau khi bắt đầu chạy, không ghi đè hay xoá một hồ sơ
76
+ lượt chạy, và không xoá bất cứ thứ gì ngoài namespace fixture của chính nó.
77
+
78
+ ## Context
79
+
80
+ | Alias | Bind | Bắt buộc |
81
+ | --- | --- | --- |
82
+ | `@worktrees/uat/<flow>/<case>` | thư mục luồng: `flow.md`, `account.json`, `seed/`, lịch sử chỉ-thêm `runs/<runId>/` và con trỏ `latest`, bind theo fingerprint từng file và chỉ ghi khi giữ lease độc quyền | có |
83
+ | `@worktrees/_templates` | khuôn luồng UAT dùng để tạo thư mục luồng mới; tiêu thụ, không sửa | có |
84
+ | `@worktrees/sessions/central-runtime` | chủ runtime đã sẵn sàng, generation và các origin chính xác của nó; sự sẵn sàng phải được chứng minh, không được giả định | có |
85
+ | `@workspaces/device-state` | sổ thông tin đăng nhập niêm phong; mật khẩu UAT dùng chung được giải theo tên ở đây lúc đăng nhập và không đọc ở đâu khác | có |
86
+ | `@workspaces/be` | checkout backend được route tại commit đã ghim, nơi luồng kiểm hành vi và nơi store giữ các bản ghi có namespace | có |
87
+
88
+ ## Đầu vào
89
+
90
+ | Kind | Từ đâu | Bắt buộc |
91
+ | --- | --- | --- |
92
+ | `frontend-surface-audit` | lượt soi bề mặt kết luận frontend sạch, lấy tại commit đã ghim | có |
93
+ | `quality-verification` | cổng chất lượng đã xanh, lấy tại đúng commit đã ghim ấy | có |
94
+
95
+ ## Yêu cầu
96
+
97
+ | Field | Kiểu | Mặc định | Hỏi |
98
+ | --- | --- | --- | --- |
99
+ | `requestedBy` | id | — | Ai yêu cầu lượt UAT này; UAT không bao giờ khởi động khi không có người đứng sau |
100
+ | `feature` | id | — | Khoá feature dùng để địa chỉ hoá thư mục luồng |
101
+ | `flow` | id | — | Luồng sản phẩm duy nhất mà lần gọi này kiểm chứng |
102
+ | `cases` | list `caseId` | every case of the flow | Chạy những case đã đóng băng nào; mặc định là mọi case `flow.md` khai, theo đúng thứ tự của nó |
103
+ | `runId` | id | the orchestrator's run id | Không hỏi người: orchestrator sinh ra nó và nó namespace mọi bản ghi lượt chạy này ghi ra |
104
+ | `lease` | token | the orchestrator's lease | Không hỏi người: orchestrator cấp lease độc quyền trên thư mục luồng trước khi nhánh bắt đầu |
105
+ | `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
106
+
107
+ ## Các bước
108
+
109
+ | # | Bước | Tham số | Đọc | Ghi | Dừng với |
110
+ | --- | --- | --- | --- | --- | --- |
111
+ | 1 | Kiểm gate, lần chạy lại, lease độc quyền và người đã yêu cầu | `requestedBy`, `lease`, `resume` | `request/request.json`, @worktrees/uat/<flow>/<case> để lấy `latest` và hồ sơ lượt chạy trước, @workspaces/be tại commit đã ghim, @tools/git | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
112
+ | 2 | Xác nhận admission: bề mặt sạch và cổng chất lượng xanh tại cùng một commit đã ghim | — | đầu vào `frontend-surface-audit`, đầu vào `quality-verification` | — | `ADMISSION_MISSING` |
113
+ | 3 | Preflight runtime: thông tin đăng nhập niêm phong giải được theo tên, tài khoản tồn tại, store trả lời | — | @workspaces/device-state để lấy thông tin đăng nhập mà `account.json` nêu tên, @worktrees/sessions/central-runtime để lấy generation và các origin, @tools/secrets, @tools/http | — | `PROVISIONING_UNAVAILABLE` |
114
+ | 4 | Đóng băng snapshot từ `flow.md`, `account.json` và `seed/` | `feature`, `flow`, `cases` | @worktrees/uat/<flow>/<case>, @worktrees/_templates để lấy khuôn luồng | @worktrees/uat/<flow>/<case> (snapshot), `response/data/snapshot.json`, @tools/sourcewrite | `CANONICAL_WRITE_DENIED` |
115
+ | 5 | Gieo các bản ghi đã đóng băng vào namespace lượt chạy | `runId` | `response/data/snapshot.json`, @workspaces/be | @tools/database | `FIXTURE_VIOLATION` |
116
+ | 6 | Chạy các case đã đóng băng theo thứ tự trên worktree phiên tại commit đã ghim | — | `response/data/snapshot.json`, @worktrees/sessions/central-runtime để lấy origin và generation, @workspaces/device-state để lấy thông tin đăng nhập chỉ lúc đăng nhập, @tools/browsercontrol, @tools/websearch | — | `LEASE_INVALID`, `RUNTIME_UNAVAILABLE` |
117
+ | 7 | Capture tại từng khẳng định có tên với ô mật khẩu đã che, rồi ghép tấm sheet | — | `response/data/snapshot.json`, @worktrees/sessions/central-runtime để lấy bằng chứng runtime trực tiếp nhất | `response/data/captures/<case>.json`, `response/artifacts/<case>.png`, `response/artifacts/sheet.png`, @tools/visualize | `EVIDENCE_UNAVAILABLE` |
118
+ | 8 | Xét ba làn tách rời nhau | — | `response/data/captures/<case>.json` | `response/data/verdicts.json` | — |
119
+ | 9 | Kiểm chỉ-đọc, rồi xoá namespace lượt chạy và không gì khác | `runId` | @workspaces/be để lấy các bản ghi mang `is_uat=true` và namespace này, `response/data/verdicts.json` | @tools/database | — |
120
+ | 10 | Thêm `runs/<runId>/`, dời `latest`, rồi phát | `runId` | mọi thứ ở trên | @worktrees/uat/<flow>/<case> (runs/<runId>/ và latest), `response/response.md`, `response/response.json`, @tools/sourcewrite | — |
121
+
122
+ Một lượt bị chặn không phát hồ sơ lượt chạy nào cả, vì một hồ sơ viết nửa vời chính là thứ người đọc
123
+ sau này sẽ nhầm thành một quyết định. Lần chạy lại bắt đầu lại từ khâu kiểm, chỉ tái dùng những quan
124
+ sát có fingerprint không đổi, và ghi dưới cùng một lease; lần chạy lại không thêm admission, lease,
125
+ bằng chứng hay case nào là `NO_PROGRESS`. Lần thử thứ hai sau một lượt đã phát hành là một `runId`
126
+ mới, không bao giờ là một lần sửa lượt cũ.
127
+
128
+ ## Đầu ra
129
+
130
+ | Kind | File | Kiểu | Bắt buộc |
131
+ | --- | --- | --- | --- |
132
+ | `uat-flow-verification` | `response/response.md` | md | có |
133
+ | `uat-snapshot` | `response/data/snapshot.json` | data | có |
134
+ | `uat-capture` | `response/data/captures/<case>.json` | data | có |
135
+ | `uat-verdicts` | `response/data/verdicts.json` | data | có |
136
+ | `screenshot` | `response/artifacts/<case>.png` | artifact | có |
137
+ | `sheet` | `response/artifacts/sheet.png` | artifact | có |
138
+
139
+ ## Dừng
140
+
141
+ | Code | Xử lý |
142
+ | --- | --- |
143
+ | `INVALID_INPUT` | terminate |
144
+ | `SOURCE_DRIFT` | terminate |
145
+ | `NO_PROGRESS` | terminate |
146
+ | `ADMISSION_MISSING` | terminate |
147
+ | `PROVISIONING_UNAVAILABLE` | terminate |
148
+ | `LEASE_INVALID` | terminate |
149
+ | `RUNTIME_UNAVAILABLE` | terminate |
150
+ | `EVIDENCE_UNAVAILABLE` | terminate |
151
+ | `FIXTURE_VIOLATION` | terminate |
152
+ | `CANONICAL_WRITE_DENIED` | terminate |
153
+
154
+ ## Kế tiếp
155
+
156
+ | Khi | Operator |
157
+ | --- | --- |
158
+ | cả ba làn đều pass | `git.publish` |
159
+ | làn UI fail trên một node do ứng dụng sở hữu | `frontend.presentation.resolve` |
160
+ | làn hành vi fail | `backend.source.apply` |
161
+ | làn UX fail: người quyết định trải nghiệm phải thế nào, và luồng chỉ được kiểm lại sau quyết định ấy | `user` |