xdrs-core 0.32.0 → 0.34.2

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,85 @@ 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
+
869
946
  test('reports missing name field in scope index frontmatter', () => {
870
947
  const workspaceRoot = createWorkspace('scope-index-missing-name', {
871
948
  '.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',
949
+ '.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
950
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
874
951
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
875
952
  });
@@ -882,7 +959,7 @@ test('reports missing name field in scope index frontmatter', () => {
882
959
  test('reports name mismatch in scope index frontmatter', () => {
883
960
  const workspaceRoot = createWorkspace('scope-index-name-mismatch', {
884
961
  '.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',
962
+ '.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
963
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
887
964
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
888
965
  });
@@ -895,7 +972,7 @@ test('reports name mismatch in scope index frontmatter', () => {
895
972
  test('reports missing description field in scope index frontmatter', () => {
896
973
  const workspaceRoot = createWorkspace('scope-index-missing-description', {
897
974
  '.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',
975
+ '.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
976
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
900
977
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
901
978
  });
@@ -908,7 +985,7 @@ test('reports missing description field in scope index frontmatter', () => {
908
985
  test('reports missing apply-to field in scope index frontmatter', () => {
909
986
  const workspaceRoot = createWorkspace('scope-index-missing-apply-to', {
910
987
  '.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',
988
+ '.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
989
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
913
990
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
914
991
  });
@@ -921,7 +998,7 @@ test('reports missing apply-to field in scope index frontmatter', () => {
921
998
  test('reports missing valid-from field in scope index frontmatter', () => {
922
999
  const workspaceRoot = createWorkspace('scope-index-missing-valid-from', {
923
1000
  '.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',
1001
+ '.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
1002
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
926
1003
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
927
1004
  });
@@ -934,7 +1011,7 @@ test('reports missing valid-from field in scope index frontmatter', () => {
934
1011
  test('reports invalid valid-from date in scope index frontmatter', () => {
935
1012
  const workspaceRoot = createWorkspace('scope-index-invalid-valid-from', {
936
1013
  '.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',
1014
+ '.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
1015
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
939
1016
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
940
1017
  });
@@ -947,7 +1024,7 @@ test('reports invalid valid-from date in scope index frontmatter', () => {
947
1024
  test('reports unknown field in scope index frontmatter', () => {
948
1025
  const workspaceRoot = createWorkspace('scope-index-unknown-field', {
949
1026
  '.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',
1027
+ '.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
1028
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
952
1029
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
953
1030
  });
@@ -960,7 +1037,7 @@ test('reports unknown field in scope index frontmatter', () => {
960
1037
  test('accepts all valid scope index frontmatter fields', () => {
961
1038
  const workspaceRoot = createWorkspace('valid-all-scope-fields', {
962
1039
  '.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',
1040
+ '.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
1041
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
965
1042
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
966
1043
  });
@@ -973,7 +1050,7 @@ test('accepts all valid scope index frontmatter fields', () => {
973
1050
  test('accepts follows as a list of core scope names in scope index frontmatter', () => {
974
1051
  const workspaceRoot = createWorkspace('valid-scope-follows-list', {
975
1052
  '.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',
1053
+ '.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
1054
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
978
1055
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
979
1056
  '.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 +1069,7 @@ test('accepts follows as a list of core scope names in scope index frontmatter',
992
1069
  test('accepts follows as a single core scope name string in scope index frontmatter', () => {
993
1070
  const workspaceRoot = createWorkspace('valid-scope-follows-string', {
994
1071
  '.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',
1072
+ '.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
1073
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
997
1074
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
998
1075
  '.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',
@@ -1008,7 +1085,7 @@ test('accepts follows as a single core scope name string in scope index frontmat
1008
1085
  test('reports invalid follows value in scope index frontmatter', () => {
1009
1086
  const workspaceRoot = createWorkspace('invalid-scope-follows', {
1010
1087
  '.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',
1088
+ '.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
1089
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1013
1090
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1014
1091
  });
@@ -1021,7 +1098,7 @@ test('reports invalid follows value in scope index frontmatter', () => {
1021
1098
  test('reports follows scope that does not exist in workspace', () => {
1022
1099
  const workspaceRoot = createWorkspace('follows-missing-scope', {
1023
1100
  '.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',
1101
+ '.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
1102
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1026
1103
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1027
1104
  });
@@ -1034,7 +1111,7 @@ test('reports follows scope that does not exist in workspace', () => {
1034
1111
  test('accepts follows scope that exists in workspace', () => {
1035
1112
  const workspaceRoot = createWorkspace('follows-existing-scope', {
1036
1113
  '.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',
1114
+ '.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
1115
  '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1039
1116
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1040
1117
  '.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,6 +1124,125 @@ test('accepts follows scope that exists in workspace', () => {
1047
1124
  expect(result.errors.join('\n')).not.toContain('follows references scope');
1048
1125
  });
1049
1126
 
1127
+ test('reports follows scope that is not core-type', () => {
1128
+ const workspaceRoot = createWorkspace('follows-non-core-scope', {
1129
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-domain](myarea-domain/index.md)']),
1130
+ '.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',
1131
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1132
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1133
+ '.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',
1134
+ '.xdrs/myarea-domain/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1135
+ '.xdrs/myarea-domain/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1136
+ });
1137
+
1138
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1139
+
1140
+ expect(result.errors.join('\n')).toContain('follows references scope "myarea-domain" which is not a core-type scope');
1141
+ });
1142
+
1143
+ test('reports scope index apply-to exceeding 30 words', () => {
1144
+ const workspaceRoot = createWorkspace('scope-apply-to-too-long', {
1145
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1146
+ '.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',
1147
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1148
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1149
+ });
1150
+
1151
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1152
+
1153
+ expect(result.errors.join('\n')).toContain('Scope index frontmatter apply-to must be 30 words or fewer');
1154
+ });
1155
+
1156
+ test('reports scope index description exceeding 40 words', () => {
1157
+ const workspaceRoot = createWorkspace('scope-description-too-long', {
1158
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1159
+ '.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',
1160
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1161
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1162
+ });
1163
+
1164
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1165
+
1166
+ expect(result.errors.join('\n')).toContain('Scope index frontmatter description must be 40 words or fewer');
1167
+ });
1168
+
1169
+ test('accepts related-scopes as a list of scope names in scope index frontmatter', () => {
1170
+ const workspaceRoot = createWorkspace('valid-scope-related-scopes-list', {
1171
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)', '[parent-domain](parent-domain/index.md)']),
1172
+ '.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',
1173
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1174
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1175
+ '.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',
1176
+ '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1177
+ '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1178
+ '.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',
1179
+ '.xdrs/parent-domain/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1180
+ '.xdrs/parent-domain/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1181
+ });
1182
+
1183
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1184
+
1185
+ expect(result.errors.join('\n')).not.toContain('Scope index frontmatter related-scopes');
1186
+ });
1187
+
1188
+ test('accepts related-scopes as a single scope name string in scope index frontmatter', () => {
1189
+ const workspaceRoot = createWorkspace('valid-scope-related-scopes-string', {
1190
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)']),
1191
+ '.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',
1192
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1193
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1194
+ '.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',
1195
+ '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1196
+ '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1197
+ });
1198
+
1199
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1200
+
1201
+ expect(result.errors.join('\n')).not.toContain('Scope index frontmatter related-scopes');
1202
+ });
1203
+
1204
+ test('reports invalid related-scopes value in scope index frontmatter', () => {
1205
+ const workspaceRoot = createWorkspace('invalid-scope-related-scopes', {
1206
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1207
+ '.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',
1208
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1209
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1210
+ });
1211
+
1212
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1213
+
1214
+ expect(result.errors.join('\n')).toContain('Scope index frontmatter related-scopes must be a scope name or list of scope names');
1215
+ });
1216
+
1217
+ test('reports related-scopes scope that does not exist in workspace', () => {
1218
+ const workspaceRoot = createWorkspace('related-scopes-missing-scope', {
1219
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1220
+ '.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',
1221
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1222
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1223
+ });
1224
+
1225
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1226
+
1227
+ expect(result.errors.join('\n')).toContain('related-scopes references scope "nonexistent-scope" which does not exist in the workspace');
1228
+ });
1229
+
1230
+ test('accepts related-scopes scope that exists in workspace', () => {
1231
+ const workspaceRoot = createWorkspace('related-scopes-existing-scope', {
1232
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)']),
1233
+ '.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',
1234
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1235
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1236
+ '.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',
1237
+ '.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
1238
+ '.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1239
+ });
1240
+
1241
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1242
+
1243
+ expect(result.errors.join('\n')).not.toContain('related-scopes references scope "sibling-team"');
1244
+ });
1245
+
1050
1246
  test('reports orphan asset files not referenced by any document', () => {
1051
1247
  const workspaceRoot = createWorkspace('orphan-asset', {
1052
1248
  '.xdrs/index.md': rootIndex(),
@@ -1171,10 +1367,72 @@ function localScopeIndex() {
1171
1367
  ].join('\n');
1172
1368
  }
1173
1369
 
1370
+ /**
1371
+ * Returns an object of file paths → content for a minimal `_core` scope containing
1372
+ * stub scope-type definition policies for each listed type name.
1373
+ * Always includes 'core' so _core can validate itself.
1374
+ */
1375
+ function coreWithScopeTypes(types) {
1376
+ const allTypes = Array.from(new Set(['core', ...types]));
1377
+ const files = {};
1378
+ const policyEntries = allTypes.map((t, i) => {
1379
+ const num = String(i + 1).padStart(3, '0');
1380
+ // _local uses 'local' in filename (no underscore allowed in NUMBERED_FILE_RE)
1381
+ const fileName = t === '_local' ? `${num}-local-scope-type.md` : `${num}-${t}-scope-type.md`;
1382
+ files[`.xdrs/_core/adrs/principles/${fileName}`] = [
1383
+ '---',
1384
+ `name: _core-adr-policy-${num}-${t}-scope-type`,
1385
+ `description: Defines the ${t} scope type.`,
1386
+ 'apply-to: All XDRS scopes',
1387
+ 'valid-from: 2026-01-01',
1388
+ '---',
1389
+ '',
1390
+ `# _core-adr-policy-${num}: ${t} scope type`,
1391
+ '',
1392
+ '## Context and Problem Statement',
1393
+ '',
1394
+ `Defines the ${t} scope type.`,
1395
+ '',
1396
+ '## Decision Outcome',
1397
+ '',
1398
+ `Use scope-type: ${t} when appropriate.`,
1399
+ ''
1400
+ ].join('\n');
1401
+ return `- [${num}-${t}-scope-type](principles/${fileName}) - ${t} scope type`;
1402
+ });
1403
+ files['.xdrs/_core/adrs/index.md'] = [
1404
+ '# _core ADR Index',
1405
+ '',
1406
+ 'Core ADRs for tests.',
1407
+ '',
1408
+ '## principles',
1409
+ '',
1410
+ ...policyEntries,
1411
+ ''
1412
+ ].join('\n');
1413
+ files['.xdrs/_core/index.md'] = [
1414
+ '---',
1415
+ 'scope-type: core',
1416
+ 'name: _core',
1417
+ 'description: Core framework scope for tests.',
1418
+ 'apply-to: All XDRS scopes',
1419
+ 'valid-from: 2026-01-01',
1420
+ '---',
1421
+ '',
1422
+ '# _core Scope Overview',
1423
+ '',
1424
+ 'Framework scope for tests.',
1425
+ '',
1426
+ '[ADRs](adrs/index.md)',
1427
+ ''
1428
+ ].join('\n');
1429
+ return files;
1430
+ }
1431
+
1174
1432
  function teamScopeIndex() {
1175
1433
  return [
1176
1434
  '---',
1177
- 'scope-type: domain',
1435
+ 'scope-type: standard',
1178
1436
  'name: myteam',
1179
1437
  'description: Team scope for tests.',
1180
1438
  'apply-to: Test team only',
@@ -1227,6 +1485,24 @@ function createWorkspace(name, files) {
1227
1485
  const workspaceRoot = path.join(tmpRoot, name);
1228
1486
  fs.mkdirSync(workspaceRoot, { recursive: true });
1229
1487
 
1488
+ // Detect all scope-type values used in the files being created.
1489
+ // If any non-_local scope types are present and _core is not already included,
1490
+ // auto-inject a minimal _core scope with the required scope-type policy stubs.
1491
+ const scopeTypeValues = new Set();
1492
+ for (const content of Object.values(files)) {
1493
+ if (typeof content !== 'string') continue;
1494
+ const matches = content.matchAll(/^scope-type:\s*(.+)$/gm);
1495
+ for (const m of matches) {
1496
+ const t = m[1].trim();
1497
+ if (t !== '_local') scopeTypeValues.add(t);
1498
+ }
1499
+ }
1500
+ const hasCoreScope = Object.keys(files).some((p) => p.includes('/_core/'));
1501
+ if (scopeTypeValues.size > 0 && !hasCoreScope) {
1502
+ const coreFiles = coreWithScopeTypes([...scopeTypeValues]);
1503
+ Object.assign(files, coreFiles);
1504
+ }
1505
+
1230
1506
  for (const [relativePath, content] of Object.entries(files)) {
1231
1507
  const filePath = path.join(workspaceRoot, relativePath);
1232
1508
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.32.0",
3
+ "version": "0.34.2",
4
4
  "description": "A framework to structure, compile and distribute Architectural (ADR), Business (BDR), and Engineering (EDR) decision records contents so that AI agents and humans can reliably find and use them with hierarchical scopes and controlled rollout in the format of distributable versioned packages.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,71 +0,0 @@
1
- ---
2
- name: _core-adr-policy-010-core-scope-naming-convention
3
- description: Defines the -core suffix naming convention for XDRS scopes used to hold meta governance content such as writing standards, structural templates, ownership, and process guidance for a domain. Use when designing or evaluating shared scopes.
4
- apply-to: All XDRS scope authors and maintainers
5
- valid-from: 2026-06-24
6
- ---
7
-
8
- # _core-adr-policy-010: Core scope naming convention
9
-
10
- ## Context and Problem Statement
11
-
12
- When teams create shared XDRS scopes (e.g., `security-standards`, `platform-engineering`), they often mix two kinds of content:
13
-
14
- - **Consumable policies**: rules and decisions that other teams or repositories must follow.
15
- - **Meta governance**: writing standards, structural templates, skill guidance, ownership definitions, and process rules that govern how content is authored within the domain.
16
-
17
- Mixing both kinds makes shared scopes harder to consume selectively and causes confusion about which content represents actual rules versus internal governance for scope contributors.
18
-
19
- How should teams separate meta governance from consumable policies within a scope domain?
20
-
21
- ## Decision Outcome
22
-
23
- **Use a `-core` suffix sibling scope to hold all meta governance content for a domain.**
24
-
25
- A scope named `[domain]-core` (e.g., `security-core`, `platform-core`) is reserved for the meta organization of the `[domain]` scope family. It contains content that governs how policies are written and maintained within the domain — not the actual policies themselves.
26
-
27
- ### Details
28
-
29
- #### 01-core-suffix-definition
30
-
31
- A scope name ending in `-core` signals that the scope holds meta governance content for scopes sharing the same prefix. It MUST NOT contain policies intended for direct consumption by other teams or repositories.
32
-
33
- Content that belongs in a `-core` scope:
34
-
35
- - Writing standards and templates specific to the domain.
36
- - Structural conventions for that domain's policies.
37
- - Process guidance for authoring or reviewing policies within the domain.
38
- - Skill files for domain-specific review or authoring workflows.
39
- - Ownership, contact, and governance information for the domain.
40
-
41
- Content that does NOT belong in a `-core` scope:
42
-
43
- - Actual rules or constraints for other teams to follow (e.g., security requirements, compliance rules).
44
- - Any policy intended to be enforced by external teams or repositories.
45
-
46
- #### 02-core-governs-same-prefix-scopes
47
-
48
- All XDRS scopes sharing the same prefix as a `-core` scope (e.g., `security-standards`, `security-cloud`) are governed by the standards defined in the corresponding `-core` scope (e.g., `security-core`). Authors of those scopes MUST follow the `-core` scope standards when writing, reviewing, or extending policies in any same-prefix scope.
49
-
50
- #### 03-core-scope-is-optional-but-recommended
51
-
52
- Creating a `-core` scope is optional. It is strongly recommended whenever a domain scope is intended to be shared with multiple teams or repositories. Separating meta governance into a `-core` scope prevents unnecessary internal content from being distributed to consumers and keeps the consumable scope focused on actual rules.
53
-
54
- #### 04-core-vs-local-scope
55
-
56
- The `-core` suffix is distinct from the `_local` scope:
57
-
58
- - `_local` holds project-specific overrides that are never shared and apply only to the current repository. It is implicitly present in every workspace and may contain a mix of local overrides unrelated to any particular domain.
59
- - A `-core` scope is a formal, named scope scoped to a domain. It can be shared selectively with contributors and is required to be followed by all authors writing content in any same-prefix scope.
60
-
61
- Use `_local` when meta governance is relevant only to the current repository. Use a `-core` scope when the governance applies to a named domain shared across teams or repositories.
62
-
63
- #### 05-core-scope-distribution
64
-
65
- A `-core` scope MAY be distributed to consumers alongside the companion consumable scope, or kept internal to the team that maintains the domain. When distributed, consumers who extend or contribute to the domain scope locally MUST follow its standards. When not distributed, internal contributors must still comply.
66
-
67
- ## Considered Options
68
-
69
- 1. **`_local` scope for meta content** — simple to set up, but mixes local project overrides with domain-level governance. Becomes confusing in large teams, especially when `_local` already contains project-specific policies.
70
- 2. **Meta content embedded inside the consumable scope** — makes the shared scope noisier and harder for consumers to parse; no clear separation between governance and rules.
71
- 3. **`-core` sibling scope (chosen)** — cleanly separates meta governance from consumable content, makes the governance explicit and discoverable, and enables selective distribution.