xdrs-core 0.33.0 → 0.35.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
@@ -588,7 +588,7 @@ test('allows index.md at scope level', () => {
588
588
  test('reports scope index missing link to type index', () => {
589
589
  const workspaceRoot = createWorkspace('scope-index-missing-type-link', {
590
590
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
591
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team scope for tests.\napply-to: Test team only\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\nNo type links here.\n',
591
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team scope for tests.\napply-to: Test team only\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\nNo type links here.\n',
592
592
  '.xdrs/myteam/adrs/index.md': teamAdrIndex([
593
593
  '- [001-team](principles/001-team.md) - Team decision'
594
594
  ]),
@@ -620,7 +620,7 @@ test('passes when scope index links to all type indexes', () => {
620
620
  test('reports broken link in scope index', () => {
621
621
  const workspaceRoot = createWorkspace('scope-index-broken-link', {
622
622
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
623
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team scope for tests.\napply-to: Test team only\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\nTeam scope for tests.\n\n[ADRs](adrs/index.md)\n[Missing](missing/index.md)\n',
623
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team scope for tests.\napply-to: Test team only\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\nTeam scope for tests.\n\n[ADRs](adrs/index.md)\n[Missing](missing/index.md)\n',
624
624
  '.xdrs/myteam/adrs/index.md': teamAdrIndex([
625
625
  '- [001-team](principles/001-team.md) - Team decision'
626
626
  ]),
@@ -683,22 +683,24 @@ test('reports missing scope-type field in scope index frontmatter', () => {
683
683
 
684
684
  test('reports invalid scope-type value in scope index frontmatter', () => {
685
685
  const workspaceRoot = createWorkspace('scope-type-invalid-value', {
686
- '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
687
- '.xdrs/myteam/index.md': '---\nscope-type: invalid-type\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
686
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[_core](_core/index.md)']),
687
+ '.xdrs/myteam/index.md': '---\nscope-type: invalid-type\nname: myteam\ndescription: Team scope for tests.\napply-to: Test team only\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
688
688
  '.xdrs/myteam/adrs/index.md': teamAdrIndex([
689
689
  '- [001-team](principles/001-team.md) - Team decision'
690
690
  ]),
691
691
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
692
+ // _core with only 'core' scope-type — no invalid-type-scope-type policy
693
+ ...coreWithScopeTypes([]),
692
694
  });
693
695
 
694
696
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
695
697
 
696
- expect(result.errors.join('\n')).toContain('scope-type must be one of');
698
+ expect(result.errors.join('\n')).toContain('has no corresponding invalid-type-scope-type policy');
697
699
  expect(result.errors.join('\n')).toContain('myteam/index.md');
698
700
  });
699
701
 
700
702
  test('accepts all valid scope-type values', () => {
701
- const workspaceRoot = createWorkspace('valid-domain-type-check', {
703
+ const workspaceRoot = createWorkspace('valid-standard-type-check', {
702
704
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
703
705
  '.xdrs/myteam/index.md': teamScopeIndex(),
704
706
  '.xdrs/myteam/adrs/index.md': teamAdrIndex([
@@ -709,7 +711,7 @@ test('accepts all valid scope-type values', () => {
709
711
 
710
712
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
711
713
 
712
- expect(result.errors.join('\n')).not.toContain('Scope index scope-type must be one of');
714
+ expect(result.errors.join('\n')).not.toContain('has no corresponding');
713
715
  expect(result.errors.join('\n')).not.toContain('Scope index frontmatter must include');
714
716
  expect(result.errors.join('\n')).not.toContain('Scope index must start with');
715
717
  });
@@ -733,7 +735,7 @@ test('accepts scope-type reference when scope name contains reference', () => {
733
735
 
734
736
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
735
737
 
736
- expect(result.errors.join('\n')).not.toContain('Scope index scope-type must be one of');
738
+ expect(result.errors.join('\n')).not.toContain('has no corresponding');
737
739
  expect(result.errors.join('\n')).not.toContain('Scope with type "reference"');
738
740
  expect(result.errors.join('\n')).not.toContain('Scope index frontmatter must include');
739
741
  });
@@ -778,7 +780,7 @@ test('accepts scope-type platform when scope name contains platform', () => {
778
780
 
779
781
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
780
782
 
781
- expect(result.errors.join('\n')).not.toContain('Scope index scope-type must be one of');
783
+ expect(result.errors.join('\n')).not.toContain('has no corresponding');
782
784
  expect(result.errors.join('\n')).not.toContain('Scope with type "platform"');
783
785
  expect(result.errors.join('\n')).not.toContain('Scope index frontmatter must include');
784
786
  });
@@ -854,7 +856,7 @@ test('reports scope-type _local used on non-_local scope', () => {
854
856
  test('reports _local scope with wrong scope-type', () => {
855
857
  const workspaceRoot = createWorkspace('scope-type-local-wrong-type', {
856
858
  '.xdrs/index.md': rootIndex(),
857
- '.xdrs/_local/index.md': '---\nscope-type: domain\nname: _local\ndescription: Local scope for tests.\napply-to: Test workspace only\nvalid-from: 2026-01-01\n---\n\n# _local Scope Overview\n\nLocal scope for tests.\n\n[ADRs](adrs/index.md)\n',
859
+ '.xdrs/_local/index.md': '---\nscope-type: standard\nname: _local\ndescription: Local scope for tests.\napply-to: Test workspace only\nvalid-from: 2026-01-01\n---\n\n# _local Scope Overview\n\nLocal scope for tests.\n\n[ADRs](adrs/index.md)\n',
858
860
  '.xdrs/_local/adrs/index.md': localAdrIndex([
859
861
  '- [001-main](principles/001-main.md) - Main decision'
860
862
  ]),
@@ -866,10 +868,284 @@ test('reports _local scope with wrong scope-type', () => {
866
868
  expect(result.errors.join('\n')).toContain('Scope "_local" must have scope-type "_local"');
867
869
  });
868
870
 
871
+ test('accepts custom scope-type when policy exists in core-type scope', () => {
872
+ const workspaceRoot = createWorkspace('custom-scope-type-valid', {
873
+ '.xdrs/index.md': rootIndex(['[my-custom](my-custom/index.md)']),
874
+ '.xdrs/my-custom/index.md': '---\nscope-type: custom\nname: my-custom\ndescription: Custom scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# my-custom Scope Overview\n\nCustom.\n\n[ADRs](adrs/index.md)\n',
875
+ '.xdrs/my-custom/adrs/index.md': teamAdrIndex([
876
+ '- [001-custom](principles/001-custom.md) - Custom'
877
+ ]),
878
+ '.xdrs/my-custom/adrs/principles/001-custom.md': teamXdrDocument('Custom decision.'),
879
+ ...coreWithScopeTypes(['custom']),
880
+ });
881
+
882
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
883
+
884
+ expect(result.errors.join('\n')).not.toContain('has no corresponding');
885
+ expect(result.errors.join('\n')).not.toContain('Scope index frontmatter');
886
+ });
887
+
888
+ test('reports custom scope-type when no policy exists', () => {
889
+ const workspaceRoot = createWorkspace('custom-scope-type-missing-policy', {
890
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
891
+ '.xdrs/my-scope/index.md': '---\nscope-type: unknown-type\nname: my-scope\ndescription: Unknown type scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# my-scope Scope Overview\n\nUnknown.\n\n[ADRs](adrs/index.md)\n',
892
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
893
+ ...coreWithScopeTypes([]),
894
+ });
895
+
896
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
897
+
898
+ expect(result.errors.join('\n')).toContain('has no corresponding unknown-type-scope-type policy');
899
+ });
900
+
901
+ test('validates parent scope-type exists in inheritance chain', () => {
902
+ const workspaceRoot = createWorkspace('scope-type-inheritance-valid', {
903
+ '.xdrs/index.md': rootIndex(['[my-custom](my-custom/index.md)', '[_core](_core/index.md)']),
904
+ '.xdrs/my-custom/index.md': '---\nscope-type: custom\nname: my-custom\ndescription: Custom scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# my-custom Scope Overview\n\nCustom.\n\n[ADRs](adrs/index.md)\n',
905
+ '.xdrs/my-custom/adrs/index.md': teamAdrIndex([]),
906
+ '.xdrs/_core/index.md': '---\nscope-type: core\nname: _core\ndescription: Core scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# _core\n\n[ADRs](adrs/index.md)\n',
907
+ '.xdrs/_core/adrs/index.md': [
908
+ '# _core ADR Index', '', 'Core ADRs.', '', '## principles', '',
909
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
910
+ '- [002-standard-scope-type](principles/002-standard-scope-type.md) - standard type',
911
+ '- [003-custom-scope-type](principles/003-custom-scope-type.md) - custom type',
912
+ ''
913
+ ].join('\n'),
914
+ '.xdrs/_core/adrs/principles/001-core-scope-type.md': '---\nname: _core-adr-policy-001-core-scope-type\ndescription: core stub\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nStub.\n\n## Decision Outcome\n\nStub.\n',
915
+ '.xdrs/_core/adrs/principles/002-standard-scope-type.md': '---\nname: _core-adr-policy-002-standard-scope-type\ndescription: standard stub\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nStub.\n\n## Decision Outcome\n\nStub.\n',
916
+ '.xdrs/_core/adrs/principles/003-custom-scope-type.md': '---\nname: _core-adr-policy-003-custom-scope-type\ndescription: custom type with parent\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nCustom type.\n\n## Decision Outcome\n\nCustom.\n\n#### 01-parent-scope-type\n\nInstances inherit all rules from the `standard` scope type.\n',
917
+ });
918
+
919
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
920
+
921
+ expect(result.errors.join('\n')).not.toContain('declares parent');
922
+ expect(result.errors.join('\n')).not.toContain('has no corresponding');
923
+ });
924
+
925
+ test('reports error when parent scope-type in chain does not exist', () => {
926
+ const workspaceRoot = createWorkspace('scope-type-inheritance-missing-parent', {
927
+ '.xdrs/index.md': rootIndex(['[my-custom](my-custom/index.md)', '[_core](_core/index.md)']),
928
+ '.xdrs/my-custom/index.md': '---\nscope-type: custom\nname: my-custom\ndescription: Custom scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# my-custom Scope Overview\n\nCustom.\n\n[ADRs](adrs/index.md)\n',
929
+ '.xdrs/my-custom/adrs/index.md': teamAdrIndex([]),
930
+ '.xdrs/_core/index.md': '---\nscope-type: core\nname: _core\ndescription: Core scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# _core\n\n[ADRs](adrs/index.md)\n',
931
+ '.xdrs/_core/adrs/index.md': [
932
+ '# _core ADR Index', '', 'Core ADRs.', '', '## principles', '',
933
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
934
+ '- [002-custom-scope-type](principles/002-custom-scope-type.md) - custom type',
935
+ ''
936
+ ].join('\n'),
937
+ '.xdrs/_core/adrs/principles/001-core-scope-type.md': '---\nname: _core-adr-policy-001-core-scope-type\ndescription: core stub\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nStub.\n\n## Decision Outcome\n\nStub.\n',
938
+ '.xdrs/_core/adrs/principles/002-custom-scope-type.md': '---\nname: _core-adr-policy-002-custom-scope-type\ndescription: custom type with missing parent\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nCustom.\n\n## Decision Outcome\n\nCustom.\n\n#### 01-parent-scope-type\n\nInstances inherit from the `nonexistent-parent` scope type.\n',
939
+ });
940
+
941
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
942
+
943
+ expect(result.errors.join('\n')).toContain('declares parent "nonexistent-parent" but no nonexistent-parent-scope-type policy exists');
944
+ });
945
+
946
+ test('reports cycle in scope-type inheritance chain', () => {
947
+ const workspaceRoot = createWorkspace('scope-type-inheritance-cycle', {
948
+ '.xdrs/index.md': rootIndex(['[my-custom](my-custom/index.md)', '[_core](_core/index.md)']),
949
+ '.xdrs/my-custom/index.md': '---\nscope-type: child\nname: my-custom\ndescription: Custom scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# my-custom Scope Overview\n\nCustom.\n\n[ADRs](adrs/index.md)\n',
950
+ '.xdrs/my-custom/adrs/index.md': teamAdrIndex([]),
951
+ '.xdrs/_core/index.md': '---\nscope-type: core\nname: _core\ndescription: Core scope.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n# _core\n\n[ADRs](adrs/index.md)\n',
952
+ '.xdrs/_core/adrs/index.md': [
953
+ '# _core ADR Index', '', 'Core ADRs.', '', '## principles', '',
954
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
955
+ '- [002-parent-scope-type](principles/002-parent-scope-type.md) - parent type',
956
+ '- [003-child-scope-type](principles/003-child-scope-type.md) - child type',
957
+ ''
958
+ ].join('\n'),
959
+ '.xdrs/_core/adrs/principles/001-core-scope-type.md': '---\nname: _core-adr-policy-001-core-scope-type\ndescription: core stub\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nStub.\n\n## Decision Outcome\n\nStub.\n',
960
+ '.xdrs/_core/adrs/principles/002-parent-scope-type.md': '---\nname: _core-adr-policy-002-parent-scope-type\ndescription: parent type that cycles back to child\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nParent.\n\n## Decision Outcome\n\nParent.\n\n#### 01-parent-scope-type\n\nInstances inherit from the `child` scope type.\n',
961
+ '.xdrs/_core/adrs/principles/003-child-scope-type.md': '---\nname: _core-adr-policy-003-child-scope-type\ndescription: child type that points to parent\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nChild.\n\n## Decision Outcome\n\nChild.\n\n#### 01-parent-scope-type\n\nInstances inherit from the `parent` scope type.\n',
962
+ });
963
+
964
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
965
+
966
+ expect(result.errors.join('\n')).toContain('creates a cycle in the scope-type inheritance chain');
967
+ expect(result.errors.join('\n')).toContain('my-custom/index.md');
968
+ });
969
+
970
+ test('reports scope-type with reserved underscore prefix', () => {
971
+ const workspaceRoot = createWorkspace('scope-type-underscore-prefix', {
972
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
973
+ '.xdrs/myteam/index.md': '---\nscope-type: _custom\nname: myteam\ndescription: Team scope for tests.\napply-to: Test team only\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\nTeam scope for tests.\n\n[ADRs](adrs/index.md)\n',
974
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
975
+ '- [001-team](principles/001-team.md) - Team decision'
976
+ ]),
977
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
978
+ ...coreWithScopeTypes([]),
979
+ });
980
+
981
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
982
+
983
+ expect(result.errors.join('\n')).toContain('uses a reserved "_" prefix');
984
+ expect(result.errors.join('\n')).toContain('myteam/index.md');
985
+ });
986
+
987
+ test('does not report underscore prefix error for valid _local scope type', () => {
988
+ const workspaceRoot = createWorkspace('scope-type-local-no-prefix-error', {
989
+ '.xdrs/index.md': rootIndex(),
990
+ '.xdrs/_local/index.md': localScopeIndex(),
991
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
992
+ '- [001-main](principles/001-main.md) - Main decision'
993
+ ]),
994
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Test body.'),
995
+ });
996
+
997
+ const result = lintWorkspace(workspaceRoot);
998
+
999
+ expect(result.errors.join('\n')).not.toContain('uses a reserved "_" prefix');
1000
+ });
1001
+
1002
+ test('reports follows that references the scope itself', () => {
1003
+ const workspaceRoot = createWorkspace('follows-self-reference', {
1004
+ '.xdrs/index.md': rootIndex(['[myarea-core](myarea-core/index.md)']),
1005
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\nfollows: myarea-core\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1006
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex([
1007
+ '- [001-team](principles/001-team.md) - Team decision'
1008
+ ]),
1009
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1010
+ });
1011
+
1012
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1013
+
1014
+ expect(result.errors.join('\n')).toContain('follows must not reference the scope itself');
1015
+ expect(result.errors.join('\n')).toContain('myarea-core/index.md');
1016
+ });
1017
+
1018
+ test('reports follows that references _core', () => {
1019
+ const workspaceRoot = createWorkspace('follows-core-reference', {
1020
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1021
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: _core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1022
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1023
+ '- [001-team](principles/001-team.md) - Team decision'
1024
+ ]),
1025
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1026
+ });
1027
+
1028
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1029
+
1030
+ expect(result.errors.join('\n')).toContain('follows must not reference "_core" as it is always applied implicitly');
1031
+ expect(result.errors.join('\n')).toContain('myteam/index.md');
1032
+ });
1033
+
1034
+ test('reports follows list that includes _core', () => {
1035
+ const workspaceRoot = createWorkspace('follows-list-includes-core', {
1036
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
1037
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows:\n - myarea-core\n - _core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1038
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1039
+ '- [001-team](principles/001-team.md) - Team decision'
1040
+ ]),
1041
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1042
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1043
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex([
1044
+ '- [001-team](principles/001-team.md) - Team decision'
1045
+ ]),
1046
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1047
+ });
1048
+
1049
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1050
+
1051
+ expect(result.errors.join('\n')).toContain('follows must not reference "_core" as it is always applied implicitly');
1052
+ });
1053
+
1054
+ test('reports duplicate scope-local standards policies in same scope', () => {
1055
+ const workspaceRoot = createWorkspace('duplicate-scope-local-standards', {
1056
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1057
+ '.xdrs/myteam/index.md': teamScopeIndex(),
1058
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1059
+ '- [001-team](principles/001-team.md) - Team decision',
1060
+ '- [002-myteam-core](principles/002-myteam-core.md) - Local standards',
1061
+ '- [003-myteam-core-dup](principles/003-myteam-core-dup.md) - Duplicate local standards',
1062
+ ]),
1063
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1064
+ '.xdrs/myteam/adrs/principles/002-myteam-core.md': [
1065
+ '---',
1066
+ 'name: myteam-adr-policy-002-myteam-core',
1067
+ 'description: Local standards for myteam.',
1068
+ 'apply-to: myteam scope',
1069
+ 'valid-from: 2026-01-01',
1070
+ '---',
1071
+ '',
1072
+ '# myteam-adr-policy-002: myteam local standards',
1073
+ '',
1074
+ '## Context and Problem Statement',
1075
+ '',
1076
+ 'Defines local authoring standards.',
1077
+ '',
1078
+ '## Decision Outcome',
1079
+ '',
1080
+ 'Standard outcome.',
1081
+ ''
1082
+ ].join('\n'),
1083
+ '.xdrs/myteam/adrs/principles/003-myteam-core-dup.md': [
1084
+ '---',
1085
+ 'name: myteam-adr-policy-003-myteam-core',
1086
+ 'description: Duplicate local standards for myteam.',
1087
+ 'apply-to: myteam scope',
1088
+ 'valid-from: 2026-01-01',
1089
+ '---',
1090
+ '',
1091
+ '# myteam-adr-policy-003: myteam local standards duplicate',
1092
+ '',
1093
+ '## Context and Problem Statement',
1094
+ '',
1095
+ 'Duplicate defines local authoring standards.',
1096
+ '',
1097
+ '## Decision Outcome',
1098
+ '',
1099
+ 'Duplicate outcome.',
1100
+ ''
1101
+ ].join('\n'),
1102
+ });
1103
+
1104
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1105
+
1106
+ expect(result.errors.join('\n')).toContain('has more than one scope-local standards policy');
1107
+ expect(result.errors.join('\n')).toContain('myteam-core');
1108
+ });
1109
+
1110
+ test('accepts single scope-local standards policy in a scope', () => {
1111
+ const workspaceRoot = createWorkspace('single-scope-local-standards', {
1112
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1113
+ '.xdrs/myteam/index.md': teamScopeIndex(),
1114
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1115
+ '- [001-team](principles/001-team.md) - Team decision',
1116
+ '- [002-myteam-core](principles/002-myteam-core.md) - Local standards',
1117
+ ]),
1118
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1119
+ '.xdrs/myteam/adrs/principles/002-myteam-core.md': [
1120
+ '---',
1121
+ 'name: myteam-adr-policy-002-myteam-core',
1122
+ 'description: Local standards for myteam.',
1123
+ 'apply-to: myteam scope',
1124
+ 'valid-from: 2026-01-01',
1125
+ '---',
1126
+ '',
1127
+ '# myteam-adr-policy-002: myteam local standards',
1128
+ '',
1129
+ '## Context and Problem Statement',
1130
+ '',
1131
+ 'Defines local authoring standards.',
1132
+ '',
1133
+ '## Decision Outcome',
1134
+ '',
1135
+ 'Standard outcome.',
1136
+ ''
1137
+ ].join('\n'),
1138
+ });
1139
+
1140
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1141
+
1142
+ expect(result.errors.join('\n')).not.toContain('has more than one scope-local standards policy');
1143
+ });
1144
+
869
1145
  test('reports missing name field in scope index frontmatter', () => {
870
1146
  const workspaceRoot = createWorkspace('scope-index-missing-name', {
871
1147
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
872
- '.xdrs/myteam/index.md': '---\nscope-type: domain\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1148
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
873
1149
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
874
1150
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
875
1151
  });
@@ -882,7 +1158,7 @@ test('reports missing name field in scope index frontmatter', () => {
882
1158
  test('reports name mismatch in scope index frontmatter', () => {
883
1159
  const workspaceRoot = createWorkspace('scope-index-name-mismatch', {
884
1160
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
885
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: wrong-name\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1161
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: wrong-name\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
886
1162
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
887
1163
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
888
1164
  });
@@ -895,7 +1171,7 @@ test('reports name mismatch in scope index frontmatter', () => {
895
1171
  test('reports missing description field in scope index frontmatter', () => {
896
1172
  const workspaceRoot = createWorkspace('scope-index-missing-description', {
897
1173
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
898
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1174
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
899
1175
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
900
1176
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
901
1177
  });
@@ -908,7 +1184,7 @@ test('reports missing description field in scope index frontmatter', () => {
908
1184
  test('reports missing apply-to field in scope index frontmatter', () => {
909
1185
  const workspaceRoot = createWorkspace('scope-index-missing-apply-to', {
910
1186
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
911
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1187
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
912
1188
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
913
1189
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
914
1190
  });
@@ -921,7 +1197,7 @@ test('reports missing apply-to field in scope index frontmatter', () => {
921
1197
  test('reports missing valid-from field in scope index frontmatter', () => {
922
1198
  const workspaceRoot = createWorkspace('scope-index-missing-valid-from', {
923
1199
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
924
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1200
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
925
1201
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
926
1202
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
927
1203
  });
@@ -934,7 +1210,7 @@ test('reports missing valid-from field in scope index frontmatter', () => {
934
1210
  test('reports invalid valid-from date in scope index frontmatter', () => {
935
1211
  const workspaceRoot = createWorkspace('scope-index-invalid-valid-from', {
936
1212
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
937
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: not-a-date\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1213
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: not-a-date\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
938
1214
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
939
1215
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
940
1216
  });
@@ -947,7 +1223,7 @@ test('reports invalid valid-from date in scope index frontmatter', () => {
947
1223
  test('reports unknown field in scope index frontmatter', () => {
948
1224
  const workspaceRoot = createWorkspace('scope-index-unknown-field', {
949
1225
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
950
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nunknown-field: value\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1226
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nunknown-field: value\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
951
1227
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
952
1228
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
953
1229
  });
@@ -960,7 +1236,7 @@ test('reports unknown field in scope index frontmatter', () => {
960
1236
  test('accepts all valid scope index frontmatter fields', () => {
961
1237
  const workspaceRoot = createWorkspace('valid-all-scope-fields', {
962
1238
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
963
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nlicense: MIT\nmetadata:\n owner: team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1239
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nlicense: MIT\nmetadata:\n owner: team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
964
1240
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
965
1241
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
966
1242
  });
@@ -973,7 +1249,7 @@ test('accepts all valid scope index frontmatter fields', () => {
973
1249
  test('accepts follows as a list of core scope names in scope index frontmatter', () => {
974
1250
  const workspaceRoot = createWorkspace('valid-scope-follows-list', {
975
1251
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)', '[shared-standards-core](shared-standards-core/index.md)']),
976
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows:\n - myarea-core\n - shared-standards-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1252
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows:\n - myarea-core\n - shared-standards-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
977
1253
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
978
1254
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
979
1255
  '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
@@ -992,7 +1268,7 @@ test('accepts follows as a list of core scope names in scope index frontmatter',
992
1268
  test('accepts follows as a single core scope name string in scope index frontmatter', () => {
993
1269
  const workspaceRoot = createWorkspace('valid-scope-follows-string', {
994
1270
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
995
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1271
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
996
1272
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
997
1273
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
998
1274
  '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
@@ -1005,10 +1281,29 @@ test('accepts follows as a single core scope name string in scope index frontmat
1005
1281
  expect(result.errors.join('\n')).not.toContain('Scope index frontmatter follows');
1006
1282
  });
1007
1283
 
1284
+ test('accepts follows as a comma-separated list of core scope names in scope index frontmatter', () => {
1285
+ const workspaceRoot = createWorkspace('valid-scope-follows-comma', {
1286
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)', '[shared-standards-core](shared-standards-core/index.md)']),
1287
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core, shared-standards-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1288
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1289
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1290
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1291
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1292
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1293
+ '.xdrs/shared-standards-core/index.md': '---\nscope-type: core\nname: shared-standards-core\ndescription: Shared standards for all areas.\napply-to: All scopes\nvalid-from: 2026-01-01\n---\n\n# shared-standards-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1294
+ '.xdrs/shared-standards-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1295
+ '.xdrs/shared-standards-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1296
+ });
1297
+
1298
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1299
+
1300
+ expect(result.errors.join('\n')).not.toContain('Scope index frontmatter follows');
1301
+ });
1302
+
1008
1303
  test('reports invalid follows value in scope index frontmatter', () => {
1009
1304
  const workspaceRoot = createWorkspace('invalid-scope-follows', {
1010
1305
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1011
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: 123\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1306
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: 123\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1012
1307
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1013
1308
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1014
1309
  });
@@ -1021,7 +1316,7 @@ test('reports invalid follows value in scope index frontmatter', () => {
1021
1316
  test('reports follows scope that does not exist in workspace', () => {
1022
1317
  const workspaceRoot = createWorkspace('follows-missing-scope', {
1023
1318
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1024
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: nonexistent-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1319
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: nonexistent-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1025
1320
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1026
1321
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1027
1322
  });
@@ -1034,7 +1329,7 @@ test('reports follows scope that does not exist in workspace', () => {
1034
1329
  test('accepts follows scope that exists in workspace', () => {
1035
1330
  const workspaceRoot = createWorkspace('follows-existing-scope', {
1036
1331
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
1037
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1332
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1038
1333
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1039
1334
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1040
1335
  '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
@@ -1047,16 +1342,58 @@ test('accepts follows scope that exists in workspace', () => {
1047
1342
  expect(result.errors.join('\n')).not.toContain('follows references scope');
1048
1343
  });
1049
1344
 
1345
+ test('reports follows scope that is not core-type', () => {
1346
+ const workspaceRoot = createWorkspace('follows-non-core-scope', {
1347
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-domain](myarea-domain/index.md)']),
1348
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-domain\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1349
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1350
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1351
+ '.xdrs/myarea-domain/index.md': '---\nscope-type: standard\nname: myarea-domain\ndescription: Domain scope, not a core scope.\napply-to: All domain teams\nvalid-from: 2026-01-01\n---\n\n# myarea-domain Scope Overview\n\n[ADRs](adrs/index.md)\n',
1352
+ '.xdrs/myarea-domain/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1353
+ '.xdrs/myarea-domain/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1354
+ });
1355
+
1356
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1357
+
1358
+ expect(result.errors.join('\n')).toContain('follows references scope "myarea-domain" which is not a core-type scope');
1359
+ });
1360
+
1361
+ test('reports scope index apply-to exceeding 30 words', () => {
1362
+ const workspaceRoot = createWorkspace('scope-apply-to-too-long', {
1363
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1364
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: All backend frontend mobile data and infrastructure teams in the organization that build deploy operate or maintain services on the shared platform including external contractors working on integration projects and testing\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1365
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1366
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1367
+ });
1368
+
1369
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1370
+
1371
+ expect(result.errors.join('\n')).toContain('Scope index frontmatter apply-to must be 30 words or fewer');
1372
+ });
1373
+
1374
+ test('reports scope index description exceeding 40 words', () => {
1375
+ const workspaceRoot = createWorkspace('scope-description-too-long', {
1376
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1377
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: This scope description is intentionally too long in order to verify that the linter correctly reports an error when the description field of a scope index frontmatter contains more than forty words in total which is supposed to exceed the enforced word count limit for descriptions\napply-to: Test team\nvalid-from: 2026-01-01\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1378
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1379
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1380
+ });
1381
+
1382
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1383
+
1384
+ expect(result.errors.join('\n')).toContain('Scope index frontmatter description must be 40 words or fewer');
1385
+ });
1386
+
1050
1387
  test('accepts related-scopes as a list of scope names in scope index frontmatter', () => {
1051
1388
  const workspaceRoot = createWorkspace('valid-scope-related-scopes-list', {
1052
1389
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)', '[parent-domain](parent-domain/index.md)']),
1053
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes:\n - sibling-team\n - parent-domain\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1390
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes:\n - sibling-team\n - parent-domain\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1054
1391
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1055
1392
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1056
- '.xdrs/sibling-team/index.md': '---\nscope-type: domain\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1393
+ '.xdrs/sibling-team/index.md': '---\nscope-type: standard\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1057
1394
  '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1058
1395
  '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1059
- '.xdrs/parent-domain/index.md': '---\nscope-type: domain\nname: parent-domain\ndescription: Parent domain.\napply-to: Parent domain\nvalid-from: 2026-01-01\n---\n\n# parent-domain Scope Overview\n\n[ADRs](adrs/index.md)\n',
1396
+ '.xdrs/parent-domain/index.md': '---\nscope-type: standard\nname: parent-domain\ndescription: Parent domain.\napply-to: Parent domain\nvalid-from: 2026-01-01\n---\n\n# parent-domain Scope Overview\n\n[ADRs](adrs/index.md)\n',
1060
1397
  '.xdrs/parent-domain/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1061
1398
  '.xdrs/parent-domain/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1062
1399
  });
@@ -1069,10 +1406,10 @@ test('accepts related-scopes as a list of scope names in scope index frontmatter
1069
1406
  test('accepts related-scopes as a single scope name string in scope index frontmatter', () => {
1070
1407
  const workspaceRoot = createWorkspace('valid-scope-related-scopes-string', {
1071
1408
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)']),
1072
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: sibling-team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1409
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: sibling-team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1073
1410
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1074
1411
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1075
- '.xdrs/sibling-team/index.md': '---\nscope-type: domain\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1412
+ '.xdrs/sibling-team/index.md': '---\nscope-type: standard\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1076
1413
  '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1077
1414
  '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1078
1415
  });
@@ -1085,7 +1422,7 @@ test('accepts related-scopes as a single scope name string in scope index frontm
1085
1422
  test('reports invalid related-scopes value in scope index frontmatter', () => {
1086
1423
  const workspaceRoot = createWorkspace('invalid-scope-related-scopes', {
1087
1424
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1088
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: 123\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1425
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: 123\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1089
1426
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1090
1427
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1091
1428
  });
@@ -1098,7 +1435,7 @@ test('reports invalid related-scopes value in scope index frontmatter', () => {
1098
1435
  test('reports related-scopes scope that does not exist in workspace', () => {
1099
1436
  const workspaceRoot = createWorkspace('related-scopes-missing-scope', {
1100
1437
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1101
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: nonexistent-scope\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1438
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: nonexistent-scope\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1102
1439
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1103
1440
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1104
1441
  });
@@ -1111,10 +1448,10 @@ test('reports related-scopes scope that does not exist in workspace', () => {
1111
1448
  test('accepts related-scopes scope that exists in workspace', () => {
1112
1449
  const workspaceRoot = createWorkspace('related-scopes-existing-scope', {
1113
1450
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)']),
1114
- '.xdrs/myteam/index.md': '---\nscope-type: domain\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: sibling-team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1451
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: sibling-team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1115
1452
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1116
1453
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1117
- '.xdrs/sibling-team/index.md': '---\nscope-type: domain\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1454
+ '.xdrs/sibling-team/index.md': '---\nscope-type: standard\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1118
1455
  '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1119
1456
  '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1120
1457
  });
@@ -1124,6 +1461,83 @@ test('accepts related-scopes scope that exists in workspace', () => {
1124
1461
  expect(result.errors.join('\n')).not.toContain('related-scopes references scope "sibling-team"');
1125
1462
  });
1126
1463
 
1464
+ test('reports duplicate entries in follows list', () => {
1465
+ const workspaceRoot = createWorkspace('follows-duplicate-entries', {
1466
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
1467
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows:\n - myarea-core\n - myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1468
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1469
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1470
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1471
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1472
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1473
+ });
1474
+
1475
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1476
+
1477
+ expect(result.errors.join('\n')).toContain('follows has duplicate entry "myarea-core"');
1478
+ });
1479
+
1480
+ test('reports duplicate entries in follows comma-separated list', () => {
1481
+ const workspaceRoot = createWorkspace('follows-comma-duplicate-entries', {
1482
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
1483
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core, myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1484
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1485
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1486
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1487
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1488
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1489
+ });
1490
+
1491
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1492
+
1493
+ expect(result.errors.join('\n')).toContain('follows has duplicate entry "myarea-core"');
1494
+ });
1495
+
1496
+ test('reports related-scopes that references the scope itself', () => {
1497
+ const workspaceRoot = createWorkspace('related-scopes-self-reference', {
1498
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1499
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes: myteam\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1500
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1501
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1502
+ });
1503
+
1504
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1505
+
1506
+ expect(result.errors.join('\n')).toContain('related-scopes must not reference the scope itself');
1507
+ });
1508
+
1509
+ test('reports duplicate entries in related-scopes list', () => {
1510
+ const workspaceRoot = createWorkspace('related-scopes-duplicate-entries', {
1511
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)']),
1512
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes:\n - sibling-team\n - sibling-team\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1513
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1514
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1515
+ '.xdrs/sibling-team/index.md': '---\nscope-type: standard\nname: sibling-team\ndescription: Sibling team.\napply-to: Sibling team\nvalid-from: 2026-01-01\n---\n\n# sibling-team Scope Overview\n\n[ADRs](adrs/index.md)\n',
1516
+ '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1517
+ '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1518
+ });
1519
+
1520
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1521
+
1522
+ expect(result.errors.join('\n')).toContain('related-scopes has duplicate entry "sibling-team"');
1523
+ });
1524
+
1525
+ test('reports related-scopes entry that is already declared in follows', () => {
1526
+ const workspaceRoot = createWorkspace('related-scopes-follows-overlap', {
1527
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
1528
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\nrelated-scopes: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
1529
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1530
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1531
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance for myarea.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1532
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1533
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1534
+ });
1535
+
1536
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1537
+
1538
+ expect(result.errors.join('\n')).toContain('related-scopes must not repeat scope "myarea-core" already declared in follows');
1539
+ });
1540
+
1127
1541
  test('reports orphan asset files not referenced by any document', () => {
1128
1542
  const workspaceRoot = createWorkspace('orphan-asset', {
1129
1543
  '.xdrs/index.md': rootIndex(),
@@ -1248,10 +1662,72 @@ function localScopeIndex() {
1248
1662
  ].join('\n');
1249
1663
  }
1250
1664
 
1665
+ /**
1666
+ * Returns an object of file paths → content for a minimal `_core` scope containing
1667
+ * stub scope-type definition policies for each listed type name.
1668
+ * Always includes 'core' so _core can validate itself.
1669
+ */
1670
+ function coreWithScopeTypes(types) {
1671
+ const allTypes = Array.from(new Set(['core', ...types]));
1672
+ const files = {};
1673
+ const policyEntries = allTypes.map((t, i) => {
1674
+ const num = String(i + 1).padStart(3, '0');
1675
+ // _local uses 'local' in filename (no underscore allowed in NUMBERED_FILE_RE)
1676
+ const fileName = t === '_local' ? `${num}-local-scope-type.md` : `${num}-${t}-scope-type.md`;
1677
+ files[`.xdrs/_core/adrs/principles/${fileName}`] = [
1678
+ '---',
1679
+ `name: _core-adr-policy-${num}-${t}-scope-type`,
1680
+ `description: Defines the ${t} scope type.`,
1681
+ 'apply-to: All XDRS scopes',
1682
+ 'valid-from: 2026-01-01',
1683
+ '---',
1684
+ '',
1685
+ `# _core-adr-policy-${num}: ${t} scope type`,
1686
+ '',
1687
+ '## Context and Problem Statement',
1688
+ '',
1689
+ `Defines the ${t} scope type.`,
1690
+ '',
1691
+ '## Decision Outcome',
1692
+ '',
1693
+ `Use scope-type: ${t} when appropriate.`,
1694
+ ''
1695
+ ].join('\n');
1696
+ return `- [${num}-${t}-scope-type](principles/${fileName}) - ${t} scope type`;
1697
+ });
1698
+ files['.xdrs/_core/adrs/index.md'] = [
1699
+ '# _core ADR Index',
1700
+ '',
1701
+ 'Core ADRs for tests.',
1702
+ '',
1703
+ '## principles',
1704
+ '',
1705
+ ...policyEntries,
1706
+ ''
1707
+ ].join('\n');
1708
+ files['.xdrs/_core/index.md'] = [
1709
+ '---',
1710
+ 'scope-type: core',
1711
+ 'name: _core',
1712
+ 'description: Core framework scope for tests.',
1713
+ 'apply-to: All XDRS scopes',
1714
+ 'valid-from: 2026-01-01',
1715
+ '---',
1716
+ '',
1717
+ '# _core Scope Overview',
1718
+ '',
1719
+ 'Framework scope for tests.',
1720
+ '',
1721
+ '[ADRs](adrs/index.md)',
1722
+ ''
1723
+ ].join('\n');
1724
+ return files;
1725
+ }
1726
+
1251
1727
  function teamScopeIndex() {
1252
1728
  return [
1253
1729
  '---',
1254
- 'scope-type: domain',
1730
+ 'scope-type: standard',
1255
1731
  'name: myteam',
1256
1732
  'description: Team scope for tests.',
1257
1733
  'apply-to: Test team only',
@@ -1304,6 +1780,24 @@ function createWorkspace(name, files) {
1304
1780
  const workspaceRoot = path.join(tmpRoot, name);
1305
1781
  fs.mkdirSync(workspaceRoot, { recursive: true });
1306
1782
 
1783
+ // Detect all scope-type values used in the files being created.
1784
+ // If any non-_local scope types are present and _core is not already included,
1785
+ // auto-inject a minimal _core scope with the required scope-type policy stubs.
1786
+ const scopeTypeValues = new Set();
1787
+ for (const content of Object.values(files)) {
1788
+ if (typeof content !== 'string') continue;
1789
+ const matches = content.matchAll(/^scope-type:\s*(.+)$/gm);
1790
+ for (const m of matches) {
1791
+ const t = m[1].trim();
1792
+ if (t !== '_local') scopeTypeValues.add(t);
1793
+ }
1794
+ }
1795
+ const hasCoreScope = Object.keys(files).some((p) => p.includes('/_core/'));
1796
+ if (scopeTypeValues.size > 0 && !hasCoreScope) {
1797
+ const coreFiles = coreWithScopeTypes([...scopeTypeValues]);
1798
+ Object.assign(files, coreFiles);
1799
+ }
1800
+
1307
1801
  for (const [relativePath, content] of Object.entries(files)) {
1308
1802
  const filePath = path.join(workspaceRoot, relativePath);
1309
1803
  fs.mkdirSync(path.dirname(filePath), { recursive: true });