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