@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
@@ -210,6 +210,425 @@ describe('doctor integration — command', () => {
210
210
  expect(process.exitCode).toBeUndefined();
211
211
  });
212
212
 
213
+ it('validate warns about each doc field that does not match its authoring type, and still exits 0', async () => {
214
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
215
+ process.chdir(tmpDir);
216
+
217
+ await createProgram().parseAsync([
218
+ 'node',
219
+ 'astryx',
220
+ 'doctor',
221
+ 'integration',
222
+ 'validate',
223
+ ]);
224
+
225
+ const output = logCalls.join('\n');
226
+ expect(output).toContain('[ok] No integration issues found.');
227
+ expect(output).toMatch(
228
+ /Checking 1 doc\(s\) against the authoring types \(TypeScript \d+\.\d+/,
229
+ );
230
+ expect(output).toContain('templates/hero.doc.mjs');
231
+ expect(output).toMatch(/export:\s+default/);
232
+ expect(output).toMatch(/type:\s+IntegrationTemplateDoc/);
233
+ expect(output).toContain(
234
+ 'Add `aspectRatio`: IntegrationBlockTemplateDoc requires it.',
235
+ );
236
+ expect(output).toContain(
237
+ 'Add `displayName`: IntegrationBlockTemplateDoc requires it.',
238
+ );
239
+ expect(output).toContain(
240
+ '2 doc field(s) in 1 file(s) do not match the authoring types.',
241
+ );
242
+ expect(process.exitCode).toBeUndefined();
243
+ });
244
+
245
+ it('validate says there is no doc to type-check, rather than asking for TypeScript', async () => {
246
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-docs-'));
247
+ try {
248
+ fs.writeFileSync(
249
+ path.join(outside, 'package.json'),
250
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
251
+ );
252
+ fs.writeFileSync(
253
+ path.join(outside, 'astryx.integration.mjs'),
254
+ "export default {components: './components'};\n",
255
+ );
256
+ fs.mkdirSync(path.join(outside, 'components'));
257
+ fs.writeFileSync(
258
+ path.join(outside, 'components', 'Widget.tsx'),
259
+ 'export function Widget() { return null; }\n',
260
+ );
261
+ process.chdir(outside);
262
+
263
+ await createProgram().parseAsync([
264
+ 'node',
265
+ 'astryx',
266
+ 'doctor',
267
+ 'integration',
268
+ 'validate',
269
+ ]);
270
+
271
+ const output = logCalls.join('\n');
272
+ expect(output).toContain('[info] No doc files to type-check.');
273
+ expect(output).not.toContain('Docs not type-checked');
274
+ expect(output).not.toContain('against the authoring types');
275
+ } finally {
276
+ process.chdir(previousCwd);
277
+ fs.rmSync(outside, {recursive: true, force: true});
278
+ }
279
+ });
280
+
281
+ it('validate says the docs matched even when another check failed', async () => {
282
+ writeIntegration({id: 'hero', missingRoot: true});
283
+ fs.writeFileSync(
284
+ path.join(tmpDir, 'astryx.integration.mjs'),
285
+ "export default {templates: './missing', components: './components'};\n",
286
+ );
287
+ fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
288
+ fs.writeFileSync(
289
+ path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
290
+ "export default {type: 'component', name: 'AcmeBadge', displayName: 'Badge', usage: {description: 'A badge.'}, props: []};\n",
291
+ );
292
+ fs.writeFileSync(
293
+ path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
294
+ 'export function AcmeBadge() { return null; }\n',
295
+ );
296
+ process.chdir(tmpDir);
297
+
298
+ await createProgram().parseAsync([
299
+ 'node',
300
+ 'astryx',
301
+ 'doctor',
302
+ 'integration',
303
+ 'validate',
304
+ ]);
305
+
306
+ const output = logCalls.join('\n');
307
+ expect(process.exitCode).toBe(1);
308
+ expect(output).toContain(
309
+ '[info] No doc field differs from its authoring type (1 checked).',
310
+ );
311
+ expect(output).not.toContain('[ok] Every doc');
312
+ });
313
+
314
+ it('validate prints every field of the doc type check that JSON carries', async () => {
315
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
316
+ fs.writeFileSync(
317
+ path.join(tmpDir, 'astryx.integration.mjs'),
318
+ "export default {templates: './templates', components: './components'};\n",
319
+ );
320
+ fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
321
+ fs.writeFileSync(
322
+ path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
323
+ "export default {type: 'component', name: 'AcmeBadge', displayName: 5, usage: {description: 'A badge.'}, props: [], keyword: ['x']};\n",
324
+ );
325
+ fs.writeFileSync(
326
+ path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
327
+ 'export function AcmeBadge() { return null; }\n',
328
+ );
329
+ fs.writeFileSync(
330
+ path.join(tmpDir, 'components', 'shared.mjs'),
331
+ `export default {type: 'component', name: 'AcmeAgain', displayName: 'Again', usage: {description: 'x'}, props: []};\n`,
332
+ );
333
+ fs.writeFileSync(
334
+ path.join(tmpDir, 'components', 'AcmeAgain.doc.mjs'),
335
+ "export {default} from './shared.mjs';\n",
336
+ );
337
+ fs.writeFileSync(
338
+ path.join(tmpDir, 'components', 'AcmeAgain.tsx'),
339
+ 'export function AcmeAgain() { return null; }\n',
340
+ );
341
+ process.chdir(tmpDir);
342
+ const run = async (/** @type {string[]} */ flags) => {
343
+ logCalls.length = 0;
344
+ await createProgram().parseAsync([
345
+ 'node',
346
+ 'astryx',
347
+ ...flags,
348
+ 'doctor',
349
+ 'integration',
350
+ 'validate',
351
+ ]);
352
+ return logCalls.join('\n');
353
+ };
354
+
355
+ const report = JSON.parse(await run(['--json'])).data.docTypes;
356
+ const output = await run([]);
357
+
358
+ expect([...new Set(report.findings.map(f => f.problem))].sort()).toEqual([
359
+ 'missing_field',
360
+ 'unknown_field',
361
+ 'wrong_type',
362
+ ]);
363
+ expect(report.unchecked).toHaveLength(1);
364
+ expectRecordsCarry(output, report.findings, ['doc', 'field', 'problem']);
365
+ expectRecordsCarry(output, report.unchecked, ['doc', 'reason']);
366
+ expect(output).toContain(
367
+ `Checking ${report.docs} doc(s) against the authoring types (TypeScript ${report.typescript})`,
368
+ );
369
+ });
370
+
371
+ it('validate type-checks each doc component discovery serves, in __fixtures__, in a dot-folder or behind a symlink', async () => {
372
+ fs.writeFileSync(
373
+ path.join(tmpDir, 'package.json'),
374
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
375
+ );
376
+ fs.writeFileSync(
377
+ path.join(tmpDir, 'astryx.integration.mjs'),
378
+ `export default {components: './components'};\n`,
379
+ );
380
+ for (const [rel, name] of [
381
+ ['components/__fixtures__/AcmeFixture.doc.mjs', 'AcmeFixture'],
382
+ ['components/.drafts/AcmeDraft.doc.mjs', 'AcmeDraft'],
383
+ ['shared/AcmeLinked.doc.mjs', 'AcmeLinked'],
384
+ ]) {
385
+ fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
386
+ fs.writeFileSync(
387
+ path.join(tmpDir, rel),
388
+ `export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: [], keyword: []};\n`,
389
+ );
390
+ }
391
+ fs.symlinkSync(
392
+ path.join(tmpDir, 'shared', 'AcmeLinked.doc.mjs'),
393
+ path.join(tmpDir, 'components', 'AcmeLinked.doc.mjs'),
394
+ );
395
+ process.chdir(tmpDir);
396
+
397
+ await createProgram().parseAsync([
398
+ 'node',
399
+ 'astryx',
400
+ '--json',
401
+ 'doctor',
402
+ 'integration',
403
+ 'validate',
404
+ ]);
405
+
406
+ const {docTypes} = JSON.parse(logCalls.join('\n')).data;
407
+ expect(docTypes).toMatchObject({status: 'checked', docs: 3, unchecked: []});
408
+ expect(
409
+ docTypes.findings.map(
410
+ (/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
411
+ ),
412
+ ).toEqual([
413
+ 'components/__fixtures__/AcmeFixture.doc.mjs keyword unknown_field',
414
+ 'components/.drafts/AcmeDraft.doc.mjs keyword unknown_field',
415
+ 'components/AcmeLinked.doc.mjs keyword unknown_field',
416
+ ]);
417
+ });
418
+
419
+ it('validate type-checks a hidden doc and a second doc for one name, and counts a test fixture', async () => {
420
+ fs.writeFileSync(
421
+ path.join(tmpDir, 'package.json'),
422
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
423
+ );
424
+ fs.writeFileSync(
425
+ path.join(tmpDir, 'astryx.integration.mjs'),
426
+ `export default {components: './components'};\n`,
427
+ );
428
+ for (const [rel, name, extra] of [
429
+ ['components/AcmeBadge.doc.mjs', 'AcmeBadge', ''],
430
+ ['components/again/AcmeBadge.doc.mjs', 'AcmeBadge', ', keyword: []'],
431
+ [
432
+ 'components/AcmeHidden.doc.mjs',
433
+ 'AcmeHidden',
434
+ ', hidden: true, keyword: []',
435
+ ],
436
+ [
437
+ 'components/__tests__/AcmeTested.doc.mjs',
438
+ 'AcmeTested',
439
+ ', keyword: []',
440
+ ],
441
+ ]) {
442
+ fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
443
+ fs.writeFileSync(
444
+ path.join(tmpDir, rel),
445
+ `export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: []${extra}};\n`,
446
+ );
447
+ fs.writeFileSync(
448
+ path.join(tmpDir, rel.replace('.doc.mjs', '.tsx')),
449
+ 'export default function Fixture() { return null; }\n',
450
+ );
451
+ }
452
+ process.chdir(tmpDir);
453
+ const run = async (/** @type {string[]} */ flags) => {
454
+ logCalls.length = 0;
455
+ await createProgram().parseAsync([
456
+ 'node',
457
+ 'astryx',
458
+ ...flags,
459
+ 'doctor',
460
+ 'integration',
461
+ 'validate',
462
+ ]);
463
+ return logCalls.join('\n');
464
+ };
465
+
466
+ const {docTypes} = JSON.parse(await run(['--json'])).data;
467
+ const output = await run([]);
468
+
469
+ expect(docTypes).toMatchObject({docs: 3, fixtures: 1, unchecked: []});
470
+ expect(
471
+ docTypes.findings.map(
472
+ (/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
473
+ ),
474
+ ).toEqual([
475
+ 'components/AcmeHidden.doc.mjs keyword unknown_field',
476
+ 'components/again/AcmeBadge.doc.mjs keyword unknown_field',
477
+ ]);
478
+ expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
479
+ expect(output).not.toContain('[ok] Every doc matches');
480
+ });
481
+
482
+ it('validate keeps the fixture count when test fixtures are the only docs', async () => {
483
+ fs.writeFileSync(
484
+ path.join(tmpDir, 'package.json'),
485
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
486
+ );
487
+ fs.writeFileSync(
488
+ path.join(tmpDir, 'astryx.integration.mjs'),
489
+ `export default {components: './components'};\n`,
490
+ );
491
+ const rel = 'components/__tests__/AcmeTested.doc.mjs';
492
+ fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
493
+ fs.writeFileSync(
494
+ path.join(tmpDir, rel),
495
+ `export default {type: 'component', name: 'AcmeTested', displayName: 'AcmeTested', usage: {description: 'Fixture.'}, props: []};\n`,
496
+ );
497
+ process.chdir(tmpDir);
498
+ const run = async (/** @type {string[]} */ flags) => {
499
+ logCalls.length = 0;
500
+ await createProgram().parseAsync([
501
+ 'node',
502
+ 'astryx',
503
+ ...flags,
504
+ 'doctor',
505
+ 'integration',
506
+ 'validate',
507
+ ]);
508
+ return logCalls.join('\n');
509
+ };
510
+
511
+ const {docTypes} = JSON.parse(await run(['--json'])).data;
512
+ const output = await run([]);
513
+
514
+ expect(docTypes).toMatchObject({docs: 0, fixtures: 1, findings: []});
515
+ expect(output).toContain('[info] No doc files to type-check.');
516
+ expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
517
+ });
518
+
519
+ it("validate keeps main's issue for a root it cannot read, and lists a root with a folder it cannot read", async () => {
520
+ fs.writeFileSync(
521
+ path.join(tmpDir, 'package.json'),
522
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
523
+ );
524
+ fs.writeFileSync(
525
+ path.join(tmpDir, 'astryx.integration.mjs'),
526
+ `export default {components: './components', docs: './docs'};\n`,
527
+ );
528
+ fs.mkdirSync(path.join(tmpDir, 'components', 'locked'), {recursive: true});
529
+ fs.mkdirSync(path.join(tmpDir, 'docs'));
530
+ const locked = [
531
+ path.join(tmpDir, 'components', 'locked'),
532
+ path.join(tmpDir, 'docs'),
533
+ ];
534
+ for (const dir of locked) fs.chmodSync(dir, 0o000);
535
+ process.chdir(tmpDir);
536
+ try {
537
+ await createProgram().parseAsync([
538
+ 'node',
539
+ 'astryx',
540
+ '--json',
541
+ 'doctor',
542
+ 'integration',
543
+ 'validate',
544
+ ]);
545
+ } finally {
546
+ for (const dir of locked) fs.chmodSync(dir, 0o755);
547
+ }
548
+
549
+ const {data} = JSON.parse(logCalls.join('\n'));
550
+ expect(data.issues.map((/** @type {any} */ i) => i.code).sort()).toEqual([
551
+ 'invalid_component',
552
+ 'unreadable_root',
553
+ ]);
554
+ expect(data.docTypes.unchecked).toEqual([
555
+ {
556
+ doc: 'components',
557
+ reason: expect.stringMatching(
558
+ /^the components root does not load, so no doc in it was checked: .*EACCES/,
559
+ ),
560
+ },
561
+ ]);
562
+ });
563
+
564
+ it('validate reports the doc type check as additive JSON', async () => {
565
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
566
+ process.chdir(tmpDir);
567
+
568
+ await createProgram().parseAsync([
569
+ 'node',
570
+ 'astryx',
571
+ '--json',
572
+ 'doctor',
573
+ 'integration',
574
+ 'validate',
575
+ ]);
576
+
577
+ const parsed = JSON.parse(logCalls.join('\n'));
578
+ expect(parsed.data.issues).toEqual([]);
579
+ expect(parsed.data.docTypes).toMatchObject({
580
+ status: 'checked',
581
+ docs: 1,
582
+ unchecked: [],
583
+ });
584
+ expect(parsed.data.docTypes.typescript).toMatch(/^\d+\.\d+/);
585
+ expect(parsed.data.docTypes.findings).toEqual([
586
+ expect.objectContaining({
587
+ doc: 'templates/hero.doc.mjs',
588
+ export: 'default',
589
+ type: 'IntegrationTemplateDoc',
590
+ field: 'aspectRatio',
591
+ problem: 'missing_field',
592
+ severity: 'warning',
593
+ }),
594
+ expect.objectContaining({field: 'displayName', problem: 'missing_field'}),
595
+ ]);
596
+ expect(process.exitCode).toBeUndefined();
597
+ });
598
+
599
+ it('validate says, in one visible line, that docs were not type-checked when the project has no TypeScript', async () => {
600
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-ts-'));
601
+ const inside = tmpDir;
602
+ try {
603
+ // The fixture helpers write to tmpDir; point it outside the repository,
604
+ // where no TypeScript resolves, and restore it for afterEach.
605
+ tmpDir = outside;
606
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
607
+ tmpDir = inside;
608
+ process.chdir(outside);
609
+
610
+ await createProgram().parseAsync([
611
+ 'node',
612
+ 'astryx',
613
+ 'doctor',
614
+ 'integration',
615
+ 'validate',
616
+ ]);
617
+
618
+ const output = logCalls.join('\n');
619
+ expect(output).toContain(
620
+ '[skip] Docs not type-checked: TypeScript is not installed in this project',
621
+ );
622
+ expect(output).not.toMatch(
623
+ /Checking \d+ doc\(s\) against the authoring types/,
624
+ );
625
+ expect(process.exitCode).toBeUndefined();
626
+ } finally {
627
+ process.chdir(previousCwd);
628
+ fs.rmSync(outside, {recursive: true, force: true});
629
+ }
630
+ });
631
+
213
632
  it('templates warns on a Core id and writes the exact --package command', async () => {
214
633
  const [core] = await discoverCoreTemplates();
215
634
  expect(core).toBeDefined();
@@ -639,12 +1058,14 @@ describe('doctor integration — command', () => {
639
1058
  `export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [${[
640
1059
  "{name: 'label', type: 'string', description: 'Text.'}",
641
1060
  "{name: 'elevation', type: 'number', description: 'Shadow.'}",
1061
+ "{name: 'size', type: 'string', description: 'Size.'}",
1062
+ "{name: 'meta', type: '{id: string; extra?: number}', description: 'Data.'}",
642
1063
  ].join(', ')}]};\n`,
643
1064
  );
644
1065
  // `key` is a prop a doc never needs, though the component declares it.
645
1066
  fs.writeFileSync(
646
1067
  path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
647
- "export function AcmeBadge(props: {label: string; tone?: 'neutral' | 'info'; key?: string}) { return null; }\n",
1068
+ "export function AcmeBadge(props: {label: string; tone?: 'neutral' | 'info'; size?: number; meta?: {id: string}; key?: string}) { return null; }\n",
648
1069
  );
649
1070
  fs.writeFileSync(
650
1071
  path.join(tmpDir, 'components', 'AcmeLonely.doc.mjs'),
@@ -674,10 +1095,30 @@ describe('doctor integration — command', () => {
674
1095
  expect(report.findings.map(f => `${f.prop} ${f.kind}`)).toEqual([
675
1096
  'elevation not_in_code',
676
1097
  'label required_mismatch',
1098
+ 'size type_mismatch',
677
1099
  'tone not_in_doc',
678
1100
  ]);
679
1101
  expectRecordsCarry(output, report.findings, ['component', 'prop', 'kind']);
680
1102
  expectRecordsCarry(output, report.unchecked, ['component', 'reason']);
1103
+ expectRecordsCarry(output, report.typesNotCompared, [
1104
+ 'component',
1105
+ 'reason',
1106
+ 'props',
1107
+ ]);
1108
+ expect(report.typesNotCompared).toHaveLength(1);
1109
+ expect(output).toContain(
1110
+ `Documented prop types judged: ${report.typesCompared}; not compared: 1.`,
1111
+ );
1112
+ // The response-type reference, which the README's JSON table comes from,
1113
+ // names every field too.
1114
+ const {doc: responseTypes} =
1115
+ await import('../../../foundation/response/response-types.doc.mjs');
1116
+ const member = responseTypes.members.find(
1117
+ m => m.value === 'integration.component-conflicts',
1118
+ );
1119
+ for (const field of Object.keys(report)) {
1120
+ expect(member?.description).toMatch(new RegExp(`\\b${field}\\b`));
1121
+ }
681
1122
  expect(output).toContain(
682
1123
  `in 1 of ${report.components} component(s) compared (TypeScript ${report.typescript}).`,
683
1124
  );
@@ -695,9 +1136,13 @@ describe('doctor integration — command', () => {
695
1136
  'components',
696
1137
  ]);
697
1138
 
698
- expect(logCalls.join('\n')).toMatch(
1139
+ const output = logCalls.join('\n');
1140
+ expect(output).toMatch(
699
1141
  /\[ok\] Documented prop names and required flags match the code in 1 component\(s\) \(TypeScript \d/,
700
1142
  );
1143
+ expect(output).toMatch(
1144
+ /\[info\] Documented prop types judged: \d+; not compared: 0\./,
1145
+ );
701
1146
  });
702
1147
 
703
1148
  it('components prints no [ok] for props it could not compare', async () => {
@@ -98,6 +98,85 @@ function issueBlocks(issues) {
98
98
  ];
99
99
  }
100
100
 
101
+ /**
102
+ * The docs checked against the authoring types: a visible line when it was
103
+ * skipped, the findings (all warnings), the files it could not check, or an
104
+ * [ok] (never after a failed check).
105
+ * @param {import('../../../api/integration/validate-integration.type.mjs').DocTypesReport | undefined} report
106
+ * @param {boolean} failed
107
+ */
108
+ function docTypesBlocks(report, failed) {
109
+ if (!report) return [];
110
+ if (
111
+ report.status === 'checked' &&
112
+ report.docs === 0 &&
113
+ report.findings.length === 0 &&
114
+ report.unchecked.length === 0
115
+ ) {
116
+ return [
117
+ text('[info] No doc files to type-check.'),
118
+ ...(report.fixtures
119
+ ? [text(`[info] ${report.fixtures} test fixture doc(s) not checked.`)]
120
+ : []),
121
+ ];
122
+ }
123
+ if (report.status === 'skipped') {
124
+ return [text(`[skip] Docs not type-checked: ${report.reason}`)];
125
+ }
126
+ const blocks = [
127
+ section(
128
+ `Checking ${report.docs} doc(s) against the authoring types${report.typescript ? ` (TypeScript ${report.typescript})` : ''}`,
129
+ ),
130
+ ];
131
+ if (report.findings.length > 0) {
132
+ const files = new Set(report.findings.map(f => f.doc)).size;
133
+ blocks.push(
134
+ records(report.findings, {
135
+ fields: [
136
+ 'severity',
137
+ 'doc',
138
+ 'line',
139
+ 'export',
140
+ 'type',
141
+ 'field',
142
+ 'problem',
143
+ 'message',
144
+ 'fix',
145
+ ],
146
+ format: {severity: statusToken},
147
+ }),
148
+ text(
149
+ `${report.findings.length} doc field(s) in ${files} file(s) do not match the authoring types. ` +
150
+ 'These are warnings; `astryx docs authoring` lists every field.',
151
+ ),
152
+ );
153
+ }
154
+ if (report.unchecked.length > 0) {
155
+ blocks.push(
156
+ text(`[info] Not type-checked: ${report.unchecked.length} doc file(s):`),
157
+ records(report.unchecked, {fields: ['doc', 'reason']}),
158
+ );
159
+ }
160
+ if (report.fixtures) {
161
+ blocks.push(
162
+ text(`[info] ${report.fixtures} test fixture doc(s) not checked.`),
163
+ );
164
+ }
165
+ if (report.findings.length === 0 && report.docs > 0) {
166
+ // After a failed check an [ok] reads as a pass, but the result still shows.
167
+ blocks.push(
168
+ text(
169
+ failed
170
+ ? `[info] No doc field differs from its authoring type (${report.docs} checked).`
171
+ : report.unchecked.length > 0
172
+ ? `[ok] Every doc that was checked matches its authoring type (${report.docs} checked).`
173
+ : `[ok] Every doc matches its authoring type (${report.docs} checked).`,
174
+ ),
175
+ );
176
+ }
177
+ return blocks;
178
+ }
179
+
101
180
  /**
102
181
  * @param {import('../../../api/integration/validate-integration.type.mjs').ValidateIntegrationResponse['data']} data
103
182
  */
@@ -119,6 +198,7 @@ function printIntegrationValidation(data) {
119
198
  `Summary: ${errors} error${errors === 1 ? '' : 's'}, ` +
120
199
  `${warnings} warning${warnings === 1 ? '' : 's'}`,
121
200
  ),
201
+ ...docTypesBlocks(data.docTypes, errors > 0),
122
202
  );
123
203
  }
124
204
 
@@ -178,6 +258,7 @@ function printTemplateConflicts(data) {
178
258
  * visible line when it was skipped, the findings (all warnings), the
179
259
  * components it could not read, and how many were compared with which
180
260
  * TypeScript, as an [ok] when nothing differs (never after a failed check).
261
+ * Then how many documented prop types were judged and which were not.
181
262
  * @param {import('../../../api/integration/authoring-checks.type.mjs').PropDriftReport | undefined} report
182
263
  * @param {boolean} failed
183
264
  */
@@ -243,6 +324,24 @@ function propDriftBlocks(report, failed) {
243
324
  ),
244
325
  );
245
326
  }
327
+ if (report.typesSkipped) {
328
+ blocks.push(text(`[skip] Prop types not judged: ${report.typesSkipped}.`));
329
+ return blocks;
330
+ }
331
+ const notCompared = report.typesNotCompared.flatMap(entry => entry.props);
332
+ blocks.push(
333
+ text(
334
+ `[info] Documented prop types judged: ${report.typesCompared}; not compared: ${notCompared.length}. ` +
335
+ 'A type is judged when the doc writes it the same as the source (exactly, if it contains `readonly`), or when both are primitives, literals, arrays and unions of those, or names for them.',
336
+ ),
337
+ );
338
+ if (notCompared.length > 0) {
339
+ blocks.push(
340
+ records(report.typesNotCompared, {
341
+ fields: ['component', 'doc', 'reason', 'props'],
342
+ }),
343
+ );
344
+ }
246
345
  return blocks;
247
346
  }
248
347
 
@@ -316,6 +415,23 @@ function printDocConflicts(data) {
316
415
  emit(...output);
317
416
  }
318
417
 
418
+ /**
419
+ * Print one `doctor integration` check's result as its own subcommand prints
420
+ * it. `astryx integration check` prints each check it runs through this.
421
+ * @param {import('../../../api/integration/integration-check.type.mjs').IntegrationCheckResponse} response
422
+ */
423
+ export function printIntegrationCheckResponse(response) {
424
+ if (response.type === 'integration.validate') {
425
+ printIntegrationValidation(response.data);
426
+ } else if (response.type === 'integration.component-conflicts') {
427
+ printComponentConflicts(response.data);
428
+ } else if (response.type === 'integration.template-conflicts') {
429
+ printTemplateConflicts(response.data);
430
+ } else {
431
+ printDocConflicts(response.data);
432
+ }
433
+ }
434
+
319
435
  /** @param {import('commander').Command} program */
320
436
  async function runProjectDoctor(program) {
321
437
  const report = await runChecks();