@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,163 @@
1
+ // Proves validate.mjs on a synthetic session branch: one conforming resolution of a three-node tree
2
+ // (one application-owned gap, one Grammar-owned padding, one recorded gap), one branch with the
3
+ // contract emission off, one blocked on RULE_MISSING before anything was written, and one mutation
4
+ // 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 { validateResolutionStep } from './validate.mjs';
10
+
11
+ const TREE = 'response/artifacts/plan-picker.resolved.tsx';
12
+ const MAIN = 'body>main';
13
+ const SECTION = 'body>main>section';
14
+ const ASIDE = 'body>aside';
15
+ const FINGERPRINT = `sha256:${'a'.repeat(64)}`;
16
+
17
+ const inventory = () => ({
18
+ treeFingerprint: FINGERPRINT,
19
+ classNames: ['gap-6', 'gap-1'],
20
+ ruleIds: ['GAP-5', 'GAP-1'],
21
+ gaps: [{ nodePath: ASIDE, property: 'gap', missingPath: 'Common exposes no public path for the compact identity pair' }],
22
+ });
23
+
24
+ const resolvedTree = ({ emission = 'on' } = {}) => (emission === 'on'
25
+ ? `export default function PlanPicker() {
26
+ return (
27
+ <main className="gap-6" data-contract="GAP-5">
28
+ <SurfaceCard />
29
+ <aside className="gap-1" data-contract="GAP-1" />
30
+ </main>
31
+ );
32
+ }
33
+ `
34
+ : `export default function PlanPicker() {
35
+ return (
36
+ <main className="gap-6">
37
+ <SurfaceCard />
38
+ <aside className="gap-1" />
39
+ </main>
40
+ );
41
+ }
42
+ `);
43
+
44
+ const responseMd = ({ gaps = 1, removed = 1 } = {}) => `# frontend-presentation-resolution — plan-picker
45
+
46
+ ## Owner map
47
+
48
+ | Node | Property | Owner | Rule |
49
+ | --- | --- | --- | --- |
50
+ | \`${MAIN}\` | gap | app | \`GAP-5\` |
51
+ | \`${SECTION}\` | padding | grammar | \`PADDING-4\` |
52
+ | \`${ASIDE}\` | gap | app | \`GAP-1\` |
53
+
54
+ ## Rules chosen
55
+
56
+ | Node | Rule | Class | Condition |
57
+ | --- | --- | --- | --- |
58
+ | \`${MAIN}\` | \`GAP-5\` | \`gap-6\` | region stack inside the page shell |
59
+ | \`${ASIDE}\` | \`GAP-1\` | \`gap-1\` | the compact identity pair |
60
+
61
+ ## Removed
62
+
63
+ | Node | Class | Because |
64
+ | --- | --- | --- |
65
+ ${removed ? `| \`${SECTION}\` | \`p-4\` | reimplements an owned relationship |` : ''}
66
+
67
+ ## Gaps
68
+
69
+ | Node | Property | Missing path |
70
+ | --- | --- | --- |
71
+ ${gaps ? `| \`${ASIDE}\` | gap | Common exposes no public path for the compact identity pair |` : ''}
72
+
73
+ ## Fallbacks taken
74
+
75
+ | Code | Action |
76
+ | --- | --- |
77
+ `;
78
+
79
+ const requestJson = ({ extra = {} } = {}) => ({
80
+ schemaVersion: 9,
81
+ operatorId: 'frontend.presentation.resolve',
82
+ step: 2,
83
+ parallel: 1,
84
+ sessionId: 's-test',
85
+ contexts: [{ alias: '@knowledge/ui/presentation', head: null }, { alias: '@grammar/core', head: null }, { alias: '@workspaces/fe', head: 'd'.repeat(40) }],
86
+ requirements: { maxRounds: 2, contractEmission: 'on', resume: null, ...extra },
87
+ inputs: { 'frontend-direction-decision': 'step-1/parallel-1/response/response.md' },
88
+ resume: null,
89
+ });
90
+
91
+ const responseJson = ({ status = 'done', stop, fields, fallbacks = [], next = ['frontend.source.apply'] } = {}) => ({
92
+ schemaVersion: 9,
93
+ operatorId: 'frontend.presentation.resolve',
94
+ step: 2,
95
+ parallel: 1,
96
+ status,
97
+ ...(stop ? { stop } : {}),
98
+ fallbacks,
99
+ fields: fields ?? { 'frontend-presentation-resolution': 'response/response.md', inventory: 'response/data/inventory.json', 'resolved-tree': TREE },
100
+ commits: [],
101
+ next,
102
+ });
103
+
104
+ function writeBranch(files) {
105
+ const session = mkdtempSync(path.join(tmpdir(), 'fe-resolve-session-'));
106
+ const branch = path.join(session, 'step-2', 'parallel-1');
107
+ for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
108
+ mkdirSync(path.join(session, 'step-1', 'parallel-1', 'response'), { recursive: true });
109
+ writeFileSync(path.join(session, 'step-1', 'parallel-1', 'response', 'response.md'), '# frontend-direction-decision — plan-picker\n');
110
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1'], ['2/1']], steps: { '1/1': 'frontend.direction.decide', '2/1': 'frontend.presentation.resolve' }, current: '2/1', status: 'running' }));
111
+ for (const [name, content] of Object.entries(files)) {
112
+ if (content === null) continue;
113
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
114
+ }
115
+ return { branch, session };
116
+ }
117
+
118
+ const baseline = () => ({
119
+ 'request/request.json': requestJson(),
120
+ 'response/response.json': responseJson(),
121
+ 'response/response.md': responseMd(),
122
+ 'response/data/inventory.json': inventory(),
123
+ [TREE]: resolvedTree(),
124
+ });
125
+
126
+ async function expectValid(files, label) {
127
+ const { branch, session } = writeBranch(files);
128
+ const { errors } = await validateResolutionStep(branch);
129
+ rmSync(session, { recursive: true, force: true });
130
+ assert.deepEqual(errors, [], `${label} should be valid`);
131
+ }
132
+ async function expectError(files, needle, label) {
133
+ const { branch, session } = writeBranch(files);
134
+ const { errors } = await validateResolutionStep(branch);
135
+ rmSync(session, { recursive: true, force: true });
136
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
137
+ }
138
+ const mutate = (change) => { const m = inventory(); change(m); return { ...baseline(), 'response/data/inventory.json': m }; };
139
+
140
+ await expectValid(baseline(), 'one application-owned gap, one Grammar-owned padding, one recorded gap');
141
+ await expectValid({ ...baseline(), 'request/request.json': requestJson({ extra: { contractEmission: 'off' } }), [TREE]: resolvedTree({ emission: 'off' }) }, 'contract emission off leaves the tree bare');
142
+ await expectValid({ 'request/request.json': requestJson(), 'response/response.json': responseJson({ status: 'blocked', stop: 'RULE_MISSING', next: [], fields: {} }) }, 'blocked on RULE_MISSING before anything was written');
143
+
144
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
145
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { maxRounds: 0 } }) }, 'maxRounds must be a positive whole number', 'no rounds allowed');
146
+ await expectError(mutate((m) => { m.ruleIds.push('GAP-99'); }), 'is outside the published presentation inventory', 'unpublished rule');
147
+ await expectError(mutate((m) => { m.classNames.push('gap-11'); }), 'is in the inventory and not in the resolved tree', 'class that never reached the tree');
148
+ await expectError(mutate((m) => { m.treeFingerprint = 'nope'; }), 'treeFingerprint', 'inventory schema');
149
+ await expectError({ ...baseline(), [TREE]: resolvedTree().replace(' data-contract="GAP-1"', '') }, 'no node claims it under data-contract', 'applied rule with no claim');
150
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { contractEmission: 'off' } }) }, 'contract emission is off and the resolved tree carries a data-contract', 'claims written under emission off');
151
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('| `body>main` | `GAP-5` | `gap-6` |', '| `body>main` | `GAP-5` | `gap-5` |'), 'response/data/inventory.json': (() => { const m = inventory(); m.classNames = ['gap-5', 'gap-1']; return m; })(), [TREE]: resolvedTree().replace('gap-6', 'gap-5') }, 'renders GAP-5 as gap-5, expected step 6', 'ordinal written as the step');
152
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace(`| \`${SECTION}\` | padding | grammar | \`PADDING-4\` |`, `| \`${SECTION}\` | padding | app | \`PADDING-4\` |`) }, 'owns PADDING-4 and chooses no class for it', 'application claims an owned property and writes nothing');
153
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace(`| \`${ASIDE}\` | \`GAP-1\` | \`gap-1\` | the compact identity pair |`, `| \`${SECTION}\` | \`PADDING-4\` | \`p-4\` | the card inset |`) }, 'chooses a class for PADDING-4, which Grammar already owns', 'application reimplements a Grammar relationship');
154
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace(`| \`${MAIN}\` | gap | app | \`GAP-5\` |`, `| \`${MAIN}\` | gap | app | \`GAP-5\` |\n| \`${MAIN}\` | gap | app | \`GAP-4\` |`) }, 'decides gap more than once in the owner map', 'one property decided twice');
155
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ gaps: 0 }) }, 'the Gaps table and inventory.gaps differ', 'gap recorded in data and not for the reader');
156
+ await expectError(mutate((m) => { m.gaps = []; }), 'the Gaps table and inventory.gaps differ', 'gap recorded for the reader and not in data');
157
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('| `p-4` | reimplements an owned relationship |', '| `gap-6` | reimplements an owned relationship |') }, 'and the inventory still carries it', 'class removed and still inventoried');
158
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Removed', '## Deleted') }, 'missing section ^## Removed$', 'receipt section renamed');
159
+ await expectError({ ...baseline(), 'response/data/inventory.json': null, 'response/response.json': responseJson({ fields: { 'frontend-presentation-resolution': 'response/response.md', 'resolved-tree': TREE } }) }, 'required output inventory is not in fields', 'missing required output');
160
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', next: [] }) }, 'not a registered code', 'unknown stop code');
161
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['RULE_MISSING'] }) }, 'has disposition terminate under these requirements', 'a terminate code taken as a fallback');
162
+
163
+ process.stdout.write('frontend.presentation.resolve self-test: 3 valid branches, 18 rejected mutations\n');
@@ -0,0 +1,143 @@
1
+ // frontend.presentation.resolve's own law over one branch, on top of the shared step check: every rule the
2
+ // inventory carries is one @knowledge/ui/presentation actually publishes; every class the inventory
3
+ // carries appears in the resolved tree; a Grammar-owned property emits no application class while an
4
+ // application-owned one emits a class whose step matches the ordinal of its rule; the gaps the
5
+ // receipt lists are exactly the gaps the inventory records; and with emission on every claimed rule
6
+ // reaches the tree as a data-contract token.
7
+ import { existsSync } from 'node:fs';
8
+ import { readFile, readdir } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import process from 'node:process';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { validateStep } from '../../scripts/validate-step.mjs';
13
+ import { tableUnder } from '../../scripts/validate-response.mjs';
14
+
15
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
16
+ const empty = (v) => v === undefined || v === null || v === '' || v === '—';
17
+ const list = (v) => (Array.isArray(v) ? v : empty(v) ? [] : [v]);
18
+
19
+ // The scale topics publish an ordinal rule identifier while the utility scale publishes a step
20
+ // number, and the two diverge above the fourth value. Writing the ordinal as the class is the defect
21
+ // this map exists to catch: GAP-5 renders `gap-6`, never `gap-5`.
22
+ const ORDINAL_TO_STEP = { 0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '6', 6: '8' };
23
+ const SCALE_PATTERN = {
24
+ GAP: /^gap(?:-[xy])?-([0-9]+)$/,
25
+ PADDING: /^p(?:[xytrbse])?-([0-9]+)$/,
26
+ MARGIN: /^-?m(?:[xytrbse])?-([0-9]+)$/,
27
+ };
28
+ const prefixOf = (ruleId) => ruleId.slice(0, ruleId.lastIndexOf('-'));
29
+ const ordinalOf = (ruleId) => Number(ruleId.slice(ruleId.lastIndexOf('-') + 1));
30
+
31
+ // The bound inventory is what @knowledge/ui/presentation publishes: one `## PREFIX-n` heading per rule.
32
+ export async function publishedRuleIds(root = ROOT) {
33
+ const dir = path.join(root, 'knowledge', 'ui', 'presentation');
34
+ const ids = new Set();
35
+ if (!existsSync(dir)) return ids;
36
+ for (const name of (await readdir(dir)).filter((f) => f.endsWith('.md') && !f.endsWith('.vi.md') && f !== 'INDEX.md')) {
37
+ const text = await readFile(path.join(dir, name), 'utf8');
38
+ for (const m of text.matchAll(/^##\s+([A-Z][A-Z0-9-]*-[0-9]+)\b/gm)) ids.add(m[1]);
39
+ }
40
+ return ids;
41
+ }
42
+
43
+ export async function validateResolutionStep(branchDir, root = ROOT) {
44
+ const base = await validateStep(root, branchDir);
45
+ const errors = [...base.errors];
46
+ const { response, requirements = {}, present = new Set() } = base;
47
+ if (!response || response.operatorId !== 'frontend.presentation.resolve') return { errors };
48
+ const has = (f) => existsSync(path.join(branchDir, f));
49
+ const read = (f) => readFile(path.join(branchDir, f), 'utf8');
50
+
51
+ const maxRounds = Number(requirements.maxRounds ?? 2);
52
+ if (!Number.isInteger(maxRounds) || maxRounds < 1) errors.push(`request.json: maxRounds must be a positive whole number, not ${requirements.maxRounds}`);
53
+ const emission = requirements.contractEmission ?? 'on';
54
+
55
+ let inventory = null;
56
+ if (present.has('inventory') && has('response/data/inventory.json')) {
57
+ try { inventory = JSON.parse(await read('response/data/inventory.json')); } catch { inventory = null; }
58
+ }
59
+ if (!inventory) {
60
+ if (response.status === 'done') errors.push('response/data/inventory.json: a done branch needs the inventory');
61
+ return { errors };
62
+ }
63
+ const at = 'response/data/inventory.json';
64
+
65
+ const published = await publishedRuleIds(root);
66
+ for (const ruleId of inventory.ruleIds) {
67
+ if (!published.has(ruleId)) errors.push(`${at}: rule ${ruleId} is outside the published presentation inventory (UNKNOWN_RULE)`);
68
+ }
69
+
70
+ // The resolved tree is the only place the classes may land, so every class the inventory claims
71
+ // must actually be in it.
72
+ const treeRefs = list(response.fields?.['resolved-tree']);
73
+ let tree = null;
74
+ for (const ref of treeRefs) if (has(ref)) tree = await read(ref);
75
+ if (tree === null) { if (response.status === 'done') errors.push('response/artifacts: the resolved tree is missing'); }
76
+ else {
77
+ for (const className of inventory.classNames) {
78
+ if (!tree.includes(className)) errors.push(`${at}: class ${className} is in the inventory and not in the resolved tree`);
79
+ }
80
+ if (emission === 'on') {
81
+ for (const ruleId of inventory.ruleIds) {
82
+ const claimed = [...tree.matchAll(/data-contract="([^"]*)"/g)].some((m) => m[1].split(/\s+/).includes(ruleId));
83
+ if (!claimed) errors.push(`${at}: rule ${ruleId} is applied and no node claims it under data-contract`);
84
+ }
85
+ } else if (/data-contract=/.test(tree)) errors.push('response/artifacts: contract emission is off and the resolved tree carries a data-contract attribute');
86
+ }
87
+
88
+ if (present.has('frontend-presentation-resolution') && has('response/response.md')) {
89
+ const text = await read('response/response.md');
90
+ const rel = 'response/response.md';
91
+ const owners = tableUnder(text, '## Owner map') ?? [];
92
+ const chosen = tableUnder(text, '## Rules chosen') ?? [];
93
+ const removed = tableUnder(text, '## Removed') ?? [];
94
+ const gaps = tableUnder(text, '## Gaps') ?? [];
95
+
96
+ const appRules = new Set();
97
+ const grammarRules = new Set();
98
+ const seen = new Set();
99
+ for (const [node, property, owner, rule] of owners) {
100
+ const key = `${node}|${property}`;
101
+ if (seen.has(key)) errors.push(`${rel}: ${node} decides ${property} more than once in the owner map`);
102
+ seen.add(key);
103
+ (owner === 'app' ? appRules : grammarRules).add(`${node}|${rule}`);
104
+ }
105
+ for (const [node, rule, className] of chosen) {
106
+ if (grammarRules.has(`${node}|${rule}`)) { errors.push(`${rel}: ${node} chooses a class for ${rule}, which Grammar already owns`); continue; }
107
+ if (!appRules.has(`${node}|${rule}`)) { errors.push(`${rel}: ${node} chooses ${rule} without an application-owned row in the owner map`); continue; }
108
+ if (!inventory.ruleIds.includes(rule)) errors.push(`${rel}: ${node} chooses ${rule}, which the inventory does not carry`);
109
+ if (!inventory.classNames.includes(className)) errors.push(`${rel}: ${node} writes ${className}, which the inventory does not carry`);
110
+ const pattern = SCALE_PATTERN[prefixOf(rule)];
111
+ if (pattern) {
112
+ const expected = ORDINAL_TO_STEP[ordinalOf(rule)];
113
+ const steps = className.split(/\s+/).map((token) => pattern.exec(token)).filter(Boolean).map((m) => m[1]);
114
+ if (expected !== undefined) {
115
+ if (steps.length === 0) errors.push(`${rel}: ${node} emits no class for ${rule}`);
116
+ else if (!steps.includes(expected)) errors.push(`${rel}: ${node} renders ${rule} as ${className}, expected step ${expected}`);
117
+ }
118
+ }
119
+ }
120
+ for (const key of appRules) {
121
+ const [node, rule] = key.split('|');
122
+ if (!chosen.some(([n, r]) => n === node && r === rule)) errors.push(`${rel}: ${node} owns ${rule} and chooses no class for it`);
123
+ }
124
+ for (const [node, className] of removed) {
125
+ if (inventory.classNames.includes(className)) errors.push(`${rel}: ${node} removed ${className} and the inventory still carries it`);
126
+ }
127
+
128
+ // The gaps a reader sees and the gaps the machine carries are one list.
129
+ const receiptGaps = gaps.map(([node, property, missing]) => `${node}|${property}|${missing}`).sort();
130
+ const dataGaps = inventory.gaps.map((g) => `${g.nodePath}|${g.property}|${g.missingPath}`).sort();
131
+ if (receiptGaps.join('\n') !== dataGaps.join('\n')) {
132
+ errors.push(`${rel}: the Gaps table and inventory.gaps differ (${receiptGaps.length} rows against ${dataGaps.length})`);
133
+ }
134
+ }
135
+ return { errors };
136
+ }
137
+
138
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
139
+ const target = process.argv[2];
140
+ if (!target) { process.stderr.write('usage: node validate.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
141
+ const { errors } = await validateResolutionStep(path.resolve(target));
142
+ if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('valid frontend.presentation.resolve branch\n');
143
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "note": "Stop codes frontend.source.apply emits. Same entry shape as operators/errors.json, scope implicit. Shared codes (INVALID_INPUT, SOURCE_DRIFT, NO_PROGRESS) come from operators/errors.json. OWNER_CONFLICT is also emitted here but is defined in operators/frontend-presentation-resolve/errors.json; it belongs in operators/errors.json with every emitting operator in scope.",
4
+ "codes": {
5
+ "RESOLUTION_STALE": {
6
+ "domain": "resolution",
7
+ "disposition": "terminate",
8
+ "meaning": {
9
+ "en": "The resolution actually read differs from the resolution the request bound.",
10
+ "vi": "Resolution thực sự đọc được khác resolution mà request đã bind."
11
+ },
12
+ "resume": {
13
+ "en": "Bind the current resolution, or resolve the tree again.",
14
+ "vi": "Bind resolution hiện tại, hoặc resolve lại cây."
15
+ }
16
+ },
17
+ "WRITE_REJECTED": {
18
+ "domain": "caller",
19
+ "disposition": "terminate",
20
+ "meaning": {
21
+ "en": "A file or a value the write would produce lies outside what was authorized, or the committed tree is not the resolved tree.",
22
+ "vi": "Một file hay một giá trị lần ghi sẽ tạo ra nằm ngoài phần đã được uỷ quyền, hoặc cây đã commit không phải cây đã resolve."
23
+ },
24
+ "resume": {
25
+ "en": "Declare a corrected write set, or publish a resolution that carries the value.",
26
+ "vi": "Khai lại write set cho đúng, hoặc publish một resolution có mang giá trị đó."
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 9,
3
+ "id": "frontend.source.apply",
4
+ "domain": "frontend",
5
+ "job": "Write one already-resolved tree into product source inside a frozen owner ceiling and a declared file set, emitting only values the bound resolution receipt already contains.",
6
+ "package": "operator.md",
7
+ "errors": "errors.json",
8
+ "validator": "validate.mjs",
9
+ "selfTest": "self-test.mjs",
10
+ "resources": {
11
+ "profile": "luna",
12
+ "grammarBound": true,
13
+ "tools": {
14
+ "@tools/fileread": "context-aliases",
15
+ "@tools/sourcewrite": "declared-write-set",
16
+ "@tools/git": "commit-session-branch",
17
+ "@tools/imagegen": "judged"
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,127 @@
1
+ # frontend.source.apply
2
+
3
+ ## Job
4
+
5
+ Write one already-resolved tree into product source on the session branch, inside a frozen owner
6
+ ceiling and a declared file set, emitting only values the bound resolution already contains, and
7
+ account for every byte that entered the repository in one commit.
8
+
9
+ ## The single writer
10
+
11
+ This is the only operator in the frontend pipeline that writes product source. Direction decides what
12
+ to build and writes nothing; resolution decides every value and writes only its own artifact; the
13
+ audit observes and writes nothing. One writer exists so that one receipt can account for every byte
14
+ that entered the repository, which is impossible when three operators each write a little. Because it
15
+ is the only writer, it is also the only place a fabricated value could enter source, and it has no way
16
+ to produce one.
17
+
18
+ ## The session branch
19
+
20
+ The operator never writes on the person's checked-out branch. It writes only on `session/<sessionId>`
21
+ of the routed checkout, in the git worktree the orchestrator prepared from the frozen head, and it
22
+ holds an exclusive lease on that worktree while it writes. The declared write set is committed exactly
23
+ once; `response.json` carries that one sha under `commits`, the `changes.md` Binding row reads
24
+ `@workspaces/fe` at the base head then the new sha on `session/<sessionId>`, and the next requests pin
25
+ `@workspaces/fe` at that sha. Nothing is pushed and nothing is merged here: `git.publish` owns both.
26
+
27
+ ## No invented value
28
+
29
+ Every class the write produces already appears in the bound resolution's class inventory, and every
30
+ identifier it carries into a claim already appears in the applied rule inventory. Both lists are
31
+ complete and frozen. A class absent from the resolution is `WRITE_REJECTED`: there is no rounding to a
32
+ nearby value, no copying from a neighbouring file, and no reformatting that changes a step. A file the
33
+ write would touch that the write set does not declare is `WRITE_REJECTED` too, even when it plainly
34
+ needs the change; the correct next step is a corrected write set, not a wider write. A declared path
35
+ whose owner root does not contain it is `OWNER_CONFLICT`, because owner membership alone is not the
36
+ ceiling. The inventory check runs on the projection, before anything is written, so a rejected
37
+ application leaves source untouched.
38
+
39
+ ## An application-owned node becomes an empty leaf
40
+
41
+ When the direction marks a node as owned by the application, a canvas for instance, the projection
42
+ writes an empty leaf file that carries that node's contract, its props and its states, so the tree
43
+ compiles and the surface can be rendered and measured. The operator never writes that leaf's logic:
44
+ the contract is what the direction decided, and the behaviour behind it belongs to whoever owns the
45
+ node.
46
+
47
+ ## Unchanged is a measurement
48
+
49
+ Every declared path is hashed before the projection and after the commit. A path whose projection
50
+ differs from its current content is created or modified; a path whose projection equals it is recorded
51
+ `unchanged`. After the commit the tree is read back and compared against the resolved tree, and a
52
+ difference is `WRITE_REJECTED` rather than a silent success. Under `mode = dry` nothing is written at
53
+ all: the branch emits the plan in `response/data/writes.json` with a null commit and stops there.
54
+
55
+ ## Boundary
56
+
57
+ The operator writes the declared write-set paths on the session branch of `@workspaces/fe`, each under
58
+ a mutable owner root, and its own `response/`. It does not write a class, value or rule identifier
59
+ absent from the bound resolution, decide a presentation value, choose a Grammar component, restructure
60
+ the tree, write the logic of an application-owned leaf, touch a file outside the declared write set,
61
+ commit to any other branch, push, merge, edit knowledge, publish Grammar, change the resolution, start
62
+ or reconfigure runtime services, or record a verdict, score or pass claim on the applied source. It
63
+ knows what it wrote, never how it renders.
64
+
65
+ ## Context
66
+
67
+ | Alias | Bind | Required |
68
+ | --- | --- | --- |
69
+ | `@workspaces/fe` | the routed frontend checkout at the frozen head; the write lands on its session branch and nowhere else | yes |
70
+
71
+ ## Inputs
72
+
73
+ | Kind | From | Required |
74
+ | --- | --- | --- |
75
+ | `frontend-presentation-resolution` | `frontend.presentation.resolve`, the resolved tree and, beside it, the frozen class and rule inventory | yes |
76
+ | `frontend-direction-decision` | `frontend.direction.decide`, the intent and the owner ceiling; never a source of values | yes |
77
+
78
+ ## Requirements
79
+
80
+ | Field | Type | Default | Ask |
81
+ | --- | --- | --- | --- |
82
+ | `mode` | choice | apply | `apply` writes and commits, `dry` emits the plan and writes nothing |
83
+ | `resume` | token | null | The blocked branch's token when re-entering after a stop |
84
+
85
+ ## Steps
86
+
87
+ | # | Step | Params | Reads | Writes | Stops with |
88
+ | --- | --- | --- | --- | --- | --- |
89
+ | 1 | Validate the gate and resume, and confirm the head | `resume`, `mode` | `request/request.json`, input `frontend-presentation-resolution`, @workspaces/fe at the frozen head | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
90
+ | 2 | Bind the resolution, the direction and the declared write set | — | inputs `frontend-presentation-resolution` (tree fingerprint, class and rule inventory, resolved tree) and `frontend-direction-decision` (intent and owner ceiling), @workspaces/fe (the declared paths and their owner roots) | — | `RESOLUTION_STALE`, `OWNER_CONFLICT` |
91
+ | 3 | Project the resolved tree onto the declared paths | — | input `frontend-presentation-resolution` (the resolved tree), @workspaces/fe (the declared write set) | — | — |
92
+ | 4 | Check every produced value against the inventory | `mode` | input `frontend-presentation-resolution` (the inventory beside the receipt) | `response/data/writes.json` | `WRITE_REJECTED` |
93
+ | 5 | Write atomically on the session branch and commit once | — | @workspaces/fe (the current content of each declared path, under an exclusive lease) | @workspaces/fe/branch/session, `response/data/writes.json`, @tools/sourcewrite, @tools/git, image assets the direction judged, @tools/imagegen | — |
94
+ | 6 | Read the tree back at the commit | — | @workspaces/fe at the commit | — | `WRITE_REJECTED` |
95
+ | 7 | Emit | — | everything above | `response/response.md`, `response/changes.md`, `response/response.json` | — |
96
+
97
+ Under `mode = dry` the branch stops after step 4 with the plan alone: `writes.json` carries a null
98
+ commit, `response.json` carries no commit, and the checkout is untouched. Under `apply`, step 5 writes
99
+ and commits exactly once and step 6 proves that the committed tree is the resolved tree. `changes.md`
100
+ is the record the next steps read: which paths moved, which claims they carry, which gates the
101
+ checkout pins for them, and which surfaces must now be observed.
102
+
103
+ ## Outputs
104
+
105
+ | Kind | File | Type | Required |
106
+ | --- | --- | --- | --- |
107
+ | `frontend-source-application` | `response/response.md` | md | yes |
108
+ | `changes` | `response/changes.md` | md | yes |
109
+ | `writes` | `response/data/writes.json` | data | yes |
110
+
111
+ ## Stops
112
+
113
+ | Code | Disposition |
114
+ | --- | --- |
115
+ | `INVALID_INPUT` | terminate |
116
+ | `SOURCE_DRIFT` | terminate |
117
+ | `OWNER_CONFLICT` | terminate |
118
+ | `RESOLUTION_STALE` | terminate |
119
+ | `WRITE_REJECTED` | terminate |
120
+ | `NO_PROGRESS` | terminate |
121
+
122
+ ## Next
123
+
124
+ | When | Operator |
125
+ | --- | --- |
126
+ | the source is committed and the rendered surface must be measured | `frontend.surface.audit` |
127
+ | the source is committed and the checkout's own gates must run | `quality.verify` |
@@ -0,0 +1,126 @@
1
+ # frontend.source.apply
2
+
3
+ ## Việc
4
+
5
+ Ghi một cây đã resolve vào source sản phẩm trên nhánh phiên, bên trong một trần owner đã đóng băng và
6
+ một tập file đã khai, chỉ phát ra những giá trị mà resolution được bind đã chứa sẵn, và chịu trách
7
+ nhiệm cho từng byte đã vào repository trong đúng một commit.
8
+
9
+ ## Người ghi duy nhất
10
+
11
+ Đây là operator duy nhất trong chuỗi frontend ghi source sản phẩm. Direction quyết dựng cái gì và
12
+ không ghi gì; resolution quyết mọi giá trị và chỉ ghi artifact của chính nó; audit quan sát và không
13
+ ghi gì. Một người ghi duy nhất tồn tại để một biên nhận duy nhất chịu trách nhiệm cho từng byte đã vào
14
+ repository, điều bất khả thi khi ba operator mỗi anh ghi một ít. Vì là người ghi duy nhất, nó cũng là
15
+ chỗ duy nhất một giá trị bịa có thể lọt vào source, nên nó không có đường nào để tạo ra một giá trị
16
+ như vậy.
17
+
18
+ ## Nhánh phiên
19
+
20
+ Operator không bao giờ ghi lên nhánh mà người dùng đang checkout. Nó chỉ ghi trên
21
+ `session/<sessionId>` của checkout được route, trong git worktree mà orchestrator đã dựng sẵn từ head
22
+ đóng băng, và nó giữ một lease độc quyền trên worktree ấy suốt lúc ghi. Write set đã khai được commit
23
+ đúng một lần; `response.json` mang đúng một sha đó trong `commits`, dòng Checkout của `changes.md` ghi
24
+ `@workspaces/fe` ở head gốc rồi tới sha mới trên `session/<sessionId>`, và các request sau ghim
25
+ `@workspaces/fe` ở sha đó. Ở đây không push và không merge: `git.publish` sở hữu cả hai việc ấy.
26
+
27
+ ## Không có giá trị tự nghĩ ra
28
+
29
+ Mọi class mà lần ghi tạo ra đều đã có sẵn trong kho class của resolution được bind, và mọi identifier
30
+ nó mang vào một lời khai đều đã có sẵn trong kho luật đã áp dụng. Cả hai danh sách đều đầy đủ và đóng
31
+ băng. Một class không có trong resolution là `WRITE_REJECTED`: không làm tròn về giá trị gần, không
32
+ chép từ file bên cạnh, không định dạng lại theo kiểu đổi mất một bước thang. Một file mà lần ghi sẽ
33
+ chạm tới nhưng write set không khai cũng là `WRITE_REJECTED`, kể cả khi rõ ràng nó cần đổi; bước đúng
34
+ tiếp theo là sửa write set, không phải ghi rộng ra. Một path đã khai mà gốc owner của nó không chứa là
35
+ `OWNER_CONFLICT`, vì thuộc về owner nào chưa phải là trần. Phép kiểm kho chạy trên bản chiếu, trước
36
+ khi ghi bất cứ thứ gì, nên một lần áp dụng bị từ chối để source y nguyên.
37
+
38
+ ## Node do ứng dụng sở hữu thành một file lá rỗng
39
+
40
+ Khi hướng đánh dấu một node là do ứng dụng sở hữu, chẳng hạn một canvas, bản chiếu ghi ra một file lá
41
+ rỗng mang contract của node đó, tức props và states của nó, để cây biên dịch được và bề mặt render ra
42
+ đo được. Operator không bao giờ viết phần logic của file lá ấy: contract là thứ hướng đã quyết, còn
43
+ hành vi phía sau nó thuộc về ai sở hữu node.
44
+
45
+ ## Không đổi là một phép đo
46
+
47
+ Mọi path đã khai đều được băm trước bản chiếu và sau commit. Path có bản chiếu khác nội dung hiện tại
48
+ thì được tạo hoặc sửa; path có bản chiếu bằng đúng nội dung hiện tại được ghi `unchanged`. Sau commit,
49
+ cây được đọc lại và đối chiếu với cây đã resolve, và một chỗ lệch là `WRITE_REJECTED` chứ không phải
50
+ một thành công lặng lẽ. Dưới `mode = dry` không có gì được ghi cả: nhánh phát ra bản kế hoạch trong
51
+ `response/data/writes.json` với commit rỗng rồi dừng ở đó.
52
+
53
+ ## Ranh giới
54
+
55
+ Operator ghi các path trong write set đã khai trên nhánh phiên của `@workspaces/fe`, mỗi path nằm
56
+ dưới một gốc owner được sửa, và ghi `response/` của chính nó. Nó không ghi class, giá trị hay
57
+ identifier vắng mặt trong resolution được bind, không quyết một giá trị trình bày, không chọn
58
+ component Grammar, không dựng lại cây, không viết logic của file lá do ứng dụng sở hữu, không chạm
59
+ file ngoài write set đã khai, không commit lên nhánh nào khác, không push, không merge, không sửa
60
+ knowledge, không publish Grammar, không đổi resolution, không khởi động hay cấu hình lại dịch vụ
61
+ runtime, và không ghi phán quyết, điểm số hay tuyên bố pass lên source đã áp dụng. Nó biết nó ghi gì,
62
+ không bao giờ biết nó render ra sao.
63
+
64
+ ## Context
65
+
66
+ | Alias | Bind | Bắt buộc |
67
+ | --- | --- | --- |
68
+ | `@workspaces/fe` | checkout frontend được route ở head đóng băng; lần ghi đáp xuống nhánh phiên của nó và không đâu khác | có |
69
+
70
+ ## Đầu vào
71
+
72
+ | Kind | Từ đâu | Bắt buộc |
73
+ | --- | --- | --- |
74
+ | `frontend-presentation-resolution` | `frontend.presentation.resolve`, cây đã resolve và, ngay bên cạnh, kho class và kho luật đã đóng băng | có |
75
+ | `frontend-direction-decision` | `frontend.direction.decide`, ý đồ và trần owner; không bao giờ là nguồn giá trị | có |
76
+
77
+ ## Yêu cầu
78
+
79
+ | Field | Kiểu | Mặc định | Hỏi |
80
+ | --- | --- | --- | --- |
81
+ | `mode` | choice | apply | `apply` ghi rồi commit, `dry` chỉ phát bản kế hoạch và không ghi gì |
82
+ | `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
83
+
84
+ ## Các bước
85
+
86
+ | # | Bước | Tham số | Đọc | Ghi | Dừng với |
87
+ | --- | --- | --- | --- | --- | --- |
88
+ | 1 | Kiểm gate, chạy lại, và xác nhận head | `resume`, `mode` | `request/request.json`, đầu vào `frontend-presentation-resolution`, @workspaces/fe ở head đóng băng | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
89
+ | 2 | Bind resolution, direction và write set đã khai | — | đầu vào `frontend-presentation-resolution` (fingerprint cây, kho class và kho luật, cây đã resolve) và `frontend-direction-decision` (ý đồ và trần owner), @workspaces/fe (các path đã khai và gốc owner của chúng) | — | `RESOLUTION_STALE`, `OWNER_CONFLICT` |
90
+ | 3 | Chiếu cây đã resolve lên các path đã khai | — | đầu vào `frontend-presentation-resolution` (cây đã resolve), @workspaces/fe (write set đã khai) | — | — |
91
+ | 4 | Đối chiếu mọi giá trị tạo ra với kho | `mode` | đầu vào `frontend-presentation-resolution` (kho nằm cạnh biên nhận) | `response/data/writes.json` | `WRITE_REJECTED` |
92
+ | 5 | Ghi nguyên khối trên nhánh phiên và commit một lần | — | @workspaces/fe (nội dung hiện tại của từng path đã khai, dưới một lease độc quyền) | @workspaces/fe/branch/session, `response/data/writes.json`, @tools/sourcewrite, @tools/git, asset ảnh mà direction phán là cần, @tools/imagegen | — |
93
+ | 6 | Đọc lại cây ở commit | — | @workspaces/fe ở commit | — | `WRITE_REJECTED` |
94
+ | 7 | Phát | — | mọi thứ ở trên | `response/response.md`, `response/changes.md`, `response/response.json` | — |
95
+
96
+ Dưới `mode = dry`, nhánh dừng sau bước 4 với riêng bản kế hoạch: `writes.json` mang commit rỗng,
97
+ `response.json` không mang commit nào, và checkout y nguyên. Dưới `apply`, bước 5 ghi rồi commit đúng
98
+ một lần và bước 6 chứng minh rằng cây đã commit đúng là cây đã resolve. `changes.md` là bản ghi mà
99
+ các bước sau đọc: path nào đã dịch chuyển, chúng mang lời khai nào, checkout ghim cổng nào cho chúng,
100
+ và bề mặt nào bây giờ phải được quan sát.
101
+
102
+ ## Đầu ra
103
+
104
+ | Kind | File | Kiểu | Bắt buộc |
105
+ | --- | --- | --- | --- |
106
+ | `frontend-source-application` | `response/response.md` | md | có |
107
+ | `changes` | `response/changes.md` | md | có |
108
+ | `writes` | `response/data/writes.json` | data | có |
109
+
110
+ ## Dừng
111
+
112
+ | Code | Xử lý |
113
+ | --- | --- |
114
+ | `INVALID_INPUT` | terminate |
115
+ | `SOURCE_DRIFT` | terminate |
116
+ | `OWNER_CONFLICT` | terminate |
117
+ | `RESOLUTION_STALE` | terminate |
118
+ | `WRITE_REJECTED` | terminate |
119
+ | `NO_PROGRESS` | terminate |
120
+
121
+ ## Kế tiếp
122
+
123
+ | Khi | Operator |
124
+ | --- | --- |
125
+ | source đã commit và bề mặt render ra phải được đo | `frontend.surface.audit` |
126
+ | source đã commit và các cổng của chính checkout phải chạy | `quality.verify` |