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/.xdrs/_core/adrs/index.md +6 -1
- package/.xdrs/_core/adrs/principles/001-xdrs-core.md +14 -11
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/006-research-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/007-plan-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +142 -0
- package/.xdrs/_core/adrs/principles/011-core-scope-type.md +69 -0
- package/.xdrs/_core/adrs/principles/012-reference-scope-type.md +59 -0
- package/.xdrs/_core/adrs/principles/013-platform-scope-type.md +47 -0
- package/.xdrs/_core/adrs/principles/014-standard-scope-type.md +48 -0
- package/.xdrs/_core/adrs/principles/015-local-scope-type.md +41 -0
- package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +4 -0
- package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +2 -0
- package/.xdrs/_core/index.md +5 -3
- package/.xdrs/index.md +1 -1
- package/lib/lint.js +140 -13
- package/lib/lint.test.js +300 -24
- package/package.json +1 -1
- package/.xdrs/_core/adrs/principles/010-core-scope-naming.md +0 -71
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:
|
|
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:
|
|
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
|
|
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-
|
|
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('
|
|
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('
|
|
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('
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|