@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4

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 (188) hide show
  1. package/README.md +2 -1
  2. package/api/build/build.type.d.mts +2 -2
  3. package/api/build/build.type.mjs +2 -2
  4. package/api/component/component.type.d.mts +6 -6
  5. package/api/component/component.type.mjs +19 -19
  6. package/api/discover/discover.type.d.mts +4 -4
  7. package/api/discover/discover.type.mjs +10 -10
  8. package/api/docs/_adapter.d.mts +37 -24
  9. package/api/docs/_adapter.mjs +169 -83
  10. package/api/docs/compiled-topics.test.mjs +78 -0
  11. package/api/docs/detail/detail.mjs +14 -63
  12. package/api/docs/detail/section/section.d.mts +1 -1
  13. package/api/docs/detail/section/section.mjs +44 -20
  14. package/api/docs/detail/section/section.test.mjs +41 -0
  15. package/api/docs/docs.d.mts +7 -2
  16. package/api/docs/docs.doc.mjs +27 -10
  17. package/api/docs/docs.mjs +16 -9
  18. package/api/docs/docs.test.mjs +6 -0
  19. package/api/docs/docs.type.d.mts +40 -3
  20. package/api/docs/docs.type.mjs +36 -8
  21. package/api/docs/index/index.d.mts +18 -0
  22. package/api/docs/index/index.mjs +32 -0
  23. package/api/docs/index/index.test.mjs +62 -0
  24. package/api/docs/integrationDocs.test.mjs +106 -0
  25. package/api/doctor/doctor.d.mts +48 -0
  26. package/api/doctor/doctor.mjs +232 -0
  27. package/api/doctor/doctor.test.mjs +196 -0
  28. package/api/hook/hook.type.d.mts +3 -3
  29. package/api/hook/hook.type.mjs +11 -11
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.mjs +5 -3
  32. package/api/integration/add-contribution.test.mjs +4 -4
  33. package/api/integration/integration-authoring.type.d.mts +1 -1
  34. package/api/integration/pack-check.mjs +49 -7
  35. package/api/integration/pack-check.test.mjs +249 -0
  36. package/api/search/search.d.mts +1 -1
  37. package/api/search/search.mjs +5 -5
  38. package/api/search/search.type.d.mts +2 -2
  39. package/api/search/search.type.mjs +1 -1
  40. package/api/swizzle/swizzle.type.d.mts +2 -2
  41. package/api/swizzle/swizzle.type.mjs +2 -2
  42. package/api/template/template.d.mts +1 -1
  43. package/api/template/template.type.d.mts +6 -6
  44. package/api/template/template.type.mjs +12 -12
  45. package/api/theme/build/build.mjs +20 -6
  46. package/api/theme/build/build.test.mjs +127 -0
  47. package/api/theme/palette/generate/generate.mjs +1 -1
  48. package/api/theme/palette/generate/generator.d.mts +10 -13
  49. package/api/theme/palette/generate/generator.mjs +7 -3
  50. package/api/theme/theme.type.d.mts +170 -11
  51. package/api/theme/theme.type.mjs +94 -27
  52. package/api/upgrade/_adapter.mjs +71 -5
  53. package/api/upgrade/project-context.test.mjs +272 -0
  54. package/api/upgrade/upgrade.doc.mjs +4 -3
  55. package/api/upgrade/upgrade.type.d.mts +5 -5
  56. package/api/upgrade/upgrade.type.mjs +11 -11
  57. package/assets/codemods/integration-discovery.mjs +40 -2
  58. package/assets/codemods/integration-discovery.test.mjs +58 -0
  59. package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
  60. package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
  61. package/assets/docs/README.md +9 -0
  62. package/assets/docs/authoring.doc.mjs +14 -0
  63. package/assets/docs/cli-integrations.doc.mjs +86 -15
  64. package/assets/docs/styling-libraries.doc.mjs +1 -1
  65. package/assets/docs/working-with-ai.doc.mjs +1 -1
  66. package/authoring/_shared/contract.ts +22 -0
  67. package/authoring/codemod/codemod.doc.mjs +6 -1
  68. package/authoring/codemod/parse.d.mts +8 -8
  69. package/authoring/codemod/parse.mjs +8 -6
  70. package/authoring/config/parse.d.mts +13 -13
  71. package/authoring/config/parse.mjs +8 -8
  72. package/authoring/config/type.ts +3 -3
  73. package/authoring/debug/parse.d.mts +5 -5
  74. package/authoring/debug/parse.mjs +3 -3
  75. package/authoring/doctypes/_schema.d.mts +788 -23
  76. package/authoring/doctypes/_schema.mjs +492 -39
  77. package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
  78. package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
  79. package/authoring/doctypes/base/type.ts +40 -0
  80. package/authoring/doctypes/command/command.doc.mjs +3 -2
  81. package/authoring/doctypes/command/parse.d.mts +2 -2
  82. package/authoring/doctypes/command/parse.mjs +1 -1
  83. package/authoring/doctypes/command/type.ts +3 -2
  84. package/authoring/doctypes/component/component.doc.mjs +6 -3
  85. package/authoring/doctypes/component/parse.d.mts +2 -2
  86. package/authoring/doctypes/component/parse.mjs +1 -1
  87. package/authoring/doctypes/component/type.ts +4 -3
  88. package/authoring/doctypes/enum/parse.d.mts +2 -2
  89. package/authoring/doctypes/enum/parse.mjs +1 -1
  90. package/authoring/doctypes/enum/type.ts +3 -1
  91. package/authoring/doctypes/function/function.doc.mjs +4 -0
  92. package/authoring/doctypes/function/parse.d.mts +2 -2
  93. package/authoring/doctypes/function/parse.mjs +1 -1
  94. package/authoring/doctypes/function/type.ts +6 -2
  95. package/authoring/doctypes/hook/hook.doc.mjs +4 -0
  96. package/authoring/doctypes/hook/parse.d.mts +2 -2
  97. package/authoring/doctypes/hook/parse.mjs +1 -1
  98. package/authoring/doctypes/hook/type.ts +3 -2
  99. package/authoring/doctypes/legacy.d.mts +8 -6
  100. package/authoring/doctypes/legacy.mjs +5 -4
  101. package/authoring/doctypes/load-contract.test.mjs +207 -0
  102. package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
  103. package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
  104. package/authoring/doctypes/namespace/parse.d.mts +12 -0
  105. package/authoring/doctypes/namespace/parse.mjs +25 -0
  106. package/authoring/doctypes/namespace/parse.test.mjs +165 -0
  107. package/authoring/doctypes/namespace/type.ts +71 -0
  108. package/authoring/doctypes/parse.d.mts +20 -18
  109. package/authoring/doctypes/parse.mjs +16 -10
  110. package/authoring/doctypes/parse.test.mjs +77 -3
  111. package/authoring/doctypes/reference/parse.d.mts +2 -2
  112. package/authoring/doctypes/reference/parse.mjs +8 -5
  113. package/authoring/doctypes/reference/reference.doc.mjs +17 -4
  114. package/authoring/doctypes/reference/type.ts +51 -5
  115. package/authoring/doctypes/schema/parse.d.mts +2 -2
  116. package/authoring/doctypes/schema/parse.mjs +1 -1
  117. package/authoring/doctypes/schema/type.ts +3 -2
  118. package/authoring/doctypes/template/parse.d.mts +92 -1
  119. package/authoring/doctypes/template/parse.mjs +36 -2
  120. package/authoring/doctypes/template/parse.test.mjs +8 -2
  121. package/authoring/doctypes/template/template.doc.mjs +4 -0
  122. package/authoring/doctypes/template/type.ts +5 -2
  123. package/authoring/doctypes/types.ts +10 -9
  124. package/authoring/gap-report/parse.d.mts +10 -10
  125. package/authoring/gap-report/parse.mjs +6 -6
  126. package/authoring/gap-report/type.ts +1 -1
  127. package/authoring/identity/identity.doc.d.mts +9 -0
  128. package/authoring/identity/identity.doc.mjs +61 -0
  129. package/authoring/identity/type.ts +132 -0
  130. package/authoring/index.d.mts +1 -0
  131. package/authoring/index.d.ts +49 -17
  132. package/authoring/index.mjs +1 -0
  133. package/authoring/integration/integration.doc.mjs +13 -6
  134. package/authoring/integration/parse.d.mts +2 -2
  135. package/authoring/integration/parse.mjs +1 -1
  136. package/authoring/integration/parse.test.mjs +10 -1
  137. package/authoring/integration/schema.d.mts +6 -4
  138. package/authoring/integration/schema.mjs +9 -3
  139. package/authoring/integration/type.ts +23 -6
  140. package/authoring/shadcn/receipt.d.mts +6 -6
  141. package/clients/cli/commands/docs.doc.mjs +13 -3
  142. package/clients/cli/commands/docs.mjs +121 -21
  143. package/clients/cli/commands/docs.test.mjs +88 -0
  144. package/clients/cli/commands/integration-authoring.test.mjs +13 -9
  145. package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
  146. package/clients/cli/commands/upgrade.doc.mjs +2 -2
  147. package/clients/cli/formatters/index.mjs +162 -1
  148. package/clients/cli/formatters/index.test.mjs +91 -0
  149. package/clients/cli/lib/manifest.mjs +7 -2
  150. package/foundation/config/project.mjs +21 -6
  151. package/foundation/discovery/authoring-self-docs.d.mts +69 -0
  152. package/foundation/discovery/authoring-self-docs.mjs +214 -0
  153. package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
  154. package/foundation/discovery/component-discovery.d.mts +1 -1
  155. package/foundation/discovery/component-discovery.mjs +2 -1
  156. package/foundation/discovery/docs-discovery.d.mts +11 -4
  157. package/foundation/discovery/docs-discovery.mjs +208 -88
  158. package/foundation/discovery/docs-discovery.test.mjs +279 -13
  159. package/foundation/discovery/docs-output-budget.d.mts +28 -0
  160. package/foundation/discovery/docs-output-budget.mjs +50 -0
  161. package/foundation/discovery/docs-section-key.d.mts +98 -0
  162. package/foundation/discovery/docs-section-key.mjs +221 -0
  163. package/foundation/discovery/docs-section-key.test.mjs +224 -0
  164. package/foundation/discovery/template-adapter.mjs +2 -1
  165. package/foundation/discovery/theming-targets.test.mjs +4 -0
  166. package/foundation/doc-compiler/compile.d.mts +162 -0
  167. package/foundation/doc-compiler/compile.mjs +262 -0
  168. package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
  169. package/foundation/doc-compiler/ir.d.mts +9 -0
  170. package/foundation/doc-compiler/ir.mjs +287 -0
  171. package/foundation/doc-compiler/lenses.d.mts +33 -0
  172. package/foundation/doc-compiler/lenses.mjs +127 -0
  173. package/foundation/identity/provider-identity.d.mts +90 -0
  174. package/foundation/identity/provider-identity.mjs +320 -0
  175. package/foundation/identity/provider-identity.test.mjs +254 -0
  176. package/foundation/identity/providers.d.mts +7 -0
  177. package/foundation/identity/providers.mjs +16 -0
  178. package/foundation/integrations/autolink.mjs +12 -5
  179. package/foundation/integrations/integration-warnings.mjs +6 -0
  180. package/foundation/integrations/integrations.d.mts +46 -2
  181. package/foundation/integrations/integrations.mjs +167 -8
  182. package/foundation/integrations/integrations.test.mjs +384 -1
  183. package/foundation/integrations/provider-conflicts.test.mjs +125 -0
  184. package/foundation/integrations/validate-contributions.d.mts +2 -0
  185. package/foundation/integrations/validate-contributions.mjs +10 -0
  186. package/foundation/response/json-contract.test.mjs +46 -17
  187. package/foundation/response/response-types.doc.mjs +6 -1
  188. package/package.json +9 -11
@@ -0,0 +1,320 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Canonical provider, artifact, and immutable-instance identity helpers.
5
+ *
6
+ * IDs use versioned, RFC 3986-escaped path segments. Provider package names are
7
+ * case-insensitive and normalize to lowercase. Artifact names preserve case and
8
+ * normalize Unicode to NFC.
9
+ */
10
+
11
+ /** @typedef {import('../../authoring/identity/type').ArtifactId} ArtifactId */
12
+ /** @typedef {import('../../authoring/identity/type').ArtifactIdentity} ArtifactIdentity */
13
+ /** @typedef {import('../../authoring/identity/type').AuthoredDoc} AuthoredDoc */
14
+ /** @typedef {import('../../authoring/identity/type').AuthoredDocEntry} AuthoredDocEntry */
15
+ /** @typedef {import('../../authoring/doctypes/base/type').AuthoredDocKind} AuthoredDocKind */
16
+ /** @typedef {import('../../authoring/identity/type').ContentDigest} ContentDigest */
17
+ /** @typedef {import('../../authoring/identity/type').ContributionKind} ContributionKind */
18
+ /** @typedef {import('../../authoring/identity/type').DocId} DocId */
19
+ /** @typedef {import('../../authoring/identity/type').ProviderId} ProviderId */
20
+ /** @typedef {import('../../authoring/identity/type').ProviderInstance} ProviderInstance */
21
+ /** @typedef {import('../../authoring/identity/type').ProviderInstanceId} ProviderInstanceId */
22
+
23
+ const ARTIFACT_PREFIX = 'astryx:artifact:v1/';
24
+ const PROVIDER_INSTANCE_PREFIX = 'astryx:provider-instance:v1/';
25
+ const PACKAGE_NAME_RE =
26
+ /^(?:@[a-z0-9][a-z0-9._~!*'()-]*\/)?[a-z0-9][a-z0-9._~!*'()-]*$/u;
27
+ const SHA256_RE = /^sha256:[0-9a-f]{64}$/u;
28
+
29
+ /** @type {ReadonlySet<ContributionKind>} */
30
+ const CONTRIBUTION_KINDS = new Set([
31
+ 'component',
32
+ 'function',
33
+ 'generic',
34
+ 'page',
35
+ 'block',
36
+ 'schema',
37
+ 'command',
38
+ 'enum',
39
+ 'namespace',
40
+ 'theme',
41
+ 'codemod',
42
+ 'agent-doc',
43
+ ]);
44
+
45
+ /** @type {ReadonlySet<AuthoredDocKind>} */
46
+ const DOC_KINDS = new Set([
47
+ 'component',
48
+ 'function',
49
+ 'generic',
50
+ 'page',
51
+ 'block',
52
+ 'schema',
53
+ 'command',
54
+ 'enum',
55
+ 'namespace',
56
+ ]);
57
+
58
+ /** RFC 3986 path-segment encoding with uppercase escapes. */
59
+ /** @param {string} value @returns {string} */
60
+ function encodeSegment(value) {
61
+ return encodeURIComponent(value).replace(
62
+ /[!'()*]/gu,
63
+ character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
64
+ );
65
+ }
66
+
67
+ /** Decode one identity segment or throw a stable, readable error. */
68
+ /** @param {string} value @param {string} label @returns {string} */
69
+ function decodeSegment(value, label) {
70
+ try {
71
+ return decodeURIComponent(value);
72
+ } catch {
73
+ throw new Error(`${label} contains invalid percent escaping.`);
74
+ }
75
+ }
76
+
77
+ /** Normalize a required identity string without changing meaningful case. */
78
+ /** @param {unknown} value @param {string} label @returns {string} */
79
+ function normalizeName(value, label) {
80
+ if (typeof value !== 'string' || value.trim() === '') {
81
+ throw new Error(`${label} must be a non-empty string.`);
82
+ }
83
+ return value.trim().normalize('NFC');
84
+ }
85
+
86
+ /**
87
+ * Normalize an npm package name into a ProviderId.
88
+ * @param {string} packageName
89
+ * @returns {ProviderId}
90
+ */
91
+ export function normalizeProviderId(packageName) {
92
+ const normalized = normalizeName(packageName, 'Provider ID').toLowerCase();
93
+ if (!PACKAGE_NAME_RE.test(normalized)) {
94
+ throw new Error(
95
+ `Provider ID "${packageName}" must be a bare npm package name.`,
96
+ );
97
+ }
98
+ return /** @type {ProviderId} */ (normalized);
99
+ }
100
+
101
+ /**
102
+ * Normalize a SHA-256 digest.
103
+ * @param {string} digest
104
+ * @returns {ContentDigest}
105
+ */
106
+ export function normalizeContentDigest(digest) {
107
+ const normalized = normalizeName(digest, 'Content digest').toLowerCase();
108
+ if (!SHA256_RE.test(normalized)) {
109
+ throw new Error(
110
+ 'Content digest must use sha256:<64 lowercase hex characters>.',
111
+ );
112
+ }
113
+ return /** @type {ContentDigest} */ (normalized);
114
+ }
115
+
116
+ /**
117
+ * Build a stable provider + contribution kind + artifact-name ID.
118
+ * @param {string} provider
119
+ * @param {ContributionKind} kind
120
+ * @param {string} name
121
+ * @returns {ArtifactId}
122
+ */
123
+ export function createArtifactId(provider, kind, name) {
124
+ const providerId = normalizeProviderId(provider);
125
+ if (!CONTRIBUTION_KINDS.has(kind)) {
126
+ throw new Error(`Unknown contribution kind "${String(kind)}".`);
127
+ }
128
+ const stableName = normalizeName(name, 'Artifact name');
129
+ return /** @type {ArtifactId} */ (
130
+ `${ARTIFACT_PREFIX}${encodeSegment(providerId)}/${kind}/${encodeSegment(stableName)}`
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Build a stable document ID from provider + authored kind + stable name.
136
+ * @param {string} provider
137
+ * @param {AuthoredDocKind} kind
138
+ * @param {string} name
139
+ * @returns {DocId}
140
+ */
141
+ export function createDocId(provider, kind, name) {
142
+ if (!DOC_KINDS.has(kind)) {
143
+ throw new Error(`Unknown authored doc kind "${String(kind)}".`);
144
+ }
145
+ return /** @type {DocId} */ (createArtifactId(provider, kind, name));
146
+ }
147
+
148
+ /**
149
+ * Build a normalized logical artifact record.
150
+ * @param {string} provider
151
+ * @param {ContributionKind} kind
152
+ * @param {string} name
153
+ * @returns {ArtifactIdentity}
154
+ */
155
+ export function createArtifactIdentity(provider, kind, name) {
156
+ const providerId = normalizeProviderId(provider);
157
+ const stableName = normalizeName(name, 'Artifact name');
158
+ return Object.freeze({
159
+ id: createArtifactId(providerId, kind, stableName),
160
+ providerId,
161
+ kind,
162
+ name: stableName,
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Parse and canonicalize an ArtifactId.
168
+ * @param {string} value
169
+ * @returns {ArtifactIdentity}
170
+ */
171
+ export function parseArtifactId(value) {
172
+ if (typeof value !== 'string' || !value.startsWith(ARTIFACT_PREFIX)) {
173
+ throw new Error(`Artifact ID must start with "${ARTIFACT_PREFIX}".`);
174
+ }
175
+ const segments = value.slice(ARTIFACT_PREFIX.length).split('/');
176
+ if (segments.length !== 3) {
177
+ throw new Error(
178
+ 'Artifact ID must contain provider, kind, and name segments.',
179
+ );
180
+ }
181
+ const provider = decodeSegment(segments[0], 'Artifact provider');
182
+ const kind = decodeSegment(segments[1], 'Artifact kind');
183
+ const name = decodeSegment(segments[2], 'Artifact name');
184
+ if (!CONTRIBUTION_KINDS.has(/** @type {ContributionKind} */ (kind))) {
185
+ throw new Error(`Unknown contribution kind "${kind}".`);
186
+ }
187
+ const identity = createArtifactIdentity(
188
+ provider,
189
+ /** @type {ContributionKind} */ (kind),
190
+ name,
191
+ );
192
+ if (identity.id !== value) {
193
+ throw new Error('Artifact ID is not in canonical serialized form.');
194
+ }
195
+ return identity;
196
+ }
197
+
198
+ /**
199
+ * Build one immutable provider instance.
200
+ * @param {{providerId: string, packageName: string, packageVersion: string, sourceDigest: string}} input
201
+ * @returns {ProviderInstance}
202
+ */
203
+ export function createProviderInstance(input) {
204
+ const providerId = normalizeProviderId(input.providerId);
205
+ const packageName = normalizeProviderId(input.packageName);
206
+ const packageVersion = normalizeName(input.packageVersion, 'Package version');
207
+ const sourceDigest = normalizeContentDigest(input.sourceDigest);
208
+ const id = /** @type {ProviderInstanceId} */ (
209
+ `${PROVIDER_INSTANCE_PREFIX}${encodeSegment(providerId)}/${encodeSegment(packageName)}/${encodeSegment(packageVersion)}/${encodeSegment(sourceDigest)}`
210
+ );
211
+ return Object.freeze({
212
+ id,
213
+ providerId,
214
+ packageName,
215
+ packageVersion,
216
+ sourceDigest,
217
+ });
218
+ }
219
+
220
+ /** Normalize a package-relative source path to forward slashes. */
221
+ /** @param {unknown} value @returns {string} */
222
+ function normalizeSourcePath(value) {
223
+ const normalized = normalizeName(value, 'Source path').replaceAll('\\', '/');
224
+ const segments = normalized.split('/');
225
+ if (
226
+ normalized.startsWith('/') ||
227
+ /^[A-Za-z]:\//u.test(normalized) ||
228
+ segments.some(
229
+ segment => segment === '' || segment === '.' || segment === '..',
230
+ )
231
+ ) {
232
+ throw new Error('Source path must be a normalized package-relative path.');
233
+ }
234
+ return normalized;
235
+ }
236
+
237
+ /** Infer an unstamped legacy document's kind from its required shape. */
238
+ /** @param {AuthoredDoc} authored @returns {AuthoredDocKind} */
239
+ function authoredKindOf(authored) {
240
+ if (authored.type != null) return authored.type;
241
+ if ('params' in authored && 'returns' in authored) return 'function';
242
+ if (
243
+ 'props' in authored ||
244
+ 'components' in authored ||
245
+ 'subComponentOf' in authored
246
+ ) {
247
+ return 'component';
248
+ }
249
+ if ('sections' in authored && 'title' in authored) return 'generic';
250
+ throw new Error('Cannot infer the kind of an unstamped authored document.');
251
+ }
252
+
253
+ /** Clone and recursively freeze plain authored data. */
254
+ /** @template T @param {T} value @returns {T} */
255
+ function frozenClone(value) {
256
+ const clone = structuredClone(value);
257
+ const seen = new WeakSet();
258
+ /** @param {unknown} current */
259
+ const freeze = current => {
260
+ if (
261
+ current == null ||
262
+ typeof current !== 'object' ||
263
+ Object.isFrozen(current) ||
264
+ seen.has(current)
265
+ ) {
266
+ return;
267
+ }
268
+ seen.add(current);
269
+ for (const nested of Object.values(current)) freeze(nested);
270
+ Object.freeze(current);
271
+ };
272
+ freeze(clone);
273
+ return clone;
274
+ }
275
+
276
+ /**
277
+ * Bind one validated authored document to immutable provider and source
278
+ * provenance. Runtime lifecycle state is intentionally absent.
279
+ * @param {{
280
+ * provider: ProviderInstance,
281
+ * kind: AuthoredDocKind,
282
+ * stableName: string,
283
+ * source: {group: string, path: string, digest: string},
284
+ * authored: AuthoredDoc,
285
+ * }} input
286
+ * @returns {AuthoredDocEntry}
287
+ */
288
+ export function createAuthoredDocEntry(input) {
289
+ const stableName = normalizeName(input.stableName, 'Stable document name');
290
+ const group = normalizeName(input.source.group, 'Source group');
291
+ const sourcePath = normalizeSourcePath(input.source.path);
292
+ const digest = normalizeContentDigest(input.source.digest);
293
+ const authoredKind = authoredKindOf(input.authored);
294
+ if (authoredKind !== input.kind) {
295
+ throw new Error(
296
+ `Document kind "${input.kind}" does not match authored type "${authoredKind}".`,
297
+ );
298
+ }
299
+
300
+ const provider = createProviderInstance({
301
+ providerId: input.provider.providerId,
302
+ packageName: input.provider.packageName,
303
+ packageVersion: input.provider.packageVersion,
304
+ sourceDigest: input.provider.sourceDigest,
305
+ });
306
+ if (provider.id !== input.provider.id) {
307
+ throw new Error(
308
+ 'Provider instance ID does not match its immutable fields.',
309
+ );
310
+ }
311
+
312
+ return Object.freeze({
313
+ id: createDocId(provider.providerId, input.kind, stableName),
314
+ provider,
315
+ kind: input.kind,
316
+ stableName,
317
+ source: Object.freeze({group, path: sourcePath, digest}),
318
+ authored: frozenClone(input.authored),
319
+ });
320
+ }
@@ -0,0 +1,254 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {
5
+ createArtifactId,
6
+ createArtifactIdentity,
7
+ createAuthoredDocEntry,
8
+ createDocId,
9
+ createProviderInstance,
10
+ normalizeContentDigest,
11
+ normalizeProviderId,
12
+ parseArtifactId,
13
+ } from './provider-identity.mjs';
14
+ import {CLI_PROVIDER_ID, CORE_PROVIDER_ID} from './providers.mjs';
15
+
16
+ const DIGEST_A = `sha256:${'a'.repeat(64)}`;
17
+ const DIGEST_B = `sha256:${'b'.repeat(64)}`;
18
+
19
+ describe('provider identity', () => {
20
+ it('uses the same canonical model for built-in providers', () => {
21
+ expect(CORE_PROVIDER_ID).toBe('@astryxdesign/core');
22
+ expect(CLI_PROVIDER_ID).toBe('@astryxdesign/cli');
23
+ });
24
+
25
+ it('normalizes npm package identity once', () => {
26
+ expect(normalizeProviderId(' @AstryxDesign/CLI ')).toBe(
27
+ '@astryxdesign/cli',
28
+ );
29
+ expect(() => normalizeProviderId('../cli')).toThrow(
30
+ /bare npm package name/u,
31
+ );
32
+ });
33
+
34
+ it('normalizes content digests', () => {
35
+ expect(normalizeContentDigest(DIGEST_A.toUpperCase())).toBe(DIGEST_A);
36
+ expect(() => normalizeContentDigest('sha256:nope')).toThrow(/sha256/u);
37
+ });
38
+
39
+ it('keeps provider, kind, and case-sensitive artifact name in the ID', () => {
40
+ const component = createArtifactId('@acme/ui', 'component', 'Button');
41
+ expect(component).not.toBe(
42
+ createArtifactId('@acme/ui', 'component', 'button'),
43
+ );
44
+ expect(component).not.toBe(
45
+ createArtifactId('@acme/ui', 'schema', 'Button'),
46
+ );
47
+ expect(component).not.toBe(
48
+ createArtifactId('@other/ui', 'component', 'Button'),
49
+ );
50
+ });
51
+
52
+ it('escapes segments canonically and round-trips without collisions', () => {
53
+ const identity = createArtifactIdentity(
54
+ '@acme/ui',
55
+ 'generic',
56
+ 'Deploy / 100%',
57
+ );
58
+ expect(identity.id).toContain('Deploy%20%2F%20100%25');
59
+ expect(parseArtifactId(identity.id)).toEqual(identity);
60
+ expect(() => parseArtifactId(identity.id.replace('%2F', '%2f'))).toThrow(
61
+ /canonical/u,
62
+ );
63
+ });
64
+
65
+ it('uses the same stable ID across immutable provider instances', () => {
66
+ const docId = createDocId('@acme/ui', 'schema', 'integration');
67
+ const first = createProviderInstance({
68
+ providerId: '@acme/ui',
69
+ packageName: '@acme/ui',
70
+ packageVersion: '1.0.0',
71
+ sourceDigest: DIGEST_A,
72
+ });
73
+ const second = createProviderInstance({
74
+ providerId: '@acme/ui',
75
+ packageName: '@acme/ui',
76
+ packageVersion: '1.0.1',
77
+ sourceDigest: DIGEST_B,
78
+ });
79
+ expect(first.id).not.toBe(second.id);
80
+ expect(createDocId(first.providerId, 'schema', 'integration')).toBe(docId);
81
+ expect(createDocId(second.providerId, 'schema', 'integration')).toBe(docId);
82
+ });
83
+
84
+ it('supports an explicit stable provider ID across a package rename', () => {
85
+ const instance = createProviderInstance({
86
+ providerId: '@acme/ui',
87
+ packageName: '@acme/design-system',
88
+ packageVersion: '2.0.0',
89
+ sourceDigest: DIGEST_A,
90
+ });
91
+ expect(instance.providerId).toBe('@acme/ui');
92
+ expect(instance.packageName).toBe('@acme/design-system');
93
+ });
94
+
95
+ it('binds a doc to stable identity and immutable source provenance', () => {
96
+ const provider = createProviderInstance({
97
+ providerId: '@acme/ui',
98
+ packageName: '@acme/ui',
99
+ packageVersion: '1.0.0',
100
+ sourceDigest: DIGEST_A,
101
+ });
102
+ const authored = {
103
+ type: 'schema',
104
+ name: 'integration',
105
+ displayName: 'Integration',
106
+ description: 'The integration manifest.',
107
+ fields: [],
108
+ };
109
+
110
+ const entry = createAuthoredDocEntry({
111
+ provider,
112
+ kind: 'schema',
113
+ stableName: 'integration',
114
+ source: {
115
+ group: 'cli-api',
116
+ path: 'authoring/integration/integration.doc.mjs',
117
+ digest: DIGEST_B,
118
+ },
119
+ authored,
120
+ });
121
+
122
+ expect(entry.id).toBe(createDocId('@acme/ui', 'schema', 'integration'));
123
+ expect(entry.provider).toEqual(provider);
124
+ expect(entry.stableName).toBe('integration');
125
+ expect(entry.source.digest).toBe(DIGEST_B);
126
+ expect(Object.isFrozen(entry)).toBe(true);
127
+ expect(Object.isFrozen(entry.provider)).toBe(true);
128
+ expect(Object.isFrozen(entry.source)).toBe(true);
129
+ expect(Object.isFrozen(entry.authored)).toBe(true);
130
+
131
+ authored.name = 'mutated';
132
+ expect(entry.authored.name).toBe('integration');
133
+ });
134
+
135
+ it('uses a discovery-owned stable name when authored display names collide', () => {
136
+ const provider = createProviderInstance({
137
+ providerId: '@astryxdesign/core',
138
+ packageName: '@astryxdesign/core',
139
+ packageVersion: '1.0.0',
140
+ sourceDigest: DIGEST_A,
141
+ });
142
+ const authored = {
143
+ type: 'block',
144
+ name: 'Banner — Statuses',
145
+ description: 'A template.',
146
+ };
147
+ const makeEntry = stableName =>
148
+ createAuthoredDocEntry({
149
+ provider,
150
+ kind: 'block',
151
+ stableName,
152
+ source: {
153
+ group: 'templates',
154
+ path: `templates/${stableName}.doc.mjs`,
155
+ digest: DIGEST_B,
156
+ },
157
+ authored,
158
+ });
159
+
160
+ expect(makeEntry('BannerShowcase').id).not.toBe(
161
+ makeEntry('BannerStatuses').id,
162
+ );
163
+ });
164
+
165
+ it('correlates an unstamped legacy reference doc with generic identity', () => {
166
+ const provider = createProviderInstance({
167
+ providerId: '@acme/ui',
168
+ packageName: '@acme/ui',
169
+ packageVersion: '1.0.0',
170
+ sourceDigest: DIGEST_A,
171
+ });
172
+ const entry = createAuthoredDocEntry({
173
+ provider,
174
+ kind: 'generic',
175
+ stableName: 'theming',
176
+ source: {group: 'guides', path: 'docs/theming.doc.mjs', digest: DIGEST_B},
177
+ authored: {
178
+ name: 'theming',
179
+ title: 'Theming',
180
+ description: 'How theming works.',
181
+ sections: [],
182
+ },
183
+ });
184
+ expect(entry.kind).toBe('generic');
185
+ });
186
+
187
+ it('rejects doc-entry identity that disagrees with stamped or legacy content', () => {
188
+ const provider = createProviderInstance({
189
+ providerId: '@acme/ui',
190
+ packageName: '@acme/ui',
191
+ packageVersion: '1.0.0',
192
+ sourceDigest: DIGEST_A,
193
+ });
194
+ const source = {
195
+ group: 'cli-api',
196
+ path: 'integration.doc.mjs',
197
+ digest: DIGEST_B,
198
+ };
199
+
200
+ expect(() =>
201
+ createAuthoredDocEntry({
202
+ provider,
203
+ kind: 'command',
204
+ stableName: 'integration',
205
+ source,
206
+ authored: {
207
+ type: 'schema',
208
+ name: 'integration',
209
+ displayName: 'Integration',
210
+ description: 'The integration manifest.',
211
+ fields: [],
212
+ },
213
+ }),
214
+ ).toThrow(/does not match authored type/u);
215
+
216
+ expect(() =>
217
+ createAuthoredDocEntry({
218
+ provider,
219
+ kind: 'enum',
220
+ stableName: 'Button',
221
+ source,
222
+ authored: {name: 'Button', props: []},
223
+ }),
224
+ ).toThrow(/does not match authored type "component"/u);
225
+ });
226
+
227
+ it('rejects source paths that escape the provider package', () => {
228
+ const provider = createProviderInstance({
229
+ providerId: '@acme/ui',
230
+ packageName: '@acme/ui',
231
+ packageVersion: '1.0.0',
232
+ sourceDigest: DIGEST_A,
233
+ });
234
+ expect(() =>
235
+ createAuthoredDocEntry({
236
+ provider,
237
+ kind: 'schema',
238
+ stableName: 'integration',
239
+ source: {
240
+ group: 'cli-api',
241
+ path: '../outside.doc.mjs',
242
+ digest: DIGEST_B,
243
+ },
244
+ authored: {
245
+ type: 'schema',
246
+ name: 'integration',
247
+ displayName: 'Integration',
248
+ description: 'The integration manifest.',
249
+ fields: [],
250
+ },
251
+ }),
252
+ ).toThrow(/package-relative path/u);
253
+ });
254
+ });
@@ -0,0 +1,7 @@
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
+ /** Core owns built-in components and templates. */
5
+ export const CORE_PROVIDER_ID: import("../../authoring/index.js").ProviderId;
6
+ /** The CLI owns built-in topic docs and CLI-authored contributions. */
7
+ export const CLI_PROVIDER_ID: import("../../authoring/index.js").ProviderId;
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Canonical identities for Astryx's built-in providers.
5
+ * @input Stable npm package names owned by Astryx.
6
+ * @output Branded ProviderId values shared by discovery and compilation.
7
+ * @position foundation/identity — built-in provider identity boundary.
8
+ */
9
+
10
+ import {normalizeProviderId} from './provider-identity.mjs';
11
+
12
+ /** Core owns built-in components and templates. */
13
+ export const CORE_PROVIDER_ID = normalizeProviderId('@astryxdesign/core');
14
+
15
+ /** The CLI owns built-in topic docs and CLI-authored contributions. */
16
+ export const CLI_PROVIDER_ID = normalizeProviderId('@astryxdesign/cli');
@@ -234,7 +234,13 @@ export async function autolinkIntegrations({
234
234
 
235
235
  /** @type {import('./integrations.mjs').LoadedIntegration[]} */
236
236
  const autolinked = [];
237
- const names = new Set(loaded.map(integration => integration.name));
237
+ // One package reached twice (an alias beside the package it aliases, at the
238
+ // same version) loads once. The same name at another version is kept, so the
239
+ // provider-identity pass in Project.load reports it instead of dropping it.
240
+ /** @param {{name: string, version?: string}} integration */
241
+ const identity = integration =>
242
+ `${integration.name}\u0000${integration.version ?? ''}`;
243
+ const seen = new Set(loaded.map(identity));
238
244
 
239
245
  for (const candidate of candidates) {
240
246
  /** @type {import('./integrations.mjs').LoadedIntegration|undefined} */
@@ -250,10 +256,11 @@ export async function autolinkIntegrations({
250
256
  continue;
251
257
  }
252
258
  if (!integration || integration.__loadError) continue;
253
- // Identity is the resolved package's own name, so an alias and the package
254
- // it aliases collapse here even when they are two directories on disk.
255
- if (names.has(integration.name)) continue;
256
- names.add(integration.name);
259
+ // Identity is the resolved package's own name and version, so an alias and
260
+ // the package it aliases collapse here even when they are two directories
261
+ // on disk.
262
+ if (seen.has(identity(integration))) continue;
263
+ seen.add(identity(integration));
257
264
  autolinked.push({
258
265
  ...integration,
259
266
  __autolinked: true,
@@ -41,6 +41,12 @@ export async function warnOnIntegrationIssues(loadedIntegrations, {json = false}
41
41
  }
42
42
  for (const integration of loadedIntegrations) {
43
43
  if (!integration || typeof integration !== 'object') continue;
44
+ // A package set aside for a provider-ID conflict is fine in isolation,
45
+ // so `doctor integration validate` would not explain it. Say it here.
46
+ if (integration.__providerConflict) {
47
+ console.error(`Warning: ${integration.__providerConflict.message}`);
48
+ continue;
49
+ }
44
50
  let issues;
45
51
  try {
46
52
  issues = await validateLoadedIntegration(integration);