@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,210 @@
1
+ // Proves validate.mjs on a synthetic session branch: one green verification, one red one that is a
2
+ // verdict and not a stop, one blocked on an unavailable gate, and one mutation per law, each of which
3
+ // 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 { validateQualityStep } from './validate.mjs';
9
+
10
+ const HEAD = '1'.repeat(40);
11
+ const OTHER_HEAD = '2'.repeat(40);
12
+ const BRANCH = 'session/s-test';
13
+ const OBSERVED = '2026-01-10T00:00:00.000Z';
14
+ const PLAN = [
15
+ { gate: 'format', commandRef: 'package.json#scripts.format', configRef: '.prettierrc', required: true },
16
+ { gate: 'lint', commandRef: 'package.json#scripts.lint', configRef: 'eslint.config.mjs', required: true },
17
+ { gate: 'unit-coverage', commandRef: 'package.json#scripts.test', configRef: 'jest.config.ts', required: true },
18
+ ];
19
+ const THRESHOLDS = { statements: 80, lines: 80, functions: 80, branches: 80 };
20
+ const DEBT = { debtId: 'lint-debt', gate: 'lint', approvalRef: '@worktrees/debts/be.md#lint', ownerRef: 'be-team', expiresAt: '2026-06-01T00:00:00.000Z' };
21
+
22
+ const gateResult = (gate, over = {}) => ({
23
+ gate, required: true, sourceHead: HEAD, sessionBranch: BRANCH, predecessorCommit: HEAD, observedAt: OBSERVED,
24
+ commandRef: PLAN.find((g) => g.gate === gate)?.commandRef ?? 'package.json#scripts.x',
25
+ configRef: PLAN.find((g) => g.gate === gate)?.configRef ?? 'config.json',
26
+ status: 'pass', exitCode: 0, evidenceRef: `gates/${gate}.log`, classification: null, sonarScope: null, debt: null,
27
+ statement: `${gate} measured at the frozen head`, ...over,
28
+ });
29
+ const failing = (gate, over = {}) => gateResult(gate, { status: 'fail', exitCode: 1, classification: 'in-boundary', ...over });
30
+
31
+ const coverage = (over = {}) => ({ statements: 90, lines: 90, functions: 90, branches: 81.4, thresholds: THRESHOLDS, evidenceRef: 'gates/unit-coverage.log', ...over });
32
+
33
+ function responseMd({ results = PLAN.map((g) => [g.gate, 'pass']), verdict = 'pass', findings = [['PREDECESSOR_CONSUMED', '—', 'the producer receipt was consumed unchanged']], plan = PLAN, sonarScope = 'new-code', head = HEAD, cov = coverage() } = {}) {
34
+ return `# quality-verification — ${head}
35
+
36
+ The delivery was verified at one frozen head against the routed gate plan, and the verdict rests on
37
+ the measured gate files alone.
38
+
39
+ ## Binding
40
+
41
+ | Field | Value |
42
+ | --- | --- |
43
+ | Operator | \`quality.verify\` |
44
+ | Step | \`step-1/parallel-1\` |
45
+ | Checkout | \`@workspaces/be\` |
46
+ | Head | \`${head}\` |
47
+ | Session branch | \`${BRANCH}\` |
48
+ | Predecessors | \`step-1/parallel-1/response/changes.md\` |
49
+
50
+ ## Gate plan
51
+
52
+ | Gate | Required | Command | Configuration |
53
+ | --- | --- | --- | --- |
54
+ ${plan.map((g) => `| \`${g.gate}\` | ${g.required === false ? 'no' : 'yes'} | \`${g.commandRef}\` | \`${g.configRef}\` |`).join('\n')}
55
+
56
+ ## Results
57
+
58
+ | Gate | Status | Exit code | Evidence | Classification | Statement |
59
+ | --- | --- | --- | --- | --- | --- |
60
+ ${results.map(([gate, status]) => `| \`${gate}\` | ${status} | ${status === 'pass' ? 0 : status === 'skipped-not-requested' ? '—' : 1} | \`gates/${gate}.log\` | ${status === 'fail' ? 'in-boundary' : '—'} | ${gate} measured |`).join('\n')}
61
+
62
+ ## Coverage
63
+
64
+ | Metric | Measured | Threshold | Verdict |
65
+ | --- | --- | --- | --- |
66
+ | branches | ${cov.branches} | ${cov.thresholds.branches} | ${cov.branches < cov.thresholds.branches ? 'below' : 'at-or-above'} |
67
+
68
+ ## Sonar
69
+
70
+ | Field | Value |
71
+ | --- | --- |
72
+ | Scope | ${sonarScope} |
73
+ | Finding | — |
74
+
75
+ ## Debts
76
+
77
+ | Debt | Gate | Approval | Owner | Expires | Statement |
78
+ | --- | --- | --- | --- | --- | --- |
79
+
80
+ ## Findings
81
+
82
+ | Code | Gate | Statement |
83
+ | --- | --- | --- |
84
+ ${findings.map(([code, gate, statement]) => `| \`${code}\` | ${gate} | ${statement} |`).join('\n')}
85
+
86
+ ## Verdict
87
+
88
+ | Field | Value |
89
+ | --- | --- |
90
+ | Verdict | \`${verdict}\` |
91
+ `;
92
+ }
93
+
94
+ const requestJson = ({ gates = PLAN, thresholds = THRESHOLDS, e2e = false, sonarScope = 'new-code', debts = [], head = HEAD, inputs = { changes: 'step-1/parallel-1/response/changes.md' }, extra = {} } = {}) => ({
95
+ schemaVersion: 9, operatorId: 'quality.verify', step: 1, parallel: 1, sessionId: 's-test',
96
+ contexts: [{ alias: '@workspaces/be', head }],
97
+ requirements: { gates, thresholds, explicitE2eRequest: e2e, sonarScope, declaredDebts: debts, resume: null, ...extra },
98
+ inputs, resume: null,
99
+ });
100
+
101
+ const responseJson = ({ status = 'done', stop, gates = PLAN.map((g) => `response/data/gates/${g.gate}.json`), coverageField = 'response/data/coverage.json', next = ['git.publish'] } = {}) => ({
102
+ schemaVersion: 9, operatorId: 'quality.verify', step: 1, parallel: 1, status, ...(stop ? { stop } : {}),
103
+ fallbacks: [],
104
+ fields: status === 'blocked' ? {} : { 'quality-verification': 'response/response.md', 'gate-result': gates, ...(coverageField ? { coverage: coverageField } : {}) },
105
+ commits: [], next,
106
+ });
107
+
108
+ function writeBranch(files) {
109
+ const session = mkdtempSync(path.join(tmpdir(), 'quality-session-'));
110
+ const branch = path.join(session, 'step-1', 'parallel-1');
111
+ for (const d of ['request', 'response/data/gates', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
112
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'quality.verify' }, current: '1/1', status: 'running' }));
113
+ mkdirSync(path.join(session, 'step-1', 'parallel-1', 'response'), { recursive: true });
114
+ writeFileSync(path.join(session, 'step-1', 'parallel-1', 'response', 'changes.md'), '# changes\n');
115
+ for (const [name, content] of Object.entries(files)) {
116
+ if (content === null) continue;
117
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
118
+ }
119
+ return { branch, session };
120
+ }
121
+
122
+ const baseline = (over = {}) => ({
123
+ 'request/request.json': requestJson(),
124
+ 'response/response.json': responseJson(),
125
+ 'response/response.md': responseMd(),
126
+ 'response/data/gates/format.json': gateResult('format'),
127
+ 'response/data/gates/lint.json': gateResult('lint'),
128
+ 'response/data/gates/unit-coverage.json': gateResult('unit-coverage'),
129
+ 'response/data/coverage.json': coverage(),
130
+ ...over,
131
+ });
132
+
133
+ // A red gate is a verdict, not a stop: the branch is done and the receipt says fail.
134
+ const red = () => baseline({
135
+ 'response/data/gates/lint.json': failing('lint'),
136
+ 'response/response.md': responseMd({ results: [['format', 'pass'], ['lint', 'fail'], ['unit-coverage', 'pass']], verdict: 'fail' }),
137
+ 'response/response.json': responseJson({ next: ['backend.source.apply'] }),
138
+ });
139
+
140
+ async function expectValid(files, label) {
141
+ const { branch, session } = writeBranch(files);
142
+ const { errors } = await validateQualityStep(branch);
143
+ rmSync(session, { recursive: true, force: true });
144
+ assert.deepEqual(errors, [], `${label} should be valid`);
145
+ }
146
+ async function expectError(files, needle, label) {
147
+ const { branch, session } = writeBranch(files);
148
+ const { errors } = await validateQualityStep(branch);
149
+ rmSync(session, { recursive: true, force: true });
150
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
151
+ }
152
+
153
+ await expectValid(baseline(), 'a green verification with one file per gate');
154
+ await expectValid(red(), 'a red gate returned as a verdict, not a stop');
155
+ await expectValid({
156
+ 'request/request.json': requestJson(),
157
+ 'response/response.json': responseJson({ status: 'blocked', stop: 'GATE_UNAVAILABLE', next: [] }),
158
+ 'response/response.md': null, 'response/data/coverage.json': null,
159
+ }, 'blocked because a required gate could not be executed');
160
+
161
+ await expectError(baseline({ 'response/response.json': { ...responseJson(), stop: 'DEBT_UNAPPROVED' } }), 'only a blocked response carries a stop', 'done with a stop');
162
+ await expectError(baseline({ 'response/response.json': responseJson({ status: 'blocked', stop: 'GATE_RED', next: [] }) }), 'not a registered code', 'unknown stop code');
163
+ await expectError(baseline({ 'request/request.json': requestJson({ extra: { cachePolicy: 'fresh' } }) }), 'requirements.cachePolicy is not a field', 'a field the operator no longer declares');
164
+ await expectError(baseline({ 'request/request.json': requestJson({ gates: [...PLAN, { gate: 'e2e', commandRef: 'p#e2e', configRef: 'e2e.config.ts', required: false }] }) }), 'cannot be planned without an explicit request', 'e2e planned with no person asking');
165
+ await expectError(baseline({
166
+ 'request/request.json': requestJson({ gates: [...PLAN, { gate: 'e2e', commandRef: 'p#e2e', configRef: 'e2e.config.ts', required: false }] }),
167
+ 'response/data/gates/e2e.json': gateResult('e2e', { required: false, status: 'pass' }),
168
+ 'response/response.json': responseJson({ gates: [...PLAN.map((g) => `response/data/gates/${g.gate}.json`), 'response/data/gates/e2e.json'] }),
169
+ 'response/response.md': responseMd({ plan: [...PLAN, { gate: 'e2e', commandRef: 'p#e2e', configRef: 'e2e.config.ts', required: false }], results: [...PLAN.map((g) => [g.gate, 'pass']), ['e2e', 'pass']] }),
170
+ }), 'ran without an explicit request', 'the e2e suite run unasked');
171
+ await expectError(baseline({ 'request/request.json': requestJson({ gates: PLAN, e2e: true }), 'response/data/gates/e2e.json': gateResult('e2e', { status: 'skipped-not-requested', exitCode: null, evidenceRef: null }) }), 'never planned it', 'a gate result for a gate nobody planned');
172
+ await expectError(baseline({ 'response/data/gates/lint.json': gateResult('lint', { exitCode: 1 }) }), 'passed with a non-zero exit code', 'a pass over a failing command');
173
+ await expectError(baseline({ 'response/data/gates/lint.json': gateResult('lint', { evidenceRef: null }) }), 'passed with no evidence to open', 'a pass with nothing to open');
174
+ await expectError(baseline({ 'response/data/gates/lint.json': failing('lint', { classification: null }), 'response/response.md': responseMd({ results: [['format', 'pass'], ['lint', 'fail'], ['unit-coverage', 'pass']], verdict: 'fail' }) }), 'without an in-boundary, boundary-drift, or flaky classification', 'a failure nobody classified');
175
+ await expectError(baseline({ 'response/data/gates/format.json': gateResult('format', { status: 'skipped-not-requested', exitCode: null, evidenceRef: null }) }), 'cannot be skipped as not requested', 'a gate other than e2e quietly skipped');
176
+ await expectError(baseline({ 'response/data/gates/lint.json': gateResult('lint', { sourceHead: OTHER_HEAD, predecessorCommit: OTHER_HEAD }) }), 'different heads', 'two gates standing on two heads');
177
+ await expectError(baseline({ 'request/request.json': requestJson({ head: OTHER_HEAD }), 'response/response.md': responseMd({ head: OTHER_HEAD }) }), 'but the request pinned', 'gates measured off the pinned commit');
178
+ await expectError(baseline({ 'response/data/gates/lint.json': gateResult('lint', { predecessorCommit: OTHER_HEAD }) }), 'which is not the head', 'a predecessor describing another commit');
179
+ await expectError(baseline({ 'response/data/gates/lint.json': gateResult('lint', { sonarScope: 'new-code' }) }), 'belongs to the sonar gate alone', 'a sonar scope on a lint result');
180
+ await expectError(baseline({ 'response/data/gates/unit-coverage.json': gateResult('unit-coverage', { gate: 'unit-coverage' }), 'response/data/coverage.json': coverage({ branches: 40 }), 'response/response.md': responseMd({ cov: coverage({ branches: 40 }) }) }), 'sit below their own threshold', 'a green unit gate over a red branch metric');
181
+ await expectError(baseline({ 'response/data/coverage.json': coverage({ thresholds: { ...THRESHOLDS, branches: 50 } }) }), 'but the request pinned', 'a threshold lowered under the request');
182
+ await expectError(baseline({ 'response/data/coverage.json': null, 'response/response.json': responseJson({ coverageField: null }) }), 'reports no coverage measurement', 'a passing unit gate with no coverage');
183
+ await expectError(baseline({ 'response/response.md': responseMd({ verdict: 'fail' }) }), 'every required gate passed or is debt-covered', 'a red verdict over green gates');
184
+ await expectError({ ...red(), 'response/response.md': responseMd({ results: [['format', 'pass'], ['lint', 'fail'], ['unit-coverage', 'pass']], verdict: 'pass' }) }, 'neither passed nor carry a debt', 'a green verdict over a red required gate');
185
+ await expectError(baseline({
186
+ 'request/request.json': requestJson({ debts: [DEBT] }),
187
+ 'response/data/gates/lint.json': failing('lint', { debt: { debtId: 'lint-debt', approvalRef: DEBT.approvalRef, ownerRef: DEBT.ownerRef, expiresAt: '2025-01-01T00:00:00.000Z', statement: 'carried' } }),
188
+ 'response/response.md': responseMd({ results: [['format', 'pass'], ['lint', 'fail'], ['unit-coverage', 'pass']], findings: [['DEBT_DECLARED', 'lint', 'the lint failure is owed']] }),
189
+ }), 'expired before the gate was measured', 'a debt whose approval has run out');
190
+ await expectError(baseline({
191
+ 'request/request.json': requestJson({ debts: [{ ...DEBT, gate: 'format' }] }),
192
+ 'response/data/gates/format.json': failing('format', { classification: 'boundary-drift', debt: { debtId: 'lint-debt', approvalRef: DEBT.approvalRef, ownerRef: DEBT.ownerRef, expiresAt: DEBT.expiresAt, statement: 'carried' } }),
193
+ 'response/response.md': responseMd({ results: [['format', 'fail'], ['lint', 'pass'], ['unit-coverage', 'pass']], findings: [['DEBT_DECLARED', 'format', 'owed']] }),
194
+ }), 'belongs to the boundary owner', 'a boundary-drift failure owed away');
195
+ await expectError(baseline({ 'response/data/gates/lint.json': failing('lint', { debt: { debtId: 'ghost-debt', approvalRef: 'x', ownerRef: 'y', expiresAt: DEBT.expiresAt, statement: 'z' } }), 'response/response.md': responseMd({ results: [['format', 'pass'], ['lint', 'fail'], ['unit-coverage', 'pass']], findings: [['DEBT_DECLARED', 'lint', 'owed']] }) }), 'was never declared in the request', 'a debt nobody declared');
196
+ await expectError({
197
+ ...baseline({
198
+ 'request/request.json': requestJson({ gates: [...PLAN, { gate: 'sonar', commandRef: 'p#sonar', configRef: 'sonar-project.properties', required: false }] }),
199
+ 'response/data/gates/sonar.json': gateResult('sonar', { required: false, sonarScope: 'new-code' }),
200
+ 'response/response.json': responseJson({ gates: [...PLAN.map((g) => `response/data/gates/${g.gate}.json`), 'response/data/gates/sonar.json'] }),
201
+ 'response/response.md': responseMd({ plan: [...PLAN, { gate: 'sonar', commandRef: 'p#sonar', configRef: 'sonar-project.properties', required: false }], results: [...PLAN.map((g) => [g.gate, 'pass']), ['sonar', 'pass']] }),
202
+ }),
203
+ }, 'must record SONAR_NEW_CODE_ONLY', 'a green new-code Sonar gate read as project health');
204
+ await expectError(baseline({ 'response/response.md': responseMd({ results: [['format', 'pass'], ['lint', 'pass']] }) }), 'Results has 2 rows', 'a receipt that drops a measured gate');
205
+ await expectError(baseline({ 'response/response.md': responseMd().replace('## Verdict', '## Result') }), 'missing section ^## Verdict$', 'receipt section renamed');
206
+ await expectError(baseline({ 'response/data/gates/lint.json': { ...gateResult('lint'), sourceHead: 'nope' } }), 'sourceHead', 'gate-result schema');
207
+ await expectError(baseline({ 'response/response.json': responseJson({ gates: ['response/data/gates/format.json', 'response/data/gates/lint.json'] }) }), 'gate-result does not list', 'a gate file the response never lists');
208
+ await expectError(baseline({ 'request/request.json': requestJson({ inputs: {} }) }), 'needs at least one of', 'a verification with no producer receipt');
209
+
210
+ process.stdout.write('quality.verify self-test: 3 valid branches, 25 rejected mutations\n');
@@ -0,0 +1,197 @@
1
+ // quality.verify's own law over one branch, on top of the shared step check: the gate plan the
2
+ // person declared is the plan that ran, one file per gate; every gate file stands on the same head,
3
+ // which is the head the request pinned and the commit the predecessor recorded; e2e runs only under
4
+ // an explicit request and sonar carries the scope it was measured at; a result's exit code, evidence
5
+ // and classification match its status; coverage exists exactly where the unit gate produced it and no
6
+ // metric sits below its own threshold beside a green unit result; a debt is live, in-boundary and
7
+ // recorded; and the receipt's verdict follows the required gates.
8
+ import { existsSync } from 'node:fs';
9
+ import { readFile, readdir } 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 METRICS = ['statements', 'lines', 'functions', 'branches'];
18
+ const DEFAULT_GATES = [
19
+ { gate: 'format', required: true },
20
+ { gate: 'lint', required: true },
21
+ { gate: 'typecheck', required: true },
22
+ { gate: 'build', required: true },
23
+ { gate: 'unit-coverage', required: true },
24
+ ];
25
+ const empty = (v) => v === undefined || v === null || v === '' || v === '—';
26
+ const asList = (v) => (Array.isArray(v) ? v : []);
27
+ const isTrue = (v) => v === true || v === 'true' || v === 'yes';
28
+
29
+ // A gate result is measured, never narrated: each status implies an exact shape for the exit code,
30
+ // the evidence and the classification, and a shape mismatch is how a claim with no command behind it
31
+ // gets recorded as a pass.
32
+ function checkResultShape(r, at, errors) {
33
+ if (r.status === 'pass') {
34
+ if (r.exitCode !== 0) errors.push(`${at} passed with a non-zero exit code`);
35
+ if (r.evidenceRef === null) errors.push(`${at} passed with no evidence to open`);
36
+ if (r.classification !== null) errors.push(`${at} passed but carries a failure classification`);
37
+ } else if (r.status === 'fail') {
38
+ if (r.exitCode === null || r.exitCode === 0) errors.push(`${at} failed with an exit code that reports success`);
39
+ if (r.evidenceRef === null) errors.push(`${at} failed with no evidence to open`);
40
+ if (r.classification === null || r.classification === 'external-blocker') errors.push(`${at} failed without an in-boundary, boundary-drift, or flaky classification`);
41
+ } else if (r.status === 'external-blocker') {
42
+ if (r.classification !== 'external-blocker') errors.push(`${at} was blocked externally but is classified otherwise`);
43
+ if (r.evidenceRef === null) errors.push(`${at} was blocked externally with no evidence to open`);
44
+ } else if (r.status === 'skipped-not-requested') {
45
+ // Available to e2e alone: a gate that quietly did not run reads exactly like a gate that passed.
46
+ if (r.gate !== 'e2e') errors.push(`${at} cannot be skipped as not requested`);
47
+ if (r.exitCode !== null) errors.push(`${at} was skipped but reports an exit code`);
48
+ if (r.evidenceRef !== null) errors.push(`${at} was skipped but names evidence`);
49
+ if (r.classification !== null) errors.push(`${at} was skipped but carries a classification`);
50
+ }
51
+ }
52
+
53
+ export async function validateQualityStep(branchDir, root = ROOT) {
54
+ const base = await validateStep(root, branchDir);
55
+ const errors = [...base.errors];
56
+ const { response, request, requirements = {}, present = new Set() } = base;
57
+ if (!response || response.operatorId !== 'quality.verify') return { errors };
58
+ const has = (f) => existsSync(path.join(branchDir, f));
59
+ const read = (f) => readFile(path.join(branchDir, f), 'utf8');
60
+
61
+ // The plan the person declared, before a command runs.
62
+ const plan = asList(requirements.gates).length ? requirements.gates : DEFAULT_GATES;
63
+ const plannedNames = plan.map((g) => g.gate);
64
+ const requiredNames = plan.filter((g) => g.required !== false).map((g) => g.gate);
65
+ if (new Set(plannedNames).size !== plannedNames.length) errors.push('request.json: gates plans the same gate more than once');
66
+ const e2eRequested = isTrue(requirements.explicitE2eRequest);
67
+ const sonarScope = empty(requirements.sonarScope) ? 'new-code' : String(requirements.sonarScope);
68
+ if (plannedNames.includes('e2e') && !e2eRequested) errors.push('request.json: the e2e gate cannot be planned without an explicit request');
69
+ if (!['new-code', 'overall'].includes(sonarScope)) errors.push(`request.json: sonarScope ${sonarScope} is neither new-code nor overall`);
70
+
71
+ const debts = asList(requirements.declaredDebts);
72
+ const debtIds = debts.map((d) => d.debtId);
73
+ if (new Set(debtIds).size !== debtIds.length) errors.push('request.json: declaredDebts repeats a debt identifier');
74
+ for (const d of debts) if (!plannedNames.includes(d.gate)) errors.push(`request.json: debt ${d.debtId} covers ${d.gate}, which is not a planned gate`);
75
+
76
+ // At least one producer receipt: a verification with no predecessor has no delivery to measure.
77
+ const inputs = Object.keys(request?.inputs ?? {});
78
+ if (response.status !== 'blocked' && !inputs.some((k) => ['backend-source-application', 'frontend-source-application', 'changes'].includes(k))) {
79
+ errors.push('request.json: quality.verify needs at least one of backend-source-application, frontend-source-application, changes');
80
+ }
81
+
82
+ // One file per gate, read from disk so a gate file nobody listed is still caught.
83
+ const gatesDir = path.join(branchDir, 'response', 'data', 'gates');
84
+ const files = existsSync(gatesDir) ? (await readdir(gatesDir)).filter((f) => f.endsWith('.json')).sort() : [];
85
+ const listed = new Set(asList(response.fields?.['gate-result']));
86
+ for (const f of files) if (!listed.has(`response/data/gates/${f}`)) errors.push(`response/response.json: gate-result does not list response/data/gates/${f}`);
87
+
88
+ const byGate = new Map();
89
+ for (const f of files) {
90
+ const rel = `response/data/gates/${f}`;
91
+ let r; try { r = JSON.parse(await read(rel)); } catch { errors.push(`${rel}: is not readable JSON`); continue; }
92
+ const at = `${rel}: gate ${r.gate}`;
93
+ if (`${r.gate}.json` !== f) errors.push(`${rel}: names gate ${r.gate}; one gate result lives in one file named after its gate`);
94
+ if (byGate.has(r.gate)) { errors.push(`${rel}: gate ${r.gate} reports more than one result`); continue; }
95
+ byGate.set(r.gate, r);
96
+ if (!plannedNames.includes(r.gate)) errors.push(`${rel}: gate ${r.gate} reports a result but the request never planned it`);
97
+ const planned = plan.find((g) => g.gate === r.gate);
98
+ if (planned && r.required !== (planned.required !== false)) errors.push(`${at} records required ${r.required} but the plan declares ${planned.required !== false}`);
99
+ checkResultShape(r, at, errors);
100
+ }
101
+ for (const g of plannedNames) if (!byGate.has(g) && response.status === 'done') errors.push(`response/data/gates/${g}.json: the plan names ${g} but no result was measured`);
102
+
103
+ // Every gate stands on one head, and that head is the one the request pinned and the predecessor recorded.
104
+ const pinned = (request?.contexts ?? []).find((c) => c.alias === '@workspaces/be' || c.alias === '@workspaces/fe')?.head ?? null;
105
+ const heads = new Set([...byGate.values()].map((r) => r.sourceHead));
106
+ if (heads.size > 1) errors.push(`response/data/gates: the gates measured ${heads.size} different heads (${[...heads].join(', ')}); two heads are two deliveries`);
107
+ for (const r of byGate.values()) {
108
+ const at = `response/data/gates/${r.gate}.json`;
109
+ if (pinned && r.sourceHead !== pinned) errors.push(`${at}: the gate measured ${r.sourceHead} but the request pinned ${pinned}`);
110
+ if (r.predecessorCommit !== r.sourceHead) errors.push(`${at}: the predecessor recorded commit ${r.predecessorCommit}, which is not the head ${r.sourceHead} the gate measured`);
111
+ }
112
+
113
+ const e2e = byGate.get('e2e');
114
+ if (e2e && e2e.status !== 'skipped-not-requested' && !e2eRequested) errors.push('response/data/gates/e2e.json: the e2e suite ran without an explicit request');
115
+
116
+ // Sonar carries the scope it was measured at, and only the sonar gate does.
117
+ for (const r of byGate.values()) {
118
+ const at = `response/data/gates/${r.gate}.json`;
119
+ if (r.gate === 'sonar') { if (r.sonarScope !== sonarScope) errors.push(`${at}: sonarScope ${r.sonarScope} differs from the request's ${sonarScope}`); }
120
+ else if (r.sonarScope !== null) errors.push(`${at}: a sonar scope belongs to the sonar gate alone`);
121
+ }
122
+
123
+ // Debt is live, in-boundary, and declared before it is carried.
124
+ const debtByGate = new Map();
125
+ for (const r of byGate.values()) {
126
+ if (r.debt === null) continue;
127
+ const at = `response/data/gates/${r.gate}.json`;
128
+ debtByGate.set(r.gate, r.debt);
129
+ if (!debtIds.includes(r.debt.debtId)) errors.push(`${at}: debt ${r.debt.debtId} was never declared in the request`);
130
+ if (Date.parse(r.debt.expiresAt) <= Date.parse(r.observedAt)) errors.push(`${at}: debt ${r.debt.debtId} carries an approval that expired before the gate was measured`);
131
+ if (r.status !== 'fail') errors.push(`${at}: debt ${r.debt.debtId} covers a gate that did not fail`);
132
+ else if (r.classification !== 'in-boundary') errors.push(`${at}: debt ${r.debt.debtId} owes away a ${r.classification} failure that belongs to the boundary owner`);
133
+ }
134
+ for (const d of debts) if (!debtByGate.has(d.gate) && response.status === 'done') errors.push(`response/data/gates/${d.gate}.json: debt ${d.debtId} was declared but the gate result carries no debt`);
135
+
136
+ let coverage = null;
137
+ if (present.has('coverage') && has('response/data/coverage.json')) { try { coverage = JSON.parse(await read('response/data/coverage.json')); } catch { coverage = null; } }
138
+ const unit = byGate.get('unit-coverage');
139
+ const unitMeasured = unit !== undefined && unit.status !== 'skipped-not-requested';
140
+ if (coverage === null && unit?.status === 'pass') errors.push('response/data/coverage.json: the unit-coverage gate passed but reports no coverage measurement');
141
+ if (coverage !== null && !unitMeasured) errors.push('response/data/coverage.json: coverage is reported without a measured unit-coverage gate');
142
+ let below = [];
143
+ if (coverage !== null) {
144
+ below = METRICS.filter((m) => coverage[m] < coverage.thresholds[m]);
145
+ if (below.length && unit?.status === 'pass') errors.push(`response/data/coverage.json: unit-coverage passed while ${below.join(', ')} sit below their own threshold`);
146
+ if (!empty(requirements.thresholds)) {
147
+ for (const m of METRICS) {
148
+ const want = requirements.thresholds?.[m];
149
+ if (want !== undefined && coverage.thresholds[m] !== want) errors.push(`response/data/coverage.json: threshold for ${m} is ${coverage.thresholds[m]} but the request pinned ${want}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // The verdict follows the required gates, and the receipt is the only place it is stated.
155
+ const unmet = requiredNames.filter((g) => {
156
+ const r = byGate.get(g);
157
+ if (r === undefined) return true;
158
+ if (r.status === 'pass') return false;
159
+ return !(r.status === 'fail' && debtByGate.has(g));
160
+ });
161
+ const expected = unmet.length ? 'fail' : 'pass';
162
+
163
+ if (present.has('quality-verification') && has('response/response.md')) {
164
+ const text = await read('response/response.md');
165
+ const findingKeys = new Set((tableUnder(text, '## Findings') ?? []).map(([code, gate]) => `${code}|${gate === '—' ? 'null' : gate}`));
166
+ const verdict = Object.fromEntries((tableUnder(text, '## Verdict') ?? []).map(([k, v]) => [k, v]));
167
+ if (verdict.Verdict !== expected) errors.push(`response/response.md: Verdict says ${verdict.Verdict} but ${unmet.length ? `required gates ${unmet.join(', ')} neither passed nor carry a debt` : 'every required gate passed or is debt-covered'}`);
168
+ const binding = Object.fromEntries((tableUnder(text, '## Binding') ?? []).map(([k, v]) => [k, v]));
169
+ if (pinned && binding.Head !== pinned) errors.push(`response/response.md: Binding names head ${binding.Head} but the request pinned ${pinned}`);
170
+ const sonar = Object.fromEntries((tableUnder(text, '## Sonar') ?? []).map(([k, v]) => [k, v]));
171
+ if (byGate.has('sonar') && sonar.Scope !== sonarScope) errors.push(`response/response.md: Sonar scope ${sonar.Scope} differs from the measured ${sonarScope}`);
172
+ if (byGate.get('sonar')?.status === 'pass' && sonarScope === 'new-code' && !findingKeys.has('SONAR_NEW_CODE_ONLY|sonar')) errors.push('response/response.md: a passing new-code Sonar gate must record SONAR_NEW_CODE_ONLY, because the project beneath it may be red');
173
+ if (e2e?.status === 'skipped-not-requested' && !findingKeys.has('E2E_NOT_REQUESTED|e2e')) errors.push('response/response.md: a skipped e2e gate must record E2E_NOT_REQUESTED so the absence is visible');
174
+ if (below.length && !findingKeys.has('COVERAGE_BELOW_THRESHOLD|unit-coverage')) errors.push('response/response.md: coverage below a threshold must record COVERAGE_BELOW_THRESHOLD');
175
+ for (const [gate, debt] of debtByGate) if (!findingKeys.has(`DEBT_DECLARED|${gate}`)) errors.push(`response/response.md: debt ${debt.debtId} is carried without recording DEBT_DECLARED`);
176
+ for (const key of findingKeys) { const gate = key.split('|')[1]; if (gate !== 'null' && !plannedNames.includes(gate)) errors.push(`response/response.md: a finding names unplanned gate ${gate}`); }
177
+ const mdResults = tableUnder(text, '## Results') ?? [];
178
+ if (mdResults.length !== byGate.size) errors.push(`response/response.md: Results has ${mdResults.length} rows, the gate files measure ${byGate.size}`);
179
+ for (const row of mdResults) {
180
+ const r = byGate.get(row[0]);
181
+ if (!r) { errors.push(`response/response.md: Results names gate ${row[0]}, which no gate file measures`); continue; }
182
+ if (row[1] !== r.status) errors.push(`response/response.md: gate ${row[0]} is ${row[1]} in the receipt and ${r.status} in its gate file`);
183
+ }
184
+ const mdPlan = tableUnder(text, '## Gate plan') ?? [];
185
+ if (mdPlan.length !== plannedNames.length) errors.push(`response/response.md: Gate plan has ${mdPlan.length} rows, the request plans ${plannedNames.length}`);
186
+ } else if (response.status === 'done') errors.push('response/response.md: a done branch needs the verification receipt');
187
+
188
+ if (response.status === 'done' && byGate.size === 0) errors.push('response/data/gates: a done branch needs at least one measured gate');
189
+ return { errors };
190
+ }
191
+
192
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
193
+ const target = process.argv[2];
194
+ if (!target) { process.stderr.write('usage: node validate.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
195
+ const { errors } = await validateQualityStep(path.resolve(target));
196
+ if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('valid quality.verify branch\n');
197
+ }
@@ -0,0 +1,158 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "note": "Stop codes only release.deploy emits. Same entry shape as operators/errors.json, scope implicit. Shared codes (INVALID_INPUT, NO_PROGRESS) come from operators/errors.json; APPROVAL_REQUIRED is defined by business.decide and belongs in operators/errors.json with both operators in scope. ROLLOUT_FAILED and RECOVERY_EXHAUSTED are the two ordered fallbacks: a failed rollout takes the recovery branch, and exhausted recovery takes the rollback branch; everything after that terminates.",
4
+ "codes": {
5
+ "AUTHORIZATION_MISSING": {
6
+ "domain": "approval",
7
+ "disposition": "terminate",
8
+ "meaning": {
9
+ "en": "No declared grant covers this project, environment, target, or the deploy action, or it had expired when the target was observed.",
10
+ "vi": "Không grant nào đã khai phủ project, môi trường, target hay hành động deploy này, hoặc nó đã hết hạn lúc target được quan sát."
11
+ },
12
+ "resume": {
13
+ "en": "Supply the declared authorization, still valid.",
14
+ "vi": "Cấp thẩm quyền đã khai và còn hiệu lực."
15
+ }
16
+ },
17
+ "MANIFEST_INVALID": {
18
+ "domain": "caller",
19
+ "disposition": "terminate",
20
+ "meaning": {
21
+ "en": "The validated manifest is pinned to another release, and that substitution is how an unreviewed image reaches a reviewed target.",
22
+ "vi": "Manifest đã kiểm được ghim vào một release khác, và phép thay thế đó chính là cách một image chưa duyệt tới được target đã duyệt."
23
+ },
24
+ "resume": {
25
+ "en": "Bring a manifest validated against this release.",
26
+ "vi": "Mang một manifest đã kiểm đúng với release này."
27
+ }
28
+ },
29
+ "ARTIFACT_MISSING": {
30
+ "domain": "provider",
31
+ "disposition": "terminate",
32
+ "meaning": {
33
+ "en": "The immutable digest cannot be resolved, and no replacement may be built and called the same release.",
34
+ "vi": "Digest bất biến không phân giải được, và không được build một cái thay thế rồi gọi nó là cùng release."
35
+ },
36
+ "resume": {
37
+ "en": "Publish the artifact at that digest.",
38
+ "vi": "Publish artifact ở đúng digest đó."
39
+ }
40
+ },
41
+ "CREDENTIAL_UNAVAILABLE": {
42
+ "domain": "platform",
43
+ "disposition": "terminate",
44
+ "meaning": {
45
+ "en": "A declared handle cannot be resolved through existing custody.",
46
+ "vi": "Một handle đã khai không phân giải được qua custody đang có."
47
+ },
48
+ "resume": {
49
+ "en": "Restore the custody; never an inline value.",
50
+ "vi": "Khôi phục custody; không bao giờ nhập giá trị trực tiếp."
51
+ }
52
+ },
53
+ "HOST_UNAVAILABLE": {
54
+ "domain": "provider",
55
+ "disposition": "terminate",
56
+ "meaning": {
57
+ "en": "The declared host cannot be prepared.",
58
+ "vi": "Host đã khai không chuẩn bị được."
59
+ },
60
+ "resume": {
61
+ "en": "Provide a reachable, prepared host.",
62
+ "vi": "Cấp một host tới được và đã chuẩn bị."
63
+ }
64
+ },
65
+ "MIGRATION_BLOCKED": {
66
+ "domain": "backend",
67
+ "disposition": "terminate",
68
+ "meaning": {
69
+ "en": "The declared migration cannot be applied safely.",
70
+ "vi": "Migration đã khai không thể áp một cách an toàn."
71
+ },
72
+ "resume": {
73
+ "en": "Approve a migration boundary the backend owner can apply.",
74
+ "vi": "Duyệt một ranh giới migration mà chủ backend áp được."
75
+ }
76
+ },
77
+ "DOMAIN_UNRECONCILED": {
78
+ "domain": "provider",
79
+ "disposition": "terminate",
80
+ "meaning": {
81
+ "en": "Domain or TLS state cannot be brought to the declaration.",
82
+ "vi": "Trạng thái domain hay TLS không đưa về đúng khai báo được."
83
+ },
84
+ "resume": {
85
+ "en": "Fix the provider state, or the provider authority.",
86
+ "vi": "Sửa trạng thái phía provider, hoặc thẩm quyền với provider."
87
+ }
88
+ },
89
+ "ROLLOUT_FAILED": {
90
+ "domain": "deployment",
91
+ "disposition": "fallback",
92
+ "meaning": {
93
+ "en": "The rollout could not place the release on the target.",
94
+ "vi": "Rollout không đặt được release lên target."
95
+ },
96
+ "resume": {
97
+ "en": "Correct the target or the plan and roll out again.",
98
+ "vi": "Sửa target hoặc plan rồi roll out lại."
99
+ },
100
+ "fallback": {
101
+ "en": "Take the recovery branch: apply only the approved reversible actions against the same release identity, one at a time, and record each attempt with its outcome under ## Fallbacks taken.",
102
+ "vi": "Đi nhánh phục hồi: chỉ áp những hành động đảo ngược được đã duyệt trên cùng release identity, từng cái một, và ghi mỗi lần thử cùng kết quả dưới ## Fallbacks taken."
103
+ }
104
+ },
105
+ "STEADY_STATE_UNPROVEN": {
106
+ "domain": "deployment",
107
+ "disposition": "terminate",
108
+ "meaning": {
109
+ "en": "The steady window never closed before the bounded deadline, and an assumed rollout is not a deployment.",
110
+ "vi": "Cửa sổ ổn định không khép lại trước deadline có chặn trên, và một rollout được giả định không phải một lần deploy."
111
+ },
112
+ "resume": {
113
+ "en": "Observe a fresh series after the target recovers.",
114
+ "vi": "Quan sát một chuỗi mới sau khi target hồi phục."
115
+ }
116
+ },
117
+ "CONCURRENT_DRIFT": {
118
+ "domain": "deployment",
119
+ "disposition": "terminate",
120
+ "meaning": {
121
+ "en": "A release that is neither this one nor its predecessor became active during execution.",
122
+ "vi": "Một release không phải release này và cũng không phải release trước nó đã trở nên active giữa lượt chạy."
123
+ },
124
+ "resume": {
125
+ "en": "Replan against the new observed state.",
126
+ "vi": "Lập kế hoạch lại theo trạng thái quan sát mới."
127
+ }
128
+ },
129
+ "RECOVERY_EXHAUSTED": {
130
+ "domain": "approval",
131
+ "disposition": "fallback",
132
+ "meaning": {
133
+ "en": "The approved reversible actions ran out.",
134
+ "vi": "Các hành động đảo ngược được đã duyệt đã cạn."
135
+ },
136
+ "resume": {
137
+ "en": "Grant rollback authority, or approve an unsafe action.",
138
+ "vi": "Cấp thẩm quyền rollback, hoặc duyệt một hành động không an toàn."
139
+ },
140
+ "fallback": {
141
+ "en": "Take the rollback branch: restore rollbackIdentity by its exact digest, never by tag, and record the restored release under ## Fallbacks taken.",
142
+ "vi": "Đi nhánh rollback: khôi phục rollbackIdentity theo đúng digest của nó, không bao giờ theo tag, và ghi release đã khôi phục dưới ## Fallbacks taken."
143
+ }
144
+ },
145
+ "ROLLBACK_IDENTITY_MISSING": {
146
+ "domain": "provider",
147
+ "disposition": "terminate",
148
+ "meaning": {
149
+ "en": "Rollback is required and its exact safe release no longer exists.",
150
+ "vi": "Cần rollback nhưng đúng release an toàn của nó không còn tồn tại."
151
+ },
152
+ "resume": {
153
+ "en": "Restore the safe release at its exact digest.",
154
+ "vi": "Khôi phục release an toàn ở đúng digest của nó."
155
+ }
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "id": "release.deploy",
4
+ "domain": "deployment",
5
+ "job": "Deploy one immutable release to one declared target under its declared authorization and prove the steady state it reached, taking the recovery or rollback branch inside the same pass rather than assuming the rollout succeeded.",
6
+ "package": "operator.md",
7
+ "errors": "errors.json",
8
+ "validator": "validate.mjs",
9
+ "selfTest": "self-test.mjs",
10
+ "resources": {
11
+ "profile": "luna",
12
+ "grammarBound": false,
13
+ "tools": {
14
+ "@tools/fileread": "context-aliases",
15
+ "@tools/git": "read",
16
+ "@tools/shell": "declared-commands",
17
+ "@tools/http": "probe",
18
+ "@tools/container": "operate",
19
+ "@tools/ci": "read",
20
+ "@tools/secrets": "resolve-by-name"
21
+ }
22
+ }
23
+ }