@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,133 @@
1
+ # architecture.decide
2
+
3
+ ## Job
4
+
5
+ Decide one architecture with its tech stack, system boundaries, and data ownership, and prove it
6
+ against the observed current state, the rejected alternatives, verified compatibility, and an
7
+ independent critique.
8
+
9
+ ## Observe before proposing
10
+
11
+ Nothing is proposed before the current state has been observed at the frozen head of
12
+ `@workspaces/be` and written to `response/data/current-state.json` with its own fingerprint. A
13
+ proposal written before the observation describes a system simpler than the real one, and every
14
+ later comparison inherits that simplification. An observation taken at another head is worse: it
15
+ looks rigorous while describing code that no longer exists.
16
+
17
+ ## Incumbency is not authority
18
+
19
+ The inventory says what the system runs today; that is the most useful and the most dangerous
20
+ context this operator receives. An existing framework, datastore, broker, or deployment shape enters
21
+ the decision in exactly two roles: as a measurable constraint the target must satisfy, or as observed
22
+ evidence about behaviour already proved. It never enters as a reason by itself. A component
23
+ justified because it is already there is rejected outright.
24
+
25
+ ## Prove, do not assume
26
+
27
+ An alternative is counted only when it is materially different, different in ownership or
28
+ mechanism, not in wording, and assessed on exactly the trade-off axes the person named. Every
29
+ retained component carries a verified verdict with evidence across runtime version, deployable
30
+ unit, communication failure, datastore ownership, and backup and restore; a verdict that skipped an
31
+ axis is a partial check wearing a complete label. Every boundary answers the data question: it owns
32
+ at least one store or states that it owns none; a store names one owning boundary that writes it,
33
+ and a second writer exists only with an explicit shared-write justification.
34
+
35
+ ## The critique is a nested exchange
36
+
37
+ After the selected alternative is deepened, the branch pauses: it emits `response/response.json`
38
+ with status `waiting` and `awaiting { exchange: critique, kind: independent-critique }`. The
39
+ orchestrator writes `critique/request/request.json` with only `response/data/stack-model.json` as
40
+ input, never the author's rationale, and runs a fresh agent on this operator's own profile with no
41
+ inherited turns. That agent writes only `critique/response/`. When its response is done the paused
42
+ agent resumes at the confirmation step. Other branches of the same step keep running throughout.
43
+
44
+ ## Boundary
45
+
46
+ Context is read-only. The operator writes only `response/` of its own branch: `response.md`,
47
+ `data/current-state.json`, `data/stack-model.json`, the alternatives page when more than one
48
+ alternative was asked for, and `response.json`; the critique agent writes only
49
+ `critique/response/`. It does not mutate routed source, publish business authority, start or
50
+ reconfigure runtime services, name implementation files in the handoff, or claim that an
51
+ implementation, a quality gate, or a UAT run has passed.
52
+
53
+ ## Context
54
+
55
+ | Alias | Bind | Required |
56
+ | --- | --- | --- |
57
+ | `@workspaces/be` | the routed backend checkout read at the frozen head; the inventory comes from its manifests and deployment files | yes |
58
+ | `@worktrees/businesses/<featureId>` | the published business head, the promise the architecture must keep | yes |
59
+ | `@knowledge/patterns` | reusable shapes the scope may bind; a shape, never a selection | no |
60
+
61
+ ## Inputs
62
+
63
+ | Kind | From | Required |
64
+ | --- | --- | --- |
65
+ | `architecture-decision` | a prior run of `architecture.decide` on the same or an adjacent boundary; lineage that may be contradicted, never ignored | no |
66
+
67
+ ## Requirements
68
+
69
+ | Field | Type | Default | Ask |
70
+ | --- | --- | --- | --- |
71
+ | `objective` | prompt | — | The objective the architecture must achieve, in the person's words |
72
+ | `decisionId` | id | slug of `objective` | The name the artifacts carry |
73
+ | `alternatives` | number 1–4 | 1 | How many materially different designs to generate; more than one only when a comparison was asked for |
74
+ | `tradeoffAxes` | list | cost, complexity, reversibility | The axes every alternative is scored on and the critique attacks along |
75
+ | `constraints` | list of `{id, kind, statement}` | — | kind is fixed-intent, measurable, preference, assumption or unknown; at least one fixed-intent |
76
+ | `selectionPolicy` | choice | automatic | `automatic`: the operator selects and records why; `approval-required`: the person selects |
77
+ | `approval` | id | null | The approved alternative id; required only under `approval-required`, supplied on resume after `CHOICE_REQUIRED` |
78
+ | `resume` | token | null | The blocked branch's token when re-entering after a stop |
79
+
80
+ ## Steps
81
+
82
+ | # | Step | Params | Reads | Writes | Stops with |
83
+ | --- | --- | --- | --- | --- | --- |
84
+ | 1 | Validate the gate and resume | `resume`, `approval` | `request/request.json`, input `architecture-decision` when present, @workspaces/be at the frozen head | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
85
+ | 2 | Observe the current state | — | @workspaces/be at the frozen head: manifests, configuration, deployment files, @tools/git | `response/data/current-state.json` | `CURRENT_STATE_UNOBSERVED` |
86
+ | 3 | Bind the inventory to the business promise | — | `response/data/current-state.json`, @worktrees/businesses/<featureId> at its published head | — | `BUSINESS_AUTHORITY_REQUIRED`, `EVIDENCE_MISSING` |
87
+ | 4 | Frame the decision | `objective`, `decisionId`, `constraints`, `tradeoffAxes` | `request/request.json` requirements | — | `CONSTRAINT_CONTRADICTION` |
88
+ | 5 | Generate the alternatives | `alternatives` | `response/data/current-state.json`, @knowledge/patterns, @tools/websearch | `response/artifacts/<decisionId>-alternatives.html` only when more than one alternative was asked for, @tools/visualize | `NO_VIABLE_ALTERNATIVE` |
89
+ | 6 | Select | `selectionPolicy`, `tradeoffAxes`, `approval` | `response/artifacts/<decisionId>-alternatives.html` when present | — | `CHOICE_REQUIRED` |
90
+ | 7 | Deepen the selected alternative | `constraints` | `response/data/current-state.json` | `response/data/stack-model.json` | `DATA_OWNERSHIP_UNASSIGNED`, `COMPATIBILITY_UNVERIFIED` |
91
+ | 8 | Await the critique: pause, a fresh agent attacks the selection, resume when it answers | — | `critique/response/critique.md` once the exchange is done | `response/response.json` (waiting, awaiting critique) | `CRITIQUE_UNRESOLVED` |
92
+ | 9 | Confirm or return the selection | `selectionPolicy` | `critique/response/critique.md`, `response/data/stack-model.json` | — | `CHOICE_REQUIRED`, `NO_VIABLE_ALTERNATIVE` |
93
+ | 10 | Write the handoff and emit | — | everything above | `response/response.md`, `response/response.json` | — |
94
+
95
+ Under the defaults, step 5 produces one design and no comparison page, step 6 has nothing to
96
+ choose, and the decision's quality rests on step 8. When the only alternative fails an attack, step 9
97
+ stops with `NO_VIABLE_ALTERNATIVE`, not `CHOICE_REQUIRED`. The handoff names contracts, never
98
+ implementation files, because choosing the files is the next domain's job.
99
+
100
+ ## Outputs
101
+
102
+ | Kind | File | Type | Required |
103
+ | --- | --- | --- | --- |
104
+ | `architecture-decision` | `response/response.md` | md | yes |
105
+ | `current-state` | `response/data/current-state.json` | data | yes |
106
+ | `stack-model` | `response/data/stack-model.json` | data | yes |
107
+ | `alternatives` | `response/artifacts/<decisionId>-alternatives.html` | artifact | no |
108
+ | `independent-critique` | `critique/response/critique.md` | md | yes |
109
+
110
+ ## Stops
111
+
112
+ | Code | Disposition |
113
+ | --- | --- |
114
+ | `INVALID_INPUT` | terminate |
115
+ | `SOURCE_DRIFT` | terminate |
116
+ | `NO_PROGRESS` | terminate |
117
+ | `EVIDENCE_MISSING` | terminate |
118
+ | `CURRENT_STATE_UNOBSERVED` | terminate |
119
+ | `BUSINESS_AUTHORITY_REQUIRED` | terminate |
120
+ | `CONSTRAINT_CONTRADICTION` | terminate |
121
+ | `NO_VIABLE_ALTERNATIVE` | terminate |
122
+ | `CHOICE_REQUIRED` | fallback |
123
+ | `COMPATIBILITY_UNVERIFIED` | fallback |
124
+ | `DATA_OWNERSHIP_UNASSIGNED` | terminate |
125
+ | `CRITIQUE_UNRESOLVED` | terminate |
126
+
127
+ ## Next
128
+
129
+ | When | Operator |
130
+ | --- | --- |
131
+ | the business promise must be modelled again against the decided boundaries | `business.decide` |
132
+ | the decision is confirmed and a backend contract changes | `backend.source.apply` |
133
+ | the decision is confirmed and a frontend surface changes | `frontend.direction.decide` |
@@ -0,0 +1,130 @@
1
+ # architecture.decide
2
+
3
+ ## Việc
4
+
5
+ Quyết một kiến trúc với stack, ranh giới hệ thống và quyền sở hữu dữ liệu, rồi chứng minh nó bằng
6
+ hiện trạng quan sát được, các phương án bị loại, tương thích đã kiểm và một phản biện độc lập.
7
+
8
+ ## Quan sát trước khi đề xuất
9
+
10
+ Không đề xuất gì trước khi hiện trạng được quan sát ở head đóng băng của `@workspaces/be` và ghi vào
11
+ `response/data/current-state.json` với fingerprint riêng. Một đề xuất viết trước khi quan sát mô tả
12
+ một hệ thống đơn giản hơn hệ thống thật, và mọi so sánh sau đó thừa hưởng sự đơn giản hoá ấy. Quan
13
+ sát ở một head khác còn tệ hơn: trông chặt chẽ nhưng mô tả mã không còn tồn tại.
14
+
15
+ ## Đang chạy sẵn không phải là lý do
16
+
17
+ Inventory nói hệ thống hôm nay chạy gì; đó là context hữu ích nhất và nguy hiểm nhất operator này
18
+ nhận. Một framework, datastore, broker hay hình dạng deploy đang có chỉ đi vào quyết định theo đúng
19
+ hai vai: ràng buộc đo được mà đích phải thoả, hoặc bằng chứng quan sát về hành vi đã được chứng minh.
20
+ Nó không bao giờ là lý do tự thân. Component được biện minh vì đã có sẵn bị loại thẳng.
21
+
22
+ ## Chứng minh, không giả định
23
+
24
+ Một phương án chỉ được tính khi khác nhau về bản chất, khác về quyền sở hữu hay cơ chế chứ không
25
+ phải câu chữ, và được chấm trên đúng các trục đánh đổi người dùng nêu. Mọi component giữ lại mang
26
+ một phán quyết đã kiểm có bằng chứng trên năm trục: phiên bản runtime, đơn vị deploy, lỗi giao
27
+ tiếp, quyền sở hữu datastore, sao lưu và khôi phục; phán quyết bỏ qua một trục là kiểm một phần
28
+ đội lốt kiểm đầy đủ. Mọi boundary trả lời câu hỏi dữ liệu: sở hữu ít nhất một store hoặc nói rõ
29
+ không sở hữu; một store gọi tên đúng một boundary chủ và boundary đó ghi nó, người ghi thứ hai chỉ
30
+ tồn tại khi có lý do chia sẻ ghi rõ ràng.
31
+
32
+ ## Phản biện là một cuộc trao đổi lồng
33
+
34
+ Sau khi phương án đã chọn được đào sâu, nhánh tạm ngưng: nó phát `response/response.json` với status
35
+ `waiting` và `awaiting { exchange: critique, kind: independent-critique }`. Orchestrator ghi
36
+ `critique/request/request.json` chỉ với `response/data/stack-model.json` làm đầu vào, không bao giờ
37
+ kèm lý lẽ của tác giả, rồi chạy một agent mới trên chính profile của operator này, không thừa hưởng
38
+ lượt. Agent đó chỉ ghi `critique/response/`. Khi response của nó done, agent đang ngưng chạy tiếp ở
39
+ bước xác nhận. Các nhánh khác cùng bậc vẫn chạy suốt thời gian đó.
40
+
41
+ ## Ranh giới
42
+
43
+ Context chỉ đọc. Operator chỉ ghi `response/` của nhánh mình: `response.md`,
44
+ `data/current-state.json`, `data/stack-model.json`, trang so sánh phương án khi được yêu cầu nhiều
45
+ hơn một phương án, và `response.json`; agent phản biện chỉ ghi `critique/response/`. Nó không sửa
46
+ source được route, không publish thẩm quyền nghiệp vụ, không khởi động hay cấu hình lại dịch vụ
47
+ runtime, không nêu tên file implementation trong handoff, và không tuyên bố implementation, cổng
48
+ chất lượng hay UAT đã qua.
49
+
50
+ ## Context
51
+
52
+ | Alias | Bind | Bắt buộc |
53
+ | --- | --- | --- |
54
+ | `@workspaces/be` | checkout backend được route, đọc ở head đóng băng; inventory lấy từ manifest và file deploy | có |
55
+ | `@worktrees/businesses/<featureId>` | head nghiệp vụ đã publish, lời hứa mà kiến trúc phải giữ | có |
56
+ | `@knowledge/patterns` | hình dạng tái dùng mà scope có thể ràng; là hình dạng, không bao giờ là lựa chọn | không |
57
+
58
+ ## Đầu vào
59
+
60
+ | Kind | Từ đâu | Bắt buộc |
61
+ | --- | --- | --- |
62
+ | `architecture-decision` | một lần chạy `architecture.decide` trước trên cùng hoặc kề ranh giới; dòng dõi có thể bị phản bác, không được bỏ qua | không |
63
+
64
+ ## Yêu cầu
65
+
66
+ | Field | Kiểu | Mặc định | Hỏi |
67
+ | --- | --- | --- | --- |
68
+ | `objective` | prompt | — | Mục tiêu kiến trúc phải đạt, bằng lời của người dùng |
69
+ | `decisionId` | id | slug of `objective` | Tên mà các artifact mang |
70
+ | `alternatives` | number 1–4 | 1 | Sinh bao nhiêu thiết kế khác nhau về bản chất; nhiều hơn một chỉ khi được yêu cầu so sánh |
71
+ | `tradeoffAxes` | list | cost, complexity, reversibility | Các trục mọi phương án được chấm và phản biện bám theo |
72
+ | `constraints` | list `{id, kind, statement}` | — | kind là fixed-intent, measurable, preference, assumption hoặc unknown; ít nhất một fixed-intent |
73
+ | `selectionPolicy` | choice | automatic | `automatic`: operator chọn và ghi lý do; `approval-required`: người chọn |
74
+ | `approval` | id | null | Id phương án được duyệt; chỉ bắt buộc dưới `approval-required`, nhập khi chạy lại sau `CHOICE_REQUIRED` |
75
+ | `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
76
+
77
+ ## Các bước
78
+
79
+ | # | Bước | Tham số | Đọc | Ghi | Dừng với |
80
+ | --- | --- | --- | --- | --- | --- |
81
+ | 1 | Kiểm gate và chạy lại | `resume`, `approval` | `request/request.json`, đầu vào `architecture-decision` nếu có, @workspaces/be ở head đóng băng | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
82
+ | 2 | Quan sát hiện trạng | — | @workspaces/be ở head đóng băng: manifest, cấu hình, file deploy, @tools/git | `response/data/current-state.json` | `CURRENT_STATE_UNOBSERVED` |
83
+ | 3 | Ràng inventory với lời hứa nghiệp vụ | — | `response/data/current-state.json`, @worktrees/businesses/<featureId> ở head đã publish | — | `BUSINESS_AUTHORITY_REQUIRED`, `EVIDENCE_MISSING` |
84
+ | 4 | Đóng khung quyết định | `objective`, `decisionId`, `constraints`, `tradeoffAxes` | phần requirements của `request/request.json` | — | `CONSTRAINT_CONTRADICTION` |
85
+ | 5 | Sinh các phương án | `alternatives` | `response/data/current-state.json`, @knowledge/patterns, @tools/websearch | `response/artifacts/<decisionId>-alternatives.html` chỉ khi được yêu cầu nhiều hơn một phương án, @tools/visualize | `NO_VIABLE_ALTERNATIVE` |
86
+ | 6 | Chọn | `selectionPolicy`, `tradeoffAxes`, `approval` | `response/artifacts/<decisionId>-alternatives.html` khi có | — | `CHOICE_REQUIRED` |
87
+ | 7 | Đào sâu phương án đã chọn | `constraints` | `response/data/current-state.json` | `response/data/stack-model.json` | `DATA_OWNERSHIP_UNASSIGNED`, `COMPATIBILITY_UNVERIFIED` |
88
+ | 8 | Chờ phản biện: tạm ngưng, một agent mới tấn công lựa chọn, chạy tiếp khi nó trả lời | — | `critique/response/critique.md` khi cuộc trao đổi done | `response/response.json` (waiting, awaiting critique) | `CRITIQUE_UNRESOLVED` |
89
+ | 9 | Xác nhận hoặc trả lại lựa chọn | `selectionPolicy` | `critique/response/critique.md`, `response/data/stack-model.json` | — | `CHOICE_REQUIRED`, `NO_VIABLE_ALTERNATIVE` |
90
+ | 10 | Viết handoff và phát | — | mọi thứ ở trên | `response/response.md`, `response/response.json` | — |
91
+
92
+ Với mặc định, bước 5 sinh một thiết kế và không có trang so sánh, bước 6 không có gì để chọn, và
93
+ chất lượng quyết định dựa vào bước 8. Khi phương án duy nhất chết dưới một đòn tấn công, bước 9 dừng
94
+ với `NO_VIABLE_ALTERNATIVE`, không phải `CHOICE_REQUIRED`. Handoff nêu tên contract, không bao giờ
95
+ nêu file implementation, vì chọn file là việc của domain kế tiếp.
96
+
97
+ ## Đầu ra
98
+
99
+ | Kind | File | Kiểu | Bắt buộc |
100
+ | --- | --- | --- | --- |
101
+ | `architecture-decision` | `response/response.md` | md | có |
102
+ | `current-state` | `response/data/current-state.json` | data | có |
103
+ | `stack-model` | `response/data/stack-model.json` | data | có |
104
+ | `alternatives` | `response/artifacts/<decisionId>-alternatives.html` | artifact | không |
105
+ | `independent-critique` | `critique/response/critique.md` | md | có |
106
+
107
+ ## Dừng
108
+
109
+ | Code | Xử lý |
110
+ | --- | --- |
111
+ | `INVALID_INPUT` | terminate |
112
+ | `SOURCE_DRIFT` | terminate |
113
+ | `NO_PROGRESS` | terminate |
114
+ | `EVIDENCE_MISSING` | terminate |
115
+ | `CURRENT_STATE_UNOBSERVED` | terminate |
116
+ | `BUSINESS_AUTHORITY_REQUIRED` | terminate |
117
+ | `CONSTRAINT_CONTRADICTION` | terminate |
118
+ | `NO_VIABLE_ALTERNATIVE` | terminate |
119
+ | `CHOICE_REQUIRED` | fallback |
120
+ | `COMPATIBILITY_UNVERIFIED` | fallback |
121
+ | `DATA_OWNERSHIP_UNASSIGNED` | terminate |
122
+ | `CRITIQUE_UNRESOLVED` | terminate |
123
+
124
+ ## Kế tiếp
125
+
126
+ | Khi | Operator |
127
+ | --- | --- |
128
+ | lời hứa nghiệp vụ phải được mô hình lại theo các boundary đã quyết | `business.decide` |
129
+ | quyết định đã xác nhận và một contract backend thay đổi | `backend.source.apply` |
130
+ | quyết định đã xác nhận và một bề mặt frontend thay đổi | `frontend.direction.decide` |
@@ -0,0 +1,221 @@
1
+ // Proves validate.mjs on a synthetic session branch: one conforming branch under the defaults
2
+ // (alternatives = 1, automatic) with its critique exchange, one with three alternatives under
3
+ // approval-required, one blocked on a terminate code, and one mutation per law, each of which must
4
+ // fail with a line that names the defect.
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import path from 'node:path';
9
+ import { validateArchitectureStep } from './validate.mjs';
10
+
11
+ const head = 'b'.repeat(40);
12
+ const fp = `sha256:${'a'.repeat(64)}`;
13
+ const ev = (p) => `${p}@${head}`;
14
+
15
+ function currentState() {
16
+ return {
17
+ observedHead: head, fingerprint: fp,
18
+ components: [
19
+ { componentId: 'nestjs', layer: 'framework', name: 'NestJS', version: '10.4.0', evidence: ev('package.json:12') },
20
+ { componentId: 'postgres', layer: 'persistence', name: 'PostgreSQL', version: '16.2', evidence: ev('compose.yaml:30-41') },
21
+ ],
22
+ boundaries: [{ boundaryId: 'entitlement', responsibility: 'answers who may read a course', stores: ['entitlement-store'], evidence: ev('src/entitlement/index.ts:1-40') }],
23
+ };
24
+ }
25
+ const verdicts = () => ['runtime-version', 'deployable-unit', 'communication-failure', 'datastore-ownership', 'backup-restore'].map((axis) => ({ axis, verified: true, evidence: ev('compose.yaml:30') }));
26
+ function stackModel({ alternatives = 1 } = {}) {
27
+ const alts = [{ alternativeId: 'shared-boundary', status: 'selected', scores: { cost: 4, complexity: 3, reversibility: 4 }, rejectedBecause: null }];
28
+ if (alternatives > 1) alts.push({ alternativeId: 'per-feature-guards', status: 'rejected', scores: { cost: 2, complexity: 2, reversibility: 3 }, rejectedBecause: 'three boundaries derive one answer' });
29
+ if (alternatives > 2) alts.push({ alternativeId: 'edge-cache', status: 'rejected', scores: { cost: 3, complexity: 1, reversibility: 1 }, rejectedBecause: 'stale claims after revocation' });
30
+ return {
31
+ decisionId: 'entitlement-read-path', selectedAlternativeId: 'shared-boundary', alternatives: alts,
32
+ boundaries: [
33
+ { boundaryId: 'entitlement', responsibility: 'one entitlement answer', owner: 'platform-team', interfaces: ['EntitlementQuery'], ownsData: true },
34
+ { boundaryId: 'course-api', responsibility: 'serves course content', owner: 'learning-team', interfaces: ['CourseQuery'], ownsData: false },
35
+ ],
36
+ stores: [{ storeId: 'entitlement-store', owningBoundaryId: 'entitlement', writers: ['entitlement'], readers: ['course-api'], migrators: ['entitlement'], transactionScope: 'per request', backup: 'nightly snapshot', restore: 'tested weekly', sharedWriteJustification: null }],
37
+ components: [
38
+ { componentId: 'nestjs', status: 'existing', justification: 'observed-evidence', evidence: ev('package.json:12'), compatibility: verdicts() },
39
+ { componentId: 'postgres', status: 'existing', justification: 'measured-constraint', evidence: ev('compose.yaml:30'), compatibility: verdicts() },
40
+ { componentId: 'redis-cache', status: 'removed', justification: null, evidence: null, compatibility: [] },
41
+ ],
42
+ };
43
+ }
44
+ function responseMd({ alternatives = 1, policy = 'automatic', handoffDetail = 'EntitlementQuery contract returns one answer per viewer' } = {}) {
45
+ const altRows = ['| `shared-boundary` | selected | cost 4 · complexity 3 · reversibility 4 | — |'];
46
+ if (alternatives > 1) altRows.push('| `per-feature-guards` | rejected | cost 2 · complexity 2 · reversibility 3 | three boundaries derive one answer |');
47
+ if (alternatives > 2) altRows.push('| `edge-cache` | rejected | cost 3 · complexity 1 · reversibility 1 | stale claims after revocation |');
48
+ return `# architecture-decision — entitlement-read-path
49
+
50
+ ## Decision
51
+
52
+ | Field | Value |
53
+ | --- | --- |
54
+ | Objective | one entitlement read path |
55
+ | Decision id | \`entitlement-read-path\` |
56
+ | Selected alternative | \`shared-boundary\` |
57
+ | Selection policy | \`${policy}\` |
58
+
59
+ ## Current state
60
+
61
+ | Boundary | Responsibility | Stores | Evidence |
62
+ | --- | --- | --- | --- |
63
+ | \`entitlement\` | answers who may read a course | \`entitlement-store\` | \`src/entitlement/index.ts:1-40@${head}\` |
64
+
65
+ ## Alternatives
66
+
67
+ | Alternative | Status | Assessment | Rejected because |
68
+ | --- | --- | --- | --- |
69
+ ${altRows.join('\n')}
70
+
71
+ ## Boundaries
72
+
73
+ | Boundary | Responsibility | Owner | Interfaces | Owns data |
74
+ | --- | --- | --- | --- | --- |
75
+ | \`entitlement\` | one entitlement answer | platform-team | EntitlementQuery | yes |
76
+ | \`course-api\` | serves course content | learning-team | CourseQuery | no |
77
+
78
+ ## Data ownership
79
+
80
+ | Store | Owning boundary | Writers | Readers | Migrators | Transaction scope | Backup | Restore |
81
+ | --- | --- | --- | --- | --- | --- | --- | --- |
82
+ | \`entitlement-store\` | \`entitlement\` | \`entitlement\` | \`course-api\` | \`entitlement\` | per request | nightly snapshot | tested weekly |
83
+
84
+ ## Stack delta
85
+
86
+ | Component | Status | Justification | Evidence | Compatibility |
87
+ | --- | --- | --- | --- | --- |
88
+ | \`nestjs\` | existing | observed-evidence | \`package.json:12@${head}\` | 5/5 verified |
89
+ | \`postgres\` | existing | measured-constraint | \`compose.yaml:30@${head}\` | 5/5 verified |
90
+ | \`redis-cache\` | removed | — | — | — |
91
+
92
+ ## Handoff
93
+
94
+ | Item | Kind | Detail |
95
+ | --- | --- | --- |
96
+ | one answer per viewer | invariant | every entitlement read returns the same answer within one request |
97
+ | entitlement query | contract | ${handoffDetail} |
98
+ | cache removal | migration | drop the cache after the shared boundary serves all readers |
99
+ | revert | rollback | restore the cache reader behind a flag |
100
+ | proof | proof | integration test asserting one answer across three readers |
101
+
102
+ ## Fallbacks taken
103
+
104
+ | Code | Action |
105
+ | --- | --- |
106
+ `;
107
+ }
108
+ function critiqueMd({ failing = false, selection = 'keep', inherited = 'none' } = {}) {
109
+ const paths = ['partial-failure', 'retry-idempotency', 'concurrency', 'stale-state', 'deletion', 'recovery', 'dependency-outage', 'rollback'];
110
+ return `# independent-critique — entitlement-read-path
111
+
112
+ ## Execution
113
+
114
+ | Field | Value |
115
+ | --- | --- |
116
+ | Reviewer execution | exec://critique-7f3a |
117
+ | Inherited turns | ${inherited} |
118
+ | Given | response/data/stack-model.json |
119
+
120
+ ## Attacks
121
+
122
+ | Adverse path | Attack | Resolution | Verdict |
123
+ | --- | --- | --- | --- |
124
+ ${paths.map((p, i) => `| ${p} | what if ${p} hits the shared boundary | idempotent read, single writer | ${failing && i === 3 ? 'fails' : 'holds'} |`).join('\n')}
125
+
126
+ ## Verdict
127
+
128
+ | Field | Value |
129
+ | --- | --- |
130
+ | Selection | ${selection} |
131
+ `;
132
+ }
133
+ const requestJson = ({ alternatives = 1, policy = 'automatic', approval = null, extra = {} } = {}) => ({
134
+ schemaVersion: 9, operatorId: 'architecture.decide', step: 1, parallel: 1, sessionId: 's-test',
135
+ contexts: [{ alias: '@workspaces/be', head }, { alias: '@worktrees/businesses/pro-subscription', head: null }],
136
+ requirements: { objective: 'one entitlement read path', decisionId: 'entitlement-read-path', alternatives, tradeoffAxes: ['cost', 'complexity', 'reversibility'], constraints: [{ id: 'fi-1', kind: 'fixed-intent', statement: 'one answer per viewer' }, { id: 'm-1', kind: 'measurable', statement: 'p95 read under 50ms' }], selectionPolicy: policy, approval, resume: null, ...extra },
137
+ inputs: {}, resume: null,
138
+ });
139
+ const critiqueRequest = (inputs = { 'stack-model': 'step-1/parallel-1/response/data/stack-model.json' }) => ({ schemaVersion: 9, operatorId: 'architecture.decide', step: 1, parallel: 1, sessionId: 's-test', exchange: 'critique', contexts: [], requirements: {}, inputs, resume: null });
140
+ const critiqueResponse = () => ({ schemaVersion: 9, operatorId: 'architecture.decide', step: 1, parallel: 1, exchange: 'critique', status: 'done', fallbacks: [], fields: { 'independent-critique': 'response/critique.md' }, commits: [], next: [] });
141
+ function responseJson({ status = 'done', stop, fallbacks = [], withAlternatives = false, next = ['backend.source.apply'] } = {}) {
142
+ const fields = { 'architecture-decision': 'response/response.md', 'current-state': 'response/data/current-state.json', 'stack-model': 'response/data/stack-model.json' };
143
+ if (withAlternatives) fields.alternatives = 'response/artifacts/entitlement-read-path-alternatives.html';
144
+ return { schemaVersion: 9, operatorId: 'architecture.decide', step: 1, parallel: 1, status, ...(stop ? { stop } : {}), fallbacks, fields, commits: [], next };
145
+ }
146
+
147
+ function writeBranch(files) {
148
+ const session = mkdtempSync(path.join(tmpdir(), 'arch-session-'));
149
+ const branch = path.join(session, 'step-1', 'parallel-1');
150
+ for (const d of ['request', 'response/data', 'response/artifacts', 'critique/request', 'critique/response']) mkdirSync(path.join(branch, d), { recursive: true });
151
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'architecture.decide' }, current: '1/1', status: 'running' }));
152
+ for (const [name, content] of Object.entries(files)) {
153
+ if (content === null) continue;
154
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
155
+ }
156
+ return { branch, session };
157
+ }
158
+ const baseline = () => ({
159
+ 'request/request.json': requestJson(),
160
+ 'response/response.json': responseJson(),
161
+ 'response/response.md': responseMd(),
162
+ 'response/data/current-state.json': currentState(),
163
+ 'response/data/stack-model.json': stackModel(),
164
+ 'critique/request/request.json': critiqueRequest(),
165
+ 'critique/response/response.json': critiqueResponse(),
166
+ 'critique/response/critique.md': critiqueMd(),
167
+ });
168
+
169
+ async function expectValid(files, label) {
170
+ const { branch, session } = writeBranch(files);
171
+ const { errors } = await validateArchitectureStep(branch);
172
+ rmSync(session, { recursive: true, force: true });
173
+ assert.deepEqual(errors, [], `${label} should be valid`);
174
+ }
175
+ async function expectError(files, needle, label) {
176
+ const { branch, session } = writeBranch(files);
177
+ const { errors } = await validateArchitectureStep(branch);
178
+ rmSync(session, { recursive: true, force: true });
179
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
180
+ }
181
+ const threeAlts = (policy, approval, extraResponse = {}) => ({
182
+ ...baseline(),
183
+ 'request/request.json': requestJson({ alternatives: 3, policy, approval }),
184
+ 'response/response.md': responseMd({ alternatives: 3, policy }),
185
+ 'response/data/stack-model.json': stackModel({ alternatives: 3 }),
186
+ 'response/response.json': responseJson({ withAlternatives: true, ...extraResponse }),
187
+ 'response/artifacts/entitlement-read-path-alternatives.html': '<!doctype html><title>alternatives</title>',
188
+ });
189
+
190
+ await expectValid(baseline(), 'defaults (one alternative, automatic, critique exchange done)');
191
+ await expectValid(threeAlts('approval-required', 'shared-boundary'), 'three alternatives under approval-required');
192
+ await expectValid({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'DATA_OWNERSHIP_UNASSIGNED', next: [] }), 'critique/request/request.json': null, 'critique/response/response.json': null, 'critique/response/critique.md': null }, 'blocked before the critique on a terminate code');
193
+ await expectValid({ ...baseline(), 'response/response.json': { ...responseJson({ next: [] }), status: 'waiting', awaiting: { exchange: 'critique', kind: 'independent-critique' }, fields: { 'current-state': 'response/data/current-state.json', 'stack-model': 'response/data/stack-model.json' } }, 'response/response.md': null, 'critique/request/request.json': null, 'critique/response/response.json': null, 'critique/response/critique.md': null }, 'waiting for the critique');
194
+ await expectValid({ ...threeAlts('approval-required', null, { status: 'blocked', stop: 'CHOICE_REQUIRED', next: [] }), 'critique/request/request.json': null, 'critique/response/response.json': null, 'critique/response/critique.md': null }, 'CHOICE_REQUIRED terminates under approval-required');
195
+
196
+ await expectError({ ...baseline(), 'response/response.json': { ...responseJson(), stop: 'CHOICE_REQUIRED' } }, 'only a blocked response carries a stop', 'done with stop');
197
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', next: [] }) }, 'not a registered code', 'unknown stop code');
198
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'CHOICE_REQUIRED', next: [] }) }, 'has disposition fallback under these requirements', 'terminating on a fallback code under automatic');
199
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['DATA_OWNERSHIP_UNASSIGNED'] }) }, 'has disposition terminate under these requirements; it cannot be taken as a fallback', 'fallback on a terminate code');
200
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['COMPATIBILITY_UNVERIFIED'] }) }, 'not recorded under ## Fallbacks taken', 'fallback missing from response');
201
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
202
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { objective: '' } }) }, 'required field objective has no value', 'missing required objective');
203
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ approval: 'shared-boundary' }) }, 'approval is bound under automatic policy', 'approval under automatic');
204
+ await expectError({ ...baseline(), 'critique/response/critique.md': critiqueMd().replace('| rollback |', '| rollbak |') }, 'lacks a row for rollback', 'critique missing an adverse path');
205
+ await expectError({ ...baseline(), 'critique/response/critique.md': critiqueMd({ failing: true }) }, 'attacks stale-state fail', 'done with a failing attack');
206
+ await expectError({ ...baseline(), 'critique/response/critique.md': critiqueMd({ inherited: 'author thread' }) }, 'no inherited turns', 'critique inherited turns');
207
+ await expectError({ ...baseline(), 'critique/request/request.json': critiqueRequest({ 'stack-model': 'step-1/parallel-1/response/data/stack-model.json', 'architecture-decision': 'step-1/parallel-1/response/response.md' }) }, "may not be given the author's response.md", 'critique given the rationale');
208
+ await expectError({ ...baseline(), 'critique/request/request.json': null, 'critique/response/response.json': null, 'critique/response/critique.md': null }, 'the branch is done, but it never ran', 'done without the critique exchange');
209
+ await expectError({ ...baseline(), 'response/data/stack-model.json': { ...stackModel(), stores: [{ ...stackModel().stores[0], writers: ['course-api'] }] } }, 'is not among its writers', 'owner does not write its store');
210
+ await expectError({ ...baseline(), 'response/data/stack-model.json': { ...stackModel(), stores: [{ ...stackModel().stores[0], writers: ['entitlement', 'course-api'] }] } }, 'no shared-write justification', 'unjustified second writer');
211
+ await expectError({ ...baseline(), 'response/data/stack-model.json': { ...stackModel(), components: stackModel().components.map((c) => (c.componentId === 'nestjs' ? { ...c, compatibility: c.compatibility.slice(0, 4) } : c)) } }, 'compatibility unverified on backup-restore', 'retained component missing an axis');
212
+ await expectError({ ...baseline(), 'response/data/stack-model.json': { ...stackModel(), alternatives: [{ ...stackModel().alternatives[0], status: 'rejected', rejectedBecause: 'x' }] } }, 'exactly one alternative must be selected', 'no selected alternative');
213
+ await expectError({ ...baseline(), 'response/data/stack-model.json': stackModel({ alternatives: 2 }) }, 'but the request asked for 1', 'more alternatives than asked');
214
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ handoffDetail: 'src/entitlement/query.ts returns one answer' }) }, 'names an implementation file', 'handoff names a file');
215
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('| Selected alternative | `shared-boundary` |', '| Selected alternative | `edge-cache` |') }, 'Decision names edge-cache', 'response and model disagree on the selection');
216
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Handoff', '## Hand-off') }, 'missing section ^## Handoff$', 'response section renamed');
217
+ await expectError({ ...baseline(), 'response/data/current-state.json': { ...currentState(), observedHead: 'nope' } }, 'observedHead', 'current-state schema');
218
+ await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields['stack-model']; return o; })() }, 'required output stack-model is not in fields', 'missing required output');
219
+ await expectError({ ...baseline(), 'response/response.json': { ...responseJson(), status: 'waiting', awaiting: { exchange: 'review', kind: 'independent-critique' } } }, 'awaiting exchange review is declared by no Output', 'waiting on an undeclared exchange');
220
+
221
+ process.stdout.write('architecture.decide self-test: 5 valid branches, 25 rejected mutations\n');
@@ -0,0 +1,112 @@
1
+ // architecture.decide's own law over one branch, on top of the shared step check: exactly one selected
2
+ // alternative that the response names; the number of alternatives the person asked for; approval
3
+ // binds the selection under approval-required and is absent under automatic; every store has an
4
+ // owner among its writers and shared writes are justified; retained components are verified on all
5
+ // five axes or carry the COMPATIBILITY_UNVERIFIED fallback; the critique came from the nested
6
+ // exchange, attacks the selected alternative, and a failing attack cannot end in status done; the
7
+ // handoff names contracts, never implementation files.
8
+ import { existsSync } from 'node:fs';
9
+ import { readFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import process from 'node:process';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { validateStep } from '../../scripts/validate-step.mjs';
14
+ import { tableUnder } from '../../scripts/validate-response.mjs';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+ const AXES = ['runtime-version', 'deployable-unit', 'communication-failure', 'datastore-ownership', 'backup-restore'];
18
+ const IMPLEMENTATION_FILE = /\b[\w./-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|rs|php|sql)\b/i;
19
+ const empty = (v) => v === undefined || v === null || v === '' || v === '—';
20
+
21
+ export async function validateArchitectureStep(branchDir, root = ROOT) {
22
+ const base = await validateStep(root, branchDir);
23
+ const errors = [...base.errors];
24
+ const { response, requirements = {}, present = new Set() } = base;
25
+ if (!response || response.operatorId !== 'architecture.decide') return { errors };
26
+ const has = (f) => existsSync(path.join(branchDir, f));
27
+ const read = (f) => readFile(path.join(branchDir, f), 'utf8');
28
+ const wanted = Number(requirements.alternatives ?? 1);
29
+ const policy = requirements.selectionPolicy ?? 'automatic';
30
+ const approval = requirements.approval;
31
+ if (policy === 'automatic' && !empty(approval)) errors.push('request.json: approval is bound under automatic policy; supplying both hides which one decided');
32
+
33
+ let model = null;
34
+ if (present.has('stack-model') && has('response/data/stack-model.json')) { try { model = JSON.parse(await read('response/data/stack-model.json')); } catch { model = null; } }
35
+ if (model) {
36
+ const selected = model.alternatives.filter((a) => a.status === 'selected');
37
+ if (selected.length !== 1) errors.push(`response/data/stack-model.json: exactly one alternative must be selected, found ${selected.length}`);
38
+ else if (selected[0].alternativeId !== model.selectedAlternativeId) errors.push('response/data/stack-model.json: selectedAlternativeId names an alternative that is not the selected one');
39
+ if (model.alternatives.length !== wanted) errors.push(`response/data/stack-model.json: ${model.alternatives.length} alternatives, but the request asked for ${wanted}`);
40
+ const axes = Array.isArray(requirements.tradeoffAxes) ? requirements.tradeoffAxes : String(requirements.tradeoffAxes ?? '').split(',').map((s) => s.trim()).filter(Boolean);
41
+ for (const a of model.alternatives) {
42
+ if (a.status === 'rejected' && !a.rejectedBecause) errors.push(`response/data/stack-model.json: ${a.alternativeId} is rejected without a reason`);
43
+ if (a.status === 'selected' && a.rejectedBecause) errors.push(`response/data/stack-model.json: ${a.alternativeId} is selected and carries a rejection reason`);
44
+ if (wanted > 1) for (const ax of axes) if (a.scores[ax] === undefined) errors.push(`response/data/stack-model.json: ${a.alternativeId} is not scored on ${ax}`);
45
+ }
46
+ if (wanted > 1 && !present.has('alternatives')) errors.push('response/response.json: more than one alternative was asked for but no alternatives page is in fields');
47
+ if (policy === 'approval-required' && response.status === 'done') {
48
+ if (empty(approval)) errors.push('response/response.json: approval-required with no approval cannot end done; CHOICE_REQUIRED terminates here');
49
+ else if (approval !== model.selectedAlternativeId) errors.push(`response/data/stack-model.json: selected ${model.selectedAlternativeId} but approval names ${approval}`);
50
+ }
51
+ const boundaries = new Map(model.boundaries.map((b) => [b.boundaryId, b]));
52
+ const owned = new Map();
53
+ for (const s of model.stores) {
54
+ if (!boundaries.has(s.owningBoundaryId)) errors.push(`response/data/stack-model.json: store ${s.storeId} is owned by unknown boundary ${s.owningBoundaryId}`);
55
+ if (!s.writers.includes(s.owningBoundaryId)) errors.push(`response/data/stack-model.json: store ${s.storeId} owner ${s.owningBoundaryId} is not among its writers`);
56
+ if (s.writers.length > 1 && !s.sharedWriteJustification) errors.push(`response/data/stack-model.json: store ${s.storeId} has ${s.writers.length} writers and no shared-write justification`);
57
+ if (s.writers.length === 1 && s.sharedWriteJustification) errors.push(`response/data/stack-model.json: store ${s.storeId} has one writer and a shared-write justification`);
58
+ owned.set(s.owningBoundaryId, (owned.get(s.owningBoundaryId) ?? 0) + 1);
59
+ }
60
+ for (const b of model.boundaries) {
61
+ const n = owned.get(b.boundaryId) ?? 0;
62
+ if (b.ownsData && n === 0) errors.push(`response/data/stack-model.json: boundary ${b.boundaryId} claims data and owns no store`);
63
+ if (!b.ownsData && n > 0) errors.push(`response/data/stack-model.json: boundary ${b.boundaryId} claims no data and owns ${n} store(s)`);
64
+ }
65
+ const fallbacks = new Set(response.fallbacks ?? []);
66
+ for (const c of model.components) {
67
+ if (c.status === 'removed') { if (c.compatibility.length) errors.push(`response/data/stack-model.json: removed component ${c.componentId} carries a compatibility verdict`); continue; }
68
+ if (!c.justification) errors.push(`response/data/stack-model.json: ${c.componentId} has no justification`);
69
+ const verified = new Set(c.compatibility.filter((v) => v.verified && v.evidence).map((v) => v.axis));
70
+ if (c.status === 'replaced-candidate') { if (!fallbacks.has('COMPATIBILITY_UNVERIFIED')) errors.push(`response/data/stack-model.json: ${c.componentId} is replaced-candidate but response.json lists no COMPATIBILITY_UNVERIFIED fallback`); continue; }
71
+ const missing = AXES.filter((ax) => !verified.has(ax));
72
+ if (missing.length) errors.push(`response/data/stack-model.json: ${c.componentId} is ${c.status} with compatibility unverified on ${missing.join(', ')}; mark it replaced-candidate under the COMPATIBILITY_UNVERIFIED fallback or terminate`);
73
+ }
74
+ }
75
+
76
+ if (present.has('architecture-decision') && has('response/response.md')) {
77
+ const text = await read('response/response.md');
78
+ const decision = Object.fromEntries((tableUnder(text, '## Decision') ?? []).map(([k, v]) => [k, v]));
79
+ if (model && decision['Selected alternative'] !== model.selectedAlternativeId) errors.push(`response/response.md: Decision names ${decision['Selected alternative']} but stack-model selected ${model.selectedAlternativeId}`);
80
+ if (decision['Selection policy'] !== policy) errors.push(`response/response.md: Selection policy ${decision['Selection policy']} differs from the request's ${policy}`);
81
+ if (!empty(requirements.decisionId) && decision['Decision id'] !== requirements.decisionId) errors.push('response/response.md: Decision id differs from the request');
82
+ const alts = tableUnder(text, '## Alternatives') ?? [];
83
+ if (model && alts.length !== model.alternatives.length) errors.push(`response/response.md: Alternatives has ${alts.length} rows, stack-model has ${model.alternatives.length}`);
84
+ for (const [item, kind, detail] of tableUnder(text, '## Handoff') ?? []) if (kind === 'contract' && IMPLEMENTATION_FILE.test(detail)) errors.push(`response/response.md: handoff contract "${item}" names an implementation file`);
85
+ for (const [component, status] of tableUnder(text, '## Stack delta') ?? []) if (status === 'replaced-candidate' && !(response.fallbacks ?? []).includes('COMPATIBILITY_UNVERIFIED')) errors.push(`response/response.md: ${component} is replaced-candidate without the COMPATIBILITY_UNVERIFIED fallback`);
86
+ }
87
+
88
+ if (present.has('independent-critique') && has('critique/response/critique.md')) {
89
+ const text = await read('critique/response/critique.md');
90
+ const attacks = tableUnder(text, '## Attacks') ?? [];
91
+ const failing = attacks.filter((r) => r[3] === 'fails').map((r) => r[0]);
92
+ const verdict = Object.fromEntries((tableUnder(text, '## Verdict') ?? []).map(([k, v]) => [k, v]));
93
+ if (response.status === 'done' && failing.length) errors.push(`critique/response/critique.md: attacks ${failing.join(', ')} fail; a done branch needs every attack to hold (CRITIQUE_UNRESOLVED or NO_VIABLE_ALTERNATIVE otherwise)`);
94
+ if (response.status === 'done' && verdict.Selection === 'return') errors.push('critique/response/critique.md: the critique returns the selection, so the branch cannot be done');
95
+ const exec = Object.fromEntries((tableUnder(text, '## Execution') ?? []).map(([k, v]) => [k, v]));
96
+ if (exec['Inherited turns'] !== 'none') errors.push('critique/response/critique.md: the critique must be a fresh execution with no inherited turns');
97
+ if (has('critique/request/request.json')) {
98
+ const creq = JSON.parse(await read('critique/request/request.json'));
99
+ const given = Object.values(creq.inputs ?? {});
100
+ if (!given.some((p) => p.endsWith('/response/data/stack-model.json'))) errors.push('critique/request/request.json: the critique must be given the stack model');
101
+ if (given.some((p) => p.endsWith('/response/response.md'))) errors.push('critique/request/request.json: the critique may not be given the author\'s response.md');
102
+ }
103
+ } else if (response.status === 'done') errors.push('critique/response/critique.md: a done branch needs the critique exchange');
104
+ return { errors };
105
+ }
106
+
107
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
108
+ const target = process.argv[2];
109
+ if (!target) { process.stderr.write('usage: node validate.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
110
+ const { errors } = await validateArchitectureStep(path.resolve(target));
111
+ if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('valid architecture.decide branch\n');
112
+ }