@astryxdesign/cli 0.6.3-canary.7ada42e → 0.6.3-canary.7ec7be0

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 (221) hide show
  1. package/README.md +21 -19
  2. package/api/build/_adapter.d.mts +50 -0
  3. package/api/build/_adapter.mjs +60 -0
  4. package/api/build/build.doc.mjs +13 -7
  5. package/api/build/build.test.mjs +184 -6
  6. package/api/build/build.type.d.mts +57 -2
  7. package/api/build/build.type.mjs +30 -7
  8. package/api/build/help/help.d.mts +4 -0
  9. package/api/build/help/help.mjs +24 -11
  10. package/api/build/kit/kit.d.mts +4 -1
  11. package/api/build/kit/kit.mjs +165 -49
  12. package/api/build/kit/rank.d.mts +44 -0
  13. package/api/build/kit/rank.mjs +432 -0
  14. package/api/build/kit/rank.test.mjs +196 -0
  15. package/api/component/_adapter.d.mts +6 -12
  16. package/api/component/_adapter.mjs +20 -10
  17. package/api/component/component.mjs +60 -12
  18. package/api/component/list/list.mjs +3 -2
  19. package/api/docs/_adapter.d.mts +251 -28
  20. package/api/docs/_adapter.mjs +918 -35
  21. package/api/docs/detail/detail.mjs +11 -3
  22. package/api/docs/detail/section/section.mjs +24 -13
  23. package/api/docs/detail/section/section.test.mjs +15 -6
  24. package/api/docs/docs.d.mts +5 -3
  25. package/api/docs/docs.doc.mjs +38 -17
  26. package/api/docs/docs.mjs +44 -8
  27. package/api/docs/docs.test.mjs +158 -4
  28. package/api/docs/docs.type.d.mts +181 -2
  29. package/api/docs/docs.type.mjs +117 -3
  30. package/api/docs/index/index.mjs +11 -3
  31. package/api/docs/index/index.test.mjs +1 -1
  32. package/api/docs/integration-tree.test.mjs +555 -0
  33. package/api/docs/integrationDocs.test.mjs +14 -14
  34. package/api/docs/list/list.mjs +25 -5
  35. package/api/docs/node/node.d.mts +43 -0
  36. package/api/docs/node/node.mjs +192 -0
  37. package/api/docs/reference-blocks.test.mjs +406 -0
  38. package/api/doctor/doctor.d.mts +30 -2
  39. package/api/doctor/doctor.mjs +151 -8
  40. package/api/doctor/doctor.test.mjs +95 -7
  41. package/api/index.d.mts +1 -1
  42. package/api/index.mjs +1 -0
  43. package/api/init/init.doc.mjs +1 -1
  44. package/api/integration/add-contribution.d.mts +2 -1
  45. package/api/integration/add-contribution.mjs +107 -5
  46. package/api/integration/add-contribution.test.mjs +137 -0
  47. package/api/integration/authoring-checks.mjs +138 -28
  48. package/api/integration/authoring-checks.test.mjs +171 -3
  49. package/api/integration/authoring-checks.type.mjs +6 -1
  50. package/api/integration/integration-authoring.type.d.mts +2 -0
  51. package/api/integration/integration-authoring.type.mjs +2 -0
  52. package/api/integration/integrationAdd.doc.mjs +6 -0
  53. package/api/integration/integrationAddDoc.doc.mjs +7 -1
  54. package/api/integration/integrationDocConflicts.doc.mjs +1 -1
  55. package/api/integration/integrationPackCheck.doc.mjs +1 -1
  56. package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
  57. package/api/integration/integrationTemplateConflicts.doc.mjs +3 -0
  58. package/api/integration/pack-check.mjs +34 -0
  59. package/api/integration/pack-check.test.mjs +121 -0
  60. package/api/integration/pack-check.type.d.mts +26 -2
  61. package/api/integration/pack-check.type.mjs +14 -1
  62. package/api/integration/template-conflict-compatibility.test.mjs +73 -0
  63. package/api/integration/validate-integration.test.mjs +3 -3
  64. package/api/integration/validateIntegration.doc.mjs +1 -1
  65. package/api/json/index.ts +2 -0
  66. package/api/layout/_adapter.mjs +20 -5
  67. package/api/search/search.d.mts +59 -1
  68. package/api/search/search.doc.mjs +4 -3
  69. package/api/search/search.mjs +442 -71
  70. package/api/search/search.test.mjs +91 -2
  71. package/api/search/search.type.d.mts +13 -1
  72. package/api/search/search.type.mjs +4 -1
  73. package/api/template/list/list.mjs +1 -0
  74. package/api/template/table-floating-bulk-actions.test.mjs +66 -0
  75. package/api/template/template-integration.test.mjs +1072 -3
  76. package/api/template/template.doc.mjs +27 -7
  77. package/api/template/template.mjs +45 -8
  78. package/api/template/template.type.d.mts +6 -8
  79. package/api/template/template.type.mjs +3 -2
  80. package/api/theme/template/template.test.mjs +5 -0
  81. package/api/theme/themeTemplate.doc.mjs +1 -1
  82. package/assets/docs/getting-started.doc.mjs +2 -2
  83. package/assets/docs/layout.doc.dense.mjs +2 -2
  84. package/assets/docs/layout.doc.mjs +1 -1
  85. package/assets/docs/principles.doc.mjs +6 -6
  86. package/assets/docs/styling-libraries.doc.mjs +3 -3
  87. package/assets/docs/styling.doc.mjs +4 -4
  88. package/assets/docs/theme.doc.mjs +4 -4
  89. package/assets/docs/tokens.doc.mjs +1 -1
  90. package/assets/docs/tree/api.doc.mjs +30 -0
  91. package/assets/docs/tree/cli.doc.mjs +23 -0
  92. package/assets/docs/tree/commands.doc.mjs +25 -0
  93. package/assets/docs/{cli-integrations.doc.mjs → tree/integrations.doc.mjs} +37 -9
  94. package/assets/docs/tree/integrations.test.mjs +62 -0
  95. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  96. package/assets/docs/working-with-ai.doc.mjs +3 -3
  97. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
  98. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
  99. package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
  100. package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
  101. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
  102. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
  103. package/authoring/debug/parse.d.mts +3 -3
  104. package/authoring/doctypes/_schema.d.mts +119 -117
  105. package/authoring/doctypes/_schema.mjs +54 -3
  106. package/authoring/doctypes/base/graph-fields.doc.mjs +7 -5
  107. package/authoring/doctypes/base/type.ts +6 -5
  108. package/authoring/doctypes/command/command.doc.mjs +1 -1
  109. package/authoring/doctypes/command/type.ts +2 -2
  110. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  111. package/authoring/doctypes/enum/type.ts +1 -1
  112. package/authoring/doctypes/function/function.doc.mjs +3 -2
  113. package/authoring/doctypes/function/type.ts +3 -2
  114. package/authoring/doctypes/load-contract.test.mjs +26 -1
  115. package/authoring/doctypes/namespace/namespace.doc.mjs +6 -10
  116. package/authoring/doctypes/namespace/parse.test.mjs +23 -25
  117. package/authoring/doctypes/namespace/type.ts +5 -2
  118. package/authoring/doctypes/parse.mjs +3 -2
  119. package/authoring/doctypes/reference/reference.doc.mjs +34 -5
  120. package/authoring/doctypes/reference/type.ts +28 -11
  121. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  122. package/authoring/doctypes/schema/type.ts +1 -1
  123. package/authoring/doctypes/template/parse.d.mts +2 -0
  124. package/authoring/doctypes/template/parse.mjs +4 -0
  125. package/authoring/doctypes/template/parse.test.mjs +18 -0
  126. package/authoring/doctypes/template/template.doc.mjs +6 -0
  127. package/authoring/doctypes/template/type.ts +8 -0
  128. package/authoring/identity/identity.doc.mjs +2 -2
  129. package/authoring/index.d.ts +1 -0
  130. package/authoring/integration/type.ts +1 -1
  131. package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
  132. package/clients/cli/commands/build-theme.mjs +1 -1
  133. package/clients/cli/commands/build.doc.mjs +11 -6
  134. package/clients/cli/commands/build.mjs +105 -67
  135. package/clients/cli/commands/build.text-fields.test.mjs +41 -0
  136. package/clients/cli/commands/component/index.mjs +1 -1
  137. package/clients/cli/commands/component-ownership.test.mjs +3 -3
  138. package/clients/cli/commands/discover.broken-integration.test.mjs +7 -8
  139. package/clients/cli/commands/discover.mjs +1 -1
  140. package/clients/cli/commands/docs.doc.mjs +20 -8
  141. package/clients/cli/commands/docs.mjs +183 -69
  142. package/clients/cli/commands/docs.test.mjs +120 -16
  143. package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -2
  144. package/clients/cli/commands/doctor-integration-templates.doc.mjs +14 -7
  145. package/clients/cli/commands/doctor-integration.test.mjs +75 -1
  146. package/clients/cli/commands/doctor.mjs +53 -16
  147. package/clients/cli/commands/integration-add.doc.mjs +10 -0
  148. package/clients/cli/commands/integration.mjs +1 -0
  149. package/clients/cli/commands/search.mjs +11 -2
  150. package/clients/cli/commands/setup-nudge.test.mjs +6 -0
  151. package/clients/cli/commands/template.doc.mjs +23 -6
  152. package/clients/cli/commands/template.mjs +4 -91
  153. package/clients/cli/index.mjs +8 -0
  154. package/clients/cli/lib/manifest.mjs +9 -2
  155. package/foundation/agent-docs/agent-docs.d.mts +3 -2
  156. package/foundation/agent-docs/agent-docs.mjs +13 -9
  157. package/foundation/agent-docs/agent-docs.test.mjs +19 -1
  158. package/foundation/config/project.d.mts +12 -11
  159. package/foundation/config/project.mjs +73 -59
  160. package/foundation/config/project.test.mjs +141 -5
  161. package/foundation/discovery/authoring-self-docs.d.mts +12 -0
  162. package/foundation/discovery/authoring-self-docs.mjs +20 -8
  163. package/foundation/discovery/authoring-self-docs.test.mjs +25 -9
  164. package/foundation/discovery/cli-self-docs.d.mts +40 -34
  165. package/foundation/discovery/cli-self-docs.mjs +91 -115
  166. package/foundation/discovery/cli-self-docs.test.mjs +104 -166
  167. package/foundation/discovery/component-discovery.d.mts +38 -0
  168. package/foundation/discovery/component-discovery.mjs +48 -0
  169. package/foundation/discovery/docs-discovery.d.mts +109 -9
  170. package/foundation/discovery/docs-discovery.mjs +223 -24
  171. package/foundation/discovery/docs-discovery.test.mjs +103 -27
  172. package/foundation/discovery/docs-output-budget.d.mts +2 -2
  173. package/foundation/discovery/docs-output-budget.mjs +1 -1
  174. package/foundation/discovery/docs-section-key.d.mts +28 -10
  175. package/foundation/discovery/docs-section-key.mjs +134 -33
  176. package/foundation/discovery/docs-section-key.test.mjs +41 -19
  177. package/foundation/discovery/template-adapter.d.mts +92 -6
  178. package/foundation/discovery/template-adapter.mjs +453 -17
  179. package/foundation/discovery/template-adapter.test.mjs +42 -0
  180. package/foundation/discovery/template-conflict-release.d.mts +13 -0
  181. package/foundation/discovery/template-conflict-release.mjs +40 -0
  182. package/foundation/discovery/template-conflict-release.test.mjs +40 -0
  183. package/foundation/doc-compiler/bundle.mjs +61 -1
  184. package/foundation/doc-compiler/bundle.test.mjs +21 -10
  185. package/foundation/doc-compiler/compile.d.mts +113 -132
  186. package/foundation/doc-compiler/compile.mjs +57 -10
  187. package/foundation/doc-compiler/diagnostics.mjs +28 -0
  188. package/foundation/doc-compiler/doc-loads.test.mjs +2 -2
  189. package/foundation/doc-compiler/inputs.d.mts +2 -2
  190. package/foundation/doc-compiler/inputs.mjs +6 -1
  191. package/foundation/doc-compiler/ir.mjs +7 -0
  192. package/foundation/doc-compiler/lenses.d.mts +8 -5
  193. package/foundation/doc-compiler/lenses.mjs +50 -4
  194. package/foundation/doc-compiler/links.d.mts +162 -0
  195. package/foundation/doc-compiler/links.mjs +294 -0
  196. package/foundation/doc-compiler/links.test.mjs +192 -0
  197. package/foundation/doc-compiler/lower-doc.test.mjs +5 -2
  198. package/foundation/doc-compiler/read.d.mts +3 -2
  199. package/foundation/doc-compiler/read.mjs +6 -1
  200. package/foundation/doc-compiler/tree.d.mts +288 -0
  201. package/foundation/doc-compiler/tree.mjs +876 -0
  202. package/foundation/doc-compiler/tree.test.mjs +598 -0
  203. package/foundation/integrations/autolink.test.mjs +1 -1
  204. package/foundation/integrations/cli-requirement.d.mts +45 -0
  205. package/foundation/integrations/cli-requirement.mjs +154 -0
  206. package/foundation/integrations/cli-requirement.test.mjs +84 -0
  207. package/foundation/integrations/contribution-inventory.d.mts +2 -1
  208. package/foundation/integrations/contribution-inventory.mjs +10 -4
  209. package/foundation/integrations/contribution-inventory.test.mjs +19 -0
  210. package/foundation/integrations/integration-warnings.d.mts +9 -2
  211. package/foundation/integrations/integration-warnings.mjs +51 -26
  212. package/foundation/integrations/integration-warnings.test.mjs +74 -1
  213. package/foundation/integrations/validate-contributions.mjs +25 -27
  214. package/foundation/response/response-types.doc.d.mts +5 -1
  215. package/foundation/response/response-types.doc.mjs +19 -10
  216. package/foundation/text/string-utils.d.mts +8 -0
  217. package/foundation/text/string-utils.mjs +18 -0
  218. package/foundation/xle/expand.mjs +1 -1
  219. package/foundation/xle/xle.test.mjs +13 -0
  220. package/package.json +9 -9
  221. package/assets/docs/cli.doc.mjs +0 -15
@@ -25,6 +25,7 @@ import * as fs from 'node:fs';
25
25
  import * as path from 'node:path';
26
26
  import {createRequire} from 'node:module';
27
27
  import {readDocView} from '../doc-compiler/read.mjs';
28
+ import {importDocModule} from '../doc-compiler/import.mjs';
28
29
  import {CLI_ROOT, discoverExternalPackages} from '../fs/paths.mjs';
29
30
  import {CORE_PROVIDER_ID} from '../identity/providers.mjs';
30
31
  import {Project} from '../config/project.mjs';
@@ -44,6 +45,53 @@ export function pkgOf(t) {
44
45
  return t.package ?? CORE_PACKAGE;
45
46
  }
46
47
 
48
+ /**
49
+ * Every id that should resolve to a discovered template. Replacements keep
50
+ * their own integration id and also own the Core id they replace.
51
+ * @param {{dirName: string, replaces?: string}} template
52
+ * @returns {string[]}
53
+ */
54
+ export function templateLookupIds(template) {
55
+ if (template.replaces && template.replaces !== template.dirName) {
56
+ return [template.dirName, template.replaces];
57
+ }
58
+ return [template.dirName];
59
+ }
60
+
61
+ /**
62
+ * Remove entries shadowed in the default discovery view while retaining them in
63
+ * package-scoped views. An active replacement owns its target id. A rejected
64
+ * same-id declaration falls back to Core when a Core template of the same kind
65
+ * owns that id; otherwise the integration template remains available by kind.
66
+ * @param {DiscoveredTemplate[]} templates
67
+ * @returns {DiscoveredTemplate[]}
68
+ */
69
+ export function effectiveTemplateDiscovery(templates) {
70
+ const activeTargets = new Set(
71
+ templates
72
+ .filter(template => template.replaces != null)
73
+ .map(template => `${template.type}:${template.replaces}`),
74
+ );
75
+ const coreIds = new Set(
76
+ templates
77
+ .filter(template => pkgOf(template) === CORE_PACKAGE)
78
+ .map(template => `${template.type}:${template.dirName}`),
79
+ );
80
+ return templates.filter(template => {
81
+ if (
82
+ template.replacementRejected &&
83
+ template.replacementTarget === template.dirName &&
84
+ coreIds.has(`${template.type}:${template.dirName}`)
85
+ ) {
86
+ return false;
87
+ }
88
+ return (
89
+ !activeTargets.has(`${template.type}:${template.dirName}`) ||
90
+ template.replaces != null
91
+ );
92
+ });
93
+ }
94
+
47
95
  /**
48
96
  * A discovered template (page or block), normalized across core, external, and
49
97
  * integration sources. Not every source populates every field, so
@@ -66,6 +114,11 @@ export function pkgOf(t) {
66
114
  * @property {string} filePath
67
115
  * @property {string} docPath
68
116
  * @property {string} [package]
117
+ * @property {boolean} [autolinked] whether the owning integration was discovered
118
+ * from package.json rather than named in astryx.config
119
+ * @property {string} [replaces] active Core template id this integration template replaces
120
+ * @property {boolean} [replacementRejected] whether an invalid declaration was disabled
121
+ * @property {string} [replacementTarget] disabled declaration target
69
122
  */
70
123
 
71
124
  /**
@@ -74,6 +127,15 @@ export function pkgOf(t) {
74
127
  * @property {string} package
75
128
  * @property {string} [template]
76
129
  * @property {string} message
130
+ * @property {string} [code]
131
+ * @property {'warning' | 'error'} [severity]
132
+ * @property {string} [replacementTarget] Core target whose replacement set is invalid
133
+ * @property {boolean} [autolinked] whether the declaration came from an autolinked integration
134
+ */
135
+
136
+ /**
137
+ * A semantic error in an integration template replacement declaration.
138
+ * @typedef {TemplateDiscoveryError & {code: 'missing_template_replacement_target' | 'ambiguous_template_replacement' | 'invalid_template_replacement', severity: 'warning' | 'error'}} TemplateReplacementError
77
139
  */
78
140
 
79
141
  /**
@@ -683,38 +745,291 @@ export async function discoverCoreTemplates() {
683
745
  }
684
746
 
685
747
  /**
748
+ * Apply valid integration replacements to a raw template set.
749
+ *
750
+ * One declaration owns its Core target. When different configured packages
751
+ * replace the same target, the later package wins and discovery returns a
752
+ * warning, matching integration-doc replacement order. Multiple declarations
753
+ * inside one package are invalid and fail closed. Missing targets and kind
754
+ * mismatches also fail closed: Core stays selected and every integration
755
+ * template remains addressable by its own id.
756
+ *
757
+ * @param {DiscoveredTemplate[]} templates
758
+ * @param {TemplateDiscoveryError[]} [declarationErrors]
759
+ * @returns {{templates: DiscoveredTemplate[], errors: TemplateReplacementError[]}}
760
+ */
761
+ export function applyTemplateReplacements(templates, declarationErrors = []) {
762
+ /** @type {Map<string, DiscoveredTemplate[]>} */
763
+ const coreById = new Map();
764
+ /** @type {Map<string, DiscoveredTemplate[]>} */
765
+ const replacementsByTarget = new Map();
766
+
767
+ for (const template of templates) {
768
+ if (pkgOf(template) === CORE_PACKAGE) {
769
+ const matches = coreById.get(template.dirName) ?? [];
770
+ matches.push(template);
771
+ coreById.set(template.dirName, matches);
772
+ }
773
+ if (template.replaces != null) {
774
+ const replacements = replacementsByTarget.get(template.replaces) ?? [];
775
+ replacements.push(template);
776
+ replacementsByTarget.set(template.replaces, replacements);
777
+ }
778
+ }
779
+
780
+ const activeReplacements = new Set();
781
+ const replacedCore = new Set();
782
+ /** @type {TemplateReplacementError[]} */
783
+ const errors = declarationErrors.map(error => ({
784
+ ...error,
785
+ code: /** @type {TemplateReplacementError['code']} */ (
786
+ error.code ?? 'invalid_template_replacement'
787
+ ),
788
+ severity: error.severity ?? 'error',
789
+ }));
790
+ /** @param {TemplateReplacementError} error */
791
+ const pushError = error => {
792
+ if (
793
+ errors.some(
794
+ existing =>
795
+ existing.code === error.code &&
796
+ existing.package === error.package &&
797
+ existing.template === error.template &&
798
+ existing.message === error.message,
799
+ )
800
+ ) {
801
+ return;
802
+ }
803
+ errors.push(error);
804
+ };
805
+ for (const [target, replacements] of replacementsByTarget) {
806
+ const targetErrors = errors.filter(
807
+ error => error.replacementTarget === target,
808
+ );
809
+ const hasExplicitIntent =
810
+ replacements.some(replacement => !replacement.autolinked) ||
811
+ targetErrors.some(error => !error.autolinked);
812
+ const contenders = hasExplicitIntent
813
+ ? replacements.filter(replacement => !replacement.autolinked)
814
+ : replacements;
815
+ let targetInvalid = targetErrors.some(
816
+ error =>
817
+ error.severity === 'error' && (!hasExplicitIntent || !error.autolinked),
818
+ );
819
+
820
+ /** @type {Map<string, DiscoveredTemplate[]>} */
821
+ const byPackage = new Map();
822
+ for (const replacement of replacements) {
823
+ const pkg = pkgOf(replacement);
824
+ const fromPackage = byPackage.get(pkg) ?? [];
825
+ fromPackage.push(replacement);
826
+ byPackage.set(pkg, fromPackage);
827
+ }
828
+ for (const [pkg, declarations] of byPackage) {
829
+ if (declarations.length < 2) continue;
830
+ if (!hasExplicitIntent || !declarations[0].autolinked) {
831
+ targetInvalid = true;
832
+ }
833
+ const message =
834
+ `${pkg} declares ${declarations.length} templates as replacements for Core ` +
835
+ `template "${target}" (${declarations.map(template => template.dirName).join(', ')}). ` +
836
+ 'One package must declare at most one replacement for a Core target.';
837
+ for (const declaration of declarations) {
838
+ pushError({
839
+ code: 'ambiguous_template_replacement',
840
+ severity: 'error',
841
+ package: pkg,
842
+ template: declaration.dirName,
843
+ replacementTarget: target,
844
+ autolinked: declaration.autolinked,
845
+ message,
846
+ });
847
+ }
848
+ }
849
+
850
+ const coreMatches = coreById.get(target) ?? [];
851
+ /** @type {Map<DiscoveredTemplate, DiscoveredTemplate>} */
852
+ const coreMatchByReplacement = new Map();
853
+ for (const replacement of replacements) {
854
+ const invalidAffectsTarget =
855
+ !hasExplicitIntent || !replacement.autolinked;
856
+ if (coreMatches.length === 0) {
857
+ if (invalidAffectsTarget) targetInvalid = true;
858
+ pushError({
859
+ code: 'missing_template_replacement_target',
860
+ severity: 'error',
861
+ package: pkgOf(replacement),
862
+ template: replacement.dirName,
863
+ replacementTarget: target,
864
+ autolinked: replacement.autolinked,
865
+ message: `Template "${replacement.dirName}" replaces "${target}", which is not a Core template id.`,
866
+ });
867
+ continue;
868
+ }
869
+ const sameType = coreMatches.filter(
870
+ core => core.type === replacement.type,
871
+ );
872
+ if (sameType.length !== 1) {
873
+ if (invalidAffectsTarget) targetInvalid = true;
874
+ const kinds = coreMatches.map(core => core.type).join(', ');
875
+ pushError({
876
+ code: 'invalid_template_replacement',
877
+ severity: 'error',
878
+ package: pkgOf(replacement),
879
+ template: replacement.dirName,
880
+ replacementTarget: target,
881
+ autolinked: replacement.autolinked,
882
+ message:
883
+ `Template "${replacement.dirName}" is a ${replacement.type} template, but Core ` +
884
+ `template "${target}" is ${kinds || 'not available'}. A replacement must have the same type.`,
885
+ });
886
+ continue;
887
+ }
888
+ coreMatchByReplacement.set(replacement, sameType[0]);
889
+ }
890
+
891
+ const validContenders = contenders.filter(replacement =>
892
+ coreMatchByReplacement.has(replacement),
893
+ );
894
+ if (targetInvalid || validContenders.length === 0) continue;
895
+
896
+ const replacement = validContenders[validContenders.length - 1];
897
+ const validReplacements = replacements.filter(candidate =>
898
+ coreMatchByReplacement.has(candidate),
899
+ );
900
+ if (validReplacements.length > 1) {
901
+ const autolinkedLost =
902
+ hasExplicitIntent &&
903
+ validReplacements.some(candidate => candidate.autolinked);
904
+ const allAutolinked = validReplacements.every(
905
+ candidate => candidate.autolinked,
906
+ );
907
+ pushError({
908
+ code: 'ambiguous_template_replacement',
909
+ severity: 'warning',
910
+ package: pkgOf(replacement),
911
+ template: replacement.dirName,
912
+ replacementTarget: target,
913
+ autolinked: replacement.autolinked,
914
+ message: autolinkedLost
915
+ ? `Core template "${target}" is replaced by ${validReplacements.map(candidate => pkgOf(candidate)).join(', ')}. ${pkgOf(replacement)} is explicitly configured, so it wins over autolinked integrations.`
916
+ : allAutolinked
917
+ ? `Core template "${target}" is replaced by autolinked dependencies ${validReplacements.map(candidate => pkgOf(candidate)).join(', ')}. ${pkgOf(replacement)} is listed later in package.json dependencies, so it wins. Add the intended package to astryx.config integrations to make precedence explicit.`
918
+ : `Core template "${target}" is replaced by ${validReplacements.map(candidate => pkgOf(candidate)).join(', ')}. ${pkgOf(replacement)} is configured later, so it wins.`,
919
+ });
920
+ }
921
+
922
+ activeReplacements.add(replacement);
923
+ const coreMatch = coreMatchByReplacement.get(replacement);
924
+ if (coreMatch) replacedCore.add(coreMatch);
925
+ }
926
+
927
+ const effective = templates.flatMap(template => {
928
+ if (replacedCore.has(template)) return [];
929
+ if (template.replaces != null && !activeReplacements.has(template)) {
930
+ const fallback = {
931
+ ...template,
932
+ replacementRejected: true,
933
+ replacementTarget: template.replaces,
934
+ };
935
+ delete fallback.replaces;
936
+ return [fallback];
937
+ }
938
+ return [template];
939
+ });
940
+
941
+ return {
942
+ templates: effective.sort((a, b) => a.name.localeCompare(b.name)),
943
+ errors,
944
+ };
945
+ }
946
+
947
+ /**
948
+ * Discover the raw Core, external, and integration template set before
949
+ * replacement declarations are applied.
686
950
  * @param {string} [cwd]
687
- * @returns {Promise<DiscoveredTemplate[]>}
951
+ * @returns {Promise<{templates: DiscoveredTemplate[], errors: TemplateDiscoveryError[]}>}
688
952
  */
689
- export async function discoverAll(cwd = process.cwd()) {
953
+ async function discoverAllSources(cwd = process.cwd()) {
690
954
  const [core, external, integration] = await Promise.all([
691
955
  discoverCoreTemplates(),
692
956
  discoverExternalBlocks(cwd),
693
957
  discoverIntegrationTemplates(cwd),
694
958
  ]);
695
- return [...core, ...external, ...integration.templates].sort((a, b) =>
959
+ return {
960
+ templates: [...core, ...external, ...integration.templates],
961
+ errors: integration.errors,
962
+ };
963
+ }
964
+
965
+ /**
966
+ * Discover every template without hiding replaced Core originals. Internal
967
+ * package-qualified selection uses this view.
968
+ * @param {string} [cwd]
969
+ * @returns {Promise<DiscoveredTemplate[]>}
970
+ */
971
+ export async function discoverAllUnresolved(cwd = process.cwd()) {
972
+ return (await discoverAllSources(cwd)).templates.sort((a, b) =>
696
973
  a.name.localeCompare(b.name),
697
974
  );
698
975
  }
699
976
 
700
977
  /**
701
- * Like {@link discoverAll} but also returns integration-template discovery
702
- * errors (missing same-stem source, missing `type`, load failure). Use this
703
- * when the caller wants to warn about malformed integration templates.
978
+ * Resolve replacement declarations while retaining package-addressable entries
979
+ * that are shadowed or rejected in the default discovery view.
980
+ * @param {string} [cwd]
981
+ * @returns {Promise<{templates: DiscoveredTemplate[], errors: Array<TemplateDiscoveryError | TemplateReplacementError>}>}
982
+ */
983
+ async function resolveAllSources(cwd = process.cwd()) {
984
+ const discovered = await discoverAllSources(cwd);
985
+ const replacementErrors = discovered.errors.filter(
986
+ error => error.replacementTarget != null,
987
+ );
988
+ const resolved = applyTemplateReplacements(
989
+ discovered.templates,
990
+ replacementErrors,
991
+ );
992
+ return {
993
+ templates: resolved.templates,
994
+ errors: [
995
+ ...discovered.errors.filter(error => error.replacementTarget == null),
996
+ ...resolved.errors,
997
+ ],
998
+ };
999
+ }
1000
+
1001
+ /**
1002
+ * Discover the resolved catalog before default-view shadowing. Internal
1003
+ * package-qualified selection uses this view.
1004
+ * @param {string} [cwd]
1005
+ * @returns {Promise<DiscoveredTemplate[]>}
1006
+ */
1007
+ export async function discoverAllResolved(cwd = process.cwd()) {
1008
+ return (await resolveAllSources(cwd)).templates;
1009
+ }
1010
+
1011
+ /**
1012
+ * @param {string} [cwd]
1013
+ * @returns {Promise<DiscoveredTemplate[]>}
1014
+ */
1015
+ export async function discoverAll(cwd = process.cwd()) {
1016
+ return effectiveTemplateDiscovery(await discoverAllResolved(cwd));
1017
+ }
1018
+
1019
+ /**
1020
+ * Like {@link discoverAll} but also returns integration-template discovery and
1021
+ * replacement-declaration errors. Use this when the caller wants to warn about
1022
+ * malformed integration templates or inactive replacement declarations.
704
1023
  *
705
1024
  * @param {string} [cwd]
706
- * @returns {Promise<{templates: DiscoveredTemplate[], errors: TemplateDiscoveryError[]}>}
1025
+ * @returns {Promise<{templates: DiscoveredTemplate[], errors: Array<TemplateDiscoveryError | TemplateReplacementError>}>}
707
1026
  */
708
1027
  export async function discoverAllWithErrors(cwd = process.cwd()) {
709
- const [pages, blocks, integration] = await Promise.all([
710
- discoverPages(),
711
- discoverAllBlocks(cwd),
712
- discoverIntegrationTemplates(cwd),
713
- ]);
714
- const templates = [...pages, ...blocks, ...integration.templates].sort(
715
- (a, b) => a.name.localeCompare(b.name),
716
- );
717
- return {templates, errors: integration.errors};
1028
+ const resolved = await resolveAllSources(cwd);
1029
+ return {
1030
+ templates: effectiveTemplateDiscovery(resolved.templates),
1031
+ errors: resolved.errors,
1032
+ };
718
1033
  }
719
1034
 
720
1035
  /**
@@ -807,13 +1122,51 @@ async function discoverIntegrationTemplates(cwd = process.cwd()) {
807
1122
  return {templates, errors};
808
1123
  }
809
1124
 
1125
+ /**
1126
+ * Report replacement declarations that cannot hold inside one package: two of
1127
+ * its templates replacing the same Core target. Cross-package precedence and
1128
+ * target checks happen in {@link applyTemplateReplacements}.
1129
+ * @param {string} pkg
1130
+ * @param {boolean | undefined} autolinked
1131
+ * @param {DiscoveredTemplate[]} templates
1132
+ * @param {TemplateDiscoveryError[]} errors
1133
+ */
1134
+ function reportSamePackageReplacements(pkg, autolinked, templates, errors) {
1135
+ /** @type {Map<string, string[]>} */
1136
+ const idsByTarget = new Map();
1137
+ for (const template of templates) {
1138
+ if (template.replaces == null) continue;
1139
+ const ids = idsByTarget.get(template.replaces) ?? [];
1140
+ ids.push(template.dirName);
1141
+ idsByTarget.set(template.replaces, ids);
1142
+ }
1143
+ for (const [target, ids] of idsByTarget) {
1144
+ if (ids.length < 2) continue;
1145
+ const message =
1146
+ `${pkg} declares ${ids.length} templates as replacements for Core template ` +
1147
+ `"${target}" (${ids.join(', ')}). One package must declare at most one ` +
1148
+ 'replacement for a Core target.';
1149
+ for (const template of ids) {
1150
+ errors.push({
1151
+ code: 'ambiguous_template_replacement',
1152
+ severity: 'error',
1153
+ package: pkg,
1154
+ autolinked,
1155
+ template,
1156
+ replacementTarget: target,
1157
+ message,
1158
+ });
1159
+ }
1160
+ }
1161
+ }
1162
+
810
1163
  /**
811
1164
  * Discover the templates contributed by a SINGLE integration. Same per-template
812
1165
  * rules as {@link discoverIntegrationTemplates} (same-stem source required,
813
1166
  * page|block type required); broken templates are recorded in `errors` rather
814
1167
  * than thrown. Exposed for `doctor integration validate` and template authoring checks.
815
1168
  *
816
- * @param {{name?: string, __spec?: string, templates?: string}} integration
1169
+ * @param {{name?: string, __spec?: string, __autolinked?: boolean, templates?: string}} integration
817
1170
  * @returns {Promise<{templates: DiscoveredTemplate[], errors: TemplateDiscoveryError[]}>}
818
1171
  */
819
1172
  export async function discoverIntegrationTemplatesForOne(integration) {
@@ -842,6 +1195,20 @@ export async function discoverIntegrationTemplatesForOne(integration) {
842
1195
  template: id,
843
1196
  message: `Template "${id}" is missing its same-stem source file ${path.basename(sourcePath)}.`,
844
1197
  });
1198
+ // A replacement this unusable template declares still counts, so its
1199
+ // target fails closed instead of going to a sibling.
1200
+ const declared = await declaredReplacement(docPath, id);
1201
+ if (declared != null) {
1202
+ errors.push({
1203
+ code: 'invalid_template_replacement',
1204
+ severity: 'error',
1205
+ package: pkgLabel,
1206
+ autolinked: integration.__autolinked,
1207
+ template: id,
1208
+ replacementTarget: declared,
1209
+ message: `Template "${id}" replaces "${declared}", but it cannot be used: it is missing its same-stem source file ${path.basename(sourcePath)}.`,
1210
+ });
1211
+ }
845
1212
  continue;
846
1213
  }
847
1214
 
@@ -854,6 +1221,20 @@ export async function discoverIntegrationTemplatesForOne(integration) {
854
1221
  template: id,
855
1222
  message: `Template "${id}" failed to load: ${/** @type {any} */ (err).message}`,
856
1223
  });
1224
+ // A replacement this unusable template declares still counts, so its
1225
+ // target fails closed instead of going to a sibling (spec:AST-035 FR4).
1226
+ const declared = await declaredReplacement(docPath, id);
1227
+ if (declared != null) {
1228
+ errors.push({
1229
+ code: 'invalid_template_replacement',
1230
+ severity: 'error',
1231
+ package: pkgLabel,
1232
+ autolinked: integration.__autolinked,
1233
+ template: id,
1234
+ replacementTarget: declared,
1235
+ message: `Template "${id}" replaces "${declared}", but it cannot be used: its metadata does not load.`,
1236
+ });
1237
+ }
857
1238
  continue;
858
1239
  }
859
1240
 
@@ -867,6 +1248,18 @@ export async function discoverIntegrationTemplatesForOne(integration) {
867
1248
  template: id,
868
1249
  message: `Template "${id}" is missing a "type" of "page" or "block". Stamp the default export with type: 'page' or type: 'block'.`,
869
1250
  });
1251
+ const declared = doc?.replaces ?? (await declaredReplacement(docPath, id));
1252
+ if (declared != null) {
1253
+ errors.push({
1254
+ code: 'invalid_template_replacement',
1255
+ severity: 'error',
1256
+ package: pkgLabel,
1257
+ autolinked: integration.__autolinked,
1258
+ template: id,
1259
+ replacementTarget: declared,
1260
+ message: `Template "${id}" replaces "${declared}", but it cannot be used: it has no "type".`,
1261
+ });
1262
+ }
870
1263
  continue;
871
1264
  }
872
1265
 
@@ -894,12 +1287,55 @@ export async function discoverIntegrationTemplatesForOne(integration) {
894
1287
  filePath: sourcePath,
895
1288
  docPath,
896
1289
  package: pkgLabel,
1290
+ autolinked: integration.__autolinked,
1291
+ replaces: doc?.replaces,
897
1292
  });
898
1293
  }
899
1294
 
1295
+ reportSamePackageReplacements(
1296
+ pkgLabel,
1297
+ integration.__autolinked,
1298
+ templates,
1299
+ errors,
1300
+ );
900
1301
  return {templates, errors};
901
1302
  }
902
1303
 
1304
+ /**
1305
+ * The replacement a template doc declares, even when the doc does not validate
1306
+ * or load: an unusable template's declaration still counts, so its target
1307
+ * fails closed (spec:AST-035 FR4). Null when it declares none.
1308
+ * @param {string} docPath
1309
+ * @param {string} id
1310
+ * @returns {Promise<string | null>}
1311
+ */
1312
+ async function declaredReplacement(docPath, id) {
1313
+ try {
1314
+ const doc = await loadIntegrationDoc(docPath, `Template "${id}"`);
1315
+ return doc?.replaces ?? null;
1316
+ } catch {
1317
+ // The doc does not validate; read what it declares without validating.
1318
+ }
1319
+ try {
1320
+ // The guarded importer: anything the doc writes to stdout as it loads
1321
+ // goes to stderr, so `--json` output stays clean.
1322
+ const raw = (await importDocModule(docPath))?.default;
1323
+ if (raw != null && typeof raw === 'object') {
1324
+ return typeof raw.replaces === 'string' ? raw.replaces : null;
1325
+ }
1326
+ } catch {
1327
+ // The module does not load at all; its text may still name a target.
1328
+ }
1329
+ try {
1330
+ const match = /\breplaces\s*:\s*['"]([^'"\n]+)['"]/u.exec(
1331
+ fs.readFileSync(docPath, 'utf8'),
1332
+ );
1333
+ return match ? match[1] : null;
1334
+ } catch {
1335
+ return null;
1336
+ }
1337
+ }
1338
+
903
1339
  /**
904
1340
  * @param {string} componentName
905
1341
  * @param {string} [cwd]
@@ -17,6 +17,7 @@ import * as fs from 'node:fs';
17
17
  import * as os from 'node:os';
18
18
  import * as path from 'node:path';
19
19
  import {
20
+ effectiveTemplateDiscovery,
20
21
  extractComponents,
21
22
  findPageDocFile,
22
23
  stripTemplateAssetRefs,
@@ -179,3 +180,44 @@ describe('findPageDocFile', () => {
179
180
  );
180
181
  });
181
182
  });
183
+
184
+ describe('effectiveTemplateDiscovery', () => {
185
+ it('shadows only the matching template kind', () => {
186
+ const pageReplacement = {
187
+ type: 'page',
188
+ dirName: 'acme-dashboard',
189
+ name: 'Acme dashboard',
190
+ description: '',
191
+ filePath: '/acme-dashboard.tsx',
192
+ docPath: '/acme-dashboard.doc.mjs',
193
+ package: '@acme/widgets',
194
+ replaces: 'dashboard',
195
+ };
196
+ const corePage = {
197
+ type: 'page',
198
+ dirName: 'dashboard',
199
+ name: 'Dashboard page',
200
+ description: '',
201
+ filePath: '/dashboard-page.tsx',
202
+ docPath: '/dashboard-page.doc.mjs',
203
+ };
204
+ const coreBlock = {
205
+ type: 'block',
206
+ dirName: 'dashboard',
207
+ name: 'Dashboard block',
208
+ description: '',
209
+ filePath: '/dashboard-block.tsx',
210
+ docPath: '/dashboard-block.doc.mjs',
211
+ };
212
+
213
+ const effective = effectiveTemplateDiscovery([
214
+ corePage,
215
+ coreBlock,
216
+ pageReplacement,
217
+ ]);
218
+
219
+ expect(effective).toContain(pageReplacement);
220
+ expect(effective).toContain(coreBlock);
221
+ expect(effective).not.toContain(corePage);
222
+ });
223
+ });
@@ -0,0 +1,13 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * Whether this release exposes replacement relationships in the public conflict API.
6
+ * Both the supported version and a deliberate projection update are required;
7
+ * a routine package-version bump cannot expand the public shape by itself.
8
+ *
9
+ * @param {string} [version]
10
+ * @param {'warning-only' | 'expanded'} [projection]
11
+ * @returns {boolean}
12
+ */
13
+ export function expandedTemplateConflictSchemaActive(version?: string, projection?: "warning-only" | "expanded"): boolean;
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Release boundary for the expanded integration-template conflict schema.
5
+ *
6
+ * @input The CLI package version and an explicit public-schema projection.
7
+ * @output Whether replacement-specific conflict fields may reach the stable API.
8
+ * @position Private compatibility gate for integration authoring diagnostics.
9
+ */
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import {semverCompare} from '../env/semver.mjs';
14
+ import {CLI_ROOT} from '../fs/paths.mjs';
15
+
16
+ const EXPANDED_TEMPLATE_CONFLICT_SCHEMA_CLI = '0.7.0';
17
+ /** @type {'warning-only' | 'expanded'} */
18
+ const TEMPLATE_CONFLICT_SCHEMA_PROJECTION = 'warning-only';
19
+ const CLI_VERSION = JSON.parse(
20
+ fs.readFileSync(path.join(CLI_ROOT, 'package.json'), 'utf8'),
21
+ ).version;
22
+
23
+ /**
24
+ * Whether this release exposes replacement relationships in the public conflict API.
25
+ * Both the supported version and a deliberate projection update are required;
26
+ * a routine package-version bump cannot expand the public shape by itself.
27
+ *
28
+ * @param {string} [version]
29
+ * @param {'warning-only' | 'expanded'} [projection]
30
+ * @returns {boolean}
31
+ */
32
+ export function expandedTemplateConflictSchemaActive(
33
+ version = CLI_VERSION,
34
+ projection = TEMPLATE_CONFLICT_SCHEMA_PROJECTION,
35
+ ) {
36
+ return (
37
+ projection === 'expanded' &&
38
+ semverCompare(version, EXPANDED_TEMPLATE_CONFLICT_SCHEMA_CLI) >= 0
39
+ );
40
+ }