@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,257 @@
1
+ // Proves validate.mjs on a synthetic session branch: one conforming implementation of a frozen
2
+ // contract committed once on the session branch, with its change record, one conformance file per
3
+ // facet and one proof file per proof kind, one dry run that plans the same write set and commits,
4
+ // measures and writes nothing, one branch blocked on a terminate code, and one mutation per law,
5
+ // each of which must fail with a line that names the defect.
6
+ import assert from 'node:assert/strict';
7
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import path from 'node:path';
10
+ import { validateBackendStep } from './validate.mjs';
11
+
12
+ const BASE = 'f'.repeat(40);
13
+ const COMMIT = 'a1b2'.repeat(10);
14
+ const BRANCH = 'session/s-test';
15
+ const hash = (c) => `sha256:${c.repeat(64)}`;
16
+ const CONTRACT_FP = hash('a');
17
+ const OP = 'enrol-course';
18
+ const WRITER = 'src/features/api/core/graphql/mutations/enrol-course.handler.ts';
19
+ const SPEC = 'src/features/api/core/graphql/mutations/enrol-course.handler.spec.ts';
20
+ const FACETS = ['transport', 'writer', 'transaction', 'idempotency', 'exception-identity', 'authorization'];
21
+ const PROOF_KINDS = ['unit', 'integration'];
22
+
23
+ const operation = (overrides = {}) => ({
24
+ operationId: OP, name: 'enrolCourse', transport: 'graphql-mutation', writerRef: WRITER,
25
+ storeRefs: ['enrollments'], transactionBoundary: 'single-transaction', idempotencyKind: 'request-token',
26
+ migrationRefs: [], authorityDecisionIds: ['BA-1'], facets: FACETS, proofKinds: PROOF_KINDS,
27
+ ...overrides,
28
+ });
29
+ const CHANGES = [
30
+ { path: WRITER, change: 'added', operationId: OP, beforeHash: null, afterHash: hash('1') },
31
+ { path: SPEC, change: 'added', operationId: OP, beforeHash: null, afterHash: hash('2') },
32
+ ];
33
+ const DRY_CHANGES = CHANGES.map((c) => ({ ...c, afterHash: null }));
34
+ const mutationsJson = ({ operations = [operation()], changes = CHANGES, commit = COMMIT, base = BASE, branch = BRANCH, mode = 'apply' } = {}) => ({
35
+ mode, contractFingerprint: CONTRACT_FP, base, branch, commit, operations, changes,
36
+ });
37
+ const conformanceRecord = (facet, overrides = {}) => ({ operationId: OP, facet, verdict: 'conforms', evidenceRef: `${SPEC}:40`, statement: `the ${facet} of the operation matches the contract`, contractFingerprint: CONTRACT_FP, ...overrides });
38
+ const proofRecord = (proofKind, overrides = {}) => ({ operationId: OP, proofKind, commandRef: `npm run test:${proofKind}`, exitCode: 0, result: 'passed', output: `${proofKind} suite: 12 passed`, statement: `the ${proofKind} proof ran and passed`, contractFingerprint: CONTRACT_FP, ...overrides });
39
+ const conformancePath = (operationId, facet) => `response/data/conformance/${operationId}.${facet}.json`;
40
+ const proofPath = (operationId, proofKind) => `response/data/proofs/${operationId}.${proofKind}.json`;
41
+
42
+ function responseMd({ operations = null, changes = null, findings = null, contractFingerprint = CONTRACT_FP, commit = COMMIT, base = BASE, branch = BRANCH, mode = 'apply' } = {}) {
43
+ const operationRows = (operations ?? [[OP, 'graphql-mutation', WRITER, 'single-transaction', 'request-token', 'BA-1']]).map((r) => `| \`${r[0]}\` | ${r[1]} | \`${r[2]}\` | ${r[3]} | ${r[4]} | ${r[5]} |`).join('\n');
44
+ const changeRows = (changes ?? [[WRITER, 'added', OP, '—', hash('1')], [SPEC, 'added', OP, '—', hash('2')]]).map((r) => `| \`${r[0]}\` | ${r[1]} | \`${r[2]}\` | ${r[3]} | ${r[4]} |`).join('\n');
45
+ const findingRows = (findings ?? [['PATTERN_BOUND', OP, WRITER, 'the mutation handler mirrors the published command family']]).map(([code, op, file, statement]) => `| \`${code}\` | ${op === null ? '—' : `\`${op}\``} | ${file === null ? '—' : `\`${file}\``} | ${statement} |`).join('\n');
46
+ return `# backend-source-application — enrol-course
47
+
48
+ The enrolment mutation, filled inside the frozen contract and measured on every declared facet.
49
+
50
+ ## Binding
51
+
52
+ | Field | Value |
53
+ | --- | --- |
54
+ | Outcome | one enrolment mutation behind the paid access promise |
55
+ | Feature | paid-access |
56
+ | Mode | ${mode} |
57
+ | Contract fingerprint | ${contractFingerprint} |
58
+ | Base | ${base} |
59
+ | Branch | ${branch} |
60
+ | Commit | ${commit} |
61
+
62
+ ## Operations
63
+
64
+ | Operation | Transport | Writer | Transaction | Idempotency | Decisions |
65
+ | --- | --- | --- | --- | --- | --- |
66
+ ${operationRows}
67
+
68
+ ## Changes
69
+
70
+ | Path | Change | Operation | Before | After |
71
+ | --- | --- | --- | --- | --- |
72
+ ${changeRows}
73
+
74
+ ## Findings
75
+
76
+ | Code | Operation | File | Statement |
77
+ | --- | --- | --- | --- |
78
+ ${findingRows}
79
+ `;
80
+ }
81
+
82
+ const changesMd = ({ files = null, checkout = `\`@workspaces/be\` at \`${BASE}\` → \`${COMMIT}\` on \`${BRANCH}\`` } = {}) => {
83
+ const rows = (files ?? [[WRITER, 'created'], [SPEC, 'created']]).map(([p, kind, why]) => `| \`${p}\` | ${kind} | ${why ?? 'the decision this file carries'} | BE-1 |`).join('\n');
84
+ return `# changes — backend.source.apply step-1/parallel-1
85
+
86
+ The enrolment mutation and its unit spec were written into the session branch of the routed backend
87
+ checkout and committed once.
88
+
89
+ ## Binding
90
+
91
+ | Field | Value |
92
+ | --- | --- |
93
+ | Operator | \`backend.source.apply\` |
94
+ | Step | \`step-1/parallel-1\` |
95
+ | Checkout | ${checkout} |
96
+ | Predecessor | \`step-1/parallel-2/response/response.md\` |
97
+
98
+ ## Files
99
+
100
+ | Path | Change | Why | Claims |
101
+ | --- | --- | --- | --- |
102
+ ${rows}
103
+
104
+ ## What the next step must know
105
+
106
+ - Gates to run: the backend lint, type and test gates the checkout pins for these paths.
107
+ - Surfaces to observe: the enrolment mutation of the api.
108
+ - Not changed on purpose: the legacy checkout path, which the contract does not carry.
109
+ `;
110
+ };
111
+
112
+ const requestJson = ({ extra = {} } = {}) => ({
113
+ schemaVersion: 9, operatorId: 'backend.source.apply', step: 1, parallel: 1, sessionId: 's-test',
114
+ contexts: [{ alias: '@workspaces/be', head: BASE }, { alias: '@worktrees/businesses/paid-access', head: null }, { alias: '@knowledge/patterns/be', head: null }],
115
+ requirements: {
116
+ featureId: 'paid-access', outcome: 'one enrolment mutation behind the paid access promise',
117
+ mutableFileRefs: [WRITER, SPEC], resume: null, ...extra,
118
+ },
119
+ inputs: { 'architecture-decision': 'step-1/parallel-2/response/response.md' }, resume: null,
120
+ });
121
+ function responseJson({ status = 'done', stop, fallbacks = [], fields = null, commits = [COMMIT], next = ['quality.verify'] } = {}) {
122
+ return {
123
+ schemaVersion: 9, operatorId: 'backend.source.apply', step: 1, parallel: 1, status, ...(stop ? { stop } : {}), fallbacks,
124
+ fields: fields ?? {
125
+ 'backend-source-application': 'response/response.md',
126
+ changes: 'response/changes.md',
127
+ mutations: 'response/data/mutations.json',
128
+ conformance: FACETS.map((facet) => conformancePath(OP, facet)),
129
+ proof: PROOF_KINDS.map((kind) => proofPath(OP, kind)),
130
+ },
131
+ commits, next,
132
+ };
133
+ }
134
+
135
+ function writeBranch(files) {
136
+ const session = mkdtempSync(path.join(tmpdir(), 'backend-session-'));
137
+ const branch = path.join(session, 'step-1', 'parallel-1');
138
+ for (const d of ['request', 'response/data/conformance', 'response/data/proofs', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
139
+ mkdirSync(path.join(session, 'step-1', 'parallel-2', 'response'), { recursive: true });
140
+ writeFileSync(path.join(session, 'step-1', 'parallel-2', 'response', 'response.md'), '# architecture-decision — enrol-course\n');
141
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'backend.source.apply' }, current: '1/1', status: 'running' }));
142
+ for (const [name, content] of Object.entries(files)) {
143
+ if (content === null) continue;
144
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
145
+ }
146
+ return { branch, session };
147
+ }
148
+ const records = (conformance = null, proofs = null) => ({
149
+ ...Object.fromEntries((conformance ?? FACETS.map((f) => conformanceRecord(f))).map((r) => [conformancePath(r.operationId, r.facet), r])),
150
+ ...Object.fromEntries((proofs ?? PROOF_KINDS.map((k) => proofRecord(k))).map((r) => [proofPath(r.operationId, r.proofKind), r])),
151
+ });
152
+ const baseline = () => ({
153
+ 'request/request.json': requestJson(),
154
+ 'response/response.json': responseJson(),
155
+ 'response/response.md': responseMd(),
156
+ 'response/changes.md': changesMd(),
157
+ 'response/data/mutations.json': mutationsJson(),
158
+ ...records(),
159
+ });
160
+
161
+ async function expectValid(files, label) {
162
+ const { branch, session } = writeBranch(files);
163
+ const { errors } = await validateBackendStep(branch);
164
+ rmSync(session, { recursive: true, force: true });
165
+ assert.deepEqual(errors, [], `${label} should be valid`);
166
+ }
167
+ async function expectError(files, needle, label) {
168
+ const { branch, session } = writeBranch(files);
169
+ const { errors } = await validateBackendStep(branch);
170
+ rmSync(session, { recursive: true, force: true });
171
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
172
+ }
173
+ // A dry run: the same plan, with nothing committed, nothing measured and nothing moved in the tree.
174
+ const dryFiles = () => ({
175
+ 'request/request.json': requestJson({ extra: { mode: 'dry' } }),
176
+ 'response/response.json': responseJson({
177
+ commits: [],
178
+ fields: { 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json' },
179
+ }),
180
+ 'response/response.md': responseMd({ mode: 'dry', commit: '—', changes: [[WRITER, 'added', OP, '—', '—'], [SPEC, 'added', OP, '—', '—']] }),
181
+ 'response/changes.md': changesMd({
182
+ files: [[WRITER, 'unchanged', 'the mutation handler this run would add'], [SPEC, 'unchanged', 'the unit spec this run would add']],
183
+ checkout: `\`@workspaces/be\` at \`${BASE}\` on \`${BRANCH}\`, nothing written`,
184
+ }),
185
+ 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: null, changes: DRY_CHANGES }),
186
+ });
187
+ const blockedFiles = () => ({
188
+ 'request/request.json': requestJson(),
189
+ 'response/response.json': responseJson({ status: 'blocked', stop: 'CONTRACT_WIDENED', fields: {}, commits: [], next: [] }),
190
+ });
191
+ // One mutation of the frozen contract, with the receipt and the records that must follow it.
192
+ const withOperation = (patch) => {
193
+ const op = operation(patch);
194
+ return {
195
+ ...baseline(),
196
+ 'response/data/mutations.json': mutationsJson({ operations: [op] }),
197
+ ...records(op.facets.map((f) => conformanceRecord(f)), op.proofKinds.map((k) => proofRecord(k))),
198
+ 'response/response.json': responseJson({
199
+ fields: {
200
+ 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json',
201
+ conformance: op.facets.map((f) => conformancePath(OP, f)), proof: op.proofKinds.map((k) => proofPath(OP, k)),
202
+ },
203
+ }),
204
+ 'response/response.md': responseMd({ operations: [[OP, op.transport, op.writerRef, op.transactionBoundary, op.idempotencyKind, op.authorityDecisionIds.join(', ')]] }),
205
+ };
206
+ };
207
+
208
+ await expectValid(baseline(), 'one operation filled inside the frozen contract and committed once');
209
+ await expectValid(dryFiles(), 'a dry run that plans the write set and commits, measures and writes nothing');
210
+ await expectValid(blockedFiles(), 'blocked on a contract the outcome would widen');
211
+
212
+ await expectError({ ...baseline(), 'response/response.json': { ...responseJson(), stop: 'CONTRACT_WIDENED' } }, 'only a blocked response carries a stop', 'done with a stop');
213
+ await expectError({ ...blockedFiles(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', fields: {}, commits: [], next: [] }) }, 'not a registered code', 'unknown stop code');
214
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['PROOF_UNAVAILABLE'] }) }, 'has disposition terminate under these requirements; it cannot be taken as a fallback', 'fallback on a terminate code');
215
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'PROOF_UNAVAILABLE', commits: [], next: [] }) }, 'a blocked branch cannot carry an implementation', 'blocked while reporting an implementation');
216
+ await expectError({ ...blockedFiles(), 'response/response.json': responseJson({ status: 'blocked', stop: 'CONTRACT_WIDENED', fields: {}, next: [] }) }, 'a blocked branch commits nothing', 'blocked with a commit');
217
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ commits: [] }) }, 'commits its whole write set once, found 0 commits', 'a done branch that never committed');
218
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ commits: [COMMIT, hash('9').slice(7, 47)] }) }, 'commits its whole write set once, found 2 commits', 'a done branch with two commits');
219
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ commit: 'b'.repeat(40) }) }, 'differs from response.json commits[0]', 'the mutation record names another commit');
220
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ commit: BASE }), 'response/response.json': responseJson({ commits: [BASE] }), 'response/response.md': responseMd({ commit: BASE }), 'response/changes.md': changesMd({ checkout: `\`@workspaces/be\` at \`${BASE}\` → \`${BASE}\` on \`${BRANCH}\`` }) }, 'nothing was written on the session branch', 'a commit equal to the base');
221
+ await expectError({ ...baseline(), 'response/changes.md': changesMd({ checkout: `\`@workspaces/be\` at \`${BASE}\` → uncommitted` }) }, 'so the next request can pin exactly what was written', 'a change record that pins no commit');
222
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
223
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { outcome: '' } }) }, 'required field outcome has no value', 'missing required outcome');
224
+ await expectError(withOperation({ writerRef: 'src/other/handler.ts' }), 'outside the mutable ceiling', 'a writer outside the mutable ceiling');
225
+ await expectError(withOperation({ migrationRefs: ['1700000000000-AddEnrolment.ts'], facets: [...FACETS, 'migration'] }), 'ships a migration without declaring the migration-replay proof', 'a migration with no replay proof');
226
+ await expectError(withOperation({ migrationRefs: ['1700000000000-AddEnrolment.ts'], proofKinds: [...PROOF_KINDS, 'migration-replay'] }), 'ships a migration without declaring the migration facet', 'a migration with no migration facet');
227
+ await expectError(withOperation({ transactionBoundary: 'read-only', migrationRefs: ['m.ts'], facets: [...FACETS, 'migration'], proofKinds: [...PROOF_KINDS, 'migration-replay'] }), 'is read-only but ships a migration', 'a read-only operation shipping a migration');
228
+ await expectError(withOperation({ transport: 'event-consumer', idempotencyKind: 'none' }), 'will apply twice on redelivery', 'an event consumer with no idempotency');
229
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, conformance: FACETS.slice(0, 5).map((f) => conformancePath(OP, f)) } }) }, 'the operation declares the authorization facet and no record proves it', 'a declared facet with no record');
230
+ await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('writer', { verdict: 'widened' }) }, 'reports widened writer conformance in a done branch', 'a widened facet in a done branch');
231
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, conformance: [...FACETS.map((f) => conformancePath(OP, f)), conformancePath(OP, 'store')] } }), [conformancePath(OP, 'store')]: conformanceRecord('store') }, 'proves undeclared facet store', 'a facet the contract never declared');
232
+ await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('transport') }, 'must be filed as response/data/conformance/enrol-course.transport.json', 'a conformance record filed under another name');
233
+ await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('writer', { contractFingerprint: hash('9') }) }, 'measured against another contract fingerprint', 'a facet measured against another contract');
234
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, proof: [proofPath(OP, 'unit')] } }) }, 'declares the integration proof and never ran it', 'a declared proof that never ran');
235
+ await expectError({ ...baseline(), [proofPath(OP, 'unit')]: proofRecord('unit', { result: 'failed', exitCode: 1 }) }, 'reports a failed unit proof in a done branch', 'a failed proof in a done branch');
236
+ await expectError({ ...baseline(), [proofPath(OP, 'unit')]: proofRecord('unit', { exitCode: 1 }) }, 'contradicts the passed verdict', 'a passing verdict over a non-zero exit code');
237
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [{ ...CHANGES[0], change: 'modified', beforeHash: hash('1'), afterHash: hash('1') }, CHANGES[1]] }), 'response/response.md': responseMd({ changes: [[WRITER, 'modified', OP, hash('1'), hash('1')], [SPEC, 'added', OP, '—', hash('2')]] }) }, 'records a modification whose hashes are identical', 'a modification that never happened');
238
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [{ ...CHANGES[0], beforeHash: hash('1') }, CHANGES[1]] }) }, 'is added with the wrong before hash', 'an added file carrying a before hash');
239
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], path: WRITER, afterHash: hash('2') }] }) }, 'carries more than one change record', 'one file changed twice');
240
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], operationId: 'ghost-op' }] }) }, 'names undeclared operation ghost-op', 'a change owned by no operation');
241
+ await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], path: 'src/other/thing.ts' }] }) }, 'lies outside the mutable ceiling', 'a change outside the ceiling');
242
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ operations: [[OP, 'rest', WRITER, 'single-transaction', 'request-token', 'BA-1']] }) }, 'reports transport rest, the contract froze graphql-mutation', 'a transport the contract did not freeze');
243
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ operations: [[OP, 'graphql-mutation', WRITER, 'single-transaction', 'request-token', 'BA-9']] }) }, 'cites decision BA-9, which the contract does not bind', 'an unapproved business decision');
244
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['BUSINESS_QUESTION_RAISED', OP, null, 'the voucher rule on a foreign gateway was never decided']] }) }, 'cannot raise an unresolved business question', 'shipping with an open business question');
245
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ contractFingerprint: hash('9') }) }, 'differs from the fingerprint the mutations were measured against', 'a receipt measured against another contract');
246
+ await expectError({ ...baseline(), 'response/changes.md': changesMd({ files: [[WRITER, 'created']] }) }, 'which the change record omits', 'a changed file missing from the change record');
247
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Changes', '## Change set') }, 'missing section ^## Changes$', 'response section renamed');
248
+ await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.mutations; return o; })() }, 'required output mutations is not in fields', 'missing required output');
249
+ await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.conformance; return o; })() }, 'required output conformance is not in fields', 'an applied branch that measured no facet');
250
+ await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: COMMIT, changes: DRY_CHANGES }) }, 'a dry run commits nothing, so commit must be null', 'a dry plan carrying a commit');
251
+ await expectError({ ...dryFiles(), 'response/response.json': responseJson({ commits: [COMMIT], fields: { 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json' } }) }, 'a dry run records no commit', 'a dry run that committed');
252
+ await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: null }) }, 'reports an after hash under a dry run', 'a dry plan carrying an after hash');
253
+ await expectError({ ...dryFiles(), ...records(), 'response/response.json': responseJson({ commits: [], fields: { ...responseJson().fields } }) }, 'a dry run measures nothing', 'a dry run carrying conformance and proof records');
254
+ await expectError({ ...dryFiles(), 'response/changes.md': changesMd({ files: [[WRITER, 'created'], [SPEC, 'created']], checkout: `\`@workspaces/be\` at \`${BASE}\` on \`${BRANCH}\`, nothing written` }) }, 'under a dry run, which leaves every path unchanged', 'a dry change record reporting a move');
255
+ await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ commit: null, changes: DRY_CHANGES }) }, "mode apply differs from the request's dry", 'a plan that re-decides the mode');
256
+
257
+ process.stdout.write('backend.source.apply self-test: 3 valid branches, 43 rejected mutations\n');
@@ -0,0 +1,240 @@
1
+ // backend.source.apply's own law over one branch, on top of the shared step check: the write set arrives
2
+ // as exactly one commit on the session branch, and the mutation record, response.json.commits and the
3
+ // change record name that same sha; every operation the frozen contract carries is restated once and
4
+ // applied; every change lies inside the mutable ceiling, names a declared operation, appears once, and
5
+ // carries the hash pair its kind demands; a dry run commits nothing, writes no after hash, measures no
6
+ // facet and runs no proof, and reports every planned path as unchanged in the change record; every
7
+ // declared facet of an applied run has its own conformance file, named for
8
+ // the operation and facet it measures, with evidence, and every verdict in a done branch conforms;
9
+ // every declared proof ran exactly once in its own file, with a command, an exit code that agrees with
10
+ // its result, and a pass; the change record and the receipt describe the same files; no finding names
11
+ // an undeclared operation, and a done branch raises no business question.
12
+ import { existsSync } from 'node:fs';
13
+ import { readFile } from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import process from 'node:process';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { validateStep } from '../../scripts/validate-step.mjs';
18
+ import { tableUnder } from '../../scripts/validate-response.mjs';
19
+
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
21
+ // A change record is the only durable trace that a file was mutated, so the hash pair has to agree with
22
+ // the kind. A modified record whose before and after hashes are equal describes a mutation that never
23
+ // happened.
24
+ const HASH_SHAPE = { added: { before: 'null', after: 'set' }, modified: { before: 'set', after: 'set' }, deleted: { before: 'set', after: 'null' } };
25
+ const empty = (v) => v === undefined || v === null || v === '' || v === '—';
26
+ const fields = (rows) => Object.fromEntries((rows ?? []).map(([k, v]) => [k, v]));
27
+ const listed = (value) => (value === undefined ? [] : Array.isArray(value) ? value : [value]);
28
+
29
+ export async function validateBackendStep(branchDir, root = ROOT) {
30
+ const base = await validateStep(root, branchDir);
31
+ const errors = [...base.errors];
32
+ const { response, requirements = {}, present = new Set() } = base;
33
+ if (!response || response.operatorId !== 'backend.source.apply') return { errors };
34
+ const has = (f) => existsSync(path.join(branchDir, f));
35
+ const read = (f) => readFile(path.join(branchDir, f), 'utf8');
36
+ const readJson = async (f) => { try { return JSON.parse(await read(f)); } catch { return null; } };
37
+ const mutable = new Set(Array.isArray(requirements.mutableFileRefs) ? requirements.mutableFileRefs : []);
38
+ // `mode` decides whether this branch touched the checkout at all; everything below reads differently under dry.
39
+ const mode = requirements.mode ?? 'apply';
40
+
41
+ // A blocked branch has no implementation at all, and nothing it may have written is committed.
42
+ if (response.status === 'blocked' && (present.has('backend-source-application') || present.has('mutations') || present.has('conformance') || present.has('proof'))) {
43
+ errors.push('response/response.json: a blocked branch cannot carry an implementation');
44
+ }
45
+ if (response.status === 'blocked' && (response.commits ?? []).length) {
46
+ errors.push('response/response.json: a blocked branch commits nothing');
47
+ }
48
+
49
+ const mutations = present.has('mutations') && has('response/data/mutations.json') ? await readJson('response/data/mutations.json') : null;
50
+ const declared = mutations ? mutations.operations : [];
51
+ const declaredById = new Map(declared.map((o) => [o.operationId, o]));
52
+
53
+ // The plan cannot re-decide the mode the request asked for.
54
+ if (mutations && mutations.mode !== mode) errors.push(`response/data/mutations.json: mode ${mutations.mode} differs from the request's ${mode}`);
55
+
56
+ // A dry run answers with the plan alone: nothing committed, nothing measured, nothing hashed after.
57
+ if (mode === 'dry') {
58
+ if (mutations && mutations.commit !== null) errors.push('response/data/mutations.json: a dry run commits nothing, so commit must be null');
59
+ if ((response.commits ?? []).length) errors.push('response/response.json: a dry run records no commit');
60
+ if (present.has('conformance') || present.has('proof')) errors.push('response/response.json: a dry run measures nothing, so it carries no conformance or proof record');
61
+ }
62
+
63
+ // One commit on the session branch, named the same way in three places.
64
+ if (response.status === 'done' && mode === 'apply') {
65
+ const commits = response.commits ?? [];
66
+ if (commits.length !== 1) errors.push(`response/response.json: a done branch commits its whole write set once, found ${commits.length} commits`);
67
+ if (mutations && commits.length === 1 && mutations.commit !== commits[0]) errors.push(`response/data/mutations.json: commit ${mutations.commit} differs from response.json commits[0] ${commits[0]}`);
68
+ // Required of an applied branch only: a dry run has no source to measure, and the Outputs table can only say yes or no.
69
+ for (const kind of ['conformance', 'proof']) if (!present.has(kind)) errors.push(`response/response.json: required output ${kind} is not in fields`);
70
+ }
71
+ if (mutations) {
72
+ if (mode === 'apply' && mutations.commit === mutations.base) errors.push('response/data/mutations.json: the commit equals the base, so nothing was written on the session branch');
73
+ for (const operation of declared) {
74
+ const at = `response/data/mutations.json: operation ${operation.operationId}`;
75
+ if (mutable.size && !mutable.has(operation.writerRef)) errors.push(`${at} names writer ${operation.writerRef} outside the mutable ceiling`);
76
+ const shipsMigration = (operation.migrationRefs ?? []).length > 0;
77
+ // A migration without a replay proof is a schema change nobody re-ran.
78
+ if (shipsMigration && !operation.proofKinds.includes('migration-replay')) errors.push(`${at} ships a migration without declaring the migration-replay proof`);
79
+ if (shipsMigration && !operation.facets.includes('migration')) errors.push(`${at} ships a migration without declaring the migration facet`);
80
+ if (!shipsMigration && operation.proofKinds.includes('migration-replay')) errors.push(`${at} declares a migration-replay proof but ships no migration`);
81
+ // A mutation arriving through a boundary declared not to mutate.
82
+ if (operation.transactionBoundary === 'read-only' && shipsMigration) errors.push(`${at} is read-only but ships a migration`);
83
+ // A redelivered event applies twice unless something makes the write idempotent.
84
+ if (operation.transport === 'event-consumer' && operation.idempotencyKind === 'none') errors.push(`${at} consumes events with no idempotency and will apply twice on redelivery`);
85
+ if (new Set(operation.facets).size !== operation.facets.length) errors.push(`${at} repeats a contract facet`);
86
+ if (new Set(operation.proofKinds).size !== operation.proofKinds.length) errors.push(`${at} repeats a proof kind`);
87
+ if (new Set(operation.authorityDecisionIds).size !== operation.authorityDecisionIds.length) errors.push(`${at} repeats a business decision identifier`);
88
+ }
89
+ if (declared.length && declaredById.size !== declared.length) errors.push('response/data/mutations.json: the contract repeats an operationId');
90
+
91
+ const changedPaths = new Set();
92
+ for (const change of mutations.changes) {
93
+ if (changedPaths.has(change.path)) errors.push(`response/data/mutations.json: file ${change.path} carries more than one change record`);
94
+ changedPaths.add(change.path);
95
+ if (declaredById.size && !declaredById.has(change.operationId)) errors.push(`response/data/mutations.json: change on ${change.path} names undeclared operation ${change.operationId}`);
96
+ if (mutable.size && !mutable.has(change.path)) errors.push(`response/data/mutations.json: change on ${change.path} lies outside the mutable ceiling`);
97
+ const shape = HASH_SHAPE[change.change];
98
+ if ((shape.before === 'set') !== (change.beforeHash !== null)) errors.push(`response/data/mutations.json: change on ${change.path} is ${change.change} with the wrong before hash`);
99
+ if (mode === 'dry') {
100
+ // A planned path has no new content yet, so an after hash could only have been invented.
101
+ if (change.afterHash !== null) errors.push(`response/data/mutations.json: change on ${change.path} reports an after hash under a dry run, which writes nothing`);
102
+ } else {
103
+ if ((shape.after === 'set') !== (change.afterHash !== null)) errors.push(`response/data/mutations.json: change on ${change.path} is ${change.change} with the wrong after hash`);
104
+ if (change.change === 'modified' && change.beforeHash === change.afterHash) errors.push(`response/data/mutations.json: change on ${change.path} records a modification whose hashes are identical`);
105
+ }
106
+ }
107
+ }
108
+
109
+ // One conformance file per declared facet, named for what it measures.
110
+ const conformanceKeys = new Set();
111
+ for (const file of listed(response.fields?.conformance)) {
112
+ const record = has(file) ? await readJson(file) : null;
113
+ if (!record) continue;
114
+ const expected = `response/data/conformance/${record.operationId}.${record.facet}.json`;
115
+ if (file !== expected) errors.push(`${file}: a conformance record of ${record.operationId} ${record.facet} must be filed as ${expected}`);
116
+ const key = `${record.operationId}|${record.facet}`;
117
+ if (conformanceKeys.has(key)) errors.push(`${file}: operation ${record.operationId} records ${record.facet} conformance twice`);
118
+ conformanceKeys.add(key);
119
+ const operation = declaredById.get(record.operationId);
120
+ if (declaredById.size && !operation) { errors.push(`${file}: conformance names undeclared operation ${record.operationId}`); continue; }
121
+ if (operation && !operation.facets.includes(record.facet)) errors.push(`${file}: operation ${record.operationId} proves undeclared facet ${record.facet}`);
122
+ if (mutations && record.contractFingerprint !== mutations.contractFingerprint) errors.push(`${file}: measured against another contract fingerprint than the one filled`);
123
+ // A widened or narrowed facet means the frozen contract was not filled as written.
124
+ if (response.status === 'done' && record.verdict !== 'conforms') errors.push(`${file}: operation ${record.operationId} reports ${record.verdict} ${record.facet} conformance in a done branch`);
125
+ if (empty(record.evidenceRef)) errors.push(`${file}: operation ${record.operationId} asserts ${record.facet} conformance with no evidence`);
126
+ }
127
+
128
+ const proofKeys = new Set();
129
+ for (const file of listed(response.fields?.proof)) {
130
+ const record = has(file) ? await readJson(file) : null;
131
+ if (!record) continue;
132
+ const expected = `response/data/proofs/${record.operationId}.${record.proofKind}.json`;
133
+ if (file !== expected) errors.push(`${file}: a proof of ${record.operationId} ${record.proofKind} must be filed as ${expected}`);
134
+ const key = `${record.operationId}|${record.proofKind}`;
135
+ if (proofKeys.has(key)) errors.push(`${file}: operation ${record.operationId} records the ${record.proofKind} proof twice`);
136
+ proofKeys.add(key);
137
+ const operation = declaredById.get(record.operationId);
138
+ if (declaredById.size && !operation) { errors.push(`${file}: proof names undeclared operation ${record.operationId}`); continue; }
139
+ if (operation && !operation.proofKinds.includes(record.proofKind)) errors.push(`${file}: operation ${record.operationId} runs undeclared proof ${record.proofKind}`);
140
+ if (mutations && record.contractFingerprint !== mutations.contractFingerprint) errors.push(`${file}: run against another contract fingerprint than the one filled`);
141
+ // The exit code is what was actually read; the verdict is a reading of it.
142
+ if ((record.exitCode === 0) !== (record.result === 'passed')) errors.push(`${file}: exit code ${record.exitCode} contradicts the ${record.result} verdict`);
143
+ if (response.status === 'done' && record.result !== 'passed') errors.push(`${file}: operation ${record.operationId} reports a failed ${record.proofKind} proof in a done branch`);
144
+ }
145
+
146
+ if (response.status === 'done' && mode === 'apply') {
147
+ for (const operation of declared) {
148
+ // Silence about a facet reads exactly like a pass.
149
+ for (const facet of operation.facets) if (!conformanceKeys.has(`${operation.operationId}|${facet}`)) errors.push(`response/data/conformance/${operation.operationId}.${facet}.json: the operation declares the ${facet} facet and no record proves it`);
150
+ for (const proofKind of operation.proofKinds) if (!proofKeys.has(`${operation.operationId}|${proofKind}`)) errors.push(`response/data/proofs/${operation.operationId}.${proofKind}.json: the operation declares the ${proofKind} proof and never ran it`);
151
+ }
152
+ }
153
+
154
+ let receiptFiles = null;
155
+ if (present.has('backend-source-application') && has('response/response.md')) {
156
+ const text = await read('response/response.md');
157
+ const binding = fields(tableUnder(text, '## Binding'));
158
+ if (!empty(requirements.outcome) && binding.Outcome !== requirements.outcome) errors.push('response/response.md: Outcome differs from the request');
159
+ if (!empty(requirements.featureId) && binding.Feature !== requirements.featureId) errors.push('response/response.md: Feature differs from the request');
160
+ if (binding.Mode !== undefined && binding.Mode !== mode) errors.push(`response/response.md: Mode ${binding.Mode} differs from the request's ${mode}`);
161
+ if (mutations) {
162
+ if (binding['Contract fingerprint'] !== mutations.contractFingerprint) errors.push('response/response.md: Contract fingerprint differs from the fingerprint the mutations were measured against');
163
+ if (binding.Base !== mutations.base) errors.push('response/response.md: Base differs from the base the session branch was cut from');
164
+ if (binding.Branch !== mutations.branch) errors.push('response/response.md: Branch differs from the session branch the write set lives on');
165
+ if (binding.Commit !== (mutations.commit ?? '—')) errors.push('response/response.md: Commit differs from the one commit the write set arrived as');
166
+ }
167
+
168
+ const applied = new Set();
169
+ for (const [operationId, transport, writer, transaction, idempotency, decisions] of tableUnder(text, '## Operations') ?? []) {
170
+ if (applied.has(operationId)) errors.push(`response/response.md: operation ${operationId} is declared more than once`);
171
+ applied.add(operationId);
172
+ const operation = declaredById.get(operationId);
173
+ if (declaredById.size && !operation) { errors.push(`response/response.md: operation ${operationId} is not in the frozen contract`); continue; }
174
+ if (!operation) continue;
175
+ if (transport !== operation.transport) errors.push(`response/response.md: operation ${operationId} reports transport ${transport}, the contract froze ${operation.transport}`);
176
+ if (writer !== operation.writerRef) errors.push(`response/response.md: operation ${operationId} reports writer ${writer}, the contract froze ${operation.writerRef}`);
177
+ if (transaction !== operation.transactionBoundary) errors.push(`response/response.md: operation ${operationId} reports transaction ${transaction}, the contract froze ${operation.transactionBoundary}`);
178
+ if (idempotency !== operation.idempotencyKind) errors.push(`response/response.md: operation ${operationId} reports idempotency ${idempotency}, the contract froze ${operation.idempotencyKind}`);
179
+ const cited = String(decisions).split(',').map((s) => s.trim()).filter(Boolean);
180
+ for (const decisionId of cited) if (!operation.authorityDecisionIds.includes(decisionId)) errors.push(`response/response.md: operation ${operationId} cites decision ${decisionId}, which the contract does not bind`);
181
+ for (const decisionId of operation.authorityDecisionIds) if (!cited.includes(decisionId)) errors.push(`response/response.md: operation ${operationId} does not restate approved decision ${decisionId}`);
182
+ }
183
+ if (response.status === 'done') for (const operationId of declaredById.keys()) if (!applied.has(operationId)) errors.push(`response/response.md: operation ${operationId} was declared but never applied`);
184
+
185
+ const seenFiles = new Set();
186
+ const changeById = new Map((mutations?.changes ?? []).map((c) => [c.path, c]));
187
+ for (const [file, changeKind, operationId, before, after] of tableUnder(text, '## Changes') ?? []) {
188
+ if (seenFiles.has(file)) errors.push(`response/response.md: file ${file} carries more than one change record`);
189
+ seenFiles.add(file);
190
+ const record = changeById.get(file);
191
+ if (mutations && !record) { errors.push(`response/response.md: change on ${file} is absent from the mutation record`); continue; }
192
+ if (!record) continue;
193
+ if (changeKind !== record.change) errors.push(`response/response.md: change on ${file} is ${changeKind} here and ${record.change} in the mutation record`);
194
+ if (operationId !== record.operationId) errors.push(`response/response.md: change on ${file} names operation ${operationId}, the mutation record names ${record.operationId}`);
195
+ if ((empty(before) ? null : before) !== record.beforeHash) errors.push(`response/response.md: change on ${file} disagrees with the mutation record on the before hash`);
196
+ if ((empty(after) ? null : after) !== record.afterHash) errors.push(`response/response.md: change on ${file} disagrees with the mutation record on the after hash`);
197
+ }
198
+ if (mutations) for (const p of changeById.keys()) if (!seenFiles.has(p)) errors.push(`response/response.md: the mutation record changed ${p}, which the receipt omits`);
199
+ receiptFiles = seenFiles;
200
+
201
+ for (const [code, operationId] of tableUnder(text, '## Findings') ?? []) {
202
+ if (!empty(operationId) && declaredById.size && !declaredById.has(operationId)) errors.push(`response/response.md: finding ${code} names undeclared operation ${operationId}`);
203
+ // Raising the business question and shipping anyway is the exact contradiction to catch.
204
+ if (code === 'BUSINESS_QUESTION_RAISED' && response.status === 'done') errors.push('response/response.md: a done branch cannot raise an unresolved business question');
205
+ }
206
+ }
207
+
208
+ // The change record the next step reads must describe the same files and pin the same commit.
209
+ if (present.has('changes') && has('response/changes.md')) {
210
+ const text = await read('response/changes.md');
211
+ if (mutations) {
212
+ const checkout = fields(tableUnder(text, '## Binding')).Checkout ?? '';
213
+ // A dry run wrote nothing, so the next request can only pin the base it read.
214
+ const expected = mode === 'dry'
215
+ ? `@workspaces/be at ${mutations.base} on ${mutations.branch}, nothing written`
216
+ : `@workspaces/be at ${mutations.base} → ${mutations.commit} on ${mutations.branch}`;
217
+ const seen = checkout.replaceAll('`', '').replace(/\s+/g, ' ').trim();
218
+ if (seen !== expected) errors.push(`response/changes.md: Checkout must read ${expected}, so the next request can pin exactly what was written; it reads ${seen}`);
219
+ }
220
+ if (mode === 'dry') {
221
+ // The Change column reports the working tree, and a dry run left every path in it alone.
222
+ for (const [file, kind] of tableUnder(text, '## Files') ?? []) {
223
+ if (kind !== 'unchanged') errors.push(`response/changes.md: ${file} is reported ${kind} under a dry run, which leaves every path unchanged in the working tree`);
224
+ }
225
+ }
226
+ if (receiptFiles) {
227
+ const changed = new Set((tableUnder(text, '## Files') ?? []).map(([p]) => p));
228
+ for (const file of changed) if (!receiptFiles.has(file)) errors.push(`response/changes.md: ${file} is not in the receipt's change records`);
229
+ for (const file of receiptFiles) if (!changed.has(file)) errors.push(`response/changes.md: the receipt changed ${file}, which the change record omits`);
230
+ }
231
+ }
232
+ return { errors };
233
+ }
234
+
235
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
236
+ const target = process.argv[2];
237
+ if (!target) { process.stderr.write('usage: node validate.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
238
+ const { errors } = await validateBackendStep(path.resolve(target));
239
+ if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('valid backend.source.apply branch\n');
240
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "note": "Stop codes only business.decide emits. Same entry shape as operators/errors.json, scope implicit. Shared codes (INVALID_INPUT, SOURCE_DRIFT, NO_PROGRESS, EVIDENCE_MISSING) come from operators/errors.json, and APPROVAL_REQUIRED is not defined here because more than one operator emits it: a code two operators emit belongs in operators/errors.json with both ids in scope. Every domain here is the routing domain the old package's owningDomain enum published for this operator: business, backend, frontend, architecture, workspace, caller.",
4
+ "codes": {
5
+ "CONTRADICTION_UNRESOLVED": {
6
+ "domain": "caller",
7
+ "disposition": "terminate",
8
+ "meaning": {
9
+ "en": "Two claims about the same behaviour disagree and nothing resolves them.",
10
+ "vi": "Hai claim về cùng một hành vi mâu thuẫn và không có gì giải quyết chúng."
11
+ },
12
+ "resume": {
13
+ "en": "The owner resolves the contradiction.",
14
+ "vi": "Người chủ giải quyết mâu thuẫn."
15
+ }
16
+ },
17
+ "COVERAGE_INCOMPLETE": {
18
+ "domain": "business",
19
+ "disposition": "terminate",
20
+ "meaning": {
21
+ "en": "A declared coverage dimension carries no disposition.",
22
+ "vi": "Một chiều phủ đã khai không mang disposition nào."
23
+ },
24
+ "resume": {
25
+ "en": "Add the missing disposition.",
26
+ "vi": "Bổ sung disposition còn thiếu."
27
+ }
28
+ },
29
+ "CONSUMER_UNPROVEN": {
30
+ "domain": "business",
31
+ "disposition": "terminate",
32
+ "meaning": {
33
+ "en": "A discovered enforcement consumer has no disposition or no proof.",
34
+ "vi": "Một consumer thực thi đã phát hiện không có disposition hoặc không có bằng chứng."
35
+ },
36
+ "resume": {
37
+ "en": "Dispose the consumer with positive and negative proof, then publish the promise again.",
38
+ "vi": "Xử lý consumer đó kèm positive và negative proof, rồi publish lại lời hứa."
39
+ }
40
+ },
41
+ "LIFECYCLE_TRANSITION_INVALID": {
42
+ "domain": "caller",
43
+ "disposition": "terminate",
44
+ "meaning": {
45
+ "en": "The requested target state is unreachable from the observed head.",
46
+ "vi": "Trạng thái đích được yêu cầu không tới được từ head đã quan sát."
47
+ },
48
+ "resume": {
49
+ "en": "Ask for a legal transition, or publish the intermediate state first.",
50
+ "vi": "Yêu cầu một chuyển trạng thái hợp lệ, hoặc publish trạng thái trung gian trước."
51
+ }
52
+ },
53
+ "AUTHORITY_CONFLICT": {
54
+ "domain": "workspace",
55
+ "disposition": "terminate",
56
+ "meaning": {
57
+ "en": "The head or the businesses root contradicts published authority.",
58
+ "vi": "Head hoặc gốc businesses mâu thuẫn với thẩm quyền đã publish."
59
+ },
60
+ "resume": {
61
+ "en": "Correct the authority binding.",
62
+ "vi": "Sửa binding thẩm quyền."
63
+ }
64
+ },
65
+ "RECONCILIATION_DISCREPANCY": {
66
+ "domain": "backend",
67
+ "disposition": "terminate",
68
+ "meaning": {
69
+ "en": "Delivered source differs from the frozen coverage matrix.",
70
+ "vi": "Source đã giao khác với ma trận phủ đã đóng băng."
71
+ },
72
+ "resume": {
73
+ "en": "Correct the source, or revise the matrix.",
74
+ "vi": "Sửa source, hoặc sửa lại ma trận."
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "id": "business.decide",
4
+ "domain": "business",
5
+ "job": "Decide and publish one evidence-backed business promise as durable backend-owned authority, frozen behind a complete promise-to-enforcement coverage matrix.",
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
+ }
19
+ }
20
+ }