@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,681 @@
1
+ // Generate the "<Topic> Common already owns" table in every `knowledge/ui/presentation` topic from
2
+ // the live `@grammar/core` package, so the ownership tables stop being hand-written prose that
3
+ // drifts away from the components they describe.
4
+ //
5
+ // Every Grammar element publishes the identifiers it claims as `data-contract`. That attribute is
6
+ // the only input here: the script parses `packages/grammar/src/core/**/*.ts|tsx`, resolves each
7
+ // `data-contract` value (a static string, a template, a ternary, a local constant, a lookup map, or
8
+ // a helper function), and prints one row per component, element-or-condition, and rule id. It never
9
+ // prints a value the source does not state: the old tables carried internal values such as
10
+ // `.75rem`, which no claim carries, so the generated table carries the rule id alone and the CSS
11
+ // stays the authority on what that rule resolves to.
12
+ //
13
+ // "Element or condition" is read from the source too, in this order: an element carrying
14
+ // `data-component="<Component>"`, or the component's own returned root, is `root`; otherwise the
15
+ // element's `data-grammar-*` marker, its `*ClassName` binding or `starci-core-*` class, its only
16
+ // child, or its tag. When the claim is computed the prop condition that selects it is appended, as
17
+ // `composition="joined"` or `density="comfortable"`.
18
+ //
19
+ // Usage:
20
+ // node scripts/generate-presentation-owned.mjs [--grammar <path to packages/grammar>]
21
+ // node scripts/generate-presentation-owned.mjs --check exits 1 when the committed tables differ
22
+ //
23
+ // NOT part of `npm test`: like `generate-grammar-dna.mjs` it needs the routed FE checkout on disk,
24
+ // which CI and a fresh clone do not have. It additionally needs that checkout's own `typescript`,
25
+ // because a claim's element and condition are read from the syntax tree rather than guessed with a
26
+ // regular expression. Run it by hand after a Grammar change, and verify with `--check`.
27
+ import { createRequire } from 'node:module';
28
+ import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
29
+ import path from 'node:path';
30
+ import process from 'node:process';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const claudeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const sourceRoot = path.resolve(claudeRoot, '..');
35
+ const knowledgeDir = path.join(claudeRoot, 'knowledge');
36
+ const presentationDir = path.join(knowledgeDir, 'ui', 'presentation');
37
+
38
+ const argv = process.argv.slice(2);
39
+ const checkOnly = argv.includes('--check');
40
+ const grammarFlag = argv.indexOf('--grammar');
41
+
42
+ function resolveGrammarRoot() {
43
+ if (grammarFlag !== -1) {
44
+ const given = argv[grammarFlag + 1];
45
+ if (!given) throw new Error('--grammar needs a path');
46
+ return path.resolve(given);
47
+ }
48
+ const route = path.join(sourceRoot, '.workspaces', 'local', 'routes', 'starci-academy', 'fe', 'config.json');
49
+ if (!existsSync(route)) throw new Error(`no routed FE checkout: ${route} is missing (pass --grammar <path>)`);
50
+ const diskPath = JSON.parse(readFileSync(route, 'utf8')).repository?.diskPath;
51
+ if (!diskPath) throw new Error(`${route}: repository.diskPath is absent`);
52
+ return path.join(diskPath, 'packages', 'grammar');
53
+ }
54
+
55
+ const grammarRoot = resolveGrammarRoot();
56
+ const srcDir = path.join(grammarRoot, 'src');
57
+ if (!existsSync(srcDir)) throw new Error(`${srcDir} is not a Grammar checkout`);
58
+
59
+ // The checkout's own compiler, so the tree that is parsed is the one the package builds with.
60
+ function loadTypeScript() {
61
+ for (const from of [grammarRoot, path.resolve(grammarRoot, '..', '..')]) {
62
+ try {
63
+ return createRequire(path.join(from, 'package.json'))('typescript');
64
+ } catch { /* try the next resolution root */ }
65
+ }
66
+ throw new Error(`typescript is not installed in ${grammarRoot}: run the FE checkout's install first`);
67
+ }
68
+ const ts = loadTypeScript();
69
+
70
+ const read = (p) => readFileSync(p, 'utf8');
71
+
72
+ // ---------------------------------------------------------------- topics
73
+ // One entry per presentation topic. `heading` is the exact block heading each file already carries;
74
+ // the generator rewrites the block between it and the next `## `, and never invents a heading text.
75
+ // `font.md` and `tone.md` carry no such block yet, so `insertBefore` names the rule heading the new
76
+ // block is placed above.
77
+ const TOPICS = [
78
+ { prefix: 'GAP', file: 'gap', heading: 'Gaps Common already owns', vi: 'Gap mà Common đã sở hữu' },
79
+ { prefix: 'PADDING', file: 'padding', heading: 'Padding Common already owns', vi: 'Padding mà Common đã sở hữu' },
80
+ { prefix: 'MARGIN', file: 'margin', heading: 'Margin Common already owns', vi: 'Margin mà Common đã sở hữu' },
81
+ { prefix: 'FONT', file: 'font', heading: 'Font Common already owns', vi: 'Font mà Common đã sở hữu', insertBefore: 'FONT-1' },
82
+ { prefix: 'TONE', file: 'tone', heading: 'Tone Common already owns', vi: 'Tone mà Common đã sở hữu', insertBefore: 'TONE-1' },
83
+ { prefix: 'MEASURE', file: 'measure', heading: 'Measure Common already owns', vi: 'Measure mà Common đã sở hữu' },
84
+ { prefix: 'FLOW', file: 'text-flow', heading: 'Text flow Common already owns', vi: 'Text flow mà Common đã sở hữu' },
85
+ { prefix: 'OVERFLOW', file: 'overflow', heading: 'Overflow Common already owns', vi: 'Overflow mà Common đã sở hữu' },
86
+ { prefix: 'SURFACE', file: 'surface', heading: 'Surfaces Common already owns', vi: 'Surface mà Common đã sở hữu' },
87
+ { prefix: 'BOUNDARY', file: 'boundary', heading: 'Boundaries Common already owns', vi: 'Boundary mà Common đã sở hữu' },
88
+ ];
89
+
90
+ // ---------------------------------------------------------------- published rule ids
91
+ // A rule exists only as a `## PREFIX-n — …` heading in a canonical knowledge file, the same
92
+ // inventory `validate-knowledge-citations.mjs` builds. Only ids of the ten presentation prefixes can
93
+ // appear in these tables, so the scan is narrowed to them.
94
+ const HEADING = /^## ([A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-(?:\d+|AUTO))\b/;
95
+ function walkMd(dir) {
96
+ const out = [];
97
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
98
+ const full = path.join(dir, entry.name);
99
+ if (entry.isDirectory()) out.push(...walkMd(full));
100
+ else if (entry.name.endsWith('.md') && !entry.name.endsWith('.vi.md')) out.push(full);
101
+ }
102
+ return out;
103
+ }
104
+ const prefixes = new Set(TOPICS.map((t) => t.prefix));
105
+ const publishedIds = new Set();
106
+ for (const file of walkMd(knowledgeDir)) {
107
+ for (const line of read(file).split(/\r?\n/)) {
108
+ const m = HEADING.exec(line);
109
+ if (m && prefixes.has(m[1].replace(/-(?:\d+|AUTO)$/, ''))) publishedIds.add(m[1]);
110
+ }
111
+ }
112
+ if (publishedIds.size === 0) throw new Error('no presentation rule ids are published');
113
+ const ID_RE = new RegExp(`\\b(?:${[...publishedIds].sort((a, b) => b.length - a.length).join('|')})\\b`, 'g');
114
+
115
+ // ---------------------------------------------------------------- source files
116
+ function walkSource(dir) {
117
+ const out = [];
118
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
119
+ const full = path.join(dir, entry.name);
120
+ if (entry.isDirectory()) { out.push(...walkSource(full)); continue; }
121
+ if (!/\.(ts|tsx)$/.test(entry.name)) continue;
122
+ if (entry.name.includes('.spec.') || entry.name.includes('.test.')) continue;
123
+ out.push(full);
124
+ }
125
+ return out;
126
+ }
127
+ // `core/**` holds every renderer folder plus the three bare renderers that are a file of their own.
128
+ const sourceFiles = walkSource(path.join(srcDir, 'core')).sort();
129
+ // A renderer folder is one unit: `Label/index.tsx` claims through `Label/classNames.ts`, so module
130
+ // scope is resolved across the folder rather than per file.
131
+ const folderFiles = new Map();
132
+ for (const file of sourceFiles) {
133
+ const folder = path.dirname(file);
134
+ if (!folderFiles.has(folder)) folderFiles.set(folder, []);
135
+ folderFiles.get(folder).push(file);
136
+ }
137
+
138
+ // ---------------------------------------------------------------- naming helpers
139
+ const words = (text) => text
140
+ .replace(/[^A-Za-z0-9]+/g, ' ')
141
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
142
+ .trim()
143
+ .toLowerCase()
144
+ .split(/\s+/)
145
+ .filter(Boolean);
146
+
147
+ // A role reads as the part of the name the component itself does not already say: inside
148
+ // `SurfaceCard`, `data-grammar-surface-label` is the "label", not the "surface label".
149
+ function roleFrom(name, component) {
150
+ const own = words(component);
151
+ const parts = words(name);
152
+ let i = 0;
153
+ while (i < parts.length && i < own.length && parts[i] === own[i]) i += 1;
154
+ const rest = parts.slice(i);
155
+ return rest.length === 0 ? '' : rest.join(' ');
156
+ }
157
+
158
+ // ---------------------------------------------------------------- per-file analysis
159
+ const claims = []; // { component, role, conditions[], id }
160
+
161
+ const textOf = (node) => node.getText().replace(/\s+/g, ' ').trim();
162
+
163
+ // `as const`, `satisfies X` and parentheses wrap a value without changing it, and a lookup map that
164
+ // keeps its wrapper would be walked as plain text instead of keyed by the prop that selects a row.
165
+ function unwrap(node) {
166
+ if (!node) return node;
167
+ const kind = node.kind;
168
+ if (kind === ts.SyntaxKind.AsExpression || kind === ts.SyntaxKind.SatisfiesExpression
169
+ || kind === ts.SyntaxKind.ParenthesizedExpression || kind === ts.SyntaxKind.TypeAssertionExpression) {
170
+ return unwrap(node.expression);
171
+ }
172
+ return node;
173
+ }
174
+
175
+ const parsed = new Map();
176
+ for (const file of sourceFiles) {
177
+ parsed.set(file, ts.createSourceFile(file, read(file), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX));
178
+ }
179
+
180
+ for (const file of sourceFiles) {
181
+ const sourceFile = parsed.get(file);
182
+ const folder = path.dirname(file);
183
+
184
+ // ---- the bindings an identifier can resolve to. Module scope spans the whole renderer folder,
185
+ // because `Label/index.tsx` claims through `Label/classNames.ts`; block scope is resolved against
186
+ // the innermost enclosing block, because `SurfaceCard` assembles `contentContract` inside its own
187
+ // body and two components in one file may name their locals alike.
188
+ const declarations = new Map(); // name -> initializer or function body
189
+ const exported = new Set();
190
+ for (const sibling of folderFiles.get(folder)) {
191
+ for (const statement of parsed.get(sibling).statements) {
192
+ const isExported = statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true;
193
+ if (ts.isVariableStatement(statement)) {
194
+ for (const decl of statement.declarationList.declarations) {
195
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
196
+ declarations.set(decl.name.text, unwrap(decl.initializer));
197
+ if (isExported) exported.add(decl.name.text);
198
+ }
199
+ } else if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
200
+ declarations.set(statement.name.text, statement.body);
201
+ if (isExported) exported.add(statement.name.text);
202
+ }
203
+ }
204
+ }
205
+
206
+ const locals = new Map(); // name -> [{ scope, init }]
207
+ const visitLocals = (node) => {
208
+ if (ts.isVariableStatement(node) && node.parent !== sourceFile) {
209
+ for (const decl of node.declarationList.declarations) {
210
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
211
+ if (!locals.has(decl.name.text)) locals.set(decl.name.text, []);
212
+ locals.get(decl.name.text).push({ scope: node.parent, init: unwrap(decl.initializer) });
213
+ }
214
+ }
215
+ ts.forEachChild(node, visitLocals);
216
+ };
217
+ visitLocals(sourceFile);
218
+
219
+ function lookup(name, at) {
220
+ const candidates = (locals.get(name) ?? []).filter((c) => c.scope.getSourceFile() === at.getSourceFile()
221
+ && c.scope.pos <= at.pos && at.end <= c.scope.end);
222
+ if (candidates.length > 0) return candidates.sort((a, b) => b.scope.pos - a.scope.pos)[0].init;
223
+ return declarations.get(name);
224
+ }
225
+
226
+ // ---- the component that owns a node: the nearest enclosing top-level `Name = …` declaration.
227
+ // A private helper renderer claims under the public component of its folder, because the private
228
+ // name is not a component an application can compose.
229
+ const folderName = path.basename(folder) === 'core' ? path.basename(file, path.extname(file)) : path.basename(folder);
230
+ function componentOf(node) {
231
+ let name;
232
+ for (let cur = node; cur && name === undefined; cur = cur.parent) {
233
+ if (ts.isVariableDeclaration(cur) && ts.isIdentifier(cur.name) && /^[A-Z]/.test(cur.name.text)) name = cur.name.text;
234
+ else if (ts.isFunctionDeclaration(cur) && cur.name && /^[A-Z]/.test(cur.name.text)) name = cur.name.text;
235
+ }
236
+ if (name === undefined) return folderName;
237
+ return exported.has(name) ? name : exported.has(folderName) ? folderName : name;
238
+ }
239
+
240
+ // ---- the JSX elements a component returns, which are its roots.
241
+ const rootsByComponent = new Map();
242
+ function collectRoots(expr, into) {
243
+ if (!expr) return;
244
+ if (ts.isParenthesizedExpression(expr)) return collectRoots(expr.expression, into);
245
+ if (ts.isConditionalExpression(expr)) { collectRoots(expr.whenTrue, into); collectRoots(expr.whenFalse, into); return; }
246
+ if (ts.isJsxElement(expr)) { into.add(expr.openingElement); return; }
247
+ if (ts.isJsxSelfClosingElement(expr)) { into.add(expr); return; }
248
+ }
249
+ const visitComponents = (node) => {
250
+ const name = ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) ? node.name.text
251
+ : ts.isFunctionDeclaration(node) && node.name ? node.name.text : undefined;
252
+ const body = ts.isVariableDeclaration(node) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
253
+ ? node.initializer.body
254
+ : ts.isFunctionDeclaration(node) ? node.body : undefined;
255
+ if (name && /^[A-Z]/.test(name) && body) {
256
+ const roots = new Set();
257
+ if (ts.isBlock(body)) {
258
+ const scan = (n) => {
259
+ if (ts.isReturnStatement(n)) collectRoots(n.expression, roots);
260
+ if (!ts.isArrowFunction(n) && !ts.isFunctionExpression(n) && !ts.isFunctionDeclaration(n)) ts.forEachChild(n, scan);
261
+ };
262
+ ts.forEachChild(body, scan);
263
+ } else collectRoots(body, roots);
264
+ rootsByComponent.set(name, roots);
265
+ }
266
+ ts.forEachChild(node, visitComponents);
267
+ };
268
+ visitComponents(sourceFile);
269
+
270
+ // ---- condition rendering, always in the vocabulary the source itself uses.
271
+ function setMembers(node) {
272
+ if (!ts.isNewExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== 'Set') return undefined;
273
+ const arg = node.arguments?.[0];
274
+ if (!arg || !ts.isArrayLiteralExpression(arg)) return undefined;
275
+ const values = arg.elements.filter(ts.isStringLiteralLike).map((e) => `"${e.text}"`);
276
+ return values.length === arg.elements.length && values.length > 0 ? values : undefined;
277
+ }
278
+
279
+ function conditionText(node, negated) {
280
+ if (ts.isParenthesizedExpression(node)) return conditionText(node.expression, negated);
281
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
282
+ return conditionText(node.operand, !negated);
283
+ }
284
+ if (ts.isBinaryExpression(node)) {
285
+ const eq = node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken;
286
+ const ne = node.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
287
+ if (eq || ne) {
288
+ const op = (eq ? !negated : negated) ? '=' : '!=';
289
+ return `${textOf(node.left)}${op}${textOf(node.right)}`;
290
+ }
291
+ if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && !negated) {
292
+ return [conditionText(node.left, false), conditionText(node.right, false)].join(', ');
293
+ }
294
+ }
295
+ // `SOME_SET.has(prop)` names a closed alternative, so it prints as that alternative.
296
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'has'
297
+ && ts.isIdentifier(node.expression.expression) && node.arguments.length === 1) {
298
+ const decl = lookup(node.expression.expression.text, node.expression.expression);
299
+ const members = decl ? setMembers(decl) : undefined;
300
+ if (members) return `${textOf(node.arguments[0])}${negated ? ' not in ' : '='}${members.join('|')}`;
301
+ }
302
+ const text = textOf(node);
303
+ // A compound expression keeps its parentheses under negation, so `not a || b` cannot be read
304
+ // as negating only `a`.
305
+ if (!negated) return text;
306
+ return ts.isBinaryExpression(node) || ts.isConditionalExpression(node) ? `not (${text})` : `not ${text}`;
307
+ }
308
+
309
+ // Conditions gathered walking from `node` up to `stop`. `stop` itself is examined, because a
310
+ // `data-contract={a ? "X" : "Y"}` attribute is entered at the conditional and the branch is the
311
+ // whole condition.
312
+ function conditionsUpTo(node, stop) {
313
+ const out = [];
314
+ for (let cur = node; cur && cur !== stop; cur = cur.parent) {
315
+ const parent = cur.parent;
316
+ if (!parent) break;
317
+ if (ts.isConditionalExpression(parent)) {
318
+ if (parent.whenTrue === cur) out.unshift(conditionText(parent.condition, false));
319
+ else if (parent.whenFalse === cur) out.unshift(conditionText(parent.condition, true));
320
+ } else if (ts.isIfStatement(parent)) {
321
+ if (parent.thenStatement === cur) out.unshift(conditionText(parent.expression, false));
322
+ else if (parent.elseStatement === cur) out.unshift(conditionText(parent.expression, true));
323
+ } else if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && parent.right === cur) {
324
+ out.unshift(conditionText(parent.left, false));
325
+ } else if (ts.isCaseClause(parent)) {
326
+ const scrutinee = parent.parent?.parent;
327
+ if (scrutinee && ts.isSwitchStatement(scrutinee)) {
328
+ out.unshift(`${textOf(scrutinee.expression)}=${textOf(parent.expression)}`);
329
+ }
330
+ }
331
+ }
332
+ return out;
333
+ }
334
+
335
+ // ---- claim collection. `base` is the condition set already established by the caller, and
336
+ // `seen` the names already followed, which is both the recursion guard and the list of local
337
+ // bindings whose own presence tests (`toneRule !== undefined`) are plumbing rather than a
338
+ // published condition.
339
+ function collect(node, base, seen, emit) {
340
+ if (!node) return;
341
+ const walk = (cur) => {
342
+ if (ts.isStringLiteralLike(cur) || ts.isTemplateHead(cur) || ts.isTemplateMiddle(cur) || ts.isTemplateTail(cur)) {
343
+ const found = (cur.text ?? '').match(ID_RE);
344
+ if (found) {
345
+ const conds = [...base, ...conditionsUpTo(cur, node)];
346
+ for (const id of found) emit(id, conds, seen);
347
+ }
348
+ return;
349
+ }
350
+ // `MAP[key]`: each entry is claimed under the key that selects it, and a literal key selects
351
+ // exactly one entry under no condition at all.
352
+ if (ts.isElementAccessExpression(cur) && ts.isIdentifier(cur.expression)) {
353
+ const decl = lookup(cur.expression.text, cur.expression);
354
+ if (decl && ts.isObjectLiteralExpression(decl) && !seen.has(cur.expression.text)) {
355
+ const key = textOf(cur.argumentExpression);
356
+ const fixed = ts.isStringLiteralLike(cur.argumentExpression) || ts.isNumericLiteral(cur.argumentExpression)
357
+ ? cur.argumentExpression.text
358
+ : undefined;
359
+ const conds = [...base, ...conditionsUpTo(cur, node)];
360
+ const next = new Set([...seen, cur.expression.text]);
361
+ for (const prop of decl.properties) {
362
+ if (!ts.isPropertyAssignment(prop)) continue;
363
+ const name = ts.isStringLiteralLike(prop.name) || ts.isIdentifier(prop.name) || ts.isNumericLiteral(prop.name)
364
+ ? prop.name.text : undefined;
365
+ if (name === undefined) continue;
366
+ if (fixed !== undefined) {
367
+ if (name !== fixed) continue;
368
+ collect(prop.initializer, conds, next, emit);
369
+ continue;
370
+ }
371
+ const literal = /^[0-9]+$/.test(name) ? name : `"${name}"`;
372
+ collect(prop.initializer, [...conds, `${key}=${literal}`], next, emit);
373
+ }
374
+ return;
375
+ }
376
+ }
377
+ // A helper or constant is followed once, so a claim assembled elsewhere still lands. The
378
+ // receiver of a property access is never followed: `ids.push(…)` and `ids.join(" ")` name a
379
+ // working list whose contents this walk already reads in place, and following it would
380
+ // re-claim every entry under the condition of the branch that happens to be pushing.
381
+ if (ts.isIdentifier(cur) && !seen.has(cur.text)
382
+ && !(cur.parent && ts.isPropertyAccessExpression(cur.parent))) {
383
+ const decl = lookup(cur.text, cur);
384
+ if (decl) {
385
+ const conds = [...base, ...conditionsUpTo(cur, node)];
386
+ collect(decl, conds, new Set([...seen, cur.text]), emit);
387
+ return;
388
+ }
389
+ }
390
+ ts.forEachChild(cur, walk);
391
+ };
392
+ // A switch inside a followed helper needs its case labels, which `conditionsUpTo` reaches
393
+ // through the clause it starts from; nothing extra is needed here.
394
+ walk(node);
395
+ }
396
+
397
+ // ---- the attribute bag a claim sits in, and the role it names.
398
+ function bagEntries(bag) {
399
+ if (bag.kind === 'jsx') {
400
+ return bag.node.attributes.properties.flatMap((p) => {
401
+ if (ts.isJsxAttribute(p) && ts.isIdentifier(p.name)) return [[p.name.text, p.initializer]];
402
+ if (ts.isJsxAttribute(p) && p.name.kind === ts.SyntaxKind.JsxNamespacedName) return [];
403
+ return [];
404
+ });
405
+ }
406
+ return bag.node.properties.flatMap((p) => {
407
+ if (!ts.isPropertyAssignment(p)) return [];
408
+ const name = ts.isStringLiteralLike(p.name) || ts.isIdentifier(p.name) ? p.name.text : undefined;
409
+ return name === undefined ? [] : [[name, p.initializer]];
410
+ });
411
+ }
412
+
413
+ function attributeValueText(value) {
414
+ if (!value) return undefined;
415
+ if (ts.isStringLiteralLike(value)) return value.text;
416
+ if (ts.isJsxExpression(value)) return value.expression ? attributeValueText(value.expression) : undefined;
417
+ return undefined;
418
+ }
419
+
420
+ function classNameRole(value, component) {
421
+ const inner = value && ts.isJsxExpression(value) ? value.expression : value;
422
+ if (!inner) return undefined;
423
+ if (ts.isIdentifier(inner) && /ClassName$/.test(inner.text)) return roleFrom(inner.text.replace(/ClassName$/, ''), component);
424
+ const literals = [];
425
+ const scan = (n) => { if (ts.isStringLiteralLike(n)) literals.push(n.text); ts.forEachChild(n, scan); };
426
+ scan(inner);
427
+ for (const literal of literals) {
428
+ const token = literal.split(/\s+/).find((t) => t.startsWith('starci-core-'));
429
+ if (token) return roleFrom(token.replace(/^starci-core-/, ''), component);
430
+ }
431
+ return undefined;
432
+ }
433
+
434
+ function childRole(element) {
435
+ const parent = element.parent;
436
+ if (!parent || !ts.isJsxElement(parent)) return undefined;
437
+ const children = parent.children.filter((c) => !(ts.isJsxText(c) && c.text.trim() === ''));
438
+ if (children.length !== 1) return undefined;
439
+ const only = children[0];
440
+ if (ts.isJsxExpression(only) && only.expression && (ts.isIdentifier(only.expression) || ts.isPropertyAccessExpression(only.expression))) {
441
+ return words(textOf(only.expression)).join(' ');
442
+ }
443
+ if (ts.isJsxElement(only)) return `${words(textOf(only.openingElement.tagName)).join(' ')} wrapper`;
444
+ if (ts.isJsxSelfClosingElement(only)) return `${words(textOf(only.tagName)).join(' ')} wrapper`;
445
+ return undefined;
446
+ }
447
+
448
+ function roleOf(bag, component) {
449
+ const entries = bagEntries(bag);
450
+ const byName = new Map(entries);
451
+ if (attributeValueText(byName.get('data-component')) === component) return 'root';
452
+ if (bag.kind === 'jsx' && rootsByComponent.get(component)?.has(bag.node)) return 'root';
453
+ if (bag.kind === 'object' && bag.spreadRoots) return 'root';
454
+ for (const [name, value] of entries) {
455
+ if (!name.startsWith('data-grammar-')) continue;
456
+ if (attributeValueText(value) !== 'true') continue;
457
+ const role = roleFrom(name.replace(/^data-grammar-/, ''), component);
458
+ return role === '' ? 'root' : role;
459
+ }
460
+ const fromClass = classNameRole(byName.get('className'), component);
461
+ if (fromClass !== undefined) return fromClass === '' ? 'root' : fromClass;
462
+ if (bag.kind === 'jsx') {
463
+ const element = ts.isJsxOpeningElement(bag.node) ? bag.node.parent : bag.node;
464
+ const child = ts.isJsxElement(element) ? childRole(ts.isJsxElement(element) ? element.openingElement : element) : undefined;
465
+ if (child) return child;
466
+ return words(textOf(bag.node.tagName)).join(' ');
467
+ }
468
+ return 'root';
469
+ }
470
+
471
+ // ---- every `data-contract` site in the file.
472
+ const visitSites = (node) => {
473
+ let value;
474
+ let bag;
475
+ if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === 'data-contract') {
476
+ value = node.initializer && ts.isJsxExpression(node.initializer) ? node.initializer.expression : node.initializer;
477
+ const attributes = node.parent;
478
+ bag = { kind: 'jsx', node: attributes.parent };
479
+ } else if (ts.isPropertyAssignment(node) && ts.isStringLiteralLike(node.name) && node.name.text === 'data-contract') {
480
+ value = node.initializer;
481
+ const object = node.parent;
482
+ // The object may be spread straight into an element, or held by a `const` that is.
483
+ let owner;
484
+ for (let cur = object; cur; cur = cur.parent) {
485
+ if (ts.isJsxSpreadAttribute(cur)) { owner = cur.parent.parent; break; }
486
+ if (ts.isVariableDeclaration(cur)) break;
487
+ }
488
+ if (owner) bag = { kind: 'jsx', node: owner };
489
+ else {
490
+ const held = (() => {
491
+ for (let cur = object; cur; cur = cur.parent) {
492
+ if (ts.isVariableDeclaration(cur) && ts.isIdentifier(cur.name)) return cur.name.text;
493
+ }
494
+ return undefined;
495
+ })();
496
+ let spreadRoots = false;
497
+ if (held) {
498
+ const component = componentOf(node);
499
+ const roots = rootsByComponent.get(component) ?? new Set();
500
+ const scan = (n) => {
501
+ if (ts.isJsxSpreadAttribute(n) && ts.isIdentifier(n.expression) && n.expression.text === held) {
502
+ if (roots.has(n.parent.parent)) spreadRoots = true;
503
+ }
504
+ ts.forEachChild(n, scan);
505
+ };
506
+ scan(sourceFile);
507
+ }
508
+ bag = { kind: 'object', node: object, spreadRoots };
509
+ }
510
+ }
511
+ if (value && bag) {
512
+ const component = componentOf(node);
513
+ const role = roleOf(bag, component);
514
+ const outer = conditionsUpTo(node, undefined).filter(Boolean);
515
+ const seenIds = new Set();
516
+ collect(value, outer, new Set(), (id, conds, followed) => {
517
+ const kept = normaliseConditions(conds, followed);
518
+ if (kept === undefined) return; // the branch contradicts itself, so it never renders
519
+ const key = `${id} ${kept.join(', ')}`;
520
+ if (seenIds.has(key)) return;
521
+ seenIds.add(key);
522
+ claims.push({ component, role, conditions: kept, id });
523
+ });
524
+ }
525
+ ts.forEachChild(node, visitSites);
526
+ };
527
+ visitSites(sourceFile);
528
+ }
529
+
530
+ // ---------------------------------------------------------------- condition hygiene
531
+ // A collected condition list can carry three kinds of noise, all of which say nothing about the
532
+ // props a caller passes: a test on a local binding the walk itself followed, an inequality already
533
+ // settled by an equality on the same subject, and a pair that cannot both hold, which means the
534
+ // branch never renders.
535
+ function escapeRe(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
536
+
537
+ function subjectOf(cond) {
538
+ const m = /^([^=!]+?)(!=|=)(.*)$/.exec(cond);
539
+ return m ? { subject: m[1].trim(), negated: m[2] === '!=', value: m[3].trim() } : undefined;
540
+ }
541
+
542
+ function normaliseConditions(conds, followed) {
543
+ const plumbing = [...followed].map((name) => new RegExp(`^(not )?${escapeRe(name)}($|[=!\\s])`));
544
+ const kept = conds.filter((cond) => !plumbing.some((re) => re.test(cond)));
545
+ const equal = new Map();
546
+ for (const cond of kept) {
547
+ const parsed = subjectOf(cond);
548
+ if (parsed && !parsed.negated) {
549
+ if (equal.has(parsed.subject) && equal.get(parsed.subject) !== parsed.value) return undefined;
550
+ equal.set(parsed.subject, parsed.value);
551
+ }
552
+ }
553
+ const out = [];
554
+ for (const cond of kept) {
555
+ const parsed = subjectOf(cond);
556
+ if (parsed?.negated && equal.has(parsed.subject)) {
557
+ if (equal.get(parsed.subject) === parsed.value) return undefined;
558
+ continue; // `level=1` already says everything `level!=4` said
559
+ }
560
+ if (!out.includes(cond)) out.push(cond);
561
+ }
562
+ return out;
563
+ }
564
+
565
+ // Two rows that differ only by one condition and its negation cover every case between them, so the
566
+ // pair collapses to the condition they share.
567
+ function complementary(a, b) {
568
+ if (a.length !== b.length) return -1;
569
+ let at = -1;
570
+ for (let i = 0; i < a.length; i += 1) {
571
+ if (a[i] === b[i]) continue;
572
+ if (at !== -1) return -1;
573
+ const x = subjectOf(a[i]);
574
+ const y = subjectOf(b[i]);
575
+ const opposite = (x && y && x.subject === y.subject && x.value === y.value && x.negated !== y.negated)
576
+ || a[i] === `not ${b[i]}` || b[i] === `not ${a[i]}`;
577
+ if (!opposite) return -1;
578
+ at = i;
579
+ }
580
+ return at;
581
+ }
582
+
583
+ function collapse(rows) {
584
+ let list = rows.map((r) => ({ ...r }));
585
+ for (let changed = true; changed;) {
586
+ changed = false;
587
+ outer:
588
+ for (let i = 0; i < list.length; i += 1) {
589
+ for (let j = i + 1; j < list.length; j += 1) {
590
+ if (list[i].component !== list[j].component || list[i].role !== list[j].role || list[i].id !== list[j].id) continue;
591
+ const at = complementary(list[i].conditions, list[j].conditions);
592
+ if (at === -1) continue;
593
+ const merged = list[i].conditions.filter((_, k) => k !== at);
594
+ list = [...list.filter((_, k) => k !== i && k !== j), { ...list[i], conditions: merged }];
595
+ changed = true;
596
+ break outer;
597
+ }
598
+ }
599
+ }
600
+ // A claim already made under fewer conditions subsumes the narrower one: `root` covers
601
+ // `root, not isClientReady`, and an unconditional claim covers every conditional one.
602
+ return list.filter((row) => !list.some((other) => other !== row
603
+ && other.component === row.component && other.role === row.role && other.id === row.id
604
+ && other.conditions.length < row.conditions.length
605
+ && other.conditions.every((c) => row.conditions.includes(c))));
606
+ }
607
+
608
+ // ---------------------------------------------------------------- rows
609
+ const ordinal = (id) => {
610
+ const n = id.replace(/^.*-/, '');
611
+ return n === 'AUTO' ? Number.MAX_SAFE_INTEGER : Number(n);
612
+ };
613
+
614
+ function rowsFor(prefix) {
615
+ const unique = new Map();
616
+ for (const claim of claims) {
617
+ if (claim.id.replace(/-(?:\d+|AUTO)$/, '') !== prefix) continue;
618
+ const key = `${claim.component}|${claim.role}|${claim.id}|${claim.conditions.join(', ')}`;
619
+ if (!unique.has(key)) unique.set(key, claim);
620
+ }
621
+ const seen = new Map();
622
+ for (const claim of collapse([...unique.values()])) {
623
+ const where = [claim.role, ...claim.conditions].filter(Boolean).join(', ');
624
+ const key = `${claim.component} ${where} ${claim.id}`;
625
+ if (!seen.has(key)) seen.set(key, { component: claim.component, where, id: claim.id });
626
+ }
627
+ return [...seen.values()].sort((a, b) => a.component.localeCompare(b.component)
628
+ || a.where.localeCompare(b.where)
629
+ || ordinal(a.id) - ordinal(b.id));
630
+ }
631
+
632
+ const EN_NOTE = 'Generated from `@grammar/core` claims by `scripts/generate-presentation-owned.mjs`; edit the component, not this table.';
633
+ const VI_NOTE = 'Sinh từ claim của `@grammar/core` bằng `scripts/generate-presentation-owned.mjs`; muốn đổi thì sửa component, đừng sửa bảng này.';
634
+
635
+ function block(heading, note, header, rows) {
636
+ const lines = [`## ${heading}`, '', note, '', header, '| --- | --- | --- |'];
637
+ if (rows.length === 0) lines.push('| — | — | — |');
638
+ else for (const row of rows) lines.push(`| \`${row.component}\` | ${row.where} | ${row.id} |`);
639
+ lines.push('');
640
+ return lines;
641
+ }
642
+
643
+ // Replace the block between `## <heading>` and the next `## `, or insert it above `insertBefore`.
644
+ function rewrite(text, heading, note, header, rows, insertBefore) {
645
+ const lines = text.split(/\r?\n/);
646
+ const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
647
+ const body = block(heading, note, header, rows);
648
+ if (start !== -1) {
649
+ let end = start + 1;
650
+ while (end < lines.length && !lines[end].startsWith('## ')) end += 1;
651
+ return [...lines.slice(0, start), ...body, ...lines.slice(end)].join('\n');
652
+ }
653
+ if (!insertBefore) throw new Error(`no "## ${heading}" block and no insertion point`);
654
+ const at = lines.findIndex((line) => line.startsWith(`## ${insertBefore} `));
655
+ if (at === -1) throw new Error(`cannot place "## ${heading}": no "## ${insertBefore}" heading`);
656
+ return [...lines.slice(0, at), ...body, ...lines.slice(at)].join('\n');
657
+ }
658
+
659
+ const outputs = [];
660
+ const counts = [];
661
+ for (const topic of TOPICS) {
662
+ const rows = rowsFor(topic.prefix);
663
+ counts.push(`${topic.prefix} ${rows.length}`);
664
+ const en = path.join(presentationDir, `${topic.file}.md`);
665
+ const vi = path.join(presentationDir, `${topic.file}.vi.md`);
666
+ outputs.push([en, rewrite(read(en), topic.heading, EN_NOTE, '| Component | Element or condition | Rule |', rows, topic.insertBefore)]);
667
+ outputs.push([vi, rewrite(read(vi), topic.vi, VI_NOTE, '| Component | Phần tử hoặc điều kiện | Rule |', rows, topic.insertBefore)]);
668
+ }
669
+
670
+ if (checkOnly) {
671
+ const drifted = outputs.filter(([file, text]) => !existsSync(file) || read(file) !== text);
672
+ if (drifted.length > 0) {
673
+ process.stderr.write(`presentation owned tables are stale: ${drifted.map(([f]) => path.relative(claudeRoot, f)).join(', ')}\n`);
674
+ process.exitCode = 1;
675
+ } else {
676
+ process.stdout.write(`presentation owned tables current: ${counts.join(', ')}\n`);
677
+ }
678
+ } else {
679
+ for (const [file, text] of outputs) writeFileSync(file, text, 'utf8');
680
+ process.stdout.write(`wrote ${outputs.length} files from ${claims.length} claim entries: ${counts.join(', ')}\n`);
681
+ }