@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,167 @@
1
+ # frontend.direction.decide
2
+
3
+ ## Job
4
+
5
+ Decide one evidence-backed, implementation-ready frontend direction for one authorized target, and
6
+ prove it against the business promise, the published Grammar, the observed implementation and a
7
+ falsification pass that no candidate survives by taste.
8
+
9
+ ## The change level decides what must be bound
10
+
11
+ The change level is the request's own authority and current source never proves it: an audit that
12
+ asks for a passing surface is `reconstruct`, not `refine`. `new` requires the business promise, and
13
+ it closes the state set and the exits before anything is drawn. `reconstruct` requires the promise
14
+ only when the state set changes, and it preserves the business facts, the behaviour authority and
15
+ the API semantics it inherits. `refine` requires no input authority at all, because it changes
16
+ nothing a promise could contradict. A backend implementation is required when a data contract
17
+ changes, an architecture decision when a boundary changes; neither is required otherwise, and
18
+ neither is ever invented. `create` occurs with `new` and only with it.
19
+
20
+ ## Evidence contradicts, it does not authorize
21
+
22
+ The current implementation, a green test, a rendered DOM and a prior UAT pass are all evidence, and
23
+ each observation is recorded as a path at the head it was read at. Evidence must be observed before
24
+ any proposal is written: the target's direct artifacts without the producer's rationale, or, for a
25
+ new target, proof that the target is absent and only the authorized host and product-family context.
26
+ None of it authorizes a direction by incumbency. A page that exists is not a reason to build that
27
+ page again.
28
+
29
+ ## Bounded research, and only where it is owed
30
+
31
+ External references are resolved only when the person supplied none and the change level is `new` or
32
+ `reconstruct`. A `refine` works from the family idioms alone. Every reference that survives is
33
+ recorded with its URL and with the exact limitation it carries; nothing copies a page, a brand, a
34
+ palette or a component anatomy. When bounded research cannot close the business or interaction
35
+ question the decision rests on, the run stops with the owning gap.
36
+
37
+ ## The Grammar filter refuses invention, not ownership
38
+
39
+ A candidate that invents a missing shared interface, bypasses the owner ceiling, imitates unpublished
40
+ Grammar locally or contradicts a published composition is rejected. `GRAMMAR_REQUIRED` is only for a
41
+ missing family component, and it goes to a person who publishes it; the operator never composes a
42
+ substitute out of parts. A node the application legitimately owns, a canvas for instance, is not a
43
+ Grammar gap and never raises one.
44
+
45
+ ## Candidates are falsified before they are chosen
46
+
47
+ Falsification attacks business and backend conformance, hierarchy, content density, action feedback,
48
+ recovery, responsive reflow, content stress, keyboard and focus behaviour, accessibility, family
49
+ coherence, reversibility and owner leakage, and every attack lands in the receipt with its verdict.
50
+ A direction is invalid while an applicable business contradiction, owner leak, Grammar invention,
51
+ responsive failure, accessibility failure, unresolved adverse state or materially stronger reversible
52
+ alternative remains. Under `refine` the candidates are element-level moves inside the approved
53
+ structure, never a new structure. When the only candidate fails an attack the run stops with
54
+ `NO_VIABLE_DIRECTION`, and when several survive, `DIRECTION_CHOICE_REQUIRED` selects the one that
55
+ survived the most attacks unless the person kept the choice.
56
+
57
+ ## Boundary
58
+
59
+ Context is read-only. The operator writes only `response/` of its own branch: the decision receipt,
60
+ the coverage enumeration, the rendered candidate pages and `response.json`. It does not modify
61
+ product or authority source, invent business, backend, architecture, authentication, persistence or
62
+ data behaviour, publish shared UI Grammar, start or reconfigure runtime services, or claim that an
63
+ implementation, a visual quality gate or a UAT run has passed.
64
+
65
+ ## Images are judged, not requested
66
+
67
+ An image is a composition decision like any other: when a candidate leaves a region that reads empty
68
+ (a hero without a subject, an empty state with only a sentence, a card row whose copy cannot carry the
69
+ width), the operator adds an image made to one stated claim of the direction and records why, in the
70
+ `## Images` table. It does not wait for a person to ask, and it does not decorate: a region that the
71
+ copy and the Grammar objects already carry gets no image, and an image never encodes a claim the
72
+ business promise did not make. The asset and its prompt land under `response/artifacts/images/`;
73
+ `frontend.source.apply` writes them with the declared write set.
74
+
75
+ ## Context
76
+
77
+ | Alias | Bind | Required |
78
+ | --- | --- | --- |
79
+ | `@grammar/core` | the published Grammar as the bound app resolves it; the compositions a direction may bind | yes |
80
+ | `@knowledge/ui/composition` | the assertions the emitted receipt has to satisfy, `COVERAGE-1` being the assertion about the receipt as a whole | yes |
81
+ | `@workspaces/fe` | the routed frontend checkout read at the frozen head; the current implementation as evidence, never as the requested direction | yes |
82
+ | `@knowledge/grammars/starci` | how the Core family is meant to realize Common; law about the Grammar, never the Grammar itself | no |
83
+ | `@worktrees/uat/<flow>/<case>` | prior behaviour, UX and UI observations with their captures; evidence and counterevidence, and a prior pass is not current authority | no |
84
+
85
+ ## Inputs
86
+
87
+ | Kind | From | Required |
88
+ | --- | --- | --- |
89
+ | `business-promise-authority` | `business.decide`; required by `new`, and by `reconstruct` when the state set changes | no |
90
+ | `backend-source-application` | `backend.source.apply`; required when a data contract changes | no |
91
+ | `architecture-decision` | `architecture.decide`; required when a boundary changes | no |
92
+ | `frontend-direction-decision` | a prior run of `frontend.direction.decide` on the same target, read when resuming | no |
93
+
94
+ ## Requirements
95
+
96
+ | Field | Type | Default | Ask |
97
+ | --- | --- | --- | --- |
98
+ | `target` | id | — | The one route, page, layout, modal, drawer, flow, block or component this direction is for |
99
+ | `intent` | choice | modify | create, modify, audit-repair or reconcile; `create` occurs with change level `new` and only with it |
100
+ | `changeLevel` | choice | — | new, reconstruct or refine; an audit that must end in a passing surface is reconstruct |
101
+ | `ownerCeiling` | choice | surface-and-nested-layouts | surface-only, surface-and-nested-layouts or ancestor-layouts-authorized |
102
+ | `candidates` | number 1–3 | 1 | How many directions to form; more than one only when a comparison is wanted |
103
+ | `preview` | choice | no | yes renders the single candidate as an inspectable page |
104
+ | `references` | list | [] | External references the person supplies; bounded research runs only when this is empty |
105
+ | `selectionPolicy` | choice | automatic | `automatic`: the operator selects and records why; `approval-required`: the person selects |
106
+ | `approval` | id | null | The approved candidate id; required only under `approval-required`, supplied on resume after `DIRECTION_CHOICE_REQUIRED` |
107
+ | `resume` | token | null | The blocked branch's token when re-entering after a stop |
108
+
109
+ ## Steps
110
+
111
+ | # | Step | Params | Reads | Writes | Stops with |
112
+ | --- | --- | --- | --- | --- | --- |
113
+ | 1 | Validate the gate and resume | `resume`, `approval` | `request/request.json`, input `frontend-direction-decision` when resuming, @workspaces/fe at the frozen head | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
114
+ | 2 | Check the request: route, scope, change level, owner ceiling | `target`, `intent`, `changeLevel`, `ownerCeiling` | @workspaces/fe at the frozen head, @tools/git | — | `ROUTE_UNVERIFIED`, `SCOPE_UNFROZEN`, `CHANGE_LEVEL_AMBIGUOUS`, `OWNER_CEILING_INVALID` |
115
+ | 3 | Bind the inputs the change level requires | `changeLevel` | inputs `business-promise-authority`, `backend-source-application` and `architecture-decision` | — | `BUSINESS_REQUIRED`, `BACKEND_REQUIRED`, `ARCHITECTURE_REQUIRED` |
116
+ | 4 | Observe the existing context | — | @workspaces/fe (the target's direct artifacts, or the authorized host and product family when the target is absent), @worktrees/uat/<flow>/<case> when present | — | `EVIDENCE_MISSING` |
117
+ | 5 | Compile one UI contract and its coverage | — | @knowledge/ui/composition, input `business-promise-authority` when present, the observed context | `response/data/coverage.json` | `SCOPE_UNFROZEN` |
118
+ | 6 | Resolve external references, bounded | `references`, `changeLevel` | @knowledge/ui/composition (the gap the research must close), @tools/websearch | — | `REFERENCE_EVIDENCE_EXHAUSTED` |
119
+ | 7 | Form the candidates | `candidates` | the compiled UI contract | — | `NO_VIABLE_DIRECTION` |
120
+ | 8 | Apply the Grammar filter | `ownerCeiling` | @grammar/core (what a component owns and which props exist), @knowledge/grammars/starci | — | `GRAMMAR_REQUIRED` |
121
+ | 9 | Render the decision evidence and the judged images | `candidates`, `preview` | the surviving candidates, `@knowledge/grammars/starci` | `response/artifacts/<candidateId>.html`, `response/artifacts/images/<slot>.png`, @tools/visualize, @tools/imagegen | — |
122
+ | 10 | Falsify | — | the candidates, inputs `business-promise-authority` and `backend-source-application`, `response/data/coverage.json` | — | `NO_VIABLE_DIRECTION` |
123
+ | 11 | Decide | `selectionPolicy`, `approval` | the falsification table | — | `DIRECTION_CHOICE_REQUIRED` |
124
+ | 12 | Emit | — | everything above | `response/response.md`, `response/response.json` | — |
125
+
126
+ Step 9 renders only when more than one candidate was formed or `preview` is yes; a single candidate
127
+ under the defaults produces no page and rests on step 10. Under `automatic`,
128
+ `DIRECTION_CHOICE_REQUIRED` selects the candidate that survived the most attacks and records the
129
+ table; under `approval-required` it stops and the person returns with `approval`. The receipt
130
+ authorizes the next domain to resolve and implement inside the frozen owner ceiling and proves
131
+ nothing about how the result renders.
132
+
133
+ ## Outputs
134
+
135
+ | Kind | File | Type | Required |
136
+ | --- | --- | --- | --- |
137
+ | `frontend-direction-decision` | `response/response.md` | md | yes |
138
+ | `ui-coverage` | `response/data/coverage.json` | data | yes |
139
+ | `candidates` | `response/artifacts/<candidateId>.html` | artifact | no |
140
+ | `direction-image` | `response/artifacts/images/<slot>.png` | artifact | no |
141
+
142
+ ## Stops
143
+
144
+ | Code | Disposition |
145
+ | --- | --- |
146
+ | `INVALID_INPUT` | terminate |
147
+ | `ROUTE_UNVERIFIED` | terminate |
148
+ | `SOURCE_DRIFT` | terminate |
149
+ | `SCOPE_UNFROZEN` | terminate |
150
+ | `CHANGE_LEVEL_AMBIGUOUS` | terminate |
151
+ | `OWNER_CEILING_INVALID` | terminate |
152
+ | `BUSINESS_REQUIRED` | terminate |
153
+ | `BACKEND_REQUIRED` | terminate |
154
+ | `ARCHITECTURE_REQUIRED` | terminate |
155
+ | `GRAMMAR_REQUIRED` | terminate |
156
+ | `EVIDENCE_MISSING` | terminate |
157
+ | `REFERENCE_EVIDENCE_EXHAUSTED` | terminate |
158
+ | `NO_VIABLE_DIRECTION` | terminate |
159
+ | `DIRECTION_CHOICE_REQUIRED` | fallback |
160
+ | `NO_PROGRESS` | terminate |
161
+
162
+ ## Next
163
+
164
+ | When | Operator |
165
+ | --- | --- |
166
+ | the direction is decided; every direction resolves its presentation values before any source is written | `frontend.presentation.resolve` |
167
+ | a family component the direction needs is unpublished, so a person publishes it and the same direction runs again | `frontend.direction.decide` |
@@ -0,0 +1,167 @@
1
+ # frontend.direction.decide
2
+
3
+ ## Việc
4
+
5
+ Quyết một hướng frontend có bằng chứng, đủ để triển khai, cho đúng một target được uỷ quyền, rồi
6
+ chứng minh nó bằng lời hứa nghiệp vụ, Grammar đã publish, hiện trạng quan sát được và một lượt phản
7
+ chứng mà không phương án nào qua được bằng khẩu vị.
8
+
9
+ ## Change level quyết định phải bind những gì
10
+
11
+ Change level là thẩm quyền của chính lời yêu cầu, và source hiện tại không bao giờ chứng minh được
12
+ nó: một lượt audit đòi bề mặt phải pass là `reconstruct`, không phải `refine`. `new` cần lời hứa
13
+ nghiệp vụ, và nó đóng tập trạng thái cùng các lối ra trước khi vẽ bất cứ thứ gì. `reconstruct` chỉ
14
+ cần lời hứa khi tập trạng thái đổi, và nó giữ nguyên các sự thật nghiệp vụ, thẩm quyền hành vi và
15
+ ngữ nghĩa API mà nó thừa hưởng. `refine` không cần thẩm quyền đầu vào nào cả, vì nó không đổi thứ gì
16
+ mà một lời hứa có thể phản bác. Backend implementation là bắt buộc khi một contract dữ liệu đổi,
17
+ architecture decision khi một ranh giới đổi; ngoài ra không bắt buộc, và không bao giờ được bịa ra.
18
+ `create` đi cùng `new` và chỉ đi cùng nó.
19
+
20
+ ## Bằng chứng phản bác, chứ không uỷ quyền
21
+
22
+ Hiện trạng implementation, một test xanh, DOM đã render và một lần UAT pass trước đều là bằng chứng,
23
+ và mỗi quan sát được ghi lại thành một path kèm head lúc đọc. Bằng chứng phải được quan sát trước khi
24
+ viết bất kỳ đề xuất nào: lấy các artifact trực tiếp của target mà bỏ lý lẽ của người tạo ra chúng,
25
+ hoặc, với target mới, chứng minh target chưa tồn tại rồi chỉ quan sát host được uỷ quyền và context
26
+ cùng họ sản phẩm. Không mảnh nào trong số đó uỷ quyền cho một hướng chỉ vì nó đang chạy sẵn. Một
27
+ trang đang tồn tại không phải lý do để dựng lại đúng trang ấy.
28
+
29
+ ## Nghiên cứu có giới hạn, và chỉ ở chỗ còn nợ
30
+
31
+ Tham chiếu ngoài chỉ được đi tìm khi người dùng không cấp cái nào và change level là `new` hay
32
+ `reconstruct`. Một lượt `refine` làm việc bằng idiom của họ là đủ. Mỗi tham chiếu còn lại được ghi
33
+ kèm URL và đúng giới hạn nó mang theo; không gì chép lại một trang, một thương hiệu, một bảng màu hay
34
+ giải phẫu của một component. Khi nghiên cứu có giới hạn không lấp được câu hỏi nghiệp vụ hay tương
35
+ tác mà quyết định dựa vào, lần chạy dừng ở đúng khoảng trống có chủ.
36
+
37
+ ## Bộ lọc Grammar từ chối sự bịa đặt, không từ chối quyền sở hữu
38
+
39
+ Một phương án bịa ra giao diện dùng chung còn thiếu, đi vòng qua trần owner, bắt chước Grammar chưa
40
+ publish ngay tại chỗ hay chỏi một composition đã publish thì bị loại. `GRAMMAR_REQUIRED` chỉ dành cho
41
+ một component của họ còn thiếu, và nó đi tới một con người để publish; operator không bao giờ ghép
42
+ tạm một cái thay thế từ các mảnh. Một node mà ứng dụng sở hữu hợp lệ, chẳng hạn một canvas, không
43
+ phải khoảng trống Grammar và không bao giờ làm phát mã ấy.
44
+
45
+ ## Phương án bị phản chứng trước khi được chọn
46
+
47
+ Phản chứng tấn công vào tính phù hợp với nghiệp vụ và backend, thứ bậc, mật độ nội dung, phản hồi
48
+ hành động, đường phục hồi, dòng chảy responsive, sức chịu nội dung, bàn phím và focus, khả năng tiếp
49
+ cận, tính nhất quán của họ, khả năng đảo ngược và rò rỉ quyền sở hữu, và mỗi đòn tấn công đều rơi vào
50
+ biên nhận kèm phán quyết của nó. Một hướng còn vô hiệu chừng nào còn một mâu thuẫn nghiệp vụ áp dụng
51
+ được, một rò rỉ owner, một Grammar tự chế, một lỗi responsive, một lỗi tiếp cận, một trạng thái bất
52
+ lợi chưa giải quyết hay một phương án đảo ngược mạnh hơn về bản chất. Dưới `refine`, các phương án là
53
+ những nước đi ở cấp phần tử bên trong cấu trúc đã duyệt, không bao giờ là một cấu trúc mới. Khi
54
+ phương án duy nhất chết dưới một đòn tấn công, lần chạy dừng với `NO_VIABLE_DIRECTION`; khi nhiều
55
+ phương án cùng sống, `DIRECTION_CHOICE_REQUIRED` chọn cái sống sót qua nhiều đòn nhất, trừ khi người
56
+ dùng giữ quyền chọn.
57
+
58
+ ## Ranh giới
59
+
60
+ Context chỉ đọc. Operator chỉ ghi `response/` của nhánh mình: biên nhận quyết định, bản liệt kê
61
+ coverage, các trang phương án đã render và `response.json`. Nó không sửa source sản phẩm hay source
62
+ thẩm quyền, không bịa hành vi nghiệp vụ, backend, kiến trúc, xác thực, lưu trữ hay dữ liệu, không
63
+ publish Grammar dùng chung, không khởi động hay cấu hình lại dịch vụ runtime, và không tuyên bố rằng
64
+ implementation, cổng chất lượng thị giác hay một lượt UAT đã qua.
65
+
66
+ ## Hình là thứ operator tự xét, không chờ yêu cầu
67
+
68
+ Một tấm hình là quyết định composition như mọi quyết định khác: khi một ứng viên để lại vùng đọc lên
69
+ thấy trống (hero không có chủ thể, empty state chỉ có một câu, hàng card mà chữ không gánh nổi chiều
70
+ rộng), operator thêm hình làm theo đúng một claim của hướng và ghi lý do vào bảng `## Images`. Nó
71
+ không chờ người nói, và cũng không trang trí: vùng mà chữ và Grammar object đã gánh được thì không
72
+ thêm hình, và hình không bao giờ mã hoá claim mà lời hứa nghiệp vụ không hề nêu. Asset và prompt nằm
73
+ dưới `response/artifacts/images/`; `frontend.source.apply` ghi chúng cùng write set đã khai.
74
+
75
+ ## Context
76
+
77
+ | Alias | Bind | Bắt buộc |
78
+ | --- | --- | --- |
79
+ | `@grammar/core` | Grammar đã publish như app đang bind resolve ra; các composition mà một hướng được phép ràng | có |
80
+ | `@knowledge/ui/composition` | các khẳng định mà biên nhận phát ra phải thoả, `COVERAGE-1` là khẳng định về toàn bộ biên nhận | có |
81
+ | `@workspaces/fe` | checkout frontend được route, đọc ở head đóng băng; hiện trạng là bằng chứng, không bao giờ là hướng được yêu cầu | có |
82
+ | `@knowledge/grammars/starci` | họ Core được kỳ vọng hiện thực hoá Common ra sao; luật về Grammar, không phải chính Grammar | không |
83
+ | `@worktrees/uat/<flow>/<case>` | quan sát hành vi, UX và UI của lần trước kèm ảnh chụp; bằng chứng và phản chứng, một lần pass cũ không phải thẩm quyền hiện tại | không |
84
+
85
+ ## Đầu vào
86
+
87
+ | Kind | Từ đâu | Bắt buộc |
88
+ | --- | --- | --- |
89
+ | `business-promise-authority` | `business.decide`; bắt buộc với `new`, và với `reconstruct` khi tập trạng thái đổi | không |
90
+ | `backend-source-application` | `backend.source.apply`; bắt buộc khi một contract dữ liệu đổi | không |
91
+ | `architecture-decision` | `architecture.decide`; bắt buộc khi một ranh giới đổi | không |
92
+ | `frontend-direction-decision` | một lần chạy `frontend.direction.decide` trước trên cùng target, đọc khi chạy lại | không |
93
+
94
+ ## Yêu cầu
95
+
96
+ | Field | Kiểu | Mặc định | Hỏi |
97
+ | --- | --- | --- | --- |
98
+ | `target` | id | — | Đúng một route, page, layout, modal, drawer, flow, block hay component mà hướng này nhắm tới |
99
+ | `intent` | choice | modify | create, modify, audit-repair hay reconcile; `create` đi cùng change level `new` và chỉ đi cùng nó |
100
+ | `changeLevel` | choice | — | new, reconstruct hay refine; một lượt audit phải kết thúc bằng bề mặt pass là reconstruct |
101
+ | `ownerCeiling` | choice | surface-and-nested-layouts | surface-only, surface-and-nested-layouts hay ancestor-layouts-authorized |
102
+ | `candidates` | number 1–3 | 1 | Hình thành bao nhiêu hướng; nhiều hơn một chỉ khi muốn so sánh |
103
+ | `preview` | choice | no | yes thì render phương án duy nhất thành một trang xem được |
104
+ | `references` | list | [] | Tham chiếu ngoài do người dùng cấp; nghiên cứu có giới hạn chỉ chạy khi ô này rỗng |
105
+ | `selectionPolicy` | choice | automatic | `automatic`: operator chọn và ghi lý do; `approval-required`: người chọn |
106
+ | `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 `DIRECTION_CHOICE_REQUIRED` |
107
+ | `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
108
+
109
+ ## Các bước
110
+
111
+ | # | Bước | Tham số | Đọc | Ghi | Dừng với |
112
+ | --- | --- | --- | --- | --- | --- |
113
+ | 1 | Kiểm gate và chạy lại | `resume`, `approval` | `request/request.json`, đầu vào `frontend-direction-decision` khi chạy lại, @workspaces/fe ở head đóng băng | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
114
+ | 2 | Soát lời yêu cầu: route, scope, change level, trần owner | `target`, `intent`, `changeLevel`, `ownerCeiling` | @workspaces/fe ở head đóng băng, @tools/git | — | `ROUTE_UNVERIFIED`, `SCOPE_UNFROZEN`, `CHANGE_LEVEL_AMBIGUOUS`, `OWNER_CEILING_INVALID` |
115
+ | 3 | Bind các đầu vào mà change level đòi | `changeLevel` | đầu vào `business-promise-authority`, `backend-source-application` và `architecture-decision` | — | `BUSINESS_REQUIRED`, `BACKEND_REQUIRED`, `ARCHITECTURE_REQUIRED` |
116
+ | 4 | Quan sát context đang có | — | @workspaces/fe (artifact trực tiếp của target, hoặc host được uỷ quyền và họ sản phẩm khi target chưa tồn tại), @worktrees/uat/<flow>/<case> nếu có | — | `EVIDENCE_MISSING` |
117
+ | 5 | Biên một UI contract và coverage của nó | — | @knowledge/ui/composition, đầu vào `business-promise-authority` nếu có, context đã quan sát | `response/data/coverage.json` | `SCOPE_UNFROZEN` |
118
+ | 6 | Đi tìm tham chiếu ngoài, có giới hạn | `references`, `changeLevel` | @knowledge/ui/composition (khoảng trống mà nghiên cứu phải lấp), @tools/websearch | — | `REFERENCE_EVIDENCE_EXHAUSTED` |
119
+ | 7 | Hình thành các phương án | `candidates` | UI contract vừa biên | — | `NO_VIABLE_DIRECTION` |
120
+ | 8 | Áp bộ lọc Grammar | `ownerCeiling` | @grammar/core (component sở hữu gì và có prop nào), @knowledge/grammars/starci | — | `GRAMMAR_REQUIRED` |
121
+ | 9 | Render bằng chứng quyết định và hình đã tự xét | `candidates`, `preview` | các phương án còn sống, @knowledge/grammars/starci | `response/artifacts/<candidateId>.html`, `response/artifacts/images/<slot>.png`, @tools/visualize, @tools/imagegen | — |
122
+ | 10 | Phản chứng | — | các phương án, đầu vào `business-promise-authority` và `backend-source-application`, `response/data/coverage.json` | — | `NO_VIABLE_DIRECTION` |
123
+ | 11 | Quyết | `selectionPolicy`, `approval` | bảng phản chứng | — | `DIRECTION_CHOICE_REQUIRED` |
124
+ | 12 | Phát | — | mọi thứ ở trên | `response/response.md`, `response/response.json` | — |
125
+
126
+ Bước 9 chỉ render khi có nhiều hơn một phương án được hình thành hoặc `preview` là yes; một phương án
127
+ duy nhất dưới mặc định không sinh trang nào và dựa vào bước 10. Dưới `automatic`,
128
+ `DIRECTION_CHOICE_REQUIRED` chọn phương án sống sót qua nhiều đòn tấn công nhất và ghi lại bảng; dưới
129
+ `approval-required` nó dừng và người dùng quay lại với `approval`. Biên nhận uỷ quyền cho domain kế
130
+ tiếp resolve và triển khai bên trong trần owner đã đóng băng, và không chứng minh gì về việc kết quả
131
+ render ra sao.
132
+
133
+ ## Đầu ra
134
+
135
+ | Kind | File | Kiểu | Bắt buộc |
136
+ | --- | --- | --- | --- |
137
+ | `frontend-direction-decision` | `response/response.md` | md | có |
138
+ | `ui-coverage` | `response/data/coverage.json` | data | có |
139
+ | `candidates` | `response/artifacts/<candidateId>.html` | artifact | không |
140
+ | `direction-image` | `response/artifacts/images/<slot>.png` | artifact | không |
141
+
142
+ ## Dừng
143
+
144
+ | Code | Xử lý |
145
+ | --- | --- |
146
+ | `INVALID_INPUT` | terminate |
147
+ | `ROUTE_UNVERIFIED` | terminate |
148
+ | `SOURCE_DRIFT` | terminate |
149
+ | `SCOPE_UNFROZEN` | terminate |
150
+ | `CHANGE_LEVEL_AMBIGUOUS` | terminate |
151
+ | `OWNER_CEILING_INVALID` | terminate |
152
+ | `BUSINESS_REQUIRED` | terminate |
153
+ | `BACKEND_REQUIRED` | terminate |
154
+ | `ARCHITECTURE_REQUIRED` | terminate |
155
+ | `GRAMMAR_REQUIRED` | terminate |
156
+ | `EVIDENCE_MISSING` | terminate |
157
+ | `REFERENCE_EVIDENCE_EXHAUSTED` | terminate |
158
+ | `NO_VIABLE_DIRECTION` | terminate |
159
+ | `DIRECTION_CHOICE_REQUIRED` | fallback |
160
+ | `NO_PROGRESS` | terminate |
161
+
162
+ ## Kế tiếp
163
+
164
+ | Khi | Operator |
165
+ | --- | --- |
166
+ | hướng đã quyết; mọi hướng đều resolve giá trị trình bày trước khi ghi source | `frontend.presentation.resolve` |
167
+ | một component của họ mà hướng cần chưa được publish, nên một người publish nó rồi chính hướng ấy chạy lại | `frontend.direction.decide` |
@@ -0,0 +1,226 @@
1
+ // Proves validate.mjs on a synthetic session branch: one conforming branch under the defaults
2
+ // (modify, reconstruct, one candidate, automatic), one three-candidate branch that takes the
3
+ // DIRECTION_CHOICE_REQUIRED fallback, one blocked on the same code under approval-required, and one
4
+ // mutation per law, each of which must 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 { validateDirectionStep } from './validate.mjs';
10
+
11
+ const HEAD = '0f1e2d3c4b5a69788796a5b4c3d2e1f009182736';
12
+ const NAMES = ['one-column', 'split-view', 'stepper'];
13
+
14
+ const coverage = ({ regions = 2, actions = 1 } = {}) => ({
15
+ directionId: 'plan-picker',
16
+ actions: [{ action: 'choose a plan', pointerRoute: 'the plan card', keyboardRoute: 'tab to the card, enter', states: ['pending', 'confirmed'], pendingPaths: [{ path: 'server accepts', settlement: 'the confirmed plan replaces the picker' }] }].slice(0, actions),
17
+ regions: [
18
+ { region: 'offer', idiomRef: 'idioms#offer-block', compositionRef: '@grammar/core#SurfaceCard' },
19
+ { region: 'decision', idiomRef: 'idioms#decision-bar', compositionRef: '@grammar/core#SurfaceListCard' },
20
+ ].slice(0, regions),
21
+ states: [{ meaning: 'empty', carrier: 'the offer region' }, { meaning: 'pending', carrier: 'the decision button' }],
22
+ responsive: [{ branch: 'narrow', owner: 'the page shell' }],
23
+ });
24
+
25
+ function responseMd({
26
+ intent = 'modify', changeLevel = 'reconstruct', ownerCeiling = 'surface-and-nested-layouts',
27
+ classification = 'dominant', policy = 'automatic', selected = 'one-column', candidates = 1,
28
+ selectedFails = false, rejectAll = true, references = 0, fallbacks = [],
29
+ } = {}) {
30
+ const formed = NAMES.slice(0, candidates);
31
+ const attacks = formed.map((id) => `| content stress | \`${id}\` | ${selectedFails && id === selected ? 'fails' : 'holds'} | the widest plan name still fits at 360px |`);
32
+ const others = rejectAll ? formed.filter((id) => id !== selected).map((id) => `| \`${id}\` | it loses the offer above the fold on the narrow branch |`) : [];
33
+ const refRows = references ? ['| a public plan picker | https://example.com/plans | it settles nothing about entitlement |'] : [];
34
+ return `# frontend-direction-decision — plan-picker
35
+
36
+ ## Decision
37
+
38
+ | Field | Value |
39
+ | --- | --- |
40
+ | Direction id | \`plan-picker\` |
41
+ | Target | \`/plans\` |
42
+ | Intent | \`${intent}\` |
43
+ | Change level | \`${changeLevel}\` |
44
+ | Owner ceiling | \`${ownerCeiling}\` |
45
+ | Classification | \`${classification}\` |
46
+ | Selection policy | \`${policy}\` |
47
+ | Selected candidate | \`${selected}\` |
48
+
49
+ ## Observed
50
+
51
+ | Item | Evidence |
52
+ | --- | --- |
53
+ | what the plans route renders today | \`app/plans/page.tsx:1-64@${HEAD}\` |
54
+
55
+ ## UI contract
56
+
57
+ | Element | Kind | Responsibility |
58
+ | --- | --- | --- |
59
+ | \`offer\` | region | names the offer and its price |
60
+ | \`decision\` | region | carries the choice and its confirmation |
61
+ | choose a plan | action | the actor picks one plan and the surface answers |
62
+ | empty | state | no plan is offered yet |
63
+ | pending | state | the choice is settling |
64
+ | narrow | responsive | the two regions stack below the breakpoint |
65
+
66
+ ## Coverage
67
+
68
+ | Concern | Enumerated |
69
+ | --- | --- |
70
+ | Actions | one action with its routes and one settled pending path |
71
+ | Regions | two regions, each with one idiom and one composition |
72
+ | States | two meanings on two carriers |
73
+ | Responsive | one branch owned by the page shell |
74
+
75
+ ## References
76
+
77
+ | Source | URL | Limitation |
78
+ | --- | --- | --- |
79
+ ${refRows.join('\n')}
80
+
81
+ ## Images
82
+
83
+ | Slot | Why | Claim | File |
84
+ | --- | --- | --- | --- |
85
+
86
+ ## Falsification
87
+
88
+ | Attack | Candidate | Verdict | Evidence |
89
+ | --- | --- | --- | --- |
90
+ ${attacks.join('\n')}
91
+
92
+ ## Why not the others
93
+
94
+ | Candidate | Rejected because |
95
+ | --- | --- |
96
+ ${others.join('\n')}
97
+
98
+ ## Fallbacks taken
99
+
100
+ | Code | Action |
101
+ | --- | --- |
102
+ ${fallbacks.map((c) => `| \`${c}\` | the candidate that survived the most attacks was selected |`).join('\n')}
103
+ `;
104
+ }
105
+
106
+ const requestJson = ({ extra = {} } = {}) => ({
107
+ schemaVersion: 9,
108
+ operatorId: 'frontend.direction.decide',
109
+ step: 2,
110
+ parallel: 1,
111
+ sessionId: 's-test',
112
+ contexts: [{ alias: '@workspaces/fe', head: HEAD }, { alias: '@grammar/core', head: null }, { alias: '@knowledge/ui/composition', head: null }],
113
+ requirements: {
114
+ target: '/plans',
115
+ intent: 'modify',
116
+ changeLevel: 'reconstruct',
117
+ ownerCeiling: 'surface-and-nested-layouts',
118
+ candidates: 1,
119
+ preview: 'no',
120
+ references: [],
121
+ selectionPolicy: 'automatic',
122
+ approval: null,
123
+ resume: null,
124
+ ...extra,
125
+ },
126
+ inputs: { 'business-promise-authority': 'step-1/parallel-1/response/response.md' },
127
+ resume: null,
128
+ });
129
+
130
+ const responseJson = ({ status = 'done', stop, fields, fallbacks = [], next = ['frontend.presentation.resolve'] } = {}) => ({
131
+ schemaVersion: 9,
132
+ operatorId: 'frontend.direction.decide',
133
+ step: 2,
134
+ parallel: 1,
135
+ status,
136
+ ...(stop ? { stop } : {}),
137
+ fallbacks,
138
+ fields: fields ?? { 'frontend-direction-decision': 'response/response.md', 'ui-coverage': 'response/data/coverage.json' },
139
+ commits: [],
140
+ next,
141
+ });
142
+
143
+ function writeBranch(files) {
144
+ const session = mkdtempSync(path.join(tmpdir(), 'fe-direction-session-'));
145
+ const branch = path.join(session, 'step-2', 'parallel-1');
146
+ for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
147
+ mkdirSync(path.join(session, 'step-1', 'parallel-1', 'response'), { recursive: true });
148
+ writeFileSync(path.join(session, 'step-1', 'parallel-1', 'response', 'response.md'), '# business-promise-authority — plans\n');
149
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1'], ['2/1']], steps: { '1/1': 'business.decide', '2/1': 'frontend.direction.decide' }, current: '2/1', status: 'running' }));
150
+ for (const [name, content] of Object.entries(files)) {
151
+ if (content === null) continue;
152
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
153
+ }
154
+ return { branch, session };
155
+ }
156
+
157
+ const baseline = () => ({
158
+ 'request/request.json': requestJson(),
159
+ 'response/response.json': responseJson(),
160
+ 'response/response.md': responseMd(),
161
+ 'response/data/coverage.json': coverage(),
162
+ });
163
+
164
+ const threeCandidates = ({ policy = 'automatic', approval = null, extraResponse = {}, md = {} } = {}) => ({
165
+ 'request/request.json': requestJson({ extra: { candidates: 3, selectionPolicy: policy, approval } }),
166
+ 'response/response.json': responseJson({
167
+ fallbacks: policy === 'automatic' ? ['DIRECTION_CHOICE_REQUIRED'] : [],
168
+ fields: { 'frontend-direction-decision': 'response/response.md', 'ui-coverage': 'response/data/coverage.json', candidates: NAMES.map((n) => `response/artifacts/${n}.html`) },
169
+ ...extraResponse,
170
+ }),
171
+ 'response/response.md': responseMd({ policy, candidates: 3, fallbacks: policy === 'automatic' ? ['DIRECTION_CHOICE_REQUIRED'] : [], ...md }),
172
+ 'response/data/coverage.json': coverage(),
173
+ ...Object.fromEntries(NAMES.map((n) => [`response/artifacts/${n}.html`, `<!doctype html><title>${n}</title>`])),
174
+ });
175
+
176
+ async function expectValid(files, label) {
177
+ const { branch, session } = writeBranch(files);
178
+ const { errors } = await validateDirectionStep(branch);
179
+ rmSync(session, { recursive: true, force: true });
180
+ assert.deepEqual(errors, [], `${label} should be valid`);
181
+ }
182
+ async function expectError(files, needle, label) {
183
+ const { branch, session } = writeBranch(files);
184
+ const { errors } = await validateDirectionStep(branch);
185
+ rmSync(session, { recursive: true, force: true });
186
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
187
+ }
188
+ const withCoverage = (change) => { const c = coverage(); change(c); return { ...baseline(), 'response/data/coverage.json': c }; };
189
+
190
+ await expectValid(baseline(), 'defaults (modify, reconstruct, one candidate, automatic)');
191
+ await expectValid(threeCandidates(), 'three candidates, the choice taken as a fallback');
192
+ await expectValid(threeCandidates({ policy: 'approval-required', approval: 'one-column' }), 'three candidates approved by the person');
193
+ await expectValid({
194
+ 'request/request.json': requestJson({ extra: { candidates: 3, selectionPolicy: 'approval-required' } }),
195
+ 'response/response.json': responseJson({ status: 'blocked', stop: 'DIRECTION_CHOICE_REQUIRED', next: [], fields: { candidates: NAMES.map((n) => `response/artifacts/${n}.html`) } }),
196
+ ...Object.fromEntries(NAMES.map((n) => [`response/artifacts/${n}.html`, `<!doctype html><title>${n}</title>`])),
197
+ }, 'DIRECTION_CHOICE_REQUIRED terminates under approval-required');
198
+
199
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { intent: 'create' } }) }, 'intent create requires changeLevel new', 'create without new');
200
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { candidates: 4 } }) }, 'candidates must be 1, 2 or 3', 'four candidates');
201
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { approval: 'one-column' } }) }, 'approval is bound under automatic policy', 'approval under automatic');
202
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
203
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { changeLevel: '' } }) }, 'required field changeLevel has no value', 'missing change level');
204
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { candidates: 2 } }) }, 'more than one candidate was formed but none was rendered', 'comparison without pages');
205
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { preview: 'yes' } }) }, 'preview was asked for but no candidate page was rendered', 'preview without a page');
206
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { 'frontend-direction-decision': 'response/response.md', 'ui-coverage': 'response/data/coverage.json', candidates: 'response/artifacts/one-column.html' } }), 'response/artifacts/one-column.html': '<!doctype html>' }, 'one candidate under no preview renders no page', 'unasked preview page');
207
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ changeLevel: 'refine' }) }, 'Change level refine differs from the request', 'receipt and request disagree on the change level');
208
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { changeLevel: 'refine' } }), 'response/response.md': responseMd({ changeLevel: 'refine' }) }, 'a refine is classified locked-refine', 'refine classified dominant');
209
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { changeLevel: 'refine' } }), 'response/response.md': responseMd({ changeLevel: 'refine', classification: 'locked-refine', references: 1 }) }, 'a refine works from the family idioms alone', 'refine that researched');
210
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { references: ['https://example.com/plans'] } }) }, 'the person supplied references and none of them is recorded', 'supplied reference dropped');
211
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ selectedFails: true }) }, 'fails an attack, so the direction is not decided', 'selected candidate fails an attack');
212
+ await expectError({ ...threeCandidates(), 'response/response.md': responseMd({ candidates: 3, rejectAll: false, fallbacks: ['DIRECTION_CHOICE_REQUIRED'] }) }, 'is not rejected by name', 'unselected candidate not rejected');
213
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ candidates: 2 }).replace('| `split-view` | it loses', '| `absent` | it loses') }, 'Falsification covers 2 candidates, the request asked for 1', 'more candidates than asked');
214
+ await expectError({ ...threeCandidates({ policy: 'approval-required', approval: 'split-view' }) }, 'but approval names split-view', 'approval names another candidate');
215
+ await expectError({ ...threeCandidates({ policy: 'approval-required' }) }, 'approval-required with no approval cannot end done', 'approval-required decided without an approval');
216
+ await expectError(withCoverage((c) => { c.regions = c.regions.slice(0, 1); }), 'COVERAGE-1: region is not covered: decision', 'region uncovered');
217
+ await expectError(withCoverage((c) => { c.actions = []; }), 'COVERAGE-1: actions must enumerate every declared action', 'actions uncovered');
218
+ await expectError(withCoverage((c) => { c.states[1].carrier = 'the offer region'; }), 'COVERAGE-1: two meanings share one carrier', 'two meanings on one carrier');
219
+ await expectError(withCoverage((c) => { c.actions[0].pendingPaths[0].settlement = ''; }), 'pending path without a settlement', 'unsettled pending path');
220
+ await expectError(withCoverage((c) => { c.directionId = 'other-picker'; }), 'differs from the receipt', 'coverage names another direction');
221
+ await expectError({ ...baseline(), 'response/data/coverage.json': null, 'response/response.json': responseJson({ fields: { 'frontend-direction-decision': 'response/response.md' } }) }, 'required output ui-coverage is not in fields', 'missing required output');
222
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Observed', '## Observations') }, 'missing section ^## Observed$', 'receipt section renamed');
223
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'DIRECTION_CHOICE_REQUIRED', next: [] }) }, 'has disposition fallback under these requirements', 'terminating on the choice under automatic');
224
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', next: [] }) }, 'not a registered code', 'unknown stop code');
225
+
226
+ process.stdout.write('frontend.direction.decide self-test: 4 valid branches, 24 rejected mutations\n');