xdrs-core 0.49.1 → 0.51.0

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.
package/lib/lint.test.js CHANGED
@@ -338,8 +338,6 @@ test('accepts all known frontmatter fields in SKILL.md files', () => {
338
338
  'license: MIT',
339
339
  'metadata:',
340
340
  ' version: "1.0"',
341
- 'compatibility: node >= 18',
342
- 'allowed-tools: read_file grep_search',
343
341
  '---',
344
342
  '',
345
343
  '## Overview',
@@ -358,6 +356,553 @@ test('accepts all known frontmatter fields in SKILL.md files', () => {
358
356
  expect(result.errors.join('\n')).not.toContain('unknown field');
359
357
  });
360
358
 
359
+ test('reports missing ### Inputs subsection in SKILL.md ## Overview', () => {
360
+ const workspaceRoot = createWorkspace('skill-missing-inputs', {
361
+ '.xdrs/index.md': rootIndex(),
362
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
363
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
364
+ ]),
365
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitInputs: true }),
366
+ });
367
+
368
+ const result = lintWorkspace(workspaceRoot);
369
+
370
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" is missing required subsection "### Inputs"');
371
+ });
372
+
373
+ test('reports missing ### Outputs subsection in SKILL.md ## Overview', () => {
374
+ const workspaceRoot = createWorkspace('skill-missing-outputs', {
375
+ '.xdrs/index.md': rootIndex(),
376
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
377
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
378
+ ]),
379
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitOutputs: true }),
380
+ });
381
+
382
+ const result = lintWorkspace(workspaceRoot);
383
+
384
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" is missing required subsection "### Outputs"');
385
+ });
386
+
387
+ test('reports missing ### Halt Conditions subsection in SKILL.md ## Overview', () => {
388
+ const workspaceRoot = createWorkspace('skill-missing-halt-conditions', {
389
+ '.xdrs/index.md': rootIndex(),
390
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
391
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
392
+ ]),
393
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitHaltConditions: true }),
394
+ });
395
+
396
+ const result = lintWorkspace(workspaceRoot);
397
+
398
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" is missing required subsection "### Halt Conditions"');
399
+ });
400
+
401
+ test('reports SKILL.md ### Inputs missing #### Optional subsection', () => {
402
+ const workspaceRoot = createWorkspace('skill-inputs-missing-optional', {
403
+ '.xdrs/index.md': rootIndex(),
404
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
405
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
406
+ ]),
407
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitInputsOptional: true }),
408
+ });
409
+
410
+ const result = lintWorkspace(workspaceRoot);
411
+
412
+ expect(result.errors.join('\n')).toContain('Skill "### Inputs" section is missing "#### Optional"');
413
+ });
414
+
415
+ test('reports SKILL.md ### Inputs subsections out of order', () => {
416
+ const workspaceRoot = createWorkspace('skill-inputs-out-of-order', {
417
+ '.xdrs/index.md': rootIndex(),
418
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
419
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
420
+ ]),
421
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ swapInputsOrder: true }),
422
+ });
423
+
424
+ const result = lintWorkspace(workspaceRoot);
425
+
426
+ expect(result.errors.join('\n')).toContain('Skill "### Inputs" subsections must appear in order (#### Required, #### Optional)');
427
+ });
428
+
429
+ test('reports SKILL.md ### Outputs missing #### Changes subsection', () => {
430
+ const workspaceRoot = createWorkspace('skill-outputs-missing-changes', {
431
+ '.xdrs/index.md': rootIndex(),
432
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
433
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
434
+ ]),
435
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitOutputsChanges: true }),
436
+ });
437
+
438
+ const result = lintWorkspace(workspaceRoot);
439
+
440
+ expect(result.errors.join('\n')).toContain('Skill "### Outputs" section is missing "#### Changes"');
441
+ });
442
+
443
+ test('passes SKILL.md with valid None bullets in Inputs, Outputs, and Halt Conditions', () => {
444
+ const workspaceRoot = createWorkspace('skill-valid-none-bullets', {
445
+ '.xdrs/index.md': rootIndex(),
446
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
447
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
448
+ ]),
449
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
450
+ inputsRequired: '- None',
451
+ inputsOptional: '- None',
452
+ outputsContents: '- None',
453
+ outputsChanges: '- None',
454
+ haltConditions: '- None',
455
+ }),
456
+ });
457
+
458
+ const result = lintWorkspace(workspaceRoot);
459
+
460
+ expect(result.errors.join('\n')).not.toContain('must be under 10 words or a single "None"');
461
+ });
462
+
463
+ test('reports SKILL.md bullet over 10 words in #### Required', () => {
464
+ const workspaceRoot = createWorkspace('skill-required-bullet-too-long', {
465
+ '.xdrs/index.md': rootIndex(),
466
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
467
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
468
+ ]),
469
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
470
+ inputsRequired: '- A file path that is way too long to fit under the ten word limit',
471
+ }),
472
+ });
473
+
474
+ const result = lintWorkspace(workspaceRoot);
475
+
476
+ expect(result.errors.join('\n')).toContain('Skill "### Inputs" -> "#### Required" bullet must be under 10 words');
477
+ });
478
+
479
+ test('passes SKILL.md when both optional sections are omitted', () => {
480
+ const workspaceRoot = createWorkspace('skill-optional-sections-omitted', {
481
+ '.xdrs/index.md': rootIndex(),
482
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
483
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
484
+ ]),
485
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument(),
486
+ });
487
+
488
+ const result = lintWorkspace(workspaceRoot);
489
+
490
+ expect(result.errors.join('\n')).not.toContain('### Runtime Requirements');
491
+ expect(result.errors.join('\n')).not.toContain('### User Interaction');
492
+ });
493
+
494
+ test('reports empty ### Runtime Requirements section in SKILL.md', () => {
495
+ const workspaceRoot = createWorkspace('skill-empty-runtime-requirements', {
496
+ '.xdrs/index.md': rootIndex(),
497
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
498
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
499
+ ]),
500
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ runtimeRequirements: '' }),
501
+ });
502
+
503
+ const result = lintWorkspace(workspaceRoot);
504
+
505
+ expect(result.errors.join('\n')).toContain('Skill section "### Runtime Requirements" must not be empty');
506
+ });
507
+
508
+ test('reports empty ### User Interaction section in SKILL.md', () => {
509
+ const workspaceRoot = createWorkspace('skill-empty-user-interaction', {
510
+ '.xdrs/index.md': rootIndex(),
511
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
512
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
513
+ ]),
514
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ userInteraction: '' }),
515
+ });
516
+
517
+ const result = lintWorkspace(workspaceRoot);
518
+
519
+ expect(result.errors.join('\n')).toContain('Skill section "### User Interaction" must not be empty');
520
+ });
521
+
522
+ test('passes SKILL.md with a long free-form Runtime Requirements bullet', () => {
523
+ const workspaceRoot = createWorkspace('skill-runtime-requirements-free-form', {
524
+ '.xdrs/index.md': rootIndex(),
525
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
526
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
527
+ ]),
528
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
529
+ runtimeRequirements: '- Write access to a directory that is not gitignored and has enough free disk space.',
530
+ }),
531
+ });
532
+
533
+ const result = lintWorkspace(workspaceRoot);
534
+
535
+ expect(result.errors.join('\n')).not.toContain('### Runtime Requirements');
536
+ });
537
+
538
+ test('reports SKILL.md User Interaction bullet over 10 words', () => {
539
+ const workspaceRoot = createWorkspace('skill-user-interaction-bullet-too-long', {
540
+ '.xdrs/index.md': rootIndex(),
541
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
542
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
543
+ ]),
544
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
545
+ userInteraction: '- A clarifying question that is much too long to fit under the ten word limit',
546
+ }),
547
+ });
548
+
549
+ const result = lintWorkspace(workspaceRoot);
550
+
551
+ expect(result.errors.join('\n')).toContain('Skill "### User Interaction" bullet must be under 10 words');
552
+ });
553
+
554
+ test('reports SKILL.md top-level sections out of template order', () => {
555
+ const workspaceRoot = createWorkspace('skill-sections-out-of-order', {
556
+ '.xdrs/index.md': rootIndex(),
557
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
558
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
559
+ ]),
560
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
561
+ '---',
562
+ 'name: check-links',
563
+ 'description: Test skill document',
564
+ '---',
565
+ '',
566
+ '## Overview',
567
+ '',
568
+ 'Test skill overview.',
569
+ '',
570
+ '### Inputs',
571
+ '',
572
+ '#### Required',
573
+ '- None',
574
+ '',
575
+ '#### Optional',
576
+ '- None',
577
+ '',
578
+ '### Outputs',
579
+ '',
580
+ '#### Contents',
581
+ '- None',
582
+ '',
583
+ '#### Changes',
584
+ '- None',
585
+ '',
586
+ '### Halt Conditions',
587
+ '- None',
588
+ '',
589
+ '## Anti-Patterns',
590
+ '',
591
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
592
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
593
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
594
+ '',
595
+ '## Instructions',
596
+ '',
597
+ 'Test instructions.',
598
+ ''
599
+ ].join('\n'),
600
+ });
601
+
602
+ const result = lintWorkspace(workspaceRoot);
603
+
604
+ expect(result.errors.join('\n')).toContain('Skill sections must appear in template order');
605
+ });
606
+
607
+ test('reports SKILL.md ## Overview subsections out of template order', () => {
608
+ const workspaceRoot = createWorkspace('skill-overview-subsections-out-of-order', {
609
+ '.xdrs/index.md': rootIndex(),
610
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
611
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
612
+ ]),
613
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
614
+ '---',
615
+ 'name: check-links',
616
+ 'description: Test skill document',
617
+ '---',
618
+ '',
619
+ '## Overview',
620
+ '',
621
+ 'Test skill overview.',
622
+ '',
623
+ '### Halt Conditions',
624
+ '- None',
625
+ '',
626
+ '### Inputs',
627
+ '',
628
+ '#### Required',
629
+ '- None',
630
+ '',
631
+ '#### Optional',
632
+ '- None',
633
+ '',
634
+ '### Outputs',
635
+ '',
636
+ '#### Contents',
637
+ '- None',
638
+ '',
639
+ '#### Changes',
640
+ '- None',
641
+ '',
642
+ '## Instructions',
643
+ '',
644
+ 'Test instructions.',
645
+ '',
646
+ '## Anti-Patterns',
647
+ '',
648
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
649
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
650
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
651
+ ''
652
+ ].join('\n'),
653
+ });
654
+
655
+ const result = lintWorkspace(workspaceRoot);
656
+
657
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" subsections must appear in order');
658
+ });
659
+
660
+ test('reports skill Anti-Patterns section with fewer than 3 entries', () => {
661
+ const workspaceRoot = createWorkspace('skill-anti-patterns-too-few', {
662
+ '.xdrs/index.md': rootIndex(),
663
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
664
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
665
+ ]),
666
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
667
+ '---',
668
+ 'name: check-links',
669
+ 'description: Test skill document',
670
+ '---',
671
+ '',
672
+ '## Overview',
673
+ '',
674
+ 'Test skill overview.',
675
+ '',
676
+ '## Instructions',
677
+ '',
678
+ 'Test instructions.',
679
+ '',
680
+ '## Anti-Patterns',
681
+ '',
682
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
683
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
684
+ ''
685
+ ].join('\n'),
686
+ });
687
+
688
+ const result = lintWorkspace(workspaceRoot);
689
+
690
+ expect(result.errors.join('\n')).toContain('"## Anti-Patterns" section must include at least 3 entries (found 2)');
691
+ });
692
+
693
+ test('passes when skill Anti-Patterns section has at least 3 entries', () => {
694
+ const workspaceRoot = createWorkspace('skill-anti-patterns-enough', {
695
+ '.xdrs/index.md': rootIndex(),
696
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
697
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
698
+ ]),
699
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
700
+ '---',
701
+ 'name: check-links',
702
+ 'description: Test skill document',
703
+ '---',
704
+ '',
705
+ '## Overview',
706
+ '',
707
+ 'Test skill overview.',
708
+ '',
709
+ '## Instructions',
710
+ '',
711
+ 'Test instructions.',
712
+ '',
713
+ '## Anti-Patterns',
714
+ '',
715
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
716
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
717
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
718
+ ''
719
+ ].join('\n'),
720
+ });
721
+
722
+ const result = lintWorkspace(workspaceRoot);
723
+
724
+ expect(result.errors.join('\n')).not.toContain('"## Anti-Patterns" section must include at least');
725
+ });
726
+
727
+ test('reports skill bundling Makefile missing required targets', () => {
728
+ const workspaceRoot = createWorkspace('skill-makefile-missing-targets', {
729
+ '.xdrs/index.md': rootIndex(),
730
+ '.gitignore': 'dist\n',
731
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
732
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
733
+ ]),
734
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
735
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
736
+ 'build:',
737
+ '\techo build',
738
+ ''
739
+ ].join('\n'),
740
+ });
741
+
742
+ const result = lintWorkspace(workspaceRoot);
743
+
744
+ expect(result.errors.join('\n')).toContain('Skill bundling Makefile is missing required "test" target');
745
+ expect(result.errors.join('\n')).toContain('Skill bundling Makefile is missing required "clean" target');
746
+ expect(result.errors.join('\n')).not.toContain('Skill bundling Makefile is missing required "build" target');
747
+ });
748
+
749
+ test('passes when skill bundling Makefile has build, test, and clean targets', () => {
750
+ const workspaceRoot = createWorkspace('skill-makefile-complete', {
751
+ '.xdrs/index.md': rootIndex(),
752
+ '.gitignore': 'dist\n',
753
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
754
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
755
+ ]),
756
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
757
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
758
+ 'build: clean',
759
+ '\techo build',
760
+ 'test:',
761
+ '\techo test',
762
+ 'clean:',
763
+ '\trm -rf dist',
764
+ ''
765
+ ].join('\n'),
766
+ });
767
+
768
+ const result = lintWorkspace(workspaceRoot);
769
+
770
+ expect(result.errors.join('\n')).not.toContain('Skill bundling Makefile is missing required');
771
+ });
772
+
773
+ test('reports skill bundling Makefile build target not depending on clean', () => {
774
+ const workspaceRoot = createWorkspace('skill-makefile-build-no-clean-dep', {
775
+ '.xdrs/index.md': rootIndex(),
776
+ '.gitignore': 'dist\n',
777
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
778
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
779
+ ]),
780
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
781
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
782
+ 'build:',
783
+ '\trm -rf dist',
784
+ '\techo build',
785
+ 'test:',
786
+ '\techo test',
787
+ 'clean:',
788
+ '\trm -rf dist',
789
+ ''
790
+ ].join('\n'),
791
+ });
792
+
793
+ const result = lintWorkspace(workspaceRoot);
794
+
795
+ expect(result.errors.join('\n')).toContain('Skill bundling Makefile\'s "build" target must depend on "clean"');
796
+ });
797
+
798
+ test('passes when skill bundling Makefile build target depends on clean', () => {
799
+ const workspaceRoot = createWorkspace('skill-makefile-build-clean-dep', {
800
+ '.xdrs/index.md': rootIndex(),
801
+ '.gitignore': 'dist\n',
802
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
803
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
804
+ ]),
805
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
806
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
807
+ 'build: clean',
808
+ '\techo build',
809
+ 'test:',
810
+ '\techo test',
811
+ 'clean:',
812
+ '\trm -rf dist',
813
+ ''
814
+ ].join('\n'),
815
+ });
816
+
817
+ const result = lintWorkspace(workspaceRoot);
818
+
819
+ expect(result.errors.join('\n')).not.toContain('must depend on "clean"');
820
+ });
821
+
822
+ test('reports skill bundling Makefile dist output not covered by any .gitignore', () => {
823
+ const workspaceRoot = createWorkspace('skill-makefile-no-gitignore', {
824
+ '.xdrs/index.md': rootIndex(),
825
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
826
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
827
+ ]),
828
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
829
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
830
+ 'build: clean',
831
+ '\techo build',
832
+ 'test:',
833
+ '\techo test',
834
+ 'clean:',
835
+ '\trm -rf dist',
836
+ ''
837
+ ].join('\n'),
838
+ });
839
+
840
+ const result = lintWorkspace(workspaceRoot);
841
+
842
+ expect(result.errors.join('\n')).toContain('is not covered by any .gitignore');
843
+ });
844
+
845
+ test('passes when skill bundling Makefile dist output is covered by a local .gitignore', () => {
846
+ const workspaceRoot = createWorkspace('skill-makefile-local-gitignore', {
847
+ '.xdrs/index.md': rootIndex(),
848
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
849
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
850
+ ]),
851
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
852
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
853
+ 'build: clean',
854
+ '\techo build',
855
+ 'test:',
856
+ '\techo test',
857
+ 'clean:',
858
+ '\trm -rf dist',
859
+ ''
860
+ ].join('\n'),
861
+ '.xdrs/_local/adrs/principles/skills/check-links/.gitignore': 'dist/\n',
862
+ });
863
+
864
+ const result = lintWorkspace(workspaceRoot);
865
+
866
+ expect(result.errors.join('\n')).not.toContain('is not covered by any .gitignore');
867
+ });
868
+
869
+ test('reports scripts/ file missing a shebang or header comment', () => {
870
+ const workspaceRoot = createWorkspace('skill-script-missing-header', {
871
+ '.xdrs/index.md': rootIndex(),
872
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
873
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
874
+ ]),
875
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
876
+ '.xdrs/_local/adrs/principles/skills/check-links/scripts/run.sh': [
877
+ 'echo "no header here"',
878
+ ''
879
+ ].join('\n'),
880
+ });
881
+
882
+ const result = lintWorkspace(workspaceRoot);
883
+
884
+ expect(result.errors.join('\n')).toContain('Script must declare its runtime/interpreter at the top');
885
+ });
886
+
887
+ test('passes when scripts/ file declares a shebang', () => {
888
+ const workspaceRoot = createWorkspace('skill-script-with-shebang', {
889
+ '.xdrs/index.md': rootIndex(),
890
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
891
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
892
+ ]),
893
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
894
+ '.xdrs/_local/adrs/principles/skills/check-links/scripts/run.sh': [
895
+ '#!/bin/bash',
896
+ 'echo "has a shebang"',
897
+ ''
898
+ ].join('\n'),
899
+ });
900
+
901
+ const result = lintWorkspace(workspaceRoot);
902
+
903
+ expect(result.errors.join('\n')).not.toContain('Script must declare its runtime/interpreter at the top');
904
+ });
905
+
361
906
  test('accepts skill package names containing digits, including as a prefix', () => {
362
907
  const workspaceRoot = createWorkspace('skill-name-with-digits', {
363
908
  '.xdrs/index.md': rootIndex(),
@@ -2577,11 +3122,15 @@ function validCompiledMetaPolicy() {
2577
3122
  '',
2578
3123
  '## Sources',
2579
3124
  '',
2580
- '- [web] https://example.com/standards',
3125
+ '- [web] example-standards: https://example.com/standards',
2581
3126
  '',
2582
3127
  '## Selectors',
2583
3128
  '',
2584
3129
  '- Include all requirement sections.',
3130
+ '',
3131
+ '## Sync Settings',
3132
+ '',
3133
+ '- Source re-sync period days: 30',
2585
3134
  ''
2586
3135
  ].join('\n');
2587
3136
  }
@@ -2591,17 +3140,258 @@ test('reports policy in compiled scope missing structured format', () => {
2591
3140
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
2592
3141
  '.xdrs/my-compiled/index.md': compiledScopeIndex(),
2593
3142
  '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
2594
- '- [001-a01](principles/001-a01.md) - A01'
3143
+ '- [001-a01](principles/001-a01.md) - A01'
3144
+ ]),
3145
+ '.xdrs/my-compiled/adrs/principles/001-a01.md': [
3146
+ '---',
3147
+ 'name: my-compiled-adr-policy-001-a01',
3148
+ 'description: Compiled test policy.',
3149
+ 'apply-to: All teams',
3150
+ 'valid-from: 2026-01-01',
3151
+ '---',
3152
+ '',
3153
+ '# my-compiled-adr-policy-001: A01',
3154
+ '',
3155
+ '## Context and Problem Statement',
3156
+ '',
3157
+ 'External source.',
3158
+ '',
3159
+ '## Decision Outcome',
3160
+ '',
3161
+ 'Follow rule.',
3162
+ '',
3163
+ '## Source',
3164
+ '',
3165
+ '- source-1/a01.md',
3166
+ ''
3167
+ ].join('\n'),
3168
+ });
3169
+
3170
+ const result = lintWorkspace(workspaceRoot);
3171
+
3172
+ expect(result.errors.join('\n')).toContain('"### Details"');
3173
+ expect(result.errors.join('\n')).toContain('019.04-structured-format-required');
3174
+ });
3175
+
3176
+ test('reports compiled policy missing ## Source section', () => {
3177
+ const workspaceRoot = createWorkspace('compiled-scope-missing-source', {
3178
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3179
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3180
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3181
+ '- [001-a01](principles/001-a01.md) - A01'
3182
+ ]),
3183
+ '.xdrs/my-compiled/adrs/principles/001-a01.md': [
3184
+ '---',
3185
+ 'name: my-compiled-adr-policy-001-a01',
3186
+ 'description: Compiled test policy.',
3187
+ 'apply-to: All teams',
3188
+ 'valid-from: 2026-01-01',
3189
+ '---',
3190
+ '',
3191
+ '# my-compiled-adr-policy-001: A01',
3192
+ '',
3193
+ '## Context and Problem Statement',
3194
+ '',
3195
+ 'External source.',
3196
+ '',
3197
+ '## Decision Outcome',
3198
+ '',
3199
+ 'Follow rule.',
3200
+ '',
3201
+ '### Details',
3202
+ '',
3203
+ '#### 01-rule',
3204
+ '',
3205
+ 'MUST comply.',
3206
+ ''
3207
+ ].join('\n'),
3208
+ });
3209
+
3210
+ const result = lintWorkspace(workspaceRoot);
3211
+
3212
+ expect(result.errors.join('\n')).toContain('Compiled policy must include a "## Source" section');
3213
+ expect(result.errors.join('\n')).toContain('019.10-source-section');
3214
+ });
3215
+
3216
+ test('reports compiled policy with markdown links in ## Source section', () => {
3217
+ const workspaceRoot = createWorkspace('compiled-scope-source-md-links', {
3218
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3219
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3220
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3221
+ '- [001-a01](principles/001-a01.md) - A01'
3222
+ ]),
3223
+ '.xdrs/my-compiled/adrs/principles/001-a01.md': [
3224
+ '---',
3225
+ 'name: my-compiled-adr-policy-001-a01',
3226
+ 'description: Compiled test policy.',
3227
+ 'apply-to: All teams',
3228
+ 'valid-from: 2026-01-01',
3229
+ '---',
3230
+ '',
3231
+ '# my-compiled-adr-policy-001: A01',
3232
+ '',
3233
+ '## Context and Problem Statement',
3234
+ '',
3235
+ 'External source.',
3236
+ '',
3237
+ '## Decision Outcome',
3238
+ '',
3239
+ 'Follow rule.',
3240
+ '',
3241
+ '### Details',
3242
+ '',
3243
+ '#### 01-rule',
3244
+ '',
3245
+ 'MUST comply.',
3246
+ '',
3247
+ '## Source',
3248
+ '',
3249
+ '- [source-1/a01.md](source-1/a01.md)',
3250
+ ''
3251
+ ].join('\n'),
3252
+ });
3253
+
3254
+ const result = lintWorkspace(workspaceRoot);
3255
+
3256
+ expect(result.errors.join('\n')).toContain('"## Source" section must not use markdown links');
3257
+ expect(result.errors.join('\n')).toContain('019.10-source-section');
3258
+ });
3259
+
3260
+ test('accepts valid compiled policy with ## Source section and no markdown links', () => {
3261
+ const workspaceRoot = createWorkspace('compiled-scope-valid', {
3262
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3263
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3264
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3265
+ '- [001-a01](principles/001-a01.md) - A01'
3266
+ ]),
3267
+ '.xdrs/my-compiled/adrs/principles/001-a01.md': validCompiledPolicy(),
3268
+ });
3269
+
3270
+ const result = lintWorkspace(workspaceRoot);
3271
+
3272
+ expect(result.errors.join('\n')).not.toContain('019');
3273
+ });
3274
+
3275
+ test('reports compiled scope meta-policy missing ## Sources section', () => {
3276
+ const workspaceRoot = createWorkspace('compiled-meta-missing-sources', {
3277
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3278
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3279
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3280
+ '- [001-core](principles/001-core.md) - meta-policy',
3281
+ '- [002-a01](principles/002-a01.md) - A01'
3282
+ ]),
3283
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3284
+ '---',
3285
+ 'name: my-compiled-adr-policy-001-core',
3286
+ 'description: Compilation meta-policy.',
3287
+ 'apply-to: Compilation process',
3288
+ 'valid-from: 2026-01-01',
3289
+ '---',
3290
+ '',
3291
+ '# my-compiled-adr-policy-001: core',
3292
+ '',
3293
+ '## Context and Problem Statement',
3294
+ '',
3295
+ 'Defines compilation config.',
3296
+ '',
3297
+ '## Decision Outcome',
3298
+ '',
3299
+ 'Use configured sources.',
3300
+ '',
3301
+ '### Details',
3302
+ '',
3303
+ '#### 01-rule',
3304
+ '',
3305
+ 'MUST use configured sources.',
3306
+ '',
3307
+ '## Selectors',
3308
+ '',
3309
+ '- Include requirement sections.',
3310
+ ''
3311
+ ].join('\n'),
3312
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3313
+ '---',
3314
+ 'name: my-compiled-adr-policy-002-a01',
3315
+ 'description: Compiled test policy.',
3316
+ 'apply-to: All teams',
3317
+ 'valid-from: 2026-01-01',
3318
+ '---',
3319
+ '',
3320
+ '# my-compiled-adr-policy-002: A01',
3321
+ '',
3322
+ '## Context and Problem Statement',
3323
+ '',
3324
+ 'External source.',
3325
+ '',
3326
+ '## Decision Outcome',
3327
+ '',
3328
+ 'Follow rule.',
3329
+ '',
3330
+ '### Details',
3331
+ '',
3332
+ '#### 01-rule',
3333
+ '',
3334
+ 'MUST comply.',
3335
+ '',
3336
+ '## Source',
3337
+ '',
3338
+ '- source-1/a01.md',
3339
+ ''
3340
+ ].join('\n'),
3341
+ });
3342
+
3343
+ const result = lintWorkspace(workspaceRoot);
3344
+
3345
+ expect(result.errors.join('\n')).toContain('"## Sources" section');
3346
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3347
+ });
3348
+
3349
+ test('reports compiled scope meta-policy missing ## Selectors section', () => {
3350
+ const workspaceRoot = createWorkspace('compiled-meta-missing-selectors', {
3351
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3352
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3353
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3354
+ '- [001-core](principles/001-core.md) - meta-policy',
3355
+ '- [002-a01](principles/002-a01.md) - A01'
2595
3356
  ]),
2596
- '.xdrs/my-compiled/adrs/principles/001-a01.md': [
3357
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
2597
3358
  '---',
2598
- 'name: my-compiled-adr-policy-001-a01',
3359
+ 'name: my-compiled-adr-policy-001-core',
3360
+ 'description: Compilation meta-policy.',
3361
+ 'apply-to: Compilation process',
3362
+ 'valid-from: 2026-01-01',
3363
+ '---',
3364
+ '',
3365
+ '# my-compiled-adr-policy-001: core',
3366
+ '',
3367
+ '## Context and Problem Statement',
3368
+ '',
3369
+ 'Defines compilation config.',
3370
+ '',
3371
+ '## Decision Outcome',
3372
+ '',
3373
+ 'Use configured sources.',
3374
+ '',
3375
+ '### Details',
3376
+ '',
3377
+ '#### 01-rule',
3378
+ '',
3379
+ 'MUST use configured sources.',
3380
+ '',
3381
+ '## Sources',
3382
+ '',
3383
+ '- [web] https://example.com/standards',
3384
+ ''
3385
+ ].join('\n'),
3386
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3387
+ '---',
3388
+ 'name: my-compiled-adr-policy-002-a01',
2599
3389
  'description: Compiled test policy.',
2600
3390
  'apply-to: All teams',
2601
3391
  'valid-from: 2026-01-01',
2602
3392
  '---',
2603
3393
  '',
2604
- '# my-compiled-adr-policy-001: A01',
3394
+ '# my-compiled-adr-policy-002: A01',
2605
3395
  '',
2606
3396
  '## Context and Problem Statement',
2607
3397
  '',
@@ -2611,6 +3401,12 @@ test('reports policy in compiled scope missing structured format', () => {
2611
3401
  '',
2612
3402
  'Follow rule.',
2613
3403
  '',
3404
+ '### Details',
3405
+ '',
3406
+ '#### 01-rule',
3407
+ '',
3408
+ 'MUST comply.',
3409
+ '',
2614
3410
  '## Source',
2615
3411
  '',
2616
3412
  '- source-1/a01.md',
@@ -2620,26 +3416,60 @@ test('reports policy in compiled scope missing structured format', () => {
2620
3416
 
2621
3417
  const result = lintWorkspace(workspaceRoot);
2622
3418
 
2623
- expect(result.errors.join('\n')).toContain('"### Details"');
2624
- expect(result.errors.join('\n')).toContain('019.04-structured-format-required');
3419
+ expect(result.errors.join('\n')).toContain('"## Selectors" section');
3420
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
2625
3421
  });
2626
3422
 
2627
- test('reports compiled policy missing ## Source section', () => {
2628
- const workspaceRoot = createWorkspace('compiled-scope-missing-source', {
3423
+ test('reports compiled scope meta-policy missing ## Sync Settings section', () => {
3424
+ const workspaceRoot = createWorkspace('compiled-meta-missing-sync-settings', {
2629
3425
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
2630
3426
  '.xdrs/my-compiled/index.md': compiledScopeIndex(),
2631
3427
  '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
2632
- '- [001-a01](principles/001-a01.md) - A01'
3428
+ '- [001-core](principles/001-core.md) - meta-policy',
3429
+ '- [002-a01](principles/002-a01.md) - A01'
2633
3430
  ]),
2634
- '.xdrs/my-compiled/adrs/principles/001-a01.md': [
3431
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
2635
3432
  '---',
2636
- 'name: my-compiled-adr-policy-001-a01',
3433
+ 'name: my-compiled-adr-policy-001-core',
3434
+ 'description: Compilation meta-policy.',
3435
+ 'apply-to: Compilation process',
3436
+ 'valid-from: 2026-01-01',
3437
+ '---',
3438
+ '',
3439
+ '# my-compiled-adr-policy-001: core',
3440
+ '',
3441
+ '## Context and Problem Statement',
3442
+ '',
3443
+ 'Defines compilation config.',
3444
+ '',
3445
+ '## Decision Outcome',
3446
+ '',
3447
+ 'Use configured sources.',
3448
+ '',
3449
+ '### Details',
3450
+ '',
3451
+ '#### 01-rule',
3452
+ '',
3453
+ 'MUST use configured sources.',
3454
+ '',
3455
+ '## Sources',
3456
+ '',
3457
+ '- [web] example-standards: https://example.com/standards',
3458
+ '',
3459
+ '## Selectors',
3460
+ '',
3461
+ '- Include requirement sections.',
3462
+ ''
3463
+ ].join('\n'),
3464
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3465
+ '---',
3466
+ 'name: my-compiled-adr-policy-002-a01',
2637
3467
  'description: Compiled test policy.',
2638
3468
  'apply-to: All teams',
2639
3469
  'valid-from: 2026-01-01',
2640
3470
  '---',
2641
3471
  '',
2642
- '# my-compiled-adr-policy-001: A01',
3472
+ '# my-compiled-adr-policy-002: A01',
2643
3473
  '',
2644
3474
  '## Context and Problem Statement',
2645
3475
  '',
@@ -2654,32 +3484,74 @@ test('reports compiled policy missing ## Source section', () => {
2654
3484
  '#### 01-rule',
2655
3485
  '',
2656
3486
  'MUST comply.',
3487
+ '',
3488
+ '## Source',
3489
+ '',
3490
+ '- source-1/a01.md',
2657
3491
  ''
2658
3492
  ].join('\n'),
2659
3493
  });
2660
3494
 
2661
3495
  const result = lintWorkspace(workspaceRoot);
2662
3496
 
2663
- expect(result.errors.join('\n')).toContain('Compiled policy must include a "## Source" section');
2664
- expect(result.errors.join('\n')).toContain('019.10-source-section');
3497
+ expect(result.errors.join('\n')).toContain('"## Sync Settings" section');
3498
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
2665
3499
  });
2666
3500
 
2667
- test('reports compiled policy with markdown links in ## Source section', () => {
2668
- const workspaceRoot = createWorkspace('compiled-scope-source-md-links', {
3501
+ test('reports missing Source re-sync period days in ## Sync Settings', () => {
3502
+ const workspaceRoot = createWorkspace('compiled-meta-missing-resync-period', {
2669
3503
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
2670
3504
  '.xdrs/my-compiled/index.md': compiledScopeIndex(),
2671
3505
  '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
2672
- '- [001-a01](principles/001-a01.md) - A01'
3506
+ '- [001-core](principles/001-core.md) - meta-policy',
3507
+ '- [002-a01](principles/002-a01.md) - A01'
2673
3508
  ]),
2674
- '.xdrs/my-compiled/adrs/principles/001-a01.md': [
3509
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
2675
3510
  '---',
2676
- 'name: my-compiled-adr-policy-001-a01',
3511
+ 'name: my-compiled-adr-policy-001-core',
3512
+ 'description: Compilation meta-policy.',
3513
+ 'apply-to: Compilation process',
3514
+ 'valid-from: 2026-01-01',
3515
+ '---',
3516
+ '',
3517
+ '# my-compiled-adr-policy-001: core',
3518
+ '',
3519
+ '## Context and Problem Statement',
3520
+ '',
3521
+ 'Defines compilation config.',
3522
+ '',
3523
+ '## Decision Outcome',
3524
+ '',
3525
+ 'Use configured sources.',
3526
+ '',
3527
+ '### Details',
3528
+ '',
3529
+ '#### 01-rule',
3530
+ '',
3531
+ 'MUST use configured sources.',
3532
+ '',
3533
+ '## Sources',
3534
+ '',
3535
+ '- [web] example-standards: https://example.com/standards',
3536
+ '',
3537
+ '## Selectors',
3538
+ '',
3539
+ '- Include requirement sections.',
3540
+ '',
3541
+ '## Sync Settings',
3542
+ '',
3543
+ '- Source storage: temporary',
3544
+ ''
3545
+ ].join('\n'),
3546
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3547
+ '---',
3548
+ 'name: my-compiled-adr-policy-002-a01',
2677
3549
  'description: Compiled test policy.',
2678
3550
  'apply-to: All teams',
2679
3551
  'valid-from: 2026-01-01',
2680
3552
  '---',
2681
3553
  '',
2682
- '# my-compiled-adr-policy-001: A01',
3554
+ '# my-compiled-adr-policy-002: A01',
2683
3555
  '',
2684
3556
  '## Context and Problem Statement',
2685
3557
  '',
@@ -2697,34 +3569,19 @@ test('reports compiled policy with markdown links in ## Source section', () => {
2697
3569
  '',
2698
3570
  '## Source',
2699
3571
  '',
2700
- '- [source-1/a01.md](source-1/a01.md)',
3572
+ '- source-1/a01.md',
2701
3573
  ''
2702
3574
  ].join('\n'),
2703
3575
  });
2704
3576
 
2705
3577
  const result = lintWorkspace(workspaceRoot);
2706
3578
 
2707
- expect(result.errors.join('\n')).toContain('"## Source" section must not use markdown links');
2708
- expect(result.errors.join('\n')).toContain('019.10-source-section');
2709
- });
2710
-
2711
- test('accepts valid compiled policy with ## Source section and no markdown links', () => {
2712
- const workspaceRoot = createWorkspace('compiled-scope-valid', {
2713
- '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
2714
- '.xdrs/my-compiled/index.md': compiledScopeIndex(),
2715
- '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
2716
- '- [001-a01](principles/001-a01.md) - A01'
2717
- ]),
2718
- '.xdrs/my-compiled/adrs/principles/001-a01.md': validCompiledPolicy(),
2719
- });
2720
-
2721
- const result = lintWorkspace(workspaceRoot);
2722
-
2723
- expect(result.errors.join('\n')).not.toContain('019');
3579
+ expect(result.errors.join('\n')).toContain('"Source re-sync period days: N" bullet with a positive integer');
3580
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
2724
3581
  });
2725
3582
 
2726
- test('reports compiled scope meta-policy missing ## Sources section', () => {
2727
- const workspaceRoot = createWorkspace('compiled-meta-missing-sources', {
3583
+ test('reports invalid Source storage value in ## Sync Settings', () => {
3584
+ const workspaceRoot = createWorkspace('compiled-meta-invalid-storage-value', {
2728
3585
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
2729
3586
  '.xdrs/my-compiled/index.md': compiledScopeIndex(),
2730
3587
  '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
@@ -2755,9 +3612,18 @@ test('reports compiled scope meta-policy missing ## Sources section', () => {
2755
3612
  '',
2756
3613
  'MUST use configured sources.',
2757
3614
  '',
3615
+ '## Sources',
3616
+ '',
3617
+ '- [web] example-standards: https://example.com/standards',
3618
+ '',
2758
3619
  '## Selectors',
2759
3620
  '',
2760
3621
  '- Include requirement sections.',
3622
+ '',
3623
+ '## Sync Settings',
3624
+ '',
3625
+ '- Source re-sync period days: 30',
3626
+ '- Source storage: permanent',
2761
3627
  ''
2762
3628
  ].join('\n'),
2763
3629
  '.xdrs/my-compiled/adrs/principles/002-a01.md': [
@@ -2793,12 +3659,12 @@ test('reports compiled scope meta-policy missing ## Sources section', () => {
2793
3659
 
2794
3660
  const result = lintWorkspace(workspaceRoot);
2795
3661
 
2796
- expect(result.errors.join('\n')).toContain('"## Sources" section');
3662
+ expect(result.errors.join('\n')).toContain('"Source storage" value must be "temporary" if present');
2797
3663
  expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
2798
3664
  });
2799
3665
 
2800
- test('reports compiled scope meta-policy missing ## Selectors section', () => {
2801
- const workspaceRoot = createWorkspace('compiled-meta-missing-selectors', {
3666
+ test('reports compiled scope ## Sources bullet missing NAME slug', () => {
3667
+ const workspaceRoot = createWorkspace('compiled-meta-sources-missing-name', {
2802
3668
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
2803
3669
  '.xdrs/my-compiled/index.md': compiledScopeIndex(),
2804
3670
  '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
@@ -2832,6 +3698,14 @@ test('reports compiled scope meta-policy missing ## Selectors section', () => {
2832
3698
  '## Sources',
2833
3699
  '',
2834
3700
  '- [web] https://example.com/standards',
3701
+ '',
3702
+ '## Selectors',
3703
+ '',
3704
+ '- Include requirement sections.',
3705
+ '',
3706
+ '## Sync Settings',
3707
+ '',
3708
+ '- Source re-sync period days: 30',
2835
3709
  ''
2836
3710
  ].join('\n'),
2837
3711
  '.xdrs/my-compiled/adrs/principles/002-a01.md': [
@@ -2867,7 +3741,7 @@ test('reports compiled scope meta-policy missing ## Selectors section', () => {
2867
3741
 
2868
3742
  const result = lintWorkspace(workspaceRoot);
2869
3743
 
2870
- expect(result.errors.join('\n')).toContain('"## Selectors" section');
3744
+ expect(result.errors.join('\n')).toContain('must include a NAME slug');
2871
3745
  expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
2872
3746
  });
2873
3747
 
@@ -2931,6 +3805,88 @@ test('skips ## Source check for local meta-policy files in compiled scope', () =
2931
3805
  expect(result.errors.join('\n')).not.toContain('Compiled policy must include a "## Source" section');
2932
3806
  });
2933
3807
 
3808
+ test('does not report orphan asset errors for files under .assets/sources in a compiled scope', () => {
3809
+ const workspaceRoot = createWorkspace('compiled-sources-orphan-exempt', {
3810
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3811
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3812
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3813
+ '- [001-core](principles/001-core.md) - meta-policy'
3814
+ ]),
3815
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3816
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/source.md': [
3817
+ '# Source',
3818
+ '',
3819
+ 'last-fetch-timestamp: 2026-01-01 10:00:00',
3820
+ 'last-compilation-timestamp: 2026-01-01 10:05:00',
3821
+ ''
3822
+ ].join('\n'),
3823
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3824
+ '.xdrs/my-compiled/adrs/principles/.assets/unused.png': Buffer.alloc(0),
3825
+ });
3826
+
3827
+ const result = lintWorkspace(workspaceRoot);
3828
+
3829
+ expect(result.errors.join('\n')).not.toContain('raw-standard.md');
3830
+ expect(result.errors.join('\n')).not.toContain('019.12-source-storage-tracking');
3831
+ expect(result.errors.join('\n')).toContain('Orphan asset file');
3832
+ expect(result.errors.join('\n')).toContain('unused.png');
3833
+ });
3834
+
3835
+ test('reports missing source.md tracking file in a compiled source snapshot folder', () => {
3836
+ const workspaceRoot = createWorkspace('compiled-sources-missing-tracking-file', {
3837
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3838
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3839
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3840
+ '- [001-core](principles/001-core.md) - meta-policy'
3841
+ ]),
3842
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3843
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3844
+ });
3845
+
3846
+ const result = lintWorkspace(workspaceRoot);
3847
+
3848
+ expect(result.errors.join('\n')).toContain('must contain a "source.md" tracking file');
3849
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3850
+ });
3851
+
3852
+ test('reports malformed source.md tracking file content', () => {
3853
+ const workspaceRoot = createWorkspace('compiled-sources-malformed-tracking-file', {
3854
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3855
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3856
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3857
+ '- [001-core](principles/001-core.md) - meta-policy'
3858
+ ]),
3859
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3860
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/source.md': [
3861
+ 'last-fetch-timestamp: not-a-date',
3862
+ ''
3863
+ ].join('\n'),
3864
+ });
3865
+
3866
+ const result = lintWorkspace(workspaceRoot);
3867
+
3868
+ expect(result.errors.join('\n')).toContain('must start with a "# Source" heading');
3869
+ expect(result.errors.join('\n')).toContain('must include a "last-compilation-timestamp:" line');
3870
+ expect(result.errors.join('\n')).toContain('must match "YYYY-MM-DD HH:MM:SS"');
3871
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3872
+ });
3873
+
3874
+ test('reports document linking directly into a compiled-source snapshot path', () => {
3875
+ const workspaceRoot = createWorkspace('source-snapshot-link-forbidden', {
3876
+ '.xdrs/index.md': rootIndex(),
3877
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
3878
+ '- [001-main](principles/001-main.md) - Main decision'
3879
+ ]),
3880
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('See [raw source](.assets/sources/example-standards/raw-standard.md).'),
3881
+ '.xdrs/_local/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3882
+ });
3883
+
3884
+ const result = lintWorkspace(workspaceRoot);
3885
+
3886
+ expect(result.errors.join('\n')).toContain('must not link directly to compiled-source snapshot content');
3887
+ expect(result.errors.join('\n')).toContain('019.14-source-not-authoritative');
3888
+ });
3889
+
2934
3890
  test('enforces compiled scope checks when scope-type is comma-separated with compiled', () => {
2935
3891
  const workspaceRoot = createWorkspace('compiled-multi-type-checks', {
2936
3892
  '.xdrs/index.md': rootIndex(['[my-compiled-scope](my-compiled-scope/index.md)']),
@@ -3226,6 +4182,71 @@ function skillDocument(body) {
3226
4182
  ].join('\n');
3227
4183
  }
3228
4184
 
4185
+ function fullSkillDocument(overrides = {}) {
4186
+ const {
4187
+ omitInputs = false,
4188
+ omitInputsOptional = false,
4189
+ swapInputsOrder = false,
4190
+ inputsRequired = '- A file path to scan',
4191
+ inputsOptional = '- None',
4192
+ runtimeRequirements = null,
4193
+ omitOutputs = false,
4194
+ omitOutputsChanges = false,
4195
+ outputsContents = '- A list of broken links',
4196
+ outputsChanges = '- None',
4197
+ omitHaltConditions = false,
4198
+ haltConditions = '- None',
4199
+ userInteraction = null,
4200
+ } = overrides;
4201
+
4202
+ const lines = ['---', 'name: check-links', 'description: Test skill document', '---', '', '## Overview', '', 'Test skill overview.', ''];
4203
+
4204
+ if (!omitInputs) {
4205
+ lines.push('### Inputs', '');
4206
+ if (omitInputsOptional) {
4207
+ lines.push('#### Required', inputsRequired, '');
4208
+ } else if (swapInputsOrder) {
4209
+ lines.push('#### Optional', inputsOptional, '', '#### Required', inputsRequired, '');
4210
+ } else {
4211
+ lines.push('#### Required', inputsRequired, '', '#### Optional', inputsOptional, '');
4212
+ }
4213
+ }
4214
+
4215
+ if (!omitOutputs) {
4216
+ lines.push('### Outputs', '');
4217
+ if (omitOutputsChanges) {
4218
+ lines.push('#### Contents', outputsContents, '');
4219
+ } else {
4220
+ lines.push('#### Contents', outputsContents, '', '#### Changes', outputsChanges, '');
4221
+ }
4222
+ }
4223
+
4224
+ if (!omitHaltConditions) {
4225
+ lines.push('### Halt Conditions', '', haltConditions, '');
4226
+ }
4227
+
4228
+ if (userInteraction !== null) {
4229
+ lines.push('### User Interaction', '', userInteraction, '');
4230
+ }
4231
+
4232
+ if (runtimeRequirements !== null) {
4233
+ lines.push('### Runtime Requirements', '', runtimeRequirements, '');
4234
+ }
4235
+
4236
+ lines.push('## Instructions', '', 'Test instructions.', '');
4237
+
4238
+ lines.push(
4239
+ '## Anti-Patterns',
4240
+ '',
4241
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
4242
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
4243
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
4244
+ ''
4245
+ );
4246
+
4247
+ return lines.join('\n');
4248
+ }
4249
+
3229
4250
  test('accepts a custom root folder name as the XDRS root when it contains index.md', () => {
3230
4251
  const doc = [
3231
4252
  '---',
@@ -3716,8 +4737,8 @@ test('reports research exceeding 5000 word limit', () => {
3716
4737
  expect(result.errors.join('\n')).toContain('Research exceeds maximum word count of 5000');
3717
4738
  });
3718
4739
 
3719
- test('reports SKILL.md exceeding 6500 word limit', () => {
3720
- const longBody = ('word '.repeat(6501)).trimEnd();
4740
+ test('reports SKILL.md exceeding 7000 word limit', () => {
4741
+ const longBody = ('word '.repeat(7001)).trimEnd();
3721
4742
  const workspaceRoot = createWorkspace('skill-too-many-words', {
3722
4743
  '.xdrs/index.md': rootIndex(),
3723
4744
  '.xdrs/_local/adrs/index.md': localAdrIndex([
@@ -3728,7 +4749,7 @@ test('reports SKILL.md exceeding 6500 word limit', () => {
3728
4749
 
3729
4750
  const result = lintWorkspace(workspaceRoot);
3730
4751
 
3731
- expect(result.errors.join('\n')).toContain('Skill exceeds maximum word count of 6500');
4752
+ expect(result.errors.join('\n')).toContain('Skill exceeds maximum word count of 7000');
3732
4753
  });
3733
4754
 
3734
4755
  // ─── Files outside .assets in skill package root ────────────────────────────