@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4

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 (212) hide show
  1. package/README.md +99 -66
  2. package/api/build/_adapter.d.mts +58 -0
  3. package/api/build/_adapter.mjs +115 -3
  4. package/api/build/blocks/blocks.d.mts +42 -0
  5. package/api/build/blocks/blocks.mjs +169 -0
  6. package/api/build/blocks/blocks.test.mjs +108 -0
  7. package/api/build/blocks/blocks.weights.json +1 -0
  8. package/api/build/build.mjs +25 -6
  9. package/api/build/kit/answer.d.mts +14 -0
  10. package/api/build/kit/answer.mjs +85 -0
  11. package/api/build/kit/answer.test.mjs +38 -0
  12. package/api/build/kit/kind.d.mts +25 -0
  13. package/api/build/kit/kind.mjs +170 -0
  14. package/api/build/kit/kit.d.mts +2 -4
  15. package/api/build/kit/kit.mjs +8 -398
  16. package/api/build/kit/rank.d.mts +3 -32
  17. package/api/build/kit/rank.mjs +25 -342
  18. package/api/build/kit/rank.test.mjs +2 -1
  19. package/api/build/kit/signals.d.mts +60 -0
  20. package/api/build/kit/signals.mjs +284 -0
  21. package/api/build/kit/start.d.mts +32 -0
  22. package/api/build/kit/start.mjs +163 -0
  23. package/api/build/kit/words.d.mts +72 -0
  24. package/api/build/kit/words.mjs +202 -0
  25. package/api/build/tree.d.mts +20 -0
  26. package/api/build/tree.mjs +52 -0
  27. package/api/build/tree.test.mjs +177 -0
  28. package/api/build/tree.weights.json +1 -0
  29. package/api/component/component.mjs +16 -2
  30. package/api/component/component.test.mjs +15 -0
  31. package/api/discover/_adapter.mjs +36 -9
  32. package/api/discover/_adapter.test.mjs +42 -0
  33. package/api/docs/_adapter.mjs +17 -5
  34. package/api/docs/docs.test.mjs +7 -1
  35. package/api/doctor/doctor.mjs +86 -12
  36. package/api/doctor/doctor.test.mjs +28 -3
  37. package/api/init/init.doc.mjs +20 -6
  38. package/api/init/init.test.mjs +18 -2
  39. package/api/init/init.type.d.mts +3 -3
  40. package/api/init/init.type.mjs +3 -3
  41. package/api/init/remove/remove.mjs +17 -4
  42. package/api/init/remove/remove.test.mjs +114 -2
  43. package/api/init/run/run.mjs +69 -6
  44. package/api/integration/authoring-checks.mjs +83 -9
  45. package/api/integration/pack-check.mjs +123 -4
  46. package/api/integration/pack-check.test.mjs +87 -0
  47. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  48. package/api/integration/validate-integration-gaps.test.mjs +1964 -0
  49. package/api/integration/validate-integration.d.mts +11 -2
  50. package/api/integration/validate-integration.mjs +226 -32
  51. package/api/integration/validate-integration.type.d.mts +58 -1
  52. package/api/integration/validate-integration.type.mjs +45 -2
  53. package/api/integration/validateIntegration.doc.mjs +1 -1
  54. package/api/search/ORDERING.md +28 -0
  55. package/api/search/_adapter.d.mts +66 -2
  56. package/api/search/_adapter.mjs +118 -10
  57. package/api/search/_candidate-score.d.mts +40 -0
  58. package/api/search/_candidate-score.mjs +238 -0
  59. package/api/search/_cards.json +6098 -0
  60. package/api/search/_compound.d.mts +50 -0
  61. package/api/search/_compound.mjs +170 -0
  62. package/api/search/_doc-searcher.d.mts +4 -0
  63. package/api/search/_doc-searcher.mjs +23 -0
  64. package/api/search/_doc-searcher.test.mjs +36 -0
  65. package/api/search/_doc-searcher.weights.json +1 -0
  66. package/api/search/_doc-text.d.mts +30 -0
  67. package/api/search/_doc-text.mjs +87 -0
  68. package/api/search/_entries.d.mts +52 -0
  69. package/api/search/_entries.mjs +158 -0
  70. package/api/search/_features.d.mts +35 -0
  71. package/api/search/_features.mjs +155 -0
  72. package/api/search/_gather-components.d.mts +26 -0
  73. package/api/search/_gather-components.mjs +284 -0
  74. package/api/search/_gather-docs.d.mts +20 -0
  75. package/api/search/_gather-docs.mjs +254 -0
  76. package/api/search/_gather-hooks.d.mts +12 -0
  77. package/api/search/_gather-hooks.mjs +52 -0
  78. package/api/search/_gather-templates.d.mts +25 -0
  79. package/api/search/_gather-templates.mjs +107 -0
  80. package/api/search/_lexical.d.mts +135 -0
  81. package/api/search/_lexical.mjs +351 -0
  82. package/api/search/_main-searcher.d.mts +19 -0
  83. package/api/search/_main-searcher.mjs +115 -0
  84. package/api/search/_main-searcher.test.mjs +106 -0
  85. package/api/search/_main-searcher.weights.json +1 -0
  86. package/api/search/_module-doc.d.mts +32 -0
  87. package/api/search/_module-doc.mjs +78 -0
  88. package/api/search/_order.d.mts +32 -0
  89. package/api/search/_order.mjs +162 -0
  90. package/api/search/_order.test.mjs +72 -0
  91. package/api/search/_priority.d.mts +50 -0
  92. package/api/search/_priority.mjs +139 -0
  93. package/api/search/_query-words.d.mts +25 -0
  94. package/api/search/_query-words.mjs +92 -0
  95. package/api/search/_query.d.mts +65 -0
  96. package/api/search/_query.mjs +176 -0
  97. package/api/search/_result.d.mts +97 -0
  98. package/api/search/_result.mjs +150 -0
  99. package/api/search/_score.d.mts +60 -0
  100. package/api/search/_score.mjs +283 -0
  101. package/api/search/_synonyms.d.mts +21 -0
  102. package/api/search/_synonyms.mjs +67 -0
  103. package/api/search/_template-searcher.d.mts +4 -0
  104. package/api/search/_template-searcher.mjs +23 -0
  105. package/api/search/_template-searcher.test.mjs +40 -0
  106. package/api/search/_template-searcher.weights.json +1 -0
  107. package/api/search/_titles.d.mts +33 -0
  108. package/api/search/_titles.mjs +65 -0
  109. package/api/search/_words.d.mts +64 -0
  110. package/api/search/_words.mjs +157 -0
  111. package/api/search/search.d.mts +22 -305
  112. package/api/search/search.doc.mjs +7 -5
  113. package/api/search/search.mjs +53 -2483
  114. package/api/search/search.type.d.mts +6 -1
  115. package/api/search/search.type.mjs +6 -1
  116. package/api/search/tree.d.mts +15 -0
  117. package/api/search/tree.mjs +40 -0
  118. package/api/search/tree.test.mjs +66 -0
  119. package/api/search/tree.weights.json +1 -0
  120. package/api/theme/build/build.packed-old-core.test.mjs +1 -0
  121. package/api/upgrade/_adapter.d.mts +57 -9
  122. package/api/upgrade/_adapter.mjs +198 -28
  123. package/api/upgrade/registry/registry.d.mts +10 -4
  124. package/api/upgrade/registry/registry.mjs +33 -16
  125. package/api/upgrade/registry/registry.test.mjs +85 -0
  126. package/api/upgrade/run/run.d.mts +4 -2
  127. package/api/upgrade/run/run.mjs +13 -2
  128. package/api/upgrade/run/run.test.mjs +417 -3
  129. package/api/upgrade/upgrade.doc.mjs +10 -5
  130. package/api/upgrade/upgrade.mjs +13 -2
  131. package/api/upgrade/upgrade.test.mjs +194 -0
  132. package/api/upgrade/upgrade.type.d.mts +5 -3
  133. package/api/upgrade/upgrade.type.mjs +8 -6
  134. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  135. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  136. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  137. package/assets/docs/tree/api.doc.mjs +6 -0
  138. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  139. package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
  140. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  141. package/assets/docs/working-with-ai.doc.mjs +6 -2
  142. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  143. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  144. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  145. package/authoring/debug/parse.d.mts +1 -1
  146. package/clients/cli/bin/astryx.mjs +7 -78
  147. package/clients/cli/commands/doctor.mjs +30 -0
  148. package/clients/cli/commands/doctor.test.mjs +65 -0
  149. package/clients/cli/commands/init.behavior.test.mjs +230 -1
  150. package/clients/cli/commands/init.doc.mjs +28 -9
  151. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  152. package/clients/cli/commands/init.mjs +2 -2
  153. package/clients/cli/commands/outside-project.test.mjs +2 -1
  154. package/clients/cli/commands/setup-nudge.test.mjs +61 -0
  155. package/clients/cli/commands/upgrade.doc.mjs +4 -2
  156. package/clients/cli/commands/upgrade.mjs +9 -2
  157. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  158. package/clients/cli/index.mjs +19 -1
  159. package/clients/cli/run.mjs +108 -0
  160. package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
  161. package/foundation/agent-docs/agent-doc-state.mjs +35 -8
  162. package/foundation/agent-docs/agent-docs.d.mts +100 -12
  163. package/foundation/agent-docs/agent-docs.mjs +1010 -99
  164. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  165. package/foundation/agent-docs/agent-docs.test.mjs +678 -35
  166. package/foundation/config/project.mjs +20 -22
  167. package/foundation/config/project.test.mjs +56 -0
  168. package/foundation/discovery/component-discovery.d.mts +37 -0
  169. package/foundation/discovery/component-discovery.mjs +80 -5
  170. package/foundation/discovery/component-loader.d.mts +1 -1
  171. package/foundation/discovery/docs-discovery.mjs +14 -1
  172. package/foundation/discovery/module-exports.d.mts +41 -0
  173. package/foundation/discovery/module-exports.mjs +253 -0
  174. package/foundation/discovery/template-adapter.mjs +18 -1
  175. package/foundation/discovery/template-needs.mjs +1 -44
  176. package/foundation/discovery/theme-discovery.mjs +147 -3
  177. package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
  178. package/foundation/doc-compiler/read.mjs +59 -1
  179. package/foundation/doc-compiler/read.test.mjs +11 -2
  180. package/foundation/fs/paths.d.mts +70 -8
  181. package/foundation/fs/paths.mjs +556 -38
  182. package/foundation/fs/paths.test.mjs +430 -54
  183. package/foundation/integrations/autolink.d.mts +2 -1
  184. package/foundation/integrations/autolink.mjs +4 -13
  185. package/foundation/integrations/computer-integrations.d.mts +6 -3
  186. package/foundation/integrations/computer-integrations.mjs +38 -9
  187. package/foundation/integrations/contribution-inventory.d.mts +15 -20
  188. package/foundation/integrations/contribution-inventory.mjs +41 -5
  189. package/foundation/integrations/integrations.d.mts +27 -1
  190. package/foundation/integrations/integrations.mjs +61 -2
  191. package/foundation/integrations/validate-contributions.d.mts +115 -0
  192. package/foundation/integrations/validate-contributions.mjs +1168 -13
  193. package/foundation/release/handoff.d.mts +23 -4
  194. package/foundation/release/handoff.mjs +175 -8
  195. package/foundation/release/run-context.mjs +14 -0
  196. package/foundation/response/response-types.doc.mjs +1 -1
  197. package/foundation/route/route.d.mts +285 -0
  198. package/foundation/route/route.mjs +324 -0
  199. package/foundation/route/route.test.mjs +153 -0
  200. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  201. package/foundation/runtime/locked-versions.d.mts +32 -0
  202. package/foundation/runtime/locked-versions.mjs +65 -0
  203. package/foundation/runtime/package-root.d.mts +91 -0
  204. package/foundation/runtime/package-root.mjs +174 -0
  205. package/foundation/runtime/package-set.d.mts +62 -0
  206. package/foundation/runtime/package-set.mjs +130 -0
  207. package/foundation/runtime/package-set.test.mjs +432 -0
  208. package/host/door-conformance.test.mjs +274 -0
  209. package/host/index.d.ts +116 -0
  210. package/host/index.mjs +295 -0
  211. package/host/index.test.mjs +302 -0
  212. package/package.json +14 -9
@@ -10,7 +10,8 @@
10
10
  */
11
11
  export function validateLocalIntegration(cwd?: string): Promise<ValidateResult>;
12
12
  /**
13
- * Validate an INSTALLED integration package resolved from `cwd`/node_modules.
13
+ * Validate an INSTALLED integration package: the nearest `node_modules/<spec>`
14
+ * at or above `cwd`, resolved the way the project loads it.
14
15
  * @param {string} spec package name
15
16
  * @param {string} [cwd]
16
17
  * @returns {Promise<ValidateResult>}
@@ -43,7 +44,6 @@ export function summarizeIssues(issues: Issue[]): {
43
44
  errors: number;
44
45
  warnings: number;
45
46
  };
46
- export { validateLoadedIntegration };
47
47
  export type Issue = import("../../foundation/integrations/issue").AstryxIntegrationIssue;
48
48
  export type ValidateResult = {
49
49
  /**
@@ -75,6 +75,15 @@ export type ValidateResult = {
75
75
  code: string;
76
76
  message: string;
77
77
  }[] | undefined;
78
+ /**
79
+ * Layout findings added after the last minor
80
+ * release: validate lists them as notes, which count nowhere, since they
81
+ * may not fail `--strict` until the next (architecture:cli-surface INV35);
82
+ * `integration verify` still fails on a package.json npm cannot pack.
83
+ */
84
+ listed?: import("../../foundation/integrations/issue").AstryxIntegrationIssue[] | undefined;
78
85
  integration?: import("../../foundation/integrations/integrations.mjs").LoadedIntegration | undefined;
79
86
  };
80
87
  import { validateLoadedIntegration } from '../../foundation/integrations/validate-contributions.mjs';
88
+ import { isListedIssue } from '../../foundation/integrations/validate-contributions.mjs';
89
+ export { validateLoadedIntegration, isListedIssue };
@@ -12,7 +12,7 @@
12
12
  * - validateLocalIntegration(cwd) — the package rooted at `cwd` (nearest
13
13
  * package.json + sibling astryx.integration.{ts,mjs,js}).
14
14
  * - validateInstalledIntegration(spec, cwd) — an installed package resolved
15
- * from `cwd`/node_modules.
15
+ * from the node_modules chain at or above `cwd`.
16
16
  *
17
17
  * Both return a { found, name, version, manifestFile, issues } result. `found`
18
18
  * is false only for the no-manifest local case, which is guidance (not an
@@ -34,6 +34,7 @@
34
34
  import * as fs from 'node:fs';
35
35
  import * as path from 'node:path';
36
36
  import {assertWithin} from '../../foundation/fs/path-safety.mjs';
37
+ import {normalizeProviderId} from '../../foundation/identity/provider-identity.mjs';
37
38
  import {integrationDocTypes} from '../docs/_adapter.mjs';
38
39
  import {integrationDocConsistency} from '../../foundation/discovery/doc-consistency.mjs';
39
40
  import {
@@ -51,20 +52,25 @@ import {
51
52
  import {
52
53
  findManifestPaths,
53
54
  loadManifest,
54
- resolvePackageDir,
55
+ resolveInstalledIntegrationDir,
55
56
  } from '../../foundation/integrations/integrations.mjs';
56
57
  // The on-disk contribution validators live in foundation: Project and
57
58
  // integration-warnings need them too, and foundation must not depend on api.
58
59
  import {
60
+ blockComponentsCheck,
61
+ componentDocNamesCheck,
59
62
  INVALID_CLI_COMMAND,
63
+ isListedIssue,
64
+ issueListed as listed,
60
65
  validateLoadedIntegration,
66
+ validateLoadedIntegrationListed,
61
67
  issueError as error,
62
68
  issueWarning as warning,
63
69
  rootProblem,
64
70
  } from '../../foundation/integrations/validate-contributions.mjs';
65
71
  import {formatCliCommand} from '../../foundation/env/package-manager.mjs';
66
72
 
67
- export {validateLoadedIntegration};
73
+ export {validateLoadedIntegration, isListedIssue};
68
74
 
69
75
  /**
70
76
  * @typedef {import('../../foundation/integrations/issue').AstryxIntegrationIssue} Issue
@@ -80,6 +86,10 @@ export {validateLoadedIntegration};
80
86
  * @property {Issue[]} issues
81
87
  * @property {Array<{code: string, message: string}>} [notes] Information that
82
88
  * fails nothing, such as `invalid_cli_command` (a malformed `cliCommand`).
89
+ * @property {Issue[]} [listed] Layout findings added after the last minor
90
+ * release: validate lists them as notes, which count nowhere, since they
91
+ * may not fail `--strict` until the next (architecture:cli-surface INV35);
92
+ * `integration verify` still fails on a package.json npm cannot pack.
83
93
  * @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration} [integration]
84
94
  */
85
95
 
@@ -99,6 +109,14 @@ function findNearestPackageJson(cwd) {
99
109
  }
100
110
  }
101
111
 
112
+ const ROOT_KINDS = /** @type {const} */ ([
113
+ 'components',
114
+ 'templates',
115
+ 'codemods',
116
+ 'docs',
117
+ 'themes',
118
+ ]);
119
+
102
120
  const UNREACHABLE_SCAN_LIMIT = 5_000;
103
121
  const UNREACHABLE_SKIP_DIRS = new Set([
104
122
  'node_modules',
@@ -148,27 +166,51 @@ function unreadThemeFolderIssues(packageDir, loaded) {
148
166
  *
149
167
  * @param {string} packageDir
150
168
  * @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration} loaded
169
+ * @param {string[]} [reported] folders whose files another issue already covers
151
170
  * @returns {Promise<Issue[]>}
152
171
  */
153
- async function findUnreachableContributionIssues(packageDir, loaded) {
172
+ async function findUnreachableContributionIssues(
173
+ packageDir,
174
+ loaded,
175
+ reported = [],
176
+ ) {
177
+ // A root that is not a readable folder holds nothing: a fix must never
178
+ // send a file into it.
179
+ const usable = {...loaded};
180
+ for (const kind of ROOT_KINDS) {
181
+ const root = usable[kind];
182
+ if (root != null && rootProblem(root)) usable[kind] = undefined;
183
+ }
184
+ // Every declared root is skipped, readable or not: one that cannot be read
185
+ // has its own issue.
154
186
  const roots = /** @type {string[]} */ (
155
- [
156
- loaded.components,
157
- loaded.templates,
158
- loaded.codemods,
159
- loaded.docs,
160
- loaded.themes,
161
- ].filter(Boolean)
187
+ ROOT_KINDS.map(kind => loaded[kind]).filter(Boolean)
162
188
  );
189
+ // A folder a symlinked root points at is read through that root.
190
+ const realRoots = roots.map(root => {
191
+ try {
192
+ return fs.realpathSync(root);
193
+ } catch {
194
+ return root;
195
+ }
196
+ });
163
197
  /** @type {Issue[]} */
164
198
  const issues = [];
165
- const context = createFixContext(packageDir, loaded);
199
+ const context = createFixContext(packageDir, usable);
166
200
  let scanned = 0;
167
201
  let truncated = false;
168
202
 
169
203
  /** @param {string} dir */
170
204
  async function walk(dir) {
171
205
  if (roots.some(root => pathIsInside(dir, root))) return;
206
+ if (reported.some(folder => pathIsInside(dir, folder))) return;
207
+ let realDir = dir;
208
+ try {
209
+ realDir = fs.realpathSync(dir);
210
+ } catch {
211
+ // Unreadable: reported below.
212
+ }
213
+ if (realRoots.some(root => pathIsInside(realDir, root))) return;
172
214
  let entries;
173
215
  try {
174
216
  entries = fs
@@ -286,12 +328,14 @@ async function validateAtPackageDir(
286
328
  let manifest;
287
329
  /** @type {string[]} */
288
330
  let unknownKeys;
331
+ /** @type {string[]} */
332
+ let declaredKeys;
289
333
  /** @type {string | undefined} */
290
334
  let agentDocsError;
291
335
  /** @type {string | undefined} */
292
336
  let cliCommandError;
293
337
  try {
294
- ({manifest, unknownKeys, agentDocsError, cliCommandError} =
338
+ ({manifest, unknownKeys, declaredKeys, agentDocsError, cliCommandError} =
295
339
  await loadManifest(
296
340
  manifestFile,
297
341
  `Integration manifest (${path.basename(manifestFile)})`,
@@ -301,9 +345,36 @@ async function validateAtPackageDir(
301
345
  return result;
302
346
  }
303
347
 
304
- /** @param {string | null | undefined} value */
305
- const resolveRoot = (value, kind = 'contribution root') => {
348
+ /** @type {string[]} absolute roots inside the package */
349
+ const absoluteRoots = [];
350
+ /**
351
+ * @param {string | null | undefined} value
352
+ * @param {string} key the manifest key that declares it
353
+ */
354
+ const resolveRoot = (value, key, kind = 'contribution root') => {
306
355
  if (value == null) return undefined;
356
+ if (path.isAbsolute(value)) {
357
+ // An absolute root names this machine's disk, which no install shares,
358
+ // even when it happens to point inside the package.
359
+ const inside = path.relative(packageDir, value);
360
+ const relative =
361
+ inside === '' || (!inside.startsWith('..') && !path.isAbsolute(inside))
362
+ ? `./${inside.split(path.sep).join('/')}`.replace(/\/$/u, '')
363
+ : null;
364
+ // The code an absolute root has always carried, reworded.
365
+ result.issues.push(
366
+ error(
367
+ 'root_outside_package',
368
+ `The ${key} root "${value}" is an absolute path, so Astryx reads nothing from it: a root is relative to the package, so it resolves the same wherever the package is installed.` +
369
+ (relative
370
+ ? ` Fix: set \`${key}: '${relative === './' ? '.' : relative}'\` in ${path.basename(manifestFile)}.`
371
+ : ` It also points outside the package. Fix: move the files into the package and set \`${key}\` to their folder relative to package.json, such as './${key}'.`),
372
+ ),
373
+ );
374
+ // Its files are reported through this issue, not one by one.
375
+ if (relative) absoluteRoots.push(path.resolve(value));
376
+ return undefined;
377
+ }
307
378
  try {
308
379
  return assertWithin(value, packageDir, {label: kind});
309
380
  } catch {
@@ -320,18 +391,20 @@ async function validateAtPackageDir(
320
391
  const loaded = {
321
392
  name: identity.name,
322
393
  version: identity.version,
323
- components: resolveRoot(manifest.components),
324
- templates: resolveRoot(manifest.templates),
325
- codemods: resolveRoot(manifest.codemods),
326
- docs: resolveRoot(manifest.docs),
327
- themes: resolveRoot(manifest.themes),
394
+ components: resolveRoot(manifest.components, 'components'),
395
+ templates: resolveRoot(manifest.templates, 'templates'),
396
+ codemods: resolveRoot(manifest.codemods, 'codemods'),
397
+ docs: resolveRoot(manifest.docs, 'docs'),
398
+ themes: resolveRoot(manifest.themes, 'themes'),
328
399
  issuesUrl: manifest.issuesUrl,
329
400
  agentDocs: manifest.agentDocs,
330
401
  cliCommand: manifest.cliCommand,
331
402
  __cliCommandError: cliCommandError,
332
403
  __agentDocsError: agentDocsError,
333
404
  __unknownKeys: unknownKeys,
405
+ __declaredKeys: declaredKeys,
334
406
  __spec: identity.name,
407
+ __declaredProviderId: manifest.providerId,
335
408
  __packageDir: packageDir,
336
409
  __manifestFile: manifestFile,
337
410
  };
@@ -343,7 +416,9 @@ async function validateAtPackageDir(
343
416
  // `integration check --strict` fails on any warning, and a new failure code
344
417
  // joins only in a minor release (architecture:cli-surface INV35). Doctor's
345
418
  // project check still warns about it.
346
- for (const issue of await validateLoadedIntegration(loaded)) {
419
+ const contribution = await validateLoadedIntegrationListed(loaded);
420
+ (result.listed ??= []).push(...contribution.listed);
421
+ for (const issue of contribution.issues) {
347
422
  if (issue.code === INVALID_CLI_COMMAND) {
348
423
  (result.notes ??= []).push({code: issue.code, message: issue.message});
349
424
  } else {
@@ -370,7 +445,11 @@ async function validateAtPackageDir(
370
445
  );
371
446
  if (scanUnreachable) {
372
447
  issues.push(
373
- ...(await findUnreachableContributionIssues(packageDir, loaded)),
448
+ ...(await findUnreachableContributionIssues(
449
+ packageDir,
450
+ loaded,
451
+ absoluteRoots,
452
+ )),
374
453
  );
375
454
  }
376
455
 
@@ -397,7 +476,7 @@ export async function validateLocalIntegration(cwd = process.cwd()) {
397
476
  return {found: false, issues: []};
398
477
  }
399
478
 
400
- /** @type {{name?: string, version?: string}} */
479
+ /** @type {{name?: string, version?: string, private?: boolean}} */
401
480
  let pkg;
402
481
  try {
403
482
  pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
@@ -416,31 +495,121 @@ export async function validateLocalIntegration(cwd = process.cwd()) {
416
495
  };
417
496
  }
418
497
 
419
- return validateAtPackageDir(
498
+ // npm publishes nothing without a name and a version, and the name is the
499
+ // provider ID every reader keys the package's contributions on.
500
+ // A package without one is still checked, under a placeholder name.
501
+ const fields = packageJsonFieldIssues(pkg, pkgJsonPath);
502
+ const name =
503
+ typeof pkg.name === 'string' && pkg.name !== ''
504
+ ? pkg.name
505
+ : '(local package)';
506
+ const result = await validateAtPackageDir(
420
507
  packageDir,
421
- {
422
- name: pkg.name ?? '(local package)',
423
- version: pkg.version,
424
- },
508
+ {name, version: pkg.version},
425
509
  {scanUnreachable: true},
426
510
  );
511
+ (result.listed ??= []).unshift(
512
+ ...fields,
513
+ // Only a manifest that loaded says whether it declares a providerId; one
514
+ // that did not is reported as invalid_manifest.
515
+ ...(result.integration
516
+ ? providerIdIssues(
517
+ pkg.name,
518
+ result.integration.__declaredProviderId,
519
+ result.integration.__manifestFile,
520
+ )
521
+ : []),
522
+ );
523
+ return result;
524
+ }
525
+
526
+ /**
527
+ * Whether `name` is a bare npm package name, the form a provider ID takes.
528
+ * @param {string} name
529
+ */
530
+ function isPackageName(name) {
531
+ try {
532
+ normalizeProviderId(name);
533
+ return true;
534
+ } catch {
535
+ return false;
536
+ }
427
537
  }
428
538
 
429
539
  /**
430
- * Validate an INSTALLED integration package resolved from `cwd`/node_modules.
540
+ * A warning when no provider ID can be derived for the package: its
541
+ * package.json `name` is not a bare package name and its manifest declares
542
+ * no `providerId` that is one. Readers key the package's contributions on
543
+ * that ID; a missing name is its own warning.
544
+ * @param {unknown} name package.json `name`
545
+ * @param {unknown} declared the manifest's `providerId`
546
+ * @param {string} manifestFile
547
+ * @returns {Issue[]}
548
+ */
549
+ function providerIdIssues(name, declared, manifestFile) {
550
+ if (typeof name !== 'string' || name === '') return [];
551
+ if (isPackageName(name)) return [];
552
+ if (typeof declared === 'string' && isPackageName(declared)) return [];
553
+ const manifest = path.basename(manifestFile);
554
+ return [
555
+ listed(
556
+ 'no_provider_id',
557
+ `No provider ID can be derived for this package: package.json "name" ${JSON.stringify(name)} is not a bare package name (an optional @scope/ and a name, each starting with a lowercase letter or digit and holding only lowercase letters, digits, "-", ".", "_" and "~"), and ${manifest} declares no providerId that is one. Astryx files the package's components, templates and docs under that ID, so links to them and selecting the package cannot work. Fix: rename the package, such as "@your-scope/your-package", or set \`providerId: '@your-scope/your-package'\` in ${manifest}.`,
558
+ ),
559
+ ];
560
+ }
561
+
562
+ /**
563
+ * A warning for each field npm needs that package.json lacks.
564
+ * @param {{name?: unknown, version?: unknown, private?: unknown}} pkg
565
+ * @param {string} pkgJsonPath
566
+ * @returns {Issue[]}
567
+ */
568
+ function packageJsonFieldIssues(pkg, pkgJsonPath) {
569
+ /** @type {Issue[]} */
570
+ const issues = [];
571
+ const shown = path.basename(pkgJsonPath);
572
+ // Listed, so validate and `--strict` keep the result they had for these
573
+ // packages; `integration verify`, which packs, reports them as errors.
574
+ if (typeof pkg.name !== 'string' || pkg.name === '') {
575
+ issues.push(
576
+ listed(
577
+ 'invalid_package_json',
578
+ `${shown} has no "name", so npm cannot pack or publish the package, and a project that installs it has no name to select it by. Fix: add "name": "@your-scope/your-package" to ${shown}.`,
579
+ ),
580
+ );
581
+ }
582
+ // A private package is never published, so it needs no version.
583
+ if (
584
+ pkg.private !== true &&
585
+ (typeof pkg.version !== 'string' || pkg.version === '')
586
+ ) {
587
+ issues.push(
588
+ listed(
589
+ 'invalid_package_json',
590
+ `${shown} has no "version", so npm cannot pack or publish the package. Fix: add "version": "0.1.0" to ${shown}.`,
591
+ ),
592
+ );
593
+ }
594
+ return issues;
595
+ }
596
+
597
+ /**
598
+ * Validate an INSTALLED integration package: the nearest `node_modules/<spec>`
599
+ * at or above `cwd`, resolved the way the project loads it.
431
600
  * @param {string} spec package name
432
601
  * @param {string} [cwd]
433
602
  * @returns {Promise<ValidateResult>}
434
603
  */
435
604
  export async function validateInstalledIntegration(spec, cwd = process.cwd()) {
436
- // resolvePackageDir throws (path-safety guard) on a spec with path segments,
605
+ // The resolver throws (path-safety guard) on a spec with path segments,
437
606
  // `..`, or an absolute path. Every other malformed input to this command
438
607
  // degrades into a diagnostic — so catch it here and return an issue instead
439
608
  // of letting the throw escape to a raw stack (human) / generic ERR_UNKNOWN
440
609
  // (--json).
441
610
  let packageDir;
442
611
  try {
443
- packageDir = resolvePackageDir(spec, cwd);
612
+ packageDir = resolveInstalledIntegrationDir(spec, cwd);
444
613
  } catch (err) {
445
614
  return {
446
615
  found: true,
@@ -504,7 +673,30 @@ export async function validateIntegration(pkg, options = {}) {
504
673
  const docTypes = integration
505
674
  ? await integrationDocTypes(integration, cwd)
506
675
  : undefined;
507
- const notes = result.found ? (result.notes ?? []) : [];
676
+ // The component names docs and blocks give: those that cannot be judged
677
+ // are listed in their reports, and those judged wrong are notes.
678
+ const names = integration
679
+ ? [
680
+ await componentDocNamesCheck(integration),
681
+ await blockComponentsCheck(integration),
682
+ ]
683
+ : undefined;
684
+ const docNames = names?.[0].report;
685
+ const blockComponents = names?.[1].report;
686
+ // Notes fail nothing: a malformed `cliCommand`, and the findings added
687
+ // after the last minor release, each with its fix in the message, which
688
+ // may not fail `--strict` until the next (architecture:cli-surface INV35):
689
+ // the layout findings and the component names judged wrong.
690
+ const notes = result.found
691
+ ? [
692
+ ...(result.notes ?? []),
693
+ ...(result.listed ?? []).map(issue => ({
694
+ code: issue.code,
695
+ message: issue.message,
696
+ })),
697
+ ...(names ?? []).flatMap(each => each.notes),
698
+ ]
699
+ : [];
508
700
  return {
509
701
  type: 'integration.validate',
510
702
  data: {
@@ -526,6 +718,8 @@ export async function validateIntegration(pkg, options = {}) {
526
718
  : result.issues,
527
719
  ...(notes.length > 0 ? {notes} : {}),
528
720
  ...(docTypes ? {docTypes} : {}),
721
+ ...(docNames ? {docNames} : {}),
722
+ ...(blockComponents ? {blockComponents} : {}),
529
723
  },
530
724
  };
531
725
  }
@@ -14,6 +14,7 @@ export type LoadedIntegration = {
14
14
  codemods?: string | undefined;
15
15
  docs?: string | undefined;
16
16
  issuesUrl?: string | undefined;
17
+ __declaredKeys?: string[] | undefined;
17
18
  __unknownKeys?: string[] | undefined;
18
19
  __spec: string;
19
20
  __packageDir: string;
@@ -36,7 +37,17 @@ export type ValidateIntegrationResponse = {
36
37
  type: "integration.validate";
37
38
  /**
38
39
  * `notes`: information beside the issues that fails nothing, such as
39
- * `invalid_cli_command`, a `cliCommand` that was ignored as malformed
40
+ * `invalid_cli_command`, a `cliCommand` that was ignored as malformed, and
41
+ * the package layout findings added after the last minor release, each with
42
+ * its fix in the message: two docs for one component
43
+ * (`duplicate_component_doc`), a root reached through a symlink
44
+ * (`symlinked_root`), a contribution file that cannot be read
45
+ * (`unreadable_file`), a package.json without `name` or `version`
46
+ * (`invalid_package_json`), no provider ID (`no_provider_id`), and a doc
47
+ * whose file name differs from its source only in letter case where the two
48
+ * still pair (`source_without_component_doc`); and `doc_name_mismatch` and
49
+ * `unknown_block_component` (see `docNames` and `blockComponents`); they
50
+ * become warnings in the next minor release
40
51
  */
41
52
  data: {
42
53
  validated: boolean;
@@ -48,6 +59,8 @@ export type ValidateIntegrationResponse = {
48
59
  message: string;
49
60
  }>;
50
61
  docTypes?: DocTypesReport;
62
+ docNames?: DocNamesReport;
63
+ blockComponents?: BlockComponentsReport;
51
64
  };
52
65
  };
53
66
  /**
@@ -57,3 +70,47 @@ export type ValidateIntegrationResponse = {
57
70
  export type DocTypesReport = import("../../foundation/discovery/doc-types.mjs").DocTypesReport & {
58
71
  fixtures?: number;
59
72
  };
73
+ /**
74
+ * Each component doc whose `name` differs from the name it is listed under,
75
+ * judged against its source. Present when the manifest loads. A name the
76
+ * module its printed import names provably does not export is a
77
+ * `doc_name_mismatch` note beside the issues, with its fix in the message
78
+ * (it becomes a warning in the next minor release), so `findings` is always
79
+ * empty; `unchecked` lists each doc whose source's
80
+ * exports could not all be read (a re-export from another package, `export
81
+ * =`, a destructured binding, a namespace): `file` (the doc, package-relative),
82
+ * `name` (what the doc says), and `reason`.
83
+ */
84
+ export type DocNamesReport = {
85
+ status: "checked";
86
+ findings: [];
87
+ unchecked: Array<{
88
+ file: string;
89
+ name: string;
90
+ reason: string;
91
+ }>;
92
+ };
93
+ /**
94
+ * Each component name a template gives in `exampleFor` or `componentsUsed`
95
+ * that could not be judged. Present when the manifest loads. A name neither
96
+ * the package nor Core has, and the template does not import, is an
97
+ * `unknown_block_component` note beside the issues, with its fix in the
98
+ * message (it becomes a warning in the next minor release), so `findings` is
99
+ * always empty;
100
+ * `unchecked` lists each name that cannot be judged: one the template may
101
+ * reach through anything other than named imports and re-exports from
102
+ * Core, React, StyleX, local helpers or the package's own entry (an import from another
103
+ * package, a default or namespace import, an alias of or member read off an imported
104
+ * binding, require(), a dynamic import(), a local module this check cannot
105
+ * read), or, without Core installed, any name Core would have to resolve: `file` (the template doc, package-relative),
106
+ * `name`, and `reason`.
107
+ */
108
+ export type BlockComponentsReport = {
109
+ status: "checked";
110
+ findings: [];
111
+ unchecked: Array<{
112
+ file: string;
113
+ name: string;
114
+ reason: string;
115
+ }>;
116
+ };
@@ -18,6 +18,7 @@
18
18
  * @property {string} [codemods]
19
19
  * @property {string} [docs]
20
20
  * @property {string} [issuesUrl]
21
+ * @property {string[]} [__declaredKeys]
21
22
  * @property {string[]} [__unknownKeys]
22
23
  * @property {string} __spec
23
24
  * @property {string} __packageDir
@@ -38,9 +39,19 @@
38
39
  * "healthy" — the two are otherwise byte-identical.
39
40
  * @typedef {object} ValidateIntegrationResponse
40
41
  * @property {'integration.validate'} type
41
- * @property {{validated: boolean, name: string | null, version: string | null, issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[], notes?: Array<{code: string, message: string}>, docTypes?: DocTypesReport}} data
42
+ * @property {{validated: boolean, name: string | null, version: string | null, issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[], notes?: Array<{code: string, message: string}>, docTypes?: DocTypesReport, docNames?: DocNamesReport, blockComponents?: BlockComponentsReport}} data
42
43
  * `notes`: information beside the issues that fails nothing, such as
43
- * `invalid_cli_command`, a `cliCommand` that was ignored as malformed
44
+ * `invalid_cli_command`, a `cliCommand` that was ignored as malformed, and
45
+ * the package layout findings added after the last minor release, each with
46
+ * its fix in the message: two docs for one component
47
+ * (`duplicate_component_doc`), a root reached through a symlink
48
+ * (`symlinked_root`), a contribution file that cannot be read
49
+ * (`unreadable_file`), a package.json without `name` or `version`
50
+ * (`invalid_package_json`), no provider ID (`no_provider_id`), and a doc
51
+ * whose file name differs from its source only in letter case where the two
52
+ * still pair (`source_without_component_doc`); and `doc_name_mismatch` and
53
+ * `unknown_block_component` (see `docNames` and `blockComponents`); they
54
+ * become warnings in the next minor release
44
55
  */
45
56
 
46
57
  /**
@@ -54,4 +65,36 @@
54
65
  * components or docs root, which those readers skip) were not checked
55
66
  */
56
67
 
68
+ /**
69
+ * Each component doc whose `name` differs from the name it is listed under,
70
+ * judged against its source. Present when the manifest loads. A name the
71
+ * module its printed import names provably does not export is a
72
+ * `doc_name_mismatch` note beside the issues, with its fix in the message
73
+ * (it becomes a warning in the next minor release), so `findings` is always
74
+ * empty; `unchecked` lists each doc whose source's
75
+ * exports could not all be read (a re-export from another package, `export
76
+ * =`, a destructured binding, a namespace): `file` (the doc, package-relative),
77
+ * `name` (what the doc says), and `reason`.
78
+ *
79
+ * @typedef {{status: 'checked', findings: [], unchecked: Array<{file: string, name: string, reason: string}>}} DocNamesReport
80
+ */
81
+
82
+ /**
83
+ * Each component name a template gives in `exampleFor` or `componentsUsed`
84
+ * that could not be judged. Present when the manifest loads. A name neither
85
+ * the package nor Core has, and the template does not import, is an
86
+ * `unknown_block_component` note beside the issues, with its fix in the
87
+ * message (it becomes a warning in the next minor release), so `findings` is
88
+ * always empty;
89
+ * `unchecked` lists each name that cannot be judged: one the template may
90
+ * reach through anything other than named imports and re-exports from
91
+ * Core, React, StyleX, local helpers or the package's own entry (an import from another
92
+ * package, a default or namespace import, an alias of or member read off an imported
93
+ * binding, require(), a dynamic import(), a local module this check cannot
94
+ * read), or, without Core installed, any name Core would have to resolve: `file` (the template doc, package-relative),
95
+ * `name`, and `reason`.
96
+ *
97
+ * @typedef {{status: 'checked', findings: [], unchecked: Array<{file: string, name: string, reason: string}>}} BlockComponentsReport
98
+ */
99
+
57
100
  export {};
@@ -62,7 +62,7 @@ export const doc = {
62
62
  {
63
63
  type: 'integration.validate',
64
64
  description:
65
- 'The result envelope: `data.validated`, false when nothing was validated — with no `package.json` at all, `data.issues` carries one `no_package` error-severity issue so the command exits 1; with a `package.json` but no integration manifest, `data.issues` is empty, which means "not looked at", not "healthy"; `data.name` and `data.version` of the validated package (both null when `data.validated` is false; `data.name` is `(local package)` when the local package.json has no readable name), plus `data.issues`, an AstryxIntegrationIssue[] of {code, severity: `warning` | `error`, message, fix?, file?, line?}: `fix` is the change that resolves the issue, and `file` and `line` where it is, relative to the package, when the check knows them; and `data.notes` when there are any: {code, message} information that fails nothing, such as `invalid_cli_command` for a `cliCommand` that was ignored as malformed. When the manifest loads, `data.docTypes` holds the doc type check: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `docs` (doc values checked), `findings` ({doc, line, export, type, field, problem: `unknown_field` | `missing_field` | `wrong_type`, severity: `warning`, message, fix}), `unchecked` ({doc, reason}: a doc that could not be fully checked, or a root its reader cannot read), and `fixtures` when the components or docs root holds test fixture docs under `__tests__`: how many, none of them checked.',
65
+ 'The result envelope: `data.validated`, false when nothing was validated — with no `package.json` at all, `data.issues` carries one `no_package` error-severity issue so the command exits 1; with a `package.json` but no integration manifest, `data.issues` is empty, which means "not looked at", not "healthy"; `data.name` and `data.version` of the validated package (both null when `data.validated` is false; `data.name` is `(local package)` when the local package.json has no readable name), plus `data.issues`, an AstryxIntegrationIssue[] of {code, severity: `warning` | `error`, message, fix?, file?, line?}: `fix` is the change that resolves the issue, and `file` and `line` where it is, relative to the package, when the check knows them; and `data.notes` when there are any: {code, message} information that fails nothing, such as `invalid_cli_command` for a `cliCommand` that was ignored as malformed, and the package layout findings added after the last minor release, each with its fix in the message: `duplicate_component_doc`, `symlinked_root`, `unreadable_file`, `invalid_package_json` (no `name` or `version`), `no_provider_id`, and `source_without_component_doc` for a doc whose file name differs from its source only in letter case where the two still pair; and `doc_name_mismatch` and `unknown_block_component` (see `docNames` and `blockComponents`); they become warnings in the next minor release. When the manifest loads, `data.docTypes` holds the doc type check: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `docs` (doc values checked), `findings` ({doc, line, export, type, field, problem: `unknown_field` | `missing_field` | `wrong_type`, severity: `warning`, message, fix}), `unchecked` ({doc, reason}: a doc that could not be fully checked, or a root its reader cannot read), and `fixtures` when the components or docs root holds test fixture docs under `__tests__`: how many, none of them checked. When the manifest loads, `data.docNames` lists the component doc names it could not judge: `status` (`checked`), `findings` (always empty; a doc name the module its import names provably does not export is a `doc_name_mismatch` note, with its fix in the message) and `unchecked` ({file, name, reason}: a doc whose `name` differs from the name it is listed under and whose source exports cannot all be read, such as a re-export from another package). When the manifest loads, `data.blockComponents` lists the component names templates give in `exampleFor` or `componentsUsed` that could not be judged: `status` (`checked`), `findings` (always empty; a name neither the package nor Core has, and the template does not import, is an `unknown_block_component` note, with its fix in the message) and `unchecked` ({file, name, reason}: a name the template may reach through anything other than named imports and re-exports from Core, React, StyleX, local helpers or the entry of the package itself, such as an import from another package, a default or namespace import, an alias of or member read off an imported binding, require() or a dynamic import(), or a local module that cannot be read, or any name Core would have to resolve when Core is not installed).',
66
66
  },
67
67
  ],
68
68
  examples: [
@@ -0,0 +1,28 @@
1
+ # Search ordering data
2
+
3
+ `astryx search` decides **what matches**, and each match's score and reason, with the lexical ranker (`_lexical.mjs` and the files it imports). The files in this folder only decide **the order of matches inside one score band**: they never add or drop a match, change a score, or move a match past one of another band or kind (doc or not). The bands are scores 195 and up, 188–194, 170–187, 160–169, 151–159, 100–150, 55–99, and under 55. A band can be wide (55–99 holds keyword matches beside one-word description hits), so inside a band a lower score can lead a higher one.
4
+
5
+ ## Rule
6
+
7
+ `spec:AST-083` owns search's result order; this file describes the data that implements it. Inside one score band, a searcher's static weights decide the order. Matches with the same score move together as one block, in the lexical ranker's order, placed by the learned position of the block's first match: moving each one on its own breaks search's existing rules for tied matches. Matches never cross bands or kinds. There is no model runtime: every number is a plain static value read at load time.
8
+
9
+ ## Files
10
+
11
+ | File | What it holds |
12
+ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `tree.weights.json` | The root classifier: for each query word or word pair, one number per class (component, hook, doc, template, theme, none). The query's tokens vote, and a topic searcher takes the query only when its class wins by the floor in `tree.mjs`. |
14
+ | `_main-searcher.weights.json`, `_doc-searcher.weights.json`, `_template-searcher.weights.json` | One weight, mean and spread per signal for each searcher (see `SIGNALS` and `PER_DOMAIN` in `_features.mjs`). A match's place inside its band is the weighted sum of its standardized signals. |
15
+ | `_cards.json` | One card per Core component and hook: what it is, the words people use for it, and its main props, written from the item's own source. Cards feed one signal, word overlap with the card. |
16
+
17
+ The signals are counts over the query and each entry's own text: the lexical score and rank, BM25 over the entry's name, docs text and card, how many query words the entry covers, whether the query holds the entry's name, and the classifier's guess of the domain.
18
+
19
+ ## Status
20
+
21
+ These files are static and reviewed. They are not computed when the CLI runs, and nothing at run time writes them.
22
+
23
+ ## Refreshing
24
+
25
+ - A Core component or hook without a card is still matched and scored as usual; inside its band it is ordered by its other signals.
26
+ - When Core adds or renames items, a reviewed change adds or updates their cards and, if needed, refreshed weights.
27
+ - Any refresh must keep every test that pins search's behavior passing, including the ordering invariants in `_order.test.mjs`. It must also keep the `astryx search` output's matches, scores and reasons identical to the lexical ranker's.
28
+ - Any refresh must also be measured as `spec:AST-083` FR6 requires: against the held-out, fresh and synthetic evaluation sets, with no row worse than the previous order (a row is worse when the paired 95% confidence interval of its change lies entirely below zero).