@astryxdesign/cli 0.6.4 → 0.6.5-canary.031021b

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 (288) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +97 -90
  3. package/api/build/build.doc.mjs +6 -1
  4. package/api/build/build.test.mjs +22 -0
  5. package/api/build/kit/kit.mjs +44 -5
  6. package/api/component/_adapter.d.mts +25 -0
  7. package/api/component/_adapter.mjs +59 -5
  8. package/api/component/component.d.mts +6 -3
  9. package/api/component/component.doc.mjs +37 -17
  10. package/api/component/component.mjs +249 -9
  11. package/api/component/component.type.d.mts +25 -0
  12. package/api/component/component.type.mjs +44 -0
  13. package/api/discover/_adapter.d.mts +114 -6
  14. package/api/discover/_adapter.mjs +372 -17
  15. package/api/discover/_adapter.test.mjs +215 -0
  16. package/api/discover/_catalog-view.d.mts +115 -0
  17. package/api/discover/_catalog-view.mjs +203 -0
  18. package/api/discover/_catalog-view.test.mjs +128 -0
  19. package/api/discover/detail/detail.d.mts +18 -6
  20. package/api/discover/detail/detail.mjs +67 -13
  21. package/api/discover/detail/detail.test.mjs +85 -0
  22. package/api/discover/detail/item/item.d.mts +26 -0
  23. package/api/discover/detail/item/item.mjs +78 -0
  24. package/api/discover/detail/item/item.test.mjs +73 -0
  25. package/api/discover/discover.d.mts +3 -9
  26. package/api/discover/discover.doc.mjs +61 -18
  27. package/api/discover/discover.mjs +220 -36
  28. package/api/discover/discover.test.mjs +11 -2
  29. package/api/discover/discover.type.d.mts +147 -8
  30. package/api/discover/discover.type.mjs +102 -12
  31. package/api/discover/list/list.d.mts +20 -6
  32. package/api/discover/list/list.mjs +45 -12
  33. package/api/discover/list/list.test.mjs +46 -0
  34. package/api/discover/search/search.d.mts +18 -16
  35. package/api/discover/search/search.mjs +102 -56
  36. package/api/discover/search/search.test.mjs +144 -10
  37. package/api/docs/_adapter.d.mts +8 -3
  38. package/api/docs/_adapter.mjs +14 -6
  39. package/api/docs/docOverlays.test.mjs +27 -1
  40. package/api/docs/docs.doc.mjs +2 -2
  41. package/api/doctor/doctor.d.mts +8 -3
  42. package/api/doctor/doctor.doc.mjs +17 -8
  43. package/api/doctor/doctor.mjs +90 -9
  44. package/api/doctor/doctor.test.mjs +122 -10
  45. package/api/doctor/doctor.type.d.mts +1 -1
  46. package/api/doctor/doctor.type.mjs +1 -1
  47. package/api/gap-report/gap-report.doc.mjs +19 -10
  48. package/api/hook/hook.doc.mjs +6 -3
  49. package/api/index.d.mts +1 -0
  50. package/api/index.mjs +5 -3
  51. package/api/init/init.doc.mjs +17 -12
  52. package/api/integration/add-helpers.d.mts +5 -2
  53. package/api/integration/add-helpers.mjs +36 -9
  54. package/api/integration/add-theme.mjs +22 -1
  55. package/api/integration/add-theme.test.mjs +34 -0
  56. package/api/integration/authoring-checks.mjs +2 -2
  57. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  58. package/api/integration/pack-check.lifecycle-output.test.mjs +107 -0
  59. package/api/integration/pack-check.mjs +82 -9
  60. package/api/integration/pack-check.test.mjs +90 -0
  61. package/api/integration/pack-check.type.mjs +1 -1
  62. package/api/json/assertResponse.doc.mjs +1 -1
  63. package/api/json/isError.doc.mjs +1 -1
  64. package/api/search/search.d.mts +27 -1
  65. package/api/search/search.doc.mjs +2 -2
  66. package/api/search/search.mjs +228 -16
  67. package/api/swizzle/swizzle.doc.mjs +7 -5
  68. package/api/template/copy/copy.mjs +1 -1
  69. package/api/template/copy/copy.test.mjs +9 -0
  70. package/api/template/template.doc.mjs +2 -1
  71. package/api/theme/add/add.mjs +17 -25
  72. package/api/theme/add/add.rollback.test.mjs +158 -0
  73. package/api/theme/add/add.staging.test.mjs +40 -23
  74. package/api/theme/build/build.family.test.mjs +7 -12
  75. package/api/theme/build/build.mjs +8 -18
  76. package/api/theme/build/build.rollback.test.mjs +148 -0
  77. package/api/theme/generateTonalPalette.doc.mjs +1 -2
  78. package/api/theme/listThemes.doc.mjs +1 -1
  79. package/api/theme/themeAdd.doc.mjs +9 -10
  80. package/api/theme/themeBuild.doc.mjs +13 -13
  81. package/api/theme/themeList.doc.mjs +1 -1
  82. package/api/theme/themeListAvailable.doc.mjs +2 -1
  83. package/api/theme/themePaletteGenerate.doc.mjs +15 -8
  84. package/api/theme/themeTargets.doc.mjs +3 -2
  85. package/api/theme/themeTemplate.doc.mjs +2 -1
  86. package/api/upgrade/run/files-changed.test.mjs +111 -0
  87. package/api/upgrade/run/run.mjs +5 -3
  88. package/api/upgrade/upgrade.doc.mjs +24 -22
  89. package/api/upgrade/upgrade.type.mjs +2 -2
  90. package/assets/codemods/__tests__/runner.test.mjs +3 -1
  91. package/assets/codemods/file-count.test.mjs +163 -0
  92. package/assets/codemods/integration-runner.mjs +3 -3
  93. package/assets/codemods/runner.mjs +5 -4
  94. package/assets/docs/README.md +4 -2
  95. package/assets/docs/browser-support.doc.mjs +11 -11
  96. package/assets/docs/color.doc.mjs +8 -2
  97. package/assets/docs/elevation.doc.mjs +6 -4
  98. package/assets/docs/getting-started.doc.mjs +5 -16
  99. package/assets/docs/icons.doc.mjs +2 -21
  100. package/assets/docs/illustrations.doc.mjs +7 -15
  101. package/assets/docs/internationalization.doc.mjs +7 -5
  102. package/assets/docs/layout.doc.dense.mjs +130 -82
  103. package/assets/docs/layout.doc.mjs +133 -77
  104. package/assets/docs/migration.doc.mjs +19 -21
  105. package/assets/docs/motion.doc.mjs +16 -3
  106. package/assets/docs/principles.doc.dense.mjs +5 -5
  107. package/assets/docs/principles.doc.mjs +8 -0
  108. package/assets/docs/principles.doc.zh.mjs +6 -6
  109. package/assets/docs/shape.doc.mjs +8 -3
  110. package/assets/docs/spacing.doc.mjs +7 -2
  111. package/assets/docs/styling-libraries.doc.mjs +6 -2
  112. package/assets/docs/styling.doc.mjs +19 -23
  113. package/assets/docs/theme.doc.dense.mjs +58 -18
  114. package/assets/docs/theme.doc.mjs +57 -47
  115. package/assets/docs/theme.doc.zh.mjs +9 -8
  116. package/assets/docs/tokens.doc.dense.mjs +2 -2
  117. package/assets/docs/tokens.doc.mjs +389 -8
  118. package/assets/docs/tokens.doc.zh.mjs +2 -2
  119. package/assets/docs/tree/add-a-component.doc.mjs +75 -0
  120. package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
  121. package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
  122. package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
  123. package/assets/docs/tree/block-template.doc.mjs +130 -0
  124. package/assets/docs/tree/build-the-template.doc.mjs +28 -0
  125. package/assets/docs/tree/building-blocks.doc.mjs +46 -0
  126. package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
  127. package/assets/docs/tree/checks.doc.mjs +119 -0
  128. package/assets/docs/tree/codemods.doc.mjs +147 -0
  129. package/assets/docs/tree/component-family.doc.mjs +113 -0
  130. package/assets/docs/tree/component-imports.doc.mjs +69 -0
  131. package/assets/docs/tree/component-lookups.doc.mjs +149 -0
  132. package/assets/docs/tree/components.doc.mjs +23 -0
  133. package/assets/docs/tree/configuration.doc.mjs +23 -0
  134. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
  135. package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
  136. package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
  137. package/assets/docs/tree/docs.doc.mjs +21 -0
  138. package/assets/docs/tree/document-the-template.doc.mjs +28 -0
  139. package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
  140. package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
  141. package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
  142. package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
  143. package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
  144. package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
  145. package/assets/docs/tree/help.doc.mjs +16 -0
  146. package/assets/docs/tree/integrations.doc.mjs +25 -451
  147. package/assets/docs/tree/links.doc.mjs +98 -0
  148. package/assets/docs/tree/package-and-test.doc.mjs +32 -0
  149. package/assets/docs/tree/page-template.doc.mjs +71 -0
  150. package/assets/docs/tree/publishing.doc.mjs +111 -0
  151. package/assets/docs/tree/quick-start.doc.mjs +272 -0
  152. package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
  153. package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
  154. package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
  155. package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
  156. package/assets/docs/tree/ship.doc.mjs +16 -0
  157. package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
  158. package/assets/docs/tree/single-component.doc.mjs +165 -0
  159. package/assets/docs/tree/start-a-template.doc.mjs +143 -0
  160. package/assets/docs/tree/subcomponent.doc.mjs +115 -0
  161. package/assets/docs/tree/template-assets.doc.mjs +64 -0
  162. package/assets/docs/tree/template-doc-overview.doc.mjs +109 -0
  163. package/assets/docs/tree/template-fonts.doc.mjs +102 -0
  164. package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
  165. package/assets/docs/tree/template-icons.doc.mjs +97 -0
  166. package/assets/docs/tree/template-images-media.doc.mjs +127 -0
  167. package/assets/docs/tree/template-styles.doc.mjs +93 -0
  168. package/assets/docs/tree/templates.doc.mjs +34 -0
  169. package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
  170. package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
  171. package/assets/docs/tree/themes.doc.mjs +39 -0
  172. package/assets/docs/tree/troubleshooting.doc.mjs +149 -0
  173. package/assets/docs/tree/upgrading.doc.mjs +103 -0
  174. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
  175. package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
  176. package/assets/docs/tree/versioning.doc.mjs +161 -0
  177. package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
  178. package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
  179. package/assets/docs/typography.doc.mjs +24 -4
  180. package/assets/docs/working-with-ai.doc.mjs +30 -22
  181. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  182. package/authoring/config/config.doc.mjs +9 -1
  183. package/authoring/config/parse.d.mts +2 -0
  184. package/authoring/config/parse.mjs +19 -0
  185. package/authoring/config/parse.test.mjs +8 -0
  186. package/authoring/config/type.ts +11 -0
  187. package/authoring/discover/discover.doc.d.mts +13 -0
  188. package/authoring/discover/discover.doc.mjs +138 -0
  189. package/authoring/discover/parse.d.mts +24 -0
  190. package/authoring/discover/parse.mjs +128 -0
  191. package/authoring/discover/parse.test.mjs +124 -0
  192. package/authoring/discover/type.ts +87 -0
  193. package/authoring/doctypes/_schema.d.mts +3 -2
  194. package/authoring/doctypes/_schema.mjs +6 -0
  195. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  196. package/authoring/doctypes/base/type.ts +4 -2
  197. package/authoring/doctypes/component/component.doc.mjs +6 -0
  198. package/authoring/doctypes/component/type.ts +8 -0
  199. package/authoring/doctypes/reference/reference.doc.mjs +7 -0
  200. package/authoring/doctypes/reference/type.ts +5 -0
  201. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  202. package/authoring/doctypes/template/template.doc.mjs +1 -1
  203. package/authoring/doctypes/template/type.ts +2 -2
  204. package/authoring/index.d.mts +1 -0
  205. package/authoring/index.d.ts +10 -0
  206. package/authoring/index.mjs +1 -0
  207. package/authoring/integration/integration.doc.mjs +12 -10
  208. package/clients/cli/commands/component/index.mjs +152 -55
  209. package/clients/cli/commands/component-batch.test.mjs +341 -0
  210. package/clients/cli/commands/component-ownership.test.mjs +89 -0
  211. package/clients/cli/commands/component.doc.mjs +27 -9
  212. package/clients/cli/commands/discover.doc.mjs +53 -9
  213. package/clients/cli/commands/discover.mjs +393 -118
  214. package/clients/cli/commands/discover.sources.test.mjs +267 -0
  215. package/clients/cli/commands/docs.doc.mjs +1 -1
  216. package/clients/cli/commands/docs.mjs +60 -17
  217. package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -2
  218. package/clients/cli/commands/doctor-integration.test.mjs +53 -0
  219. package/clients/cli/commands/doctor.doc.mjs +3 -1
  220. package/clients/cli/commands/doctor.mjs +49 -5
  221. package/clients/cli/commands/gap-report.doc.mjs +10 -9
  222. package/clients/cli/commands/init.doc.mjs +9 -6
  223. package/clients/cli/commands/integration-add.doc.mjs +9 -9
  224. package/clients/cli/commands/integration-authoring.test.mjs +61 -10
  225. package/clients/cli/commands/integration-pack.doc.mjs +5 -9
  226. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  227. package/clients/cli/commands/integration-verify.doc.mjs +22 -0
  228. package/clients/cli/commands/integration.doc.mjs +4 -4
  229. package/clients/cli/commands/integration.mjs +74 -43
  230. package/clients/cli/commands/manifest.doc.mjs +1 -1
  231. package/clients/cli/commands/search.doc.mjs +10 -3
  232. package/clients/cli/commands/search.mjs +21 -2
  233. package/clients/cli/commands/search.test.mjs +21 -4
  234. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  235. package/clients/cli/commands/template.doc.mjs +1 -1
  236. package/clients/cli/commands/text-json-parity.test.mjs +7 -1
  237. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  238. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -2
  239. package/clients/cli/commands/theme-palette.doc.mjs +1 -2
  240. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  241. package/clients/cli/commands/theme.doc.mjs +2 -1
  242. package/clients/cli/commands/upgrade.doc.mjs +62 -3
  243. package/clients/cli/index.mjs +28 -6
  244. package/clients/cli/lib/define-command.mjs +28 -4
  245. package/clients/cli/lib/define-command.test.mjs +54 -0
  246. package/clients/cli/lib/exit-codes.test.mjs +17 -1
  247. package/clients/cli/lib/json-shim.mjs +24 -14
  248. package/clients/cli/lib/manifest.mjs +18 -5
  249. package/clients/cli/lib/manifest.test.mjs +5 -2
  250. package/clients/cli/lib/parse-error-format.test.mjs +81 -0
  251. package/foundation/agent-docs/agent-docs.mjs +1 -1
  252. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  253. package/foundation/discovery/authoring-self-docs.test.mjs +6 -2
  254. package/foundation/discovery/cli-self-docs.mjs +16 -2
  255. package/foundation/discovery/cli-self-docs.test.mjs +20 -0
  256. package/foundation/discovery/docs-discovery.mjs +5 -1
  257. package/foundation/discovery/docs-discovery.test.mjs +21 -0
  258. package/foundation/discovery/docs-section-key.d.mts +1 -1
  259. package/foundation/discovery/docs-section-key.mjs +1 -1
  260. package/foundation/doc-compiler/doc-loads.test.mjs +3 -2
  261. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  262. package/foundation/doc-compiler/tree.d.mts +4 -0
  263. package/foundation/doc-compiler/tree.mjs +6 -1
  264. package/foundation/integrations/cli-requirement.d.mts +26 -6
  265. package/foundation/integrations/cli-requirement.mjs +46 -11
  266. package/foundation/integrations/cli-requirement.test.mjs +7 -2
  267. package/foundation/integrations/contribution-inventory.mjs +1 -1
  268. package/foundation/integrations/integrations.d.mts +14 -1
  269. package/foundation/integrations/integrations.mjs +41 -1
  270. package/foundation/integrations/integrations.test.mjs +31 -0
  271. package/foundation/response/batch.type.d.mts +33 -0
  272. package/foundation/response/batch.type.mjs +34 -0
  273. package/foundation/response/error-codes.doc.mjs +6 -8
  274. package/foundation/response/error-codes.test.mjs +30 -5
  275. package/foundation/response/response-types.doc.d.mts +4 -3
  276. package/foundation/response/response-types.doc.mjs +40 -10
  277. package/foundation/response/response-types.doc.test.mjs +23 -0
  278. package/foundation/response/response.doc.mjs +11 -10
  279. package/package.json +9 -9
  280. package/api/docs/docs.test.mjs +0 -243
  281. package/api/docs/integration-tree.test.mjs +0 -555
  282. package/api/docs/integrationDocs.test.mjs +0 -314
  283. package/api/search/search.test.mjs +0 -512
  284. package/assets/docs/tree/integrations.test.mjs +0 -62
  285. package/assets/docs/tree/writing-docs.doc.mjs +0 -286
  286. package/clients/cli/commands/docs.test.mjs +0 -294
  287. package/foundation/agent-docs/agent-docs.test.mjs +0 -1159
  288. package/foundation/doc-compiler/tree.test.mjs +0 -598
@@ -27,30 +27,34 @@ export const doc = {
27
27
  {
28
28
  name: 'configPath',
29
29
  type: 'string',
30
- description: 'JSON generation request, resolved within cwd.',
30
+ description:
31
+ 'Path to a JSON file holding a TonalPaletteGenerationInput (the object generateTonalPalette() takes), resolved within cwd.',
31
32
  required: true,
32
33
  },
33
34
  {
34
35
  name: 'options.out',
35
36
  type: 'string',
36
37
  description:
37
- 'Optional candidate JSON destination. A sibling .receipt.json path is derived from it.',
38
+ 'Where to write the candidate: a path ending in .ts (a TypeScript module) or .json. A sibling <name>.receipt.json is written next to it.',
38
39
  },
39
40
  {
40
41
  name: 'options.preview',
41
42
  type: 'string',
42
- description: 'Optional path for a self-contained HTML review artifact.',
43
+ description:
44
+ 'Optional path, ending in .html, for a self-contained HTML review page.',
43
45
  },
44
46
  {
45
47
  name: 'options.overwrite',
46
48
  type: 'boolean',
47
- description: 'Replace existing candidate and receipt files.',
49
+ description:
50
+ "Replace existing candidate, receipt and preview files. Without it, if any target exists, nothing is written and the result has written: false, reason: 'exists'.",
48
51
  default: 'false',
49
52
  },
50
53
  {
51
54
  name: 'ctx.cwd',
52
55
  type: 'string',
53
56
  description: 'Directory used to resolve the input and output paths.',
57
+ default: 'process.cwd()',
54
58
  },
55
59
  ],
56
60
  returns: [
@@ -64,17 +68,20 @@ export const doc = {
64
68
  {code: 'ERR_FILE_NOT_FOUND', when: 'the config file does not exist'},
65
69
  {
66
70
  code: 'ERR_PALETTE_GENERATION',
67
- when: 'the request, seed, stop layout, mode, or anchor constraint is invalid',
71
+ when: 'the config is not valid JSON; the request, seed, stop layout, mode or anchor is invalid; out does not end in .ts or .json; or preview does not end in .html',
68
72
  },
69
73
  {
70
74
  code: 'ERR_PATH_TRAVERSAL',
71
75
  when: 'an input or output path escapes cwd, or output would replace input',
72
76
  },
73
- {code: 'ERR_WRITE_FAILED', when: 'the candidate pair cannot be written'},
77
+ {
78
+ code: 'ERR_WRITE_FAILED',
79
+ when: 'the candidate, receipt or preview file cannot be written',
80
+ },
74
81
  ],
75
82
  examples: [
76
83
  {
77
- label: 'Preview a candidate',
84
+ label: 'Generate a candidate without writing files',
78
85
  code: "themePaletteGenerate('palette.config.json');",
79
86
  },
80
87
  {
@@ -83,5 +90,5 @@ export const doc = {
83
90
  },
84
91
  ],
85
92
  command: 'theme palette generate',
86
- related: ['themeBuild', 'themeTemplate'],
93
+ related: ['generateTonalPalette', 'themeBuild', 'themeTemplate'],
87
94
  };
@@ -22,7 +22,7 @@ export const doc = {
22
22
  'Same source as the Theming table `astryx component <Name>` prints ' +
23
23
  '(the component docs), so the list cannot drift from the components, and `theme build` ' +
24
24
  'validates overrides against this exact set. A filter naming a component gives that ' +
25
- "component's set; anything else is a substring search over the keys.",
25
+ "component's set; anything else is a case-insensitive substring search over each target's key, class and component.",
26
26
  importPath: '@astryxdesign/cli/api',
27
27
  signature:
28
28
  'themeTargets(filter?: string, ctx?: {cwd?: string}): Promise<ThemeTargetsResponse>',
@@ -41,13 +41,14 @@ export const doc = {
41
41
  name: 'filter',
42
42
  type: 'string',
43
43
  description:
44
- 'A component name (exact, case-insensitive) or a substring of a target key. Omit for the whole surface.',
44
+ 'A component name (exact, case-insensitive), or a substring of a target key, class or component. Omit for the whole surface.',
45
45
  },
46
46
  {
47
47
  name: 'ctx.cwd',
48
48
  type: 'string',
49
49
  description:
50
50
  "Directory the project's @astryxdesign/core is resolved from.",
51
+ default: 'process.cwd()',
51
52
  },
52
53
  ],
53
54
  returns: [
@@ -43,6 +43,7 @@ export const doc = {
43
43
  name: 'options.cwd',
44
44
  type: 'string',
45
45
  description: 'Directory the target path resolves against.',
46
+ default: 'process.cwd()',
46
47
  },
47
48
  ],
48
49
  returns: [
@@ -64,5 +65,5 @@ export const doc = {
64
65
  },
65
66
  ],
66
67
  command: 'theme template',
67
- related: ['themeAdd', 'themeBuild', 'themeList'],
68
+ related: ['themeAdd', 'themeBuild', 'themeListAvailable', 'themeTargets'],
68
69
  };
@@ -0,0 +1,111 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The upgrade receipt counts a file once when a core codemod AND an
5
+ * integration codemod both change it: `filesChanged` is the union of the two
6
+ * runners' files, while `transformsApplied` adds their changes.
7
+ *
8
+ * Uses a real consumer under a repo-local temp dir (Vite blocks dynamic import
9
+ * of config and integration modules from /tmp), the real core registry, and an
10
+ * installed integration whose code codemod stamps the same file.
11
+ */
12
+
13
+ import {describe, it, expect, afterEach} from 'vitest';
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import {upgrade} from '../upgrade.mjs';
17
+ import {
18
+ latestVersion,
19
+ versions,
20
+ getTransformsBetween,
21
+ } from '../../../assets/codemods/registry.mjs';
22
+
23
+ const SLOW = 60_000;
24
+
25
+ /** The registry version whose manifest ships the authoring migration. */
26
+ async function authoringTier() {
27
+ const all = await getTransformsBetween('0.0.0', latestVersion);
28
+ const tier = all.find(({transforms}) =>
29
+ transforms.some(t => t.name === 'migrate-authoring-imports'),
30
+ );
31
+ if (!tier) throw new Error('no registry version ships the authoring migration');
32
+ return tier.version;
33
+ }
34
+
35
+ /**
36
+ * A consumer with installed core at the registry's latest version and one
37
+ * old-surface file that the core authoring codemods rewrite.
38
+ * @param {string} dir
39
+ * @param {{integrationCodemod: boolean}} options
40
+ */
41
+ function seed(dir, {integrationCodemod}) {
42
+ fs.writeFileSync(
43
+ path.join(dir, 'package.json'),
44
+ JSON.stringify({name: 'consumer', version: '1.0.0'}),
45
+ );
46
+ const core = path.join(dir, 'node_modules', '@astryxdesign', 'core');
47
+ fs.mkdirSync(core, {recursive: true});
48
+ fs.writeFileSync(
49
+ path.join(core, 'package.json'),
50
+ JSON.stringify({name: '@astryxdesign/core', version: latestVersion}),
51
+ );
52
+ fs.mkdirSync(path.join(dir, 'src'), {recursive: true});
53
+ fs.writeFileSync(
54
+ path.join(dir, 'src', 'Button.doc.mjs'),
55
+ [
56
+ "import {createComponentDoc} from '@astryxdesign/core/authoring';",
57
+ "export default createComponentDoc({name: 'Button', props: []});",
58
+ '',
59
+ ].join('\n'),
60
+ );
61
+ if (!integrationCodemod) return;
62
+ fs.writeFileSync(
63
+ path.join(dir, 'astryx.config.mjs'),
64
+ "export default {integrations: ['@acme/widgets']};\n",
65
+ );
66
+ const pkg = path.join(dir, 'node_modules', '@acme', 'widgets');
67
+ fs.mkdirSync(path.join(pkg, 'codemods', latestVersion), {recursive: true});
68
+ fs.writeFileSync(
69
+ path.join(pkg, 'package.json'),
70
+ JSON.stringify({name: '@acme/widgets', version: '1.0.0'}),
71
+ );
72
+ fs.writeFileSync(
73
+ path.join(pkg, 'astryx.integration.mjs'),
74
+ "export default {codemods: './codemods'};\n",
75
+ );
76
+ fs.writeFileSync(
77
+ path.join(pkg, 'codemods', latestVersion, 'acme-stamp.mjs'),
78
+ "export default {type: 'code', title: 'Stamp', transform: file => (file.source.includes('// acme') ? null : `${file.source}// acme\\n`)};\n",
79
+ );
80
+ }
81
+
82
+ describe('upgrade receipt — filesChanged across core and integration codemods', () => {
83
+ /** @type {string[]} */
84
+ const dirs = [];
85
+ afterEach(() => {
86
+ for (const dir of dirs.splice(0)) fs.rmSync(dir, {recursive: true, force: true});
87
+ });
88
+
89
+ /** @param {{integrationCodemod: boolean}} options */
90
+ async function run(options) {
91
+ const dir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-files-changed-'));
92
+ dirs.push(dir);
93
+ seed(dir, options);
94
+ const tier = await authoringTier();
95
+ const from = versions[versions.indexOf(tier) - 1];
96
+ const res = await upgrade({from, path: 'src'}, {cwd: dir});
97
+ expect(res.type).toBe('upgrade.run');
98
+ return res.data;
99
+ }
100
+
101
+ it('counts a file changed by both a core and an integration codemod once', async () => {
102
+ const coreOnly = await run({integrationCodemod: false});
103
+ expect(coreOnly.filesChanged).toBe(1);
104
+ expect(coreOnly.transformsApplied).toBeGreaterThan(0);
105
+
106
+ const both = await run({integrationCodemod: true});
107
+ expect(both.integrations).toEqual(['@acme/widgets']);
108
+ expect(both.filesChanged).toBe(1);
109
+ expect(both.transformsApplied).toBe(coreOnly.transformsApplied + 1);
110
+ }, SLOW);
111
+ });
@@ -437,9 +437,11 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
437
437
 
438
438
  const registryResult = await reconcileCompositions();
439
439
 
440
- const mergedFilesChanged =
441
- (coreResult?.totalFilesChanged ?? 0) +
442
- (integrationResult?.totalFilesChanged ?? 0);
440
+ // A file a core codemod AND an integration codemod both changed is one file.
441
+ const mergedFilesChanged = new Set([
442
+ ...(coreResult?.changedFiles ?? []),
443
+ ...(integrationResult?.changedFiles ?? []),
444
+ ]).size;
443
445
  const mergedTransformsApplied =
444
446
  (coreResult?.totalTransformsApplied ?? 0) +
445
447
  (integrationResult?.totalTransformsApplied ?? 0);
@@ -13,19 +13,16 @@ export const doc = {
13
13
  name: 'upgrade',
14
14
  namespace: 'cli/api',
15
15
  displayName: 'upgrade()',
16
- summary: 'Run version migrations and reconcile copied compositions.',
16
+ summary:
17
+ 'After bumping @astryxdesign/core, migrate project source with codemods and update copied compositions.',
17
18
  description:
18
- 'Migrates project source from a previous Astryx version to the currently ' +
19
- 'installed one by running the registered codemods, and compares the fully ' +
20
- 'rendered managed agent-docs block on every migration path, including ' +
21
- 'same-Core integration guidance changes; list and registry-only modes do not ' +
22
- 'run migration reconciliation. Dry-run previews without writing; `apply` ' +
23
- 'writes the prepared block only after selected codemods and hooks succeed. ' +
24
- 'Core codemods run before ' +
25
- 'the config is loaded so a config codemod can repair an otherwise-invalid ' +
26
- 'astryx.config. Copied compositions carry adjacent receipts with exact canonical and format-specific install bases; upgrade ' +
27
- 'compares those installed bases with the matching registry release, updates pristine ' +
28
- 'files, merges non-overlapping edits, and leaves conflicting originals untouched.',
19
+ 'Runs the codemods between `from` and the installed Core version, then refreshes the ' +
20
+ 'managed agent-docs block. Dry-run by default; `apply` writes changes only after the ' +
21
+ 'selected codemods and hooks succeed. Config codemods run before astryx.config is ' +
22
+ 'loaded, so they can repair an invalid config. Also updates copied compositions from ' +
23
+ 'their install receipts: unchanged files are updated, non-overlapping edits are merged, ' +
24
+ 'and conflicts are left untouched. `list` only lists codemods; `registry` only updates ' +
25
+ 'copied compositions.',
29
26
  importPath: '@astryxdesign/cli/api',
30
27
  signature:
31
28
  'upgrade(options?: UpgradeOptions, ctx?: {cwd?: string}): Promise<UpgradeListResponse | UpgradeRegistryResponse | UpgradeStatusResponse | UpgradeRunResponse>',
@@ -42,7 +39,7 @@ export const doc = {
42
39
  name: 'options.from',
43
40
  type: 'string',
44
41
  description:
45
- 'Version before the dependency bump. Required unless `list` or `registry` is set.',
42
+ 'Version before the dependency bump; the target is the installed @astryxdesign/core (or legacy @xds/core). Required unless `list` or `registry` is set.',
46
43
  },
47
44
  {
48
45
  name: 'options.apply',
@@ -55,11 +52,13 @@ export const doc = {
55
52
  type: 'boolean',
56
53
  description:
57
54
  'Run codemods even when `from` is at/after the installed version.',
55
+ default: 'false',
58
56
  },
59
57
  {
60
58
  name: 'options.codemod',
61
59
  type: 'string',
62
- description: 'Run a single named transform instead of the full set.',
60
+ description:
61
+ 'Run only this codemod. Optional codemods run only when named here. Setting it also skips copied-composition reconciliation.',
63
62
  },
64
63
  {
65
64
  name: 'options.skipCodemod',
@@ -83,23 +82,26 @@ export const doc = {
83
82
  type: 'boolean',
84
83
  description:
85
84
  'Install jscodeshift when it is missing; otherwise a missing jscodeshift throws ERR_DEP_MISSING.',
85
+ default: 'false',
86
86
  },
87
87
  {
88
88
  name: 'options.registry',
89
89
  type: 'boolean',
90
90
  description:
91
- 'Reconcile copied compositions from their install receipts without requiring `from`.',
91
+ 'Only reconcile copied compositions from their install receipts; `from` is not required. Cannot be combined with `list`, `from`, `force`, `codemod`, `skipCodemod`, `integration` or `installDeps`.',
92
92
  default: 'false',
93
93
  },
94
94
  {
95
95
  name: 'options.list',
96
96
  type: 'boolean',
97
97
  description: 'Return the available codemods instead of running any.',
98
+ default: 'false',
98
99
  },
99
100
  {
100
101
  name: 'ctx.cwd',
101
102
  type: 'string',
102
103
  description: 'Directory to run the upgrade in.',
104
+ default: 'process.cwd()',
103
105
  },
104
106
  ],
105
107
  returns: [
@@ -116,36 +118,36 @@ export const doc = {
116
118
  {
117
119
  type: 'upgrade.status',
118
120
  description:
119
- 'A short-circuit outcome (no codemods executed): `up_to_date` (`from` is at/after the installed target and no `force`), `no_codemods` (none apply to the range), or `config_fixable` (dry-run preview that a pending config codemod would repair an invalid astryx.config). Each carries the agent-docs summary and, when found, the copied-composition registry summary.',
121
+ 'A short-circuit outcome (no codemods executed): `up_to_date` (`from` is at/after the installed target and no `force`), `no_codemods` (none apply to the range), or `config_fixable` (dry-run preview that a pending config codemod would repair an invalid astryx.config). up_to_date and no_codemods carry the agent-docs summary and, when receipts are found, the copied-composition summary; config_fixable carries configError, configCodemods, suggestedCommand, message, note, and the agent-docs summary.',
120
122
  },
121
123
  {
122
124
  type: 'upgrade.run',
123
125
  description:
124
- 'The terminal run receipt: from/to versions, codemod count, integrations processed, agent-docs and registry summaries, modifiedFiles, protectedFiles, declinedCandidates, and completion state. A protected required change returns complete: false with ERR_CODEMOD_PROTECTED; the CLI exits nonzero while preserving the structured receipt.',
126
+ 'The terminal run receipt: from, to, codemods (count), integrations, agentDocs, agentDocsRefreshed, registryCompositions (when receipts are found), filesChanged, transformsApplied, modifiedFiles, protectedFiles, declinedCandidates, errors, and complete. When a protected file still requires a change, complete is false and errorCode is ERR_CODEMOD_PROTECTED; the CLI exits nonzero while preserving the structured receipt.',
125
127
  },
126
128
  ],
127
129
  throws: [
128
130
  {
129
131
  code: 'ERR_INVALID_ARGUMENT',
130
- when: '`from` is missing (and neither `list` nor `registry` is set), or the project config fails strict validation and no pending config codemod can repair it',
132
+ when: '`from` is missing (and neither `list` nor `registry` is set); `list` and `registry` are both set; `registry` is combined with `from`, `force`, `codemod`, `skipCodemod`, `integration` or `installDeps`; an `integration` specifier is invalid or not installed; or astryx.config fails to load or validate and no pending config codemod repairs it',
131
133
  },
132
134
  {code: 'ERR_INVALID_VERSION', when: '`from` is not a valid semver string'},
133
135
  {code: 'ERR_PATH_TRAVERSAL', when: '`path` resolves outside cwd'},
134
136
  {
135
137
  code: 'ERR_VERSION_DETECT',
136
- when: 'the installed @astryxdesign/core version cannot be detected',
138
+ when: 'neither @astryxdesign/core nor legacy @xds/core is installed in cwd; with `registry`, only when copied-composition receipts exist and @astryxdesign/core is not installed',
137
139
  },
138
140
  {
139
141
  code: 'ERR_DEP_MISSING',
140
- when: 'jscodeshift is required but could not be installed',
142
+ when: 'jscodeshift is missing and `installDeps` is not set, or installing it failed',
141
143
  },
142
144
  {
143
145
  code: 'ERR_UNKNOWN_CODEMOD',
144
- when: 'a `codemod` name matches no registered codemod',
146
+ when: 'the version range has codemods but none remain selected: `codemod` names no codemod in the range, or `skipCodemod` excludes all of them',
145
147
  },
146
148
  {
147
149
  code: 'ERR_CODEMOD_FAILED',
148
- when: 'one or more codemods failed, or a post-codemod hook failed',
150
+ when: 'one or more codemods failed, or a post-codemod hook failed, and no protected file still needs a change (otherwise an upgrade.run receipt with complete: false is returned)',
149
151
  },
150
152
  {
151
153
  code: 'ERR_CODEMOD_PROTECTION_SOURCE',
@@ -119,11 +119,11 @@
119
119
  * @property {RegistryCompositionSummary} [data.registryCompositions]
120
120
  * @property {boolean} [data.complete] False when protected required changes remain.
121
121
  * @property {'ERR_CODEMOD_PROTECTED'} [data.errorCode] Stable incomplete-result code when complete is false.
122
- * @property {number} [data.filesChanged] Total files changed across core + integration codemods (apply mode).
122
+ * @property {number} [data.filesChanged] Distinct files changed across core + integration codemods (apply mode). One file that four codemods each changed counts once.
123
123
  * @property {string[]} [data.modifiedFiles] Project-relative files changed or previewed.
124
124
  * @property {ProtectedCodemodFile[]} [data.protectedFiles] Protected files that still require a codemod change after regeneration.
125
125
  * @property {Array<{file: string, location?: string, reason: string}>} [data.declinedCandidates] Candidates left unchanged because proof was insufficient.
126
- * @property {number} [data.transformsApplied] Total transforms that reported a change.
126
+ * @property {number} [data.transformsApplied] Total codemod changes. A code or config codemod counts once for each file it changed, so one file changed by four of them counts four times; a project codemod counts once, however many files it writes.
127
127
  * @property {Array<{file: string, codemod: string, error: string}>} [data.errors] Per-codemod errors, when any codemod failed.
128
128
  */
129
129
 
@@ -46,7 +46,9 @@ describe('runCodemods — ordered dry-run state', () => {
46
46
  silent: true,
47
47
  });
48
48
 
49
- expect(preview.totalFilesChanged).toBe(2);
49
+ // One file that two transforms changed is one file and two changes.
50
+ expect(preview.totalFilesChanged).toBe(1);
51
+ expect(preview.totalTransformsApplied).toBe(2);
50
52
  expect(preview.changedFiles).toEqual([
51
53
  path.join(srcDir, 'a.ts'),
52
54
  path.join(srcDir, 'a.ts'),
@@ -0,0 +1,163 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `filesChanged` counts FILES, not (codemod, file) pairs.
5
+ *
6
+ * One source file that four codemods each changed was reported as four files
7
+ * changed — the total was incremented once per transform per file, so it
8
+ * equalled `transformsApplied` in every run and the documented meaning of the
9
+ * field ("Total files changed") was never true. The two numbers answer
10
+ * different questions and both are in the receipt.
11
+ */
12
+
13
+ import {describe, it, expect, beforeEach, afterEach} from 'vitest';
14
+ import * as fs from 'node:fs';
15
+ import * as os from 'node:os';
16
+ import * as path from 'node:path';
17
+ import jscodeshift from 'jscodeshift';
18
+ import {runCodemods} from './runner.mjs';
19
+ import {runIntegrationCodemods} from './integration-runner.mjs';
20
+
21
+ let dir;
22
+
23
+ beforeEach(() => {
24
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-file-count-'));
25
+ });
26
+ afterEach(() => fs.rmSync(dir, {recursive: true, force: true}));
27
+
28
+ /** A transform that rewrites one distinctive token, so several can stack. */
29
+ const renaming = (from, to) => (file) =>
30
+ file.source.includes(from) ? file.source.split(from).join(to) : null;
31
+
32
+ /** @param {string} name @param {string[]} contents */
33
+ function writeSources(...contents) {
34
+ return contents.map((content, i) => {
35
+ const file = path.join(dir, `file${i}.ts`);
36
+ fs.writeFileSync(file, content);
37
+ return file;
38
+ });
39
+ }
40
+
41
+ describe('core codemod runner — filesChanged counts files', () => {
42
+ it('reports 1 file for one file changed by four codemods', async () => {
43
+ writeSources('const a = ONE + TWO + THREE + FOUR;\n');
44
+
45
+ const result = await runCodemods(
46
+ [
47
+ {
48
+ version: '0.0.2',
49
+ transforms: [
50
+ {name: 'one', transform: renaming('ONE', '1'), meta: {title: 'one'}},
51
+ {name: 'two', transform: renaming('TWO', '2'), meta: {title: 'two'}},
52
+ {name: 'three', transform: renaming('THREE', '3'), meta: {title: 'three'}},
53
+ {name: 'four', transform: renaming('FOUR', '4'), meta: {title: 'four'}},
54
+ ],
55
+ },
56
+ ],
57
+ {apply: true, path: dir, root: dir, codemod: undefined, skipCodemods: new Set(), silent: true},
58
+ );
59
+
60
+ expect(result.totalTransformsApplied).toBe(4);
61
+ expect(result.totalFilesChanged).toBe(1);
62
+ expect(new Set(result.changedFiles).size).toBe(1);
63
+ });
64
+
65
+ it('still counts two files as two', async () => {
66
+ writeSources('const a = ONE;\n', 'const b = ONE;\n');
67
+
68
+ const result = await runCodemods(
69
+ [
70
+ {
71
+ version: '0.0.2',
72
+ transforms: [
73
+ {name: 'one', transform: renaming('ONE', '1'), meta: {title: 'one'}},
74
+ ],
75
+ },
76
+ ],
77
+ {apply: true, path: dir, root: dir, codemod: undefined, skipCodemods: new Set(), silent: true},
78
+ );
79
+
80
+ expect(result.totalTransformsApplied).toBe(2);
81
+ expect(result.totalFilesChanged).toBe(2);
82
+ });
83
+
84
+ it('reports 0 when nothing matched', async () => {
85
+ writeSources('const a = 1;\n');
86
+
87
+ const result = await runCodemods(
88
+ [
89
+ {
90
+ version: '0.0.2',
91
+ transforms: [
92
+ {name: 'one', transform: renaming('ONE', '1'), meta: {title: 'one'}},
93
+ ],
94
+ },
95
+ ],
96
+ {apply: true, path: dir, root: dir, codemod: undefined, skipCodemods: new Set(), silent: true},
97
+ );
98
+
99
+ expect(result.totalFilesChanged).toBe(0);
100
+ expect(result.totalTransformsApplied).toBe(0);
101
+ });
102
+ });
103
+
104
+ describe('integration codemod runner — filesChanged counts files', () => {
105
+ it('reports 1 file for one file changed by three integration codemods', () => {
106
+ writeSources('const a = ONE + TWO + THREE;\n');
107
+
108
+ const entry = (id, from, to) => ({
109
+ id,
110
+ package: '@acme/widgets',
111
+ type: 'code',
112
+ codemod: {title: id, transform: renaming(from, to)},
113
+ });
114
+
115
+ const result = runIntegrationCodemods(
116
+ [
117
+ {
118
+ version: '1.0.0',
119
+ codemods: [
120
+ entry('one', 'ONE', '1'),
121
+ entry('two', 'TWO', '2'),
122
+ entry('three', 'THREE', '3'),
123
+ ],
124
+ },
125
+ ],
126
+ {apply: true, path: dir, root: dir, skipCodemods: new Set(), jscodeshift, silent: true},
127
+ );
128
+
129
+ expect(result.totalTransformsApplied).toBe(3);
130
+ expect(result.totalFilesChanged).toBe(1);
131
+ expect(new Set(result.changedFiles).size).toBe(1);
132
+ });
133
+ });
134
+
135
+ describe('core codemod runner — a project codemod', () => {
136
+ it('counts every file it writes, and one change', async () => {
137
+ const result = await runCodemods(
138
+ [
139
+ {
140
+ version: '0.0.2',
141
+ transforms: [
142
+ {
143
+ name: 'project-plan',
144
+ meta: {title: 'project plan', codemodType: 'project'},
145
+ transform: async root => ({
146
+ writes: [
147
+ {path: path.join(root, 'a.ts'), contents: 'a\n'},
148
+ {path: path.join(root, 'b.ts'), contents: 'b\n'},
149
+ ],
150
+ deletes: [],
151
+ problems: [],
152
+ }),
153
+ },
154
+ ],
155
+ },
156
+ ],
157
+ {apply: true, path: dir, root: dir, codemod: undefined, skipCodemods: new Set(), silent: true},
158
+ );
159
+
160
+ expect(result.totalFilesChanged).toBe(2);
161
+ expect(result.totalTransformsApplied).toBe(1);
162
+ });
163
+ });
@@ -67,7 +67,6 @@ export function runIntegrationCodemods(
67
67
  /** In-memory pipeline state keeps ordered dry-runs equivalent to apply. */
68
68
  const virtualContents = new Map(providedContents ?? []);
69
69
 
70
- let totalFilesChanged = 0;
71
70
  let totalTransformsApplied = 0;
72
71
  /** @type {string[]} */
73
72
  const changedFiles = [];
@@ -115,7 +114,6 @@ export function runIntegrationCodemods(
115
114
  protection,
116
115
  contents: virtualContents,
117
116
  });
118
- totalFilesChanged += r.filesChanged;
119
117
  totalTransformsApplied += r.filesChanged;
120
118
  changedFiles.push(...r.changedFiles);
121
119
  writtenFiles.push(...r.writtenFiles);
@@ -145,7 +143,6 @@ export function runIntegrationCodemods(
145
143
  protection,
146
144
  contents: virtualContents,
147
145
  });
148
- totalFilesChanged += r.filesChanged;
149
146
  totalTransformsApplied += r.filesChanged;
150
147
  changedFiles.push(...r.changedFiles);
151
148
  writtenFiles.push(...r.writtenFiles);
@@ -154,6 +151,9 @@ export function runIntegrationCodemods(
154
151
  }
155
152
  }
156
153
 
154
+ // A file several codemods changed is one file; transforms count each change.
155
+ const totalFilesChanged = new Set(changedFiles).size;
156
+
157
157
  return {
158
158
  totalFilesChanged,
159
159
  totalTransformsApplied,
@@ -486,7 +486,6 @@ export async function runCodemods(
486
486
  (await import('jscodeshift')).default
487
487
  );
488
488
 
489
- let totalFilesChanged = 0;
490
489
  let totalTransformsApplied = 0;
491
490
  let totalValidationBlocked = 0;
492
491
  /** @type {Array<{file: string, codemod: string, error: string}>} */
@@ -554,7 +553,6 @@ export async function runCodemods(
554
553
  protectionWriteCount = writtenFiles.length;
555
554
  }
556
555
  if (result.filesChanged > 0) {
557
- totalFilesChanged += result.filesChanged;
558
556
  totalTransformsApplied += 1;
559
557
  }
560
558
  continue;
@@ -577,7 +575,6 @@ export async function runCodemods(
577
575
  changedFiles.push(...result.changedFiles);
578
576
  writtenFiles.push(...result.writtenFiles);
579
577
  if (result.filesChanged > 0) {
580
- totalFilesChanged += result.filesChanged;
581
578
  totalTransformsApplied += result.filesChanged;
582
579
  }
583
580
  continue;
@@ -592,7 +589,6 @@ export async function runCodemods(
592
589
  protectedFiles.push(...result.protectedFiles);
593
590
  changedFiles.push(...result.changedFiles);
594
591
  writtenFiles.push(...result.writtenFiles);
595
- totalFilesChanged += result.filesChanged;
596
592
  totalTransformsApplied += result.filesChanged;
597
593
  totalValidationBlocked += result.errors.filter(
598
594
  error =>
@@ -630,6 +626,11 @@ export async function runCodemods(
630
626
  );
631
627
  }
632
628
 
629
+ // A file several codemods changed is one file. `totalTransformsApplied` is
630
+ // unchanged: a code or config codemod counts each file it changed, and a
631
+ // project codemod counts once. The two answer different questions.
632
+ const totalFilesChanged = new Set(changedFiles).size;
633
+
633
634
  if (protectedFiles.length > 0) {
634
635
  const files = [...new Set(protectedFiles.map(item => item.file))];
635
636
  log.warn(
@@ -21,13 +21,15 @@ Someone building a product with Astryx. Their questions:
21
21
  ## Tells that you are writing for us instead
22
22
 
23
23
  - second person aimed at the wrong reader — "reviewers should…", "before promoting a component…", "attach evidence for…"
24
- - **rubric, readiness, gate, audit, checklist, sign-off, promotion, evidence** as things the reader must produce
24
+ - an internal rubric, readiness gate, audit checklist, or sign-off that the reader must satisfy for Astryx maintainers
25
25
  - a table of things to verify rather than things to use
26
26
  - anything about lab → core, which is our lifecycle, not theirs
27
27
  - Storybook, Playwright, CI or the Simulator named as tools the reader runs
28
28
 
29
29
  One subtlety: a statement about the **system's behavior** is caller-facing even when it sounds like process. "A component's theme targets are stable once published" tells a caller what they can rely on; "reviewers must check that theme targets are stable" is ours. Same fact, different reader — **rewrite it rather than move it**.
30
30
 
31
+ A public authoring-quality rubric is also caller-facing when it helps someone evaluate an artifact they create through Astryx. It must be complete and actionable from public inputs. It must not include Astryx's internal approval, promotion, evidence-publication, or CI process. A current system spec must assign the shipped guide as the rubric's owner.
32
+
31
33
  ## Where the rest goes
32
34
 
33
35
  The material is usually good; the finding is placement, not quality. It goes in the [wiki](https://github.com/facebook/astryx/wiki) — **as a section on the page that already covers it, not a new page.** The wiki is at nearly 60 pages, several of them overlapping, because every stray section got its own.
@@ -55,5 +57,5 @@ Worked example: a responsive-and-interaction readiness rubric is grading criteri
55
57
  one section by its key. A section's key is its `id`, or a key derived from its
56
58
  title when it has none. Give a section an `id` when its title may change, since
57
59
  readers and extensions link to the key. Two sections in one topic cannot share
58
- a key. Keep each section small enough to read on its own: `astryx doctor` fails
60
+ a key. Keep each section small enough to read on its own: `astryx doctor` warns on
59
61
  any section over 32 KB.