@astryxdesign/cli 0.7.2-canary.e26723d → 0.7.2-canary.e4bdd70

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 (124) hide show
  1. package/README.md +9 -8
  2. package/api/docs/_adapter.d.mts +11 -0
  3. package/api/docs/_adapter.mjs +37 -0
  4. package/api/index.d.mts +2 -0
  5. package/api/index.mjs +2 -0
  6. package/api/integration/add-contribution.mjs +1 -1
  7. package/api/integration/add-contribution.test.mjs +4 -2
  8. package/api/integration/check/check.d.mts +96 -0
  9. package/api/integration/check/check.mjs +357 -0
  10. package/api/integration/check/check.test.mjs +489 -0
  11. package/api/integration/integration-check.type.d.mts +118 -0
  12. package/api/integration/integration-check.type.mjs +84 -0
  13. package/api/integration/integrationCheck.doc.d.mts +10 -0
  14. package/api/integration/integrationCheck.doc.mjs +73 -0
  15. package/api/integration/integrationComponentConflicts.doc.mjs +19 -4
  16. package/api/integration/validate-integration.mjs +8 -0
  17. package/api/integration/validate-integration.type.d.mts +8 -0
  18. package/api/integration/validate-integration.type.mjs +12 -1
  19. package/api/integration/validateIntegration.doc.mjs +17 -3
  20. package/api/search/search.d.mts +13 -6
  21. package/api/search/search.domain-priority.test.mjs +93 -18
  22. package/api/search/search.integration-check.test.mjs +24 -0
  23. package/api/search/search.mjs +33 -9
  24. package/api/theme/build/build.family.test.mjs +54 -6
  25. package/api/theme/build/build.mjs +110 -13
  26. package/api/theme/themeBuild.doc.mjs +4 -4
  27. package/assets/docs/styling-overview.doc.mjs +1 -1
  28. package/assets/docs/tree/add-a-component.doc.mjs +1 -1
  29. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  30. package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
  31. package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
  32. package/assets/docs/tree/block-template.doc.mjs +8 -20
  33. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  34. package/assets/docs/tree/check-the-props.doc.mjs +51 -0
  35. package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
  36. package/assets/docs/tree/checks.doc.mjs +21 -13
  37. package/assets/docs/tree/codemods.doc.mjs +4 -4
  38. package/assets/docs/tree/component-family.doc.mjs +3 -5
  39. package/assets/docs/tree/component-imports.doc.mjs +2 -2
  40. package/assets/docs/tree/components.doc.mjs +1 -1
  41. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
  42. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  43. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  44. package/assets/docs/tree/docs.doc.mjs +3 -3
  45. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  46. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  47. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  48. package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
  49. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  50. package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
  51. package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
  52. package/assets/docs/tree/integrations.doc.mjs +8 -0
  53. package/assets/docs/tree/links.doc.mjs +5 -4
  54. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  55. package/assets/docs/tree/page-template.doc.mjs +4 -8
  56. package/assets/docs/tree/publishing.doc.mjs +1 -1
  57. package/assets/docs/tree/quick-start.doc.mjs +37 -21
  58. package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
  59. package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
  60. package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
  61. package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
  62. package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
  63. package/assets/docs/tree/single-component.doc.mjs +12 -4
  64. package/assets/docs/tree/start-a-template.doc.mjs +3 -3
  65. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  66. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  67. package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
  68. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  69. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  70. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  71. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  72. package/assets/docs/tree/templates.doc.mjs +1 -1
  73. package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
  74. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  75. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  76. package/assets/docs/tree/troubleshooting.doc.mjs +4 -4
  77. package/assets/docs/tree/upgrading.doc.mjs +3 -3
  78. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
  79. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  80. package/assets/docs/tree/versioning.doc.mjs +6 -6
  81. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -2
  82. package/authoring/codemod/codemod.doc.mjs +7 -3
  83. package/authoring/config/config.doc.mjs +9 -6
  84. package/authoring/config/type.ts +4 -2
  85. package/authoring/discover/discover.doc.mjs +1 -1
  86. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  87. package/authoring/doctypes/command/command.doc.mjs +14 -12
  88. package/authoring/doctypes/component/component.doc.mjs +28 -25
  89. package/authoring/doctypes/component/type.ts +7 -5
  90. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  91. package/authoring/doctypes/function/function.doc.mjs +9 -8
  92. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  93. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  94. package/authoring/doctypes/reference/reference.doc.mjs +11 -10
  95. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  96. package/authoring/doctypes/template/integration-template.doc.mjs +1 -1
  97. package/authoring/doctypes/template/template.doc.mjs +3 -3
  98. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  99. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  100. package/authoring/identity/identity.doc.mjs +3 -3
  101. package/authoring/integration/integration.doc.mjs +4 -3
  102. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  103. package/clients/cli/commands/doctor-integration-components.doc.mjs +8 -3
  104. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  105. package/clients/cli/commands/doctor-integration.test.mjs +447 -2
  106. package/clients/cli/commands/doctor.mjs +116 -0
  107. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  108. package/clients/cli/commands/integration-check.test.mjs +267 -0
  109. package/clients/cli/commands/integration.doc.mjs +6 -2
  110. package/clients/cli/commands/integration.mjs +107 -1
  111. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  112. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  113. package/clients/cli/index.mjs +1 -0
  114. package/foundation/discovery/authoring-self-docs.mjs +1 -1
  115. package/foundation/discovery/doc-types.mjs +2 -2
  116. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  117. package/foundation/discovery/integration-doc-files.mjs +231 -0
  118. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  119. package/foundation/discovery/prop-drift.d.mts +61 -3
  120. package/foundation/discovery/prop-drift.mjs +559 -61
  121. package/foundation/discovery/prop-drift.test.mjs +1032 -10
  122. package/foundation/doc-compiler/doc-loads.test.mjs +2 -0
  123. package/foundation/response/response-types.doc.mjs +7 -2
  124. package/package.json +9 -9
@@ -18,7 +18,8 @@ export const doc = {
18
18
  'accessibility, migration guides. Unlike ComponentDoc it is not tied to a component: ' +
19
19
  'drop a `.doc.mjs` in the docs directory and it shows up in `astryx docs`. Content is ' +
20
20
  'built from ordered sections of mixed content blocks.',
21
- appliesTo: 'assets/docs/<topic>.doc.mjs',
21
+ appliesTo:
22
+ "<topic>.doc.{ts,mjs,js} in the folder the integration manifest's `docs` names",
22
23
  fields: [
23
24
  {
24
25
  name: 'type',
@@ -69,7 +70,7 @@ export const doc = {
69
70
  name: 'extends',
70
71
  type: 'string',
71
72
  description:
72
- "Name of an existing topic this doc merges onto, section by section: a section whose title matches one in the base replaces it, a section the base does not have is appended. The topic keeps the base's title and description. For correcting or adding to a topic rather than owning it. Exclusive with `replaces`.",
73
+ "Name of an existing topic this doc merges onto, section by section. A section replaces the base section with the same `id`, or else the one with exactly its title when either of the two has no `id`. A key derived from a title counts only against an `id`: a section without an `id` also matches a base section whose `id` is the key its title derives, and a section with an `id` also matches a base section without one whose title derives that `id`. Two different `id`s never match, two sections without an `id` whose titles differ only in case or punctuation stay separate, and a section that matches none is appended. The topic keeps the base's title and description. For correcting or adding to a topic rather than owning it. Exclusive with `replaces`.",
73
74
  example: "'theme'",
74
75
  },
75
76
  {
@@ -83,7 +84,7 @@ export const doc = {
83
84
  name: 'sections[].id',
84
85
  type: 'string',
85
86
  description:
86
- 'Stable section anchor. New docs should set this instead of relying on a mutable title.',
87
+ 'Stable section anchor. New docs should set this instead of relying on a mutable title. Published @astryxdesign/cli 0.6.3 and earlier reject the field and hide every doc topic the package ships, so `astryx integration verify` fails an integration that sets one until it declares an `@astryxdesign/cli` peer of `>=0.6.4`.',
87
88
  },
88
89
  {
89
90
  name: 'sections[].title',
@@ -109,7 +110,7 @@ export const doc = {
109
110
  name: 'sections[].previewType',
110
111
  type: 'ReferenceTokenPreviewType',
111
112
  description:
112
- "Preview type for token tables in this section. When set, the docsite renders a visual preview column from the token's computed value. Omit for non-token sections.",
113
+ 'Preview type for token tables in this section. The CLI rejects a value not in the list below and returns the value in `astryx docs --json`, but draws no preview from it. Omit for non-token sections.',
113
114
  },
114
115
  ],
115
116
  },
@@ -117,17 +118,17 @@ export const doc = {
117
118
  name: 'tokenCategory',
118
119
  type: 'string',
119
120
  description:
120
- "Token category for foundational docs that map to a token section (e.g. 'color'). Lets the tokens overview link to this doc for detailed guidance.",
121
+ "Token category for foundational docs that map to a token section (e.g. 'color'). `astryx docs --json` returns it as authored; no tokens page links to the doc through it.",
121
122
  },
122
123
  ],
123
124
  examples: [
124
125
  {
125
126
  label: 'A reference doc with one section',
126
127
  code: `/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
127
- export const docs = {
128
+ export default {
128
129
  type: 'generic',
129
- name: 'spacing',
130
- title: 'Spacing',
130
+ name: 'acme-spacing',
131
+ title: 'Acme Spacing',
131
132
  description: 'Spacing tokens for gap, margin, and padding.',
132
133
  category: 'foundations',
133
134
  tokenCategory: 'spacing',
@@ -194,7 +195,7 @@ type ReferenceDocBlock = {
194
195
 
195
196
  type GraphContentBlock =
196
197
  | { type: 'workflow'; title?: string; steps: WorkflowStep[] }
197
- | { type: 'collection'; source: {slot: string}; presentation?: 'list' | 'cards' | 'compact'; whenEmpty?: 'show' | 'omit' }
198
+ | { type: 'collection'; title?: string; source: {slot: string}; presentation?: 'list' | 'cards' | 'compact'; whenEmpty?: 'show' | 'omit' }
198
199
  | ReferenceDocBlock;`,
199
200
  },
200
201
  {
@@ -203,7 +204,7 @@ type GraphContentBlock =
203
204
  },
204
205
  {
205
206
  type: 'prose',
206
- text: "A section may set `previewType` to render a visual preview column for token tables: one of 'swatch' | 'shadow-box' | 'radius-box' | 'spacing-bar' | 'size-bar' | 'border-line' | 'duration-bar' | 'easing-curve' | 'font-sample'.",
207
+ text: "A section may set `previewType`, a hint for previewing its token table, to one of 'swatch' | 'shadow-box' | 'radius-box' | 'spacing-bar' | 'size-bar' | 'border-line' | 'duration-bar' | 'easing-curve' | 'font-sample'; the CLI rejects any other value.",
207
208
  },
208
209
  ],
209
210
  };
@@ -55,7 +55,7 @@ export const doc = {
55
55
  name: 'aliases',
56
56
  type: 'string[]',
57
57
  description:
58
- 'Alternate slugs that also resolve to this doc (back-compat).',
58
+ 'Reserved: alternate slugs for this doc. Nothing reads it yet: `astryx docs` finds a schema doc by its `name` only.',
59
59
  },
60
60
  {
61
61
  name: 'appliesTo',
@@ -141,7 +141,7 @@ export const doc = {
141
141
  name: 'notes',
142
142
  type: 'ReferenceContentBlock[]',
143
143
  description:
144
- 'Freeform prose/notes rendered after the field table. Same block union as ReferenceDoc (prose, heading, code, table, list, token-ref).',
144
+ 'Freeform prose/notes rendered after the field table and examples. The stable ReferenceContentBlock union (prose, heading, code, table, list, token-ref), without the reference block a ReferenceDoc section also takes; a token-ref note is accepted but not rendered.',
145
145
  },
146
146
  ],
147
147
  examples: [
@@ -152,17 +152,17 @@ export const doc = {
152
152
  type: 'schema',
153
153
  name: 'integration',
154
154
  displayName: 'Astryx Integration',
155
- namespace: 'cli',
156
- description: 'The astryx.integration.* manifest that registers the components a package provides.',
155
+ namespace: 'authoring',
156
+ description: 'The astryx.integration.* manifest that points the CLI at what a package contributes.',
157
157
  appliesTo: 'astryx.integration.{ts,mjs,js}',
158
158
  fields: [
159
- {name: 'name', type: 'string', description: 'Package name.', required: true},
159
+ {name: 'docs', type: 'string', description: 'The folder that holds your doc topics, relative to package.json.'},
160
160
  {
161
- name: 'components',
162
- type: '{ dir: string }',
163
- description: 'Where component sources live.',
161
+ name: 'agentDocs',
162
+ type: '{ append?: readonly string[] }',
163
+ description: 'Guidance your package adds to the agent instructions the CLI manages.',
164
164
  fields: [
165
- {name: 'components.dir', type: 'string', description: 'Glob root for Acme*.tsx files.', required: true},
165
+ {name: 'agentDocs.append', type: 'readonly string[]', description: 'Lines added at the end, at most eight.'},
166
166
  ],
167
167
  },
168
168
  ],
@@ -43,7 +43,7 @@ export default {
43
43
  notes: [
44
44
  {
45
45
  type: 'prose',
46
- text: "Every other field, and the page and block variants, are TemplateDoc's. Core's own templates use TemplateDoc, whose `category` is a TemplateCategory value.",
46
+ text: "Every other field, and the page and block variants, are TemplateDoc's. Core's own templates use TemplateDoc, whose `category` is a TemplateCategory value. `astryx doctor integration validate` checks an integration's template docs against IntegrationTemplateDoc.",
47
47
  },
48
48
  ],
49
49
  };
@@ -31,14 +31,14 @@ export const doc = {
31
31
  name: 'name',
32
32
  type: 'string',
33
33
  description:
34
- 'Stable identifier for block templates; change `displayName`, not `name`, to edit their visible label. For page templates it is a human-readable label, while the existing template-directory/CLI slug owns the default registry path.',
34
+ "Stable identifier for block templates; change `displayName`, not `name`, to edit their visible label in the gallery. For page templates it is a human-readable label, while the existing template-directory/CLI slug owns the default registry path. `astryx template --list` and `astryx search` print `name` as the label of both, and `astryx template <id>` finds a template by its id, never by `name`: an integration template's id is its path under the templates folder without the `.doc.*` suffix.",
35
35
  required: true,
36
36
  },
37
37
  {
38
38
  name: 'displayName',
39
39
  type: 'string',
40
40
  description:
41
- "Human-readable label for the gallery/CLI. Spaces out block names that mirror a PascalCase component ('ChatMessageMetadata' → 'Chat Message Metadata').",
41
+ "Human-readable label for the gallery. The CLI prints `name` instead; `astryx template --list --json` also returns an integration template's `displayName`. Spaces out block names that mirror a PascalCase component ('ChatMessageMetadata' → 'Chat Message Metadata').",
42
42
  required: true,
43
43
  },
44
44
  {
@@ -162,7 +162,7 @@ export default {
162
162
  notes: [
163
163
  {
164
164
  type: 'prose',
165
- text: 'When it loads, a template may leave out `displayName` and `aspectRatio` and use its own `category`, as integration templates already published do; a block with no `aspectRatio` previews at 1. Write to the type anyway; it is the contract.',
165
+ text: 'When it loads, a template may leave out `displayName` and `aspectRatio` and use its own `category`, as integration templates already published do; a block with no `aspectRatio` previews at 1. Write to the type anyway; it is the contract, and `astryx doctor integration validate` warns about an integration template that leaves out `displayName` or `aspectRatio`.',
166
166
  },
167
167
  {
168
168
  type: 'prose',
@@ -12,7 +12,7 @@ export const doc = {
12
12
  displayName: 'ThemeDoc',
13
13
  namespace: 'authoring',
14
14
  description:
15
- 'The strongly typed descriptor for one integration or CLI-bundled source theme.',
15
+ "The strongly typed descriptor for one source theme: bundled with the CLI, shipped by an integration, or kept in the project's src/themes, where `theme eject` writes one.",
16
16
  appliesTo: '<theme-source-stem>.doc.mjs',
17
17
  fields: [
18
18
  {
@@ -65,7 +65,7 @@ export default {
65
65
  notes: [
66
66
  {
67
67
  type: 'prose',
68
- text: 'ThemeDoc is static metadata. Default-export one object with literal string and boolean fields; discovery validates it without executing the descriptor or theme source.',
68
+ text: "ThemeDoc is static metadata. The descriptor holds one default-exported object of literal string and boolean fields, directly under a `/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */` comment, and nothing else: without that comment, or with any other statement, it fails to load. Discovery validates it without executing the descriptor or theme source.",
69
69
  },
70
70
  {
71
71
  type: 'prose',
@@ -15,7 +15,7 @@ export const doc = {
15
15
  displayName: 'GapReportHandler',
16
16
  namespace: 'authoring',
17
17
  description:
18
- 'A handler for `astryx gap-report`: a plain object with an `audience` and a `handle` function. Set it as `gapReport` in astryx.config, or export it as `gapReport` from an integration manifest. Every handler runs: the project handler first, then each integration handler in config order.',
18
+ "A handler for `astryx gap-report`: a plain object with an `audience` and a `handle` function. Set it as `gapReport` in astryx.config, or export it as `gapReport` from an integration manifest. Handlers add up rather than replace each other: the project handler first, then each loaded integration's in load order (the `integrations` list, then autolinked ones), whichever package the report is about.",
19
19
  appliesTo:
20
20
  '`gapReport` in astryx.config.*, or the `gapReport` named export of astryx.integration.*',
21
21
  fields: [
@@ -104,7 +104,7 @@ export const doc = {
104
104
  name: 'report.target.version',
105
105
  type: 'string',
106
106
  description:
107
- 'The installed version. Null when it is not known.',
107
+ "The integration's version from its package.json. Null for Core, and when the package.json has none.",
108
108
  required: true,
109
109
  },
110
110
  {
@@ -12,7 +12,7 @@ export const doc = {
12
12
  displayName: 'Provider and artifact identity',
13
13
  namespace: 'authoring',
14
14
  description:
15
- 'Separates stable provider/artifact identity from package instances and runtime lifecycle state. Provider identity names integration manifests and settles provider conflicts. Docs-tree nodes are identified by doc identity; the tree covers the CLI\'s own docs. Artifact, instance, and compiler-input identities are defined for every provider-owned artifact; no command reads them.',
15
+ "Separates stable provider/artifact identity from package instances and runtime lifecycle state. Provider identity names integration manifests and settles provider conflicts. Docs-tree nodes are identified by doc identity; the tree covers the CLI's docs and each integration's. Artifact IDs for kinds outside the docs tree, provider instances, and compiler-input entries are defined for every provider-owned artifact; no command builds them.",
16
16
  appliesTo:
17
17
  'Integration manifests, provider conflicts, and docs-tree node ids.',
18
18
  fields: [
@@ -41,14 +41,14 @@ export const doc = {
41
41
  name: 'ProviderInstance',
42
42
  type: '{ id; providerId; packageName; packageVersion; sourceDigest }',
43
43
  description:
44
- 'One immutable package version and source/content digest. Installed, configured, loaded, selected, and healthy state is a separate runtime overlay.',
44
+ 'One immutable package version and source/content digest. It carries no installed, configured, loaded, selected, or healthy state.',
45
45
  required: true,
46
46
  },
47
47
  {
48
48
  name: 'AuthoredDocEntry',
49
49
  type: '{ id; provider; kind; stableName; source; authored }',
50
50
  description:
51
- 'Normalized compiler input. Discovery supplies stableName independently from the authored display name, source paths remain package-relative, and provider provenance plus the authored snapshot are immutable.',
51
+ 'Normalized compiler input. `stableName`, the key in its doc ID, is given separately from the authored display name; source paths remain package-relative, and provider provenance plus the authored snapshot are immutable.',
52
52
  required: true,
53
53
  },
54
54
  ],
@@ -51,7 +51,7 @@ export const doc = {
51
51
  name: 'docs',
52
52
  type: 'string',
53
53
  description:
54
- 'The folder that holds your doc topics, relative to package.json. Every {topic}.doc.{ts,mjs,js} in it shows up in `astryx docs` next to the built-in topics; a topic can also set `replaces` or `extends` to take over a built-in topic or add to it.',
54
+ 'The folder that holds your doc topics, relative to package.json. Every {topic}.doc.{ts,mjs,js} under it shows up in `astryx docs`: a flat topic next to the built-in topics, where it can set `replaces` or `extends` to take over a built-in topic or add to it, and a guide with `placement` at its own route in the docs tree, under a namespace doc of the same package.',
55
55
  example: "'./docs'",
56
56
  },
57
57
  {
@@ -71,7 +71,8 @@ export const doc = {
71
71
  {
72
72
  name: 'issuesUrl',
73
73
  type: 'string',
74
- description: 'Where to file issues/feedback for this integration.',
74
+ description:
75
+ "Where people file issues for this package: an absolute URL, such as its GitHub issues page. A value that is not a URL fails the whole manifest; the scheme is not checked. Nothing warns when it is missing, but without it `astryx gap-report` about this package fails unless the project or a loaded integration has a gapReport handler, and `astryx swizzle` prints no feedback link for this package's components. When no handler is loaded, a github.com issues URL is filed with the GitHub CLI once the reporter passes `--confirm-public`; any other URL is returned for the reporter to open.",
75
76
  example: "'https://github.com/acme/widgets/issues'",
76
77
  },
77
78
  {
@@ -118,7 +119,7 @@ export const doc = {
118
119
  },
119
120
  {
120
121
  type: 'prose',
121
- text: 'A themes root is forward-compatible but version-gated: a CLI released before this field ignores it with a warning and continues loading every contribution kind it understands. That older CLI cannot list or add the contributed themes.',
122
+ text: 'A themes root is version-gated: an older CLI lists and adds none of its themes. Depending on its age, that CLI ignores `themes` with an `unknown_manifest_key` warning and loads every other contribution kind, reports the root as invalid because it expects the earlier `manifest.json` theme catalog, or, if it is old enough to treat an unknown field as an error, rejects the whole manifest, so the package contributes nothing there.',
122
123
  },
123
124
  {
124
125
  type: 'prose',
@@ -9,9 +9,12 @@
9
9
  * - Multi-file write is atomic: if one file fails, none are left behind.
10
10
  * - An `--out` escape, or an output directory that cannot be created, fails
11
11
  * with a registered error code.
12
- * - Every output stays inside the working directory: an absolute `--out`
13
- * path or a theme file outside it fails before anything is written, while
14
- * absolute paths inside it build, also through a symlinked spelling.
12
+ * - Every output a build writes stays inside the working directory: an
13
+ * absolute `--out` path or a theme file outside it fails before anything
14
+ * is written, while absolute paths inside it build, also through a
15
+ * symlinked spelling. `--check` writes nothing, so it compares those
16
+ * outside outputs read-only, and a stale report for them ends with a
17
+ * remedy that works: the directory to build and check from.
15
18
  *
16
19
  * Runs the CLI in-process (shared runCli harness) against a tiny synthetic theme file.
17
20
  */
@@ -25,6 +28,7 @@ import {runCli} from '../../../test-utils/run-cli.mjs';
25
28
  import {themeBuild} from '../../../api/theme/build/build.mjs';
26
29
  import {AstryxError} from '../../../api/error.mjs';
27
30
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
31
+ import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
28
32
 
29
33
  function writeTheme(dir, name) {
30
34
  fs.mkdirSync(dir, {recursive: true});
@@ -62,7 +66,9 @@ describe('theme build path safety', () => {
62
66
  );
63
67
 
64
68
  expect(result.code).not.toBe(0);
65
- expect(result.stderr + result.stdout).toMatch(/path separator|invalid theme name/i);
69
+ expect(result.stderr + result.stdout).toMatch(
70
+ /path separator|invalid theme name/i,
71
+ );
66
72
 
67
73
  // The traversal target must not exist.
68
74
  expect(fs.existsSync(path.join(tmpDir, 'escaped.js'))).toBe(false);
@@ -95,7 +101,10 @@ describe('theme build path safety', () => {
95
101
 
96
102
  const themeFile = writeTheme(themesDir, '../leak');
97
103
 
98
- await runCli(['theme', 'build', path.relative(project, themeFile)], project);
104
+ await runCli(
105
+ ['theme', 'build', path.relative(project, themeFile)],
106
+ project,
107
+ );
99
108
 
100
109
  // Neither the CSS nor any sibling JS should have been written.
101
110
  const entries = fs.readdirSync(themesDir).sort();
@@ -117,41 +126,88 @@ describe('theme build output-path failures carry registered codes', () => {
117
126
  return {result, envelope: JSON.parse(result.stdout)};
118
127
  }
119
128
 
120
- it('reports an --out path outside the project as ERR_PATH_TRAVERSAL', async () => {
129
+ it('reports a relative --out path outside the project as ERR_PATH_TRAVERSAL, with --check too', async () => {
121
130
  const project = path.join(tmpDir, 'project');
122
131
  const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
123
132
 
133
+ for (const extra of [[], ['--check']]) {
134
+ const {result, envelope} = await buildJson(
135
+ [
136
+ path.relative(project, themeFile),
137
+ '--out',
138
+ '../escaped.css',
139
+ ...extra,
140
+ ],
141
+ project,
142
+ );
143
+
144
+ expect(result.code).toBe(1);
145
+ expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
146
+ expect(registered.has(envelope.code)).toBe(true);
147
+ }
148
+ expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
149
+ });
150
+
151
+ it('reports an absolute --out path outside the project as ERR_PATH_TRAVERSAL', async () => {
152
+ const project = path.join(tmpDir, 'project');
153
+ const outside = path.join(tmpDir, 'outside');
154
+ fs.mkdirSync(outside, {recursive: true});
155
+ const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
156
+ const out = path.join(outside, 'ocean.css');
157
+
124
158
  const {result, envelope} = await buildJson(
125
- [path.relative(project, themeFile), '--out', '../escaped.css'],
159
+ [path.relative(project, themeFile), '--out', out],
126
160
  project,
127
161
  );
128
162
 
129
163
  expect(result.code).toBe(1);
130
164
  expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
131
- expect(registered.has(envelope.code)).toBe(true);
132
- expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
165
+ expect(fs.readdirSync(outside)).toEqual([]);
133
166
  });
134
167
 
135
- it('reports an absolute --out path outside the project as ERR_PATH_TRAVERSAL, with --check too', async () => {
168
+ it('checks an absolute --out path outside the project read-only', async () => {
136
169
  const project = path.join(tmpDir, 'project');
137
170
  const outside = path.join(tmpDir, 'outside');
138
171
  fs.mkdirSync(outside, {recursive: true});
139
- const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
172
+ const themeFile = path.relative(
173
+ project,
174
+ writeTheme(path.join(project, 'themes'), 'ocean'),
175
+ );
140
176
  const out = path.join(outside, 'ocean.css');
141
177
 
142
- for (const extra of [[], ['--check']]) {
143
- const {result, envelope} = await buildJson(
144
- [path.relative(project, themeFile), '--out', out, ...extra],
145
- project,
146
- );
178
+ const missing = await buildJson(
179
+ [themeFile, '--out', out, '--check'],
180
+ project,
181
+ );
182
+ expect(missing.result.code).toBe(1);
183
+ expect(missing.envelope.type).toBe('theme.build.check');
184
+ expect(missing.envelope.data.upToDate).toBe(false);
185
+ expect(fs.readdirSync(outside)).toEqual([]);
147
186
 
148
- expect(result.code).toBe(1);
149
- expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
187
+ // Outputs built inside the project compare equal once copied outside:
188
+ // only the volatile Command line, which --check ignores, names --out.
189
+ const dist = path.join(project, 'dist');
190
+ const built = await buildJson(
191
+ [themeFile, '--out', path.join(dist, 'ocean.css')],
192
+ project,
193
+ );
194
+ expect(built.result.code).toBe(0);
195
+ for (const file of fs.readdirSync(dist)) {
196
+ fs.copyFileSync(path.join(dist, file), path.join(outside, file));
150
197
  }
151
- expect(fs.readdirSync(outside)).toEqual([]);
198
+ const copied = fs.readdirSync(outside).sort();
199
+
200
+ const current = await buildJson(
201
+ [themeFile, '--out', out, '--check'],
202
+ project,
203
+ );
204
+ expect(current.result.code).toBe(0);
205
+ expect(current.envelope.type).toBe('theme.build.check');
206
+ expect(current.envelope.data.upToDate).toBe(true);
207
+ expect(fs.readdirSync(outside).sort()).toEqual(copied);
152
208
  });
153
209
 
154
- it('reports a theme file outside the project as ERR_PATH_TRAVERSAL, because its outputs go beside it', async () => {
210
+ it('reports a theme file outside the project as ERR_PATH_TRAVERSAL, because its outputs go beside it; --check only compares them', async () => {
155
211
  const project = path.join(tmpDir, 'project');
156
212
  fs.mkdirSync(project, {recursive: true});
157
213
  const shared = path.join(tmpDir, 'shared');
@@ -162,10 +218,82 @@ describe('theme build output-path failures carry registered codes', () => {
162
218
 
163
219
  expect(result.code).toBe(1);
164
220
  expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
221
+
222
+ const checked = await buildJson([file, '--check'], project);
223
+ expect(checked.result.code).toBe(1);
224
+ expect(checked.envelope.type).toBe('theme.build.check');
225
+ expect(checked.envelope.data.upToDate).toBe(false);
165
226
  }
166
227
  expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
167
228
  });
168
229
 
230
+ /**
231
+ * Read the remedy that ends a stale report for outputs outside cwd: the
232
+ * directory to run from, then the build and check arguments after `theme
233
+ * build`. Each command starts with the install-aware invocation for it.
234
+ * @param {string} stderr
235
+ */
236
+ function readRemedy(stderr) {
237
+ const lines = stderr.split('\n').map(line => line.trim());
238
+ const cd = lines.find(line => line.startsWith('cd '));
239
+ expect(cd).toBeDefined();
240
+ const dir = /** @type {string} */ (cd).slice('cd '.length);
241
+ const stem = `${getCliInvocation(dir)} theme build `;
242
+ const commands = lines.filter(line => line.startsWith(stem));
243
+ expect(commands).toHaveLength(2);
244
+ const [build, check] = commands.map(line =>
245
+ line.slice(stem.length).split(' '),
246
+ );
247
+ expect(check).toEqual([...build, '--check']);
248
+ return {dir, build, check};
249
+ }
250
+
251
+ it('ends a stale report for outputs outside cwd with a remedy that works, and keeps the report for outputs inside it', async () => {
252
+ const project = path.join(tmpDir, 'project');
253
+ const inside = path.relative(
254
+ project,
255
+ writeTheme(path.join(project, 'themes'), 'ocean'),
256
+ );
257
+ const shared = writeTheme(path.join(tmpDir, 'shared'), 'ocean');
258
+ const outside = path.join(tmpDir, 'outside');
259
+
260
+ for (const args of [
261
+ [path.relative(project, shared)],
262
+ [inside, '--out', path.join(outside, 'ocean.css')],
263
+ ]) {
264
+ const stale = await runCli(
265
+ ['theme', 'build', ...args, '--check'],
266
+ project,
267
+ );
268
+ expect(stale.code).toBe(1);
269
+ // The old remedy ran the build from cwd, which refuses these outputs.
270
+ expect(stale.stderr).not.toContain('Rebuild with:');
271
+ expect(stale.stderr).not.toContain(`theme build ${args.join(' ')}`);
272
+
273
+ // Following the remedy builds, and the check from there passes.
274
+ const remedy = readRemedy(stale.stderr);
275
+ const built = await runCli(
276
+ ['theme', 'build', ...remedy.build],
277
+ remedy.dir,
278
+ );
279
+ expect(built.code).toBe(0);
280
+ const checked = await runCli(
281
+ ['--json', 'theme', 'build', ...remedy.check],
282
+ remedy.dir,
283
+ );
284
+ expect(checked.code).toBe(0);
285
+ expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
286
+ }
287
+
288
+ // Outputs inside cwd keep the plain rebuild line.
289
+ const local = await runCli(['theme', 'build', inside, '--check'], project);
290
+ expect(local.code).toBe(1);
291
+ expect(local.stderr).toContain(
292
+ `Rebuild with: astryx theme build ${inside}`,
293
+ );
294
+ expect(local.stderr).not.toMatch(/^\s*cd /m);
295
+ });
296
+
169
297
  it('builds absolute paths inside the project, also when they reach it through a symlink', async () => {
170
298
  const project = path.join(tmpDir, 'project');
171
299
  const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
@@ -243,7 +371,7 @@ describe('theme build output-path failures carry registered codes', () => {
243
371
  });
244
372
  });
245
373
 
246
- it('themeBuild() rejects an output outside cwd: an absolute out path, or the default beside a theme file outside cwd', async () => {
374
+ it('themeBuild() rejects an output outside cwd, an absolute out path or the default beside a theme file outside cwd, and checks it read-only', async () => {
247
375
  const project = path.join(tmpDir, 'project');
248
376
  const inside = writeTheme(path.join(project, 'themes'), 'ocean');
249
377
  const shared = path.join(tmpDir, 'shared');
@@ -257,6 +385,12 @@ describe('theme build output-path failures carry registered codes', () => {
257
385
  await expect(
258
386
  themeBuild(file, options, {cwd: project}),
259
387
  ).rejects.toMatchObject({code: ERROR_CODES.ERR_PATH_TRAVERSAL});
388
+ await expect(
389
+ themeBuild(file, {...options, check: true}, {cwd: project}),
390
+ ).resolves.toMatchObject({
391
+ type: 'theme.build.check',
392
+ data: {upToDate: false},
393
+ });
260
394
  }
261
395
  expect(fs.existsSync(outDir)).toBe(false);
262
396
  expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
@@ -21,8 +21,10 @@ export const doc = {
21
21
  'with a warning for each such component, naming the range to declare. ' +
22
22
  "Then it compares each component doc's props with the component's TypeScript " +
23
23
  "props, using the project's own `typescript`, and warns about a documented prop " +
24
- 'the code lacks, a prop the package declares that the doc omits, and a wrong ' +
25
- '`required`. Without TypeScript it prints one ' +
24
+ 'the code lacks, a prop the package declares that the doc omits, a wrong ' +
25
+ '`required`, and a type that does not match where both sides read exactly ' +
26
+ '(it lists every other documented type as not compared, with the reason). ' +
27
+ 'Without TypeScript it prints one ' +
26
28
  'line saying the comparison was skipped.',
27
29
  fn: 'integrationComponentConflicts',
28
30
  args: [
@@ -35,7 +37,10 @@ export const doc = {
35
37
  },
36
38
  ],
37
39
  examples: [
38
- {label: 'Check the local integration', cli: 'astryx doctor integration components'},
40
+ {
41
+ label: 'Check the local integration',
42
+ cli: 'astryx doctor integration components',
43
+ },
39
44
  {
40
45
  label: 'Check an installed integration',
41
46
  cli: 'astryx doctor integration components @acme/widgets --json',
@@ -14,7 +14,13 @@ export const doc = {
14
14
  description:
15
15
  'Validates one integration at a time: the local package rooted at cwd, or an ' +
16
16
  'installed package resolved by name. It schema-checks the manifest, verifies ' +
17
- 'every declared contribution root, and reports every finding. Safe as a CI gate.',
17
+ 'every declared contribution root, and reports every finding. Safe as a CI gate. ' +
18
+ 'It then checks every doc the package ships under its roots against the ' +
19
+ "published authoring type for its kind, with the project's own `typescript`, and " +
20
+ 'warns about each field that does not match, naming the field and a fix. A doc ' +
21
+ 'it cannot fully check is listed with the reason, and test fixtures under ' +
22
+ '`__tests__` in the components or docs root are counted, not checked. Without ' +
23
+ 'TypeScript it prints one line saying the docs were not type-checked.',
18
24
  fn: 'validateIntegration',
19
25
  args: [
20
26
  {
@@ -36,7 +42,10 @@ export const doc = {
36
42
  },
37
43
  ],
38
44
  exitCodes: [
39
- {code: 0, when: 'no error-severity issues (warnings are allowed)'},
45
+ {
46
+ code: 0,
47
+ when: 'no error-severity issues (warnings, including doc type findings, are allowed)',
48
+ },
40
49
  {code: 1, when: 'one or more error-severity issues'},
41
50
  ],
42
51
  related: ['doctor integration templates', 'template'],