xdrs-core 0.41.1 → 0.43.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.
Files changed (39) hide show
  1. package/.xdrs/_core/adrs/index.md +2 -1
  2. package/.xdrs/_core/adrs/principles/{001-xdrs-core.md → 001-xdrs-standards.md} +4 -4
  3. package/.xdrs/_core/adrs/principles/002-policy-standards.md +2 -2
  4. package/.xdrs/_core/adrs/principles/003-skill-standards.md +1 -1
  5. package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -1
  6. package/.xdrs/_core/adrs/principles/006-research-standards.md +1 -1
  7. package/.xdrs/_core/adrs/principles/007-plan-standards.md +1 -1
  8. package/.xdrs/_core/adrs/principles/008-policy-structured-standards.md +1 -1
  9. package/.xdrs/_core/adrs/principles/009-presentation-standards.md +1 -1
  10. package/.xdrs/_core/adrs/principles/010-scope-governance.md +76 -27
  11. package/.xdrs/_core/adrs/principles/011-core-scope-type.md +3 -3
  12. package/.xdrs/_core/adrs/principles/012-reference-scope-type.md +1 -1
  13. package/.xdrs/_core/adrs/principles/013-platform-scope-type.md +1 -1
  14. package/.xdrs/_core/adrs/principles/014-standard-scope-type.md +1 -1
  15. package/.xdrs/_core/adrs/principles/015-local-scope-type.md +1 -1
  16. package/.xdrs/_core/adrs/principles/016-policy-subjects.md +1 -1
  17. package/.xdrs/_core/adrs/principles/017-policy-numbering-ranges.md +1 -1
  18. package/.xdrs/_core/adrs/principles/018-external-path-symlinks.md +49 -0
  19. package/.xdrs/_core/adrs/principles/articles/.assets/001-xdrs-overview-slides.md +1 -1
  20. package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +3 -3
  21. package/.xdrs/_core/adrs/principles/skills/.assets/conflict-detection.md +76 -0
  22. package/.xdrs/_core/adrs/principles/skills/001-review/001-review.test.int.js +2 -2
  23. package/.xdrs/_core/adrs/principles/skills/001-review/001-review.test.int.report +2 -2
  24. package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +5 -4
  25. package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +12 -9
  26. package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +9 -6
  27. package/.xdrs/_core/adrs/principles/skills/004-write-article/004-write-article.test.int.js +1 -1
  28. package/.xdrs/_core/adrs/principles/skills/004-write-article/004-write-article.test.int.report +1 -1
  29. package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +9 -6
  30. package/.xdrs/_core/adrs/principles/skills/005-write-research/005-write-research.test.int.report +1 -1
  31. package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +9 -6
  32. package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +9 -6
  33. package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +7 -4
  34. package/.xdrs/_core/adrs/principles/skills/008-write-xdrs-doc/SKILL.md +5 -2
  35. package/.xdrs/_core/index.md +5 -3
  36. package/README.md +32 -2
  37. package/lib/lint.js +207 -47
  38. package/lib/lint.test.js +694 -25
  39. package/package.json +1 -1
package/lib/lint.test.js CHANGED
@@ -109,7 +109,7 @@ test('skips broken links to missing scope directories when ignoring external sco
109
109
  '- [001-main](principles/001-main.md) - Main decision'
110
110
  ]),
111
111
  '.xdrs/_local/adrs/principles/001-main.md': xdrDocument(
112
- 'See [core doc](../../../_core/adrs/principles/001-xdrs-core.md).'
112
+ 'See [core doc](../../../_core/adrs/principles/001-xdrs-standards.md).'
113
113
  ),
114
114
  });
115
115
 
@@ -995,6 +995,109 @@ test('reports cycle in scope-type inheritance chain', () => {
995
995
  expect(result.errors.join('\n')).toContain('my-custom/index.md');
996
996
  });
997
997
 
998
+ // ─── Companion scope-type file checks ─────────────────────────────────────────
999
+
1000
+ test('accepts companion scope-type file when primary exists in same type folder', () => {
1001
+ const workspaceRoot = createWorkspace('scope-type-companion-valid', {
1002
+ '.xdrs/index.md': rootIndex(['[myarea-core](myarea-core/index.md)', '[my-custom](my-custom/index.md)']),
1003
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance scope.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\nMeta-governance.\n\n[ADRs](adrs/index.md)\n',
1004
+ '.xdrs/myarea-core/adrs/index.md': [
1005
+ '# myarea-core ADR Index', '', 'ADRs.', '', '## principles', '',
1006
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
1007
+ '- [002-custom-scope-type](principles/002-custom-scope-type.md) - custom type primary',
1008
+ '- [003-custom-scope-type-writing-style](principles/003-custom-scope-type-writing-style.md) - custom type companion',
1009
+ ''
1010
+ ].join('\n'),
1011
+ '.xdrs/myarea-core/adrs/principles/001-core-scope-type.md': '---\nname: myarea-core-adr-policy-001-core-scope-type\ndescription: Core type 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',
1012
+ '.xdrs/myarea-core/adrs/principles/002-custom-scope-type.md': '---\nname: myarea-core-adr-policy-002-custom-scope-type\ndescription: Defines the custom scope type.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nDefines custom type.\n\n## Decision Outcome\n\nUse scope-type: custom.\n',
1013
+ '.xdrs/myarea-core/adrs/principles/003-custom-scope-type-writing-style.md': '---\nname: myarea-core-adr-policy-003-custom-scope-type-writing-style\ndescription: Writing style companion for custom scope type.\napply-to: All custom scopes\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nCompanion writing style rules.\n\n## Decision Outcome\n\nFollow writing style.\n',
1014
+ '.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',
1015
+ '.xdrs/my-custom/adrs/index.md': teamAdrIndex([]),
1016
+ });
1017
+
1018
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1019
+
1020
+ expect(result.errors.join('\n')).not.toContain('companion-requires-primary');
1021
+ expect(result.errors.join('\n')).not.toContain('has no corresponding');
1022
+ });
1023
+
1024
+ test('reports companion scope-type file with no corresponding primary', () => {
1025
+ const workspaceRoot = createWorkspace('scope-type-companion-no-primary', {
1026
+ '.xdrs/index.md': rootIndex(['[myarea-core](myarea-core/index.md)']),
1027
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance scope.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\nMeta-governance.\n\n[ADRs](adrs/index.md)\n',
1028
+ '.xdrs/myarea-core/adrs/index.md': [
1029
+ '# myarea-core ADR Index', '', 'ADRs.', '', '## principles', '',
1030
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
1031
+ '- [002-custom-scope-type-extra](principles/002-custom-scope-type-extra.md) - companion with no primary',
1032
+ ''
1033
+ ].join('\n'),
1034
+ '.xdrs/myarea-core/adrs/principles/001-core-scope-type.md': '---\nname: myarea-core-adr-policy-001-core-scope-type\ndescription: Core type 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',
1035
+ '.xdrs/myarea-core/adrs/principles/002-custom-scope-type-extra.md': '---\nname: myarea-core-adr-policy-002-custom-scope-type-extra\ndescription: Companion file with no primary.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nNo primary exists.\n\n## Decision Outcome\n\nNo primary.\n',
1036
+ });
1037
+
1038
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1039
+
1040
+ expect(result.errors.join('\n')).toContain('companion-requires-primary');
1041
+ expect(result.errors.join('\n')).toContain('custom-scope-type');
1042
+ });
1043
+
1044
+ test('reports companion scope-type file when primary is in a different type folder of same scope', () => {
1045
+ const workspaceRoot = createWorkspace('scope-type-companion-wrong-folder', {
1046
+ '.xdrs/index.md': rootIndex(['[myarea-core](myarea-core/index.md)']),
1047
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance scope.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\nMeta-governance.\n\n[ADRs](adrs/index.md)\n[BDRs](bdrs/index.md)\n',
1048
+ // Primary is in adrs/
1049
+ '.xdrs/myarea-core/adrs/index.md': [
1050
+ '# myarea-core ADR Index', '', 'ADRs.', '', '## principles', '',
1051
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
1052
+ '- [002-custom-scope-type](principles/002-custom-scope-type.md) - custom type primary',
1053
+ ''
1054
+ ].join('\n'),
1055
+ '.xdrs/myarea-core/adrs/principles/001-core-scope-type.md': '---\nname: myarea-core-adr-policy-001-core-scope-type\ndescription: Core type 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',
1056
+ '.xdrs/myarea-core/adrs/principles/002-custom-scope-type.md': '---\nname: myarea-core-adr-policy-002-custom-scope-type\ndescription: Defines the custom scope type.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nDefines custom type.\n\n## Decision Outcome\n\nUse scope-type: custom.\n',
1057
+ // Companion is in bdrs/ — different type folder from the primary (adrs/)
1058
+ '.xdrs/myarea-core/bdrs/index.md': [
1059
+ '# myarea-core BDR Index', '', 'BDRs.', '', '## principles', '',
1060
+ '- [001-custom-scope-type-extra](principles/001-custom-scope-type-extra.md) - companion in wrong folder',
1061
+ ''
1062
+ ].join('\n'),
1063
+ '.xdrs/myarea-core/bdrs/principles/001-custom-scope-type-extra.md': '---\nname: myarea-core-bdr-policy-001-custom-scope-type-extra\ndescription: Companion file in wrong type folder.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nWrong folder.\n\n## Decision Outcome\n\nWrong folder.\n',
1064
+ });
1065
+
1066
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1067
+
1068
+ expect(result.errors.join('\n')).toContain('companion-requires-primary');
1069
+ expect(result.errors.join('\n')).toContain('custom-scope-type');
1070
+ });
1071
+
1072
+ test('reports duplicate primary scope-type definitions in two type folders of same scope', () => {
1073
+ const workspaceRoot = createWorkspace('scope-type-duplicate-primary', {
1074
+ '.xdrs/index.md': rootIndex(['[myarea-core](myarea-core/index.md)']),
1075
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance scope.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\nMeta-governance.\n\n[ADRs](adrs/index.md)\n[BDRs](bdrs/index.md)\n',
1076
+ // Primary in adrs/
1077
+ '.xdrs/myarea-core/adrs/index.md': [
1078
+ '# myarea-core ADR Index', '', 'ADRs.', '', '## principles', '',
1079
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
1080
+ '- [002-custom-scope-type](principles/002-custom-scope-type.md) - custom type (adrs)',
1081
+ ''
1082
+ ].join('\n'),
1083
+ '.xdrs/myarea-core/adrs/principles/001-core-scope-type.md': '---\nname: myarea-core-adr-policy-001-core-scope-type\ndescription: Core type 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',
1084
+ '.xdrs/myarea-core/adrs/principles/002-custom-scope-type.md': '---\nname: myarea-core-adr-policy-002-custom-scope-type\ndescription: Defines the custom scope type (adrs).\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nDefines custom type.\n\n## Decision Outcome\n\nUse scope-type: custom.\n',
1085
+ // Duplicate primary also in bdrs/ — same scope-type name in two type folders
1086
+ '.xdrs/myarea-core/bdrs/index.md': [
1087
+ '# myarea-core BDR Index', '', 'BDRs.', '', '## principles', '',
1088
+ '- [001-custom-scope-type](principles/001-custom-scope-type.md) - custom type duplicate (bdrs)',
1089
+ ''
1090
+ ].join('\n'),
1091
+ '.xdrs/myarea-core/bdrs/principles/001-custom-scope-type.md': '---\nname: myarea-core-bdr-policy-001-custom-scope-type\ndescription: Duplicate primary for custom scope type in bdrs.\napply-to: All\nvalid-from: 2026-01-01\n---\n\n## Context and Problem Statement\n\nDuplicate primary.\n\n## Decision Outcome\n\nDuplicate.\n',
1092
+ });
1093
+
1094
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1095
+
1096
+ expect(result.errors.join('\n')).toContain('02-def-naming');
1097
+ expect(result.errors.join('\n')).toContain('custom');
1098
+ expect(result.errors.join('\n')).toContain('myarea-core');
1099
+ });
1100
+
998
1101
  test('reports scope-type with reserved underscore prefix', () => {
999
1102
  const workspaceRoot = createWorkspace('scope-type-underscore-prefix', {
1000
1103
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
@@ -1079,25 +1182,25 @@ test('reports follows list that includes _core', () => {
1079
1182
  expect(result.errors.join('\n')).toContain('follows must not reference "_core" as it is always applied implicitly');
1080
1183
  });
1081
1184
 
1082
- test('reports duplicate scope-local standards policies in same scope', () => {
1083
- const workspaceRoot = createWorkspace('duplicate-scope-local-standards', {
1185
+ test('accepts multiple local meta-policies in same scope', () => {
1186
+ const workspaceRoot = createWorkspace('multiple-local-meta-policies', {
1084
1187
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1085
1188
  '.xdrs/myteam/index.md': teamScopeIndex(),
1086
1189
  '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1087
1190
  '- [001-team](principles/001-team.md) - Team decision',
1088
- '- [002-myteam-core](principles/002-myteam-core.md) - Local standards',
1089
- '- [003-myteam-core-dup](principles/003-myteam-core-dup.md) - Duplicate local standards',
1191
+ '- [002-core](principles/002-core.md) - Local meta-policy primary',
1192
+ '- [003-core-writing](principles/003-core-writing.md) - Local meta-policy companion',
1090
1193
  ]),
1091
1194
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1092
- '.xdrs/myteam/adrs/principles/002-myteam-core.md': [
1195
+ '.xdrs/myteam/adrs/principles/002-core.md': [
1093
1196
  '---',
1094
- 'name: myteam-adr-policy-002-myteam-core',
1095
- 'description: Local standards for myteam.',
1197
+ 'name: myteam-adr-policy-002-core',
1198
+ 'description: Local meta-policy for myteam.',
1096
1199
  'apply-to: myteam scope',
1097
1200
  'valid-from: 2026-01-01',
1098
1201
  '---',
1099
1202
  '',
1100
- '# myteam-adr-policy-002: myteam local standards',
1203
+ '# myteam-adr-policy-002: Core',
1101
1204
  '',
1102
1205
  '## Context and Problem Statement',
1103
1206
  '',
@@ -1108,51 +1211,51 @@ test('reports duplicate scope-local standards policies in same scope', () => {
1108
1211
  'Standard outcome.',
1109
1212
  ''
1110
1213
  ].join('\n'),
1111
- '.xdrs/myteam/adrs/principles/003-myteam-core-dup.md': [
1214
+ '.xdrs/myteam/adrs/principles/003-core-writing.md': [
1112
1215
  '---',
1113
- 'name: myteam-adr-policy-003-myteam-core',
1114
- 'description: Duplicate local standards for myteam.',
1216
+ 'name: myteam-adr-policy-003-core-writing',
1217
+ 'description: Writing style meta-policy for myteam.',
1115
1218
  'apply-to: myteam scope',
1116
1219
  'valid-from: 2026-01-01',
1117
1220
  '---',
1118
1221
  '',
1119
- '# myteam-adr-policy-003: myteam local standards duplicate',
1222
+ '# myteam-adr-policy-003: Core writing',
1120
1223
  '',
1121
1224
  '## Context and Problem Statement',
1122
1225
  '',
1123
- 'Duplicate defines local authoring standards.',
1226
+ 'Defines writing standards.',
1124
1227
  '',
1125
1228
  '## Decision Outcome',
1126
1229
  '',
1127
- 'Duplicate outcome.',
1230
+ 'Writing outcome.',
1128
1231
  ''
1129
1232
  ].join('\n'),
1130
1233
  });
1131
1234
 
1132
1235
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1133
1236
 
1134
- expect(result.errors.join('\n')).toContain('has more than one scope-local standards policy');
1135
- expect(result.errors.join('\n')).toContain('myteam-core');
1237
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.10');
1238
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.09-local-naming]');
1136
1239
  });
1137
1240
 
1138
- test('accepts single scope-local standards policy in a scope', () => {
1139
- const workspaceRoot = createWorkspace('single-scope-local-standards', {
1241
+ test('accepts single local meta-policy in a scope', () => {
1242
+ const workspaceRoot = createWorkspace('single-local-meta-policy', {
1140
1243
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1141
1244
  '.xdrs/myteam/index.md': teamScopeIndex(),
1142
1245
  '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1143
1246
  '- [001-team](principles/001-team.md) - Team decision',
1144
- '- [002-myteam-core](principles/002-myteam-core.md) - Local standards',
1247
+ '- [002-core](principles/002-core.md) - Local meta-policy',
1145
1248
  ]),
1146
1249
  '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1147
- '.xdrs/myteam/adrs/principles/002-myteam-core.md': [
1250
+ '.xdrs/myteam/adrs/principles/002-core.md': [
1148
1251
  '---',
1149
- 'name: myteam-adr-policy-002-myteam-core',
1150
- 'description: Local standards for myteam.',
1252
+ 'name: myteam-adr-policy-002-core',
1253
+ 'description: Local meta-policy for myteam.',
1151
1254
  'apply-to: myteam scope',
1152
1255
  'valid-from: 2026-01-01',
1153
1256
  '---',
1154
1257
  '',
1155
- '# myteam-adr-policy-002: myteam local standards',
1258
+ '# myteam-adr-policy-002: Core',
1156
1259
  '',
1157
1260
  '## Context and Problem Statement',
1158
1261
  '',
@@ -1167,7 +1270,224 @@ test('accepts single scope-local standards policy in a scope', () => {
1167
1270
 
1168
1271
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1169
1272
 
1170
- expect(result.errors.join('\n')).not.toContain('has more than one scope-local standards policy');
1273
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.10');
1274
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.09-local-naming]');
1275
+ });
1276
+
1277
+ test('errors when local meta-policy companion exists without primary in same type folder', () => {
1278
+ const workspaceRoot = createWorkspace('companion-without-primary', {
1279
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1280
+ '.xdrs/myteam/index.md': teamScopeIndex(),
1281
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1282
+ '- [001-team](principles/001-team.md) - Team decision',
1283
+ '- [002-core-writing](principles/002-core-writing.md) - Companion without primary',
1284
+ ]),
1285
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
1286
+ '.xdrs/myteam/adrs/principles/002-core-writing.md': [
1287
+ '---',
1288
+ 'name: myteam-adr-policy-002-core-writing',
1289
+ 'description: Writing style without primary.',
1290
+ 'apply-to: myteam scope',
1291
+ 'valid-from: 2026-01-01',
1292
+ '---',
1293
+ '',
1294
+ '# myteam-adr-policy-002: Core writing',
1295
+ '',
1296
+ '## Context and Problem Statement',
1297
+ '',
1298
+ 'Companion only.',
1299
+ '',
1300
+ '## Decision Outcome',
1301
+ '',
1302
+ 'Companion outcome.',
1303
+ ''
1304
+ ].join('\n'),
1305
+ });
1306
+
1307
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1308
+
1309
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.10b-local-primary-required]');
1310
+ });
1311
+
1312
+ test('errors when local meta-policy same qualifier appears in two type folders', () => {
1313
+ const workspaceRoot = createWorkspace('duplicate-qualifier-across-types', {
1314
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1315
+ '.xdrs/myteam/index.md': [
1316
+ '---',
1317
+ 'scope-type: standard',
1318
+ 'name: myteam',
1319
+ 'description: Team scope for tests.',
1320
+ 'apply-to: Test team only',
1321
+ 'valid-from: 2026-01-01',
1322
+ '---',
1323
+ '',
1324
+ '# myteam Scope Overview',
1325
+ '',
1326
+ '[ADRs](adrs/index.md)',
1327
+ '[EDRs](edrs/index.md)',
1328
+ ''
1329
+ ].join('\n'),
1330
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1331
+ '- [001-core](principles/001-core.md) - Primary in adrs',
1332
+ ]),
1333
+ '.xdrs/myteam/adrs/principles/001-core.md': [
1334
+ '---',
1335
+ 'name: myteam-adr-policy-001-core',
1336
+ 'description: Primary in adrs.',
1337
+ 'apply-to: myteam scope',
1338
+ 'valid-from: 2026-01-01',
1339
+ '---',
1340
+ '',
1341
+ '# myteam-adr-policy-001: Core',
1342
+ '',
1343
+ '## Context and Problem Statement',
1344
+ '',
1345
+ 'Primary in adrs.',
1346
+ '',
1347
+ '## Decision Outcome',
1348
+ '',
1349
+ 'Outcome.',
1350
+ ''
1351
+ ].join('\n'),
1352
+ '.xdrs/myteam/edrs/index.md': '# myteam EDR Index\n\n## principles\n\n- [001-core](principles/001-core.md) - Primary in edrs\n',
1353
+ '.xdrs/myteam/edrs/principles/001-core.md': [
1354
+ '---',
1355
+ 'name: myteam-edr-policy-001-core',
1356
+ 'description: Primary in edrs.',
1357
+ 'apply-to: myteam scope',
1358
+ 'valid-from: 2026-01-01',
1359
+ '---',
1360
+ '',
1361
+ '# myteam-edr-policy-001: Core',
1362
+ '',
1363
+ '## Context and Problem Statement',
1364
+ '',
1365
+ 'Primary in edrs.',
1366
+ '',
1367
+ '## Decision Outcome',
1368
+ '',
1369
+ 'Outcome.',
1370
+ ''
1371
+ ].join('\n'),
1372
+ });
1373
+
1374
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1375
+
1376
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.10c-local-unique-qualifier]');
1377
+ });
1378
+
1379
+ test('errors when policy filename contains core as word outside principles', () => {
1380
+ const workspaceRoot = createWorkspace('core-word-outside-principles', {
1381
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1382
+ '.xdrs/myteam/index.md': teamScopeIndex(),
1383
+ '.xdrs/myteam/adrs/index.md': '# myteam ADR Index\n\n## application\n\n- [101-some-core-thing](application/101-some-core-thing.md) - Uses core in slug\n',
1384
+ '.xdrs/myteam/adrs/application/101-some-core-thing.md': [
1385
+ '---',
1386
+ 'name: myteam-adr-policy-101-some-core-thing',
1387
+ 'description: Policy with core in non-principles.',
1388
+ 'apply-to: myteam scope',
1389
+ 'valid-from: 2026-01-01',
1390
+ '---',
1391
+ '',
1392
+ '# myteam-adr-policy-101: Some core thing',
1393
+ '',
1394
+ '## Context and Problem Statement',
1395
+ '',
1396
+ 'Uses core in application subject.',
1397
+ '',
1398
+ '## Decision Outcome',
1399
+ '',
1400
+ 'Outcome.',
1401
+ ''
1402
+ ].join('\n'),
1403
+ });
1404
+
1405
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1406
+
1407
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.09-local-naming]');
1408
+ });
1409
+
1410
+ test('errors when policy filename contains core as non-first word in principles', () => {
1411
+ const workspaceRoot = createWorkspace('core-non-first-in-principles', {
1412
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
1413
+ '.xdrs/myteam/index.md': teamScopeIndex(),
1414
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
1415
+ '- [001-xdrs-core-like](principles/001-xdrs-core-like.md) - Core not first word',
1416
+ ]),
1417
+ '.xdrs/myteam/adrs/principles/001-xdrs-core-like.md': [
1418
+ '---',
1419
+ 'name: myteam-adr-policy-001-xdrs-core-like',
1420
+ 'description: Core is not first word.',
1421
+ 'apply-to: myteam scope',
1422
+ 'valid-from: 2026-01-01',
1423
+ '---',
1424
+ '',
1425
+ '# myteam-adr-policy-001: Xdrs core like',
1426
+ '',
1427
+ '## Context and Problem Statement',
1428
+ '',
1429
+ 'Core not first.',
1430
+ '',
1431
+ '## Decision Outcome',
1432
+ '',
1433
+ 'Outcome.',
1434
+ ''
1435
+ ].join('\n'),
1436
+ });
1437
+
1438
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1439
+
1440
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.09-local-naming]');
1441
+ });
1442
+
1443
+ test('accepts NNN-core-scope-type.md in principles without triggering core-word error', () => {
1444
+ const workspaceRoot = createWorkspace('core-scope-type-in-principles', {
1445
+ '.xdrs/index.md': rootIndex(['[myarea-core](myarea-core/index.md)']),
1446
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core\nname: myarea-core\ndescription: Meta-governance.\napply-to: All myarea scopes\nvalid-from: 2026-01-01\n---\n\n# myarea-core Scope Overview\n\n[ADRs](adrs/index.md)\n',
1447
+ '.xdrs/myarea-core/adrs/index.md': '# myarea-core ADR Index\n\n## principles\n\n- [001-custom-scope-type](principles/001-custom-scope-type.md) - Scope type\n- [002-core-scope-type](principles/002-core-scope-type.md) - Core scope type def\n',
1448
+ '.xdrs/myarea-core/adrs/principles/001-custom-scope-type.md': [
1449
+ '---',
1450
+ 'name: myarea-core-adr-policy-001-custom-scope-type',
1451
+ 'description: Custom scope type.',
1452
+ 'apply-to: myarea-core scope',
1453
+ 'valid-from: 2026-01-01',
1454
+ '---',
1455
+ '',
1456
+ '# myarea-core-adr-policy-001: Custom scope type',
1457
+ '',
1458
+ '## Context and Problem Statement',
1459
+ '',
1460
+ 'Defines custom scope type.',
1461
+ '',
1462
+ '## Decision Outcome',
1463
+ '',
1464
+ 'Outcome.',
1465
+ ''
1466
+ ].join('\n'),
1467
+ '.xdrs/myarea-core/adrs/principles/002-core-scope-type.md': [
1468
+ '---',
1469
+ 'name: myarea-core-adr-policy-002-core-scope-type',
1470
+ 'description: Core scope type definition.',
1471
+ 'apply-to: myarea-core scope',
1472
+ 'valid-from: 2026-01-01',
1473
+ '---',
1474
+ '',
1475
+ '# myarea-core-adr-policy-002: Core scope type',
1476
+ '',
1477
+ '## Context and Problem Statement',
1478
+ '',
1479
+ 'Defines core scope type.',
1480
+ '',
1481
+ '## Decision Outcome',
1482
+ '',
1483
+ 'Outcome.',
1484
+ ''
1485
+ ].join('\n'),
1486
+ });
1487
+
1488
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1489
+
1490
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.09-local-naming]');
1171
1491
  });
1172
1492
 
1173
1493
  test('reports missing name field in scope index frontmatter', () => {
@@ -1672,6 +1992,355 @@ test('allows nested directories in .assets', () => {
1672
1992
  expect(result.errors.join('\n')).not.toContain('.assets directory must be flat');
1673
1993
  });
1674
1994
 
1995
+ // ─── Multi-type scope-type tests ──────────────────────────────────────────────
1996
+
1997
+ /**
1998
+ * Create a minimal structured scope-type policy for use in multi-type tests.
1999
+ */
2000
+ function structuredScopeTypePolicy(scopeName, num, typeName) {
2001
+ return [
2002
+ '---',
2003
+ `name: ${scopeName}-adr-policy-${num}-${typeName}-scope-type`,
2004
+ `description: Defines the ${typeName} scope type.`,
2005
+ 'apply-to: All XDRS scopes',
2006
+ 'valid-from: 2026-01-01',
2007
+ '---',
2008
+ '',
2009
+ `# ${scopeName}-adr-policy-${num}: ${typeName} scope type`,
2010
+ '',
2011
+ '## Context and Problem Statement',
2012
+ '',
2013
+ `Defines the ${typeName} scope type.`,
2014
+ '',
2015
+ '## Decision Outcome',
2016
+ '',
2017
+ `Use scope-type: ${typeName} when appropriate.`,
2018
+ '',
2019
+ '### Details',
2020
+ '',
2021
+ '#### 01-rule-example',
2022
+ '',
2023
+ `Scopes of type \`${typeName}\` MUST follow these conventions.`,
2024
+ ''
2025
+ ].join('\n');
2026
+ }
2027
+
2028
+ /**
2029
+ * Returns file paths → content for a _core scope with structured scope-type policies.
2030
+ */
2031
+ function coreWithStructuredScopeTypes(types) {
2032
+ const allTypes = Array.from(new Set(['core', ...types]));
2033
+ const files = {};
2034
+ const policyEntries = allTypes.map((t, i) => {
2035
+ const num = String(i + 1).padStart(3, '0');
2036
+ const fileName = t === '_local' ? `${num}-local-scope-type.md` : `${num}-${t}-scope-type.md`;
2037
+ files[`.xdrs/_core/adrs/principles/${fileName}`] = structuredScopeTypePolicy('_core', num, t);
2038
+ return `- [${num}-${t}-scope-type](principles/${fileName}) - ${t} scope type`;
2039
+ });
2040
+ files['.xdrs/_core/adrs/index.md'] = [
2041
+ '# _core ADR Index', '', 'Core ADRs for tests.', '', '## principles', '',
2042
+ ...policyEntries, ''
2043
+ ].join('\n');
2044
+ files['.xdrs/_core/index.md'] = [
2045
+ '---', 'scope-type: core', 'name: _core', 'description: Core framework scope for tests.',
2046
+ 'apply-to: All XDRS scopes', 'valid-from: 2026-01-01', '---', '',
2047
+ '# _core Scope Overview', '', 'Framework scope for tests.', '', '[ADRs](adrs/index.md)', ''
2048
+ ].join('\n');
2049
+ return files;
2050
+ }
2051
+
2052
+ test('accepts multi-type scope-type array (two types)', () => {
2053
+ const workspaceRoot = createWorkspace('multi-type-valid-two', {
2054
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2055
+ '.xdrs/my-scope/index.md': [
2056
+ '---', 'scope-type: customa, customb', 'name: my-scope',
2057
+ 'description: Multi-type scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2058
+ '# my-scope Scope Overview', '', 'Multi-type.', '', '[ADRs](adrs/index.md)', ''
2059
+ ].join('\n'),
2060
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2061
+ ...coreWithStructuredScopeTypes(['customa', 'customb']),
2062
+ });
2063
+
2064
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2065
+
2066
+ expect(result.errors.join('\n')).not.toContain('scope-type');
2067
+ });
2068
+
2069
+ test('accepts single-type scope-type string (backward compat)', () => {
2070
+ const workspaceRoot = createWorkspace('single-type-compat', {
2071
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2072
+ '.xdrs/my-scope/index.md': [
2073
+ '---', 'scope-type: customa', 'name: my-scope',
2074
+ 'description: Single-type scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2075
+ '# my-scope Scope Overview', '', 'Single type.', '', '[ADRs](adrs/index.md)', ''
2076
+ ].join('\n'),
2077
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2078
+ ...coreWithStructuredScopeTypes(['customa']),
2079
+ });
2080
+
2081
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2082
+
2083
+ expect(result.errors.join('\n')).not.toContain('scope-type');
2084
+ });
2085
+
2086
+ test('reports scope-type with only commas (all empty elements)', () => {
2087
+ const workspaceRoot = createWorkspace('multi-type-empty-elements', {
2088
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2089
+ '.xdrs/my-scope/index.md': [
2090
+ '---', 'scope-type: ,', 'name: my-scope',
2091
+ 'description: Empty elements scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2092
+ '# my-scope Scope Overview', '', 'Empty.', '', '[ADRs](adrs/index.md)', ''
2093
+ ].join('\n'),
2094
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2095
+ ...coreWithStructuredScopeTypes([]),
2096
+ });
2097
+
2098
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2099
+
2100
+ expect(result.errors.join('\n')).toContain('invalid characters');
2101
+ });
2102
+
2103
+ test('reports duplicate values in scope-type list', () => {
2104
+ const workspaceRoot = createWorkspace('multi-type-duplicate', {
2105
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2106
+ '.xdrs/my-scope/index.md': [
2107
+ '---', 'scope-type: customa, customa', 'name: my-scope',
2108
+ 'description: Dup-type scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2109
+ '# my-scope Scope Overview', '', 'Dup.', '', '[ADRs](adrs/index.md)', ''
2110
+ ].join('\n'),
2111
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2112
+ ...coreWithStructuredScopeTypes(['customa']),
2113
+ });
2114
+
2115
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2116
+
2117
+ expect(result.errors.join('\n')).toContain('duplicate value "customa"');
2118
+ });
2119
+
2120
+ test('reports invalid characters in scope-type list element', () => {
2121
+ const workspaceRoot = createWorkspace('multi-type-invalid-chars', {
2122
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2123
+ '.xdrs/my-scope/index.md': [
2124
+ '---', 'scope-type: valid-type, bad type!', 'name: my-scope',
2125
+ 'description: Invalid char scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2126
+ '# my-scope Scope Overview', '', 'Invalid.', '', '[ADRs](adrs/index.md)', ''
2127
+ ].join('\n'),
2128
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2129
+ ...coreWithStructuredScopeTypes(['valid-type']),
2130
+ });
2131
+
2132
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2133
+
2134
+ expect(result.errors.join('\n')).toContain('invalid characters');
2135
+ });
2136
+
2137
+ test('reports when one type in array has no corresponding scope-type policy', () => {
2138
+ const workspaceRoot = createWorkspace('multi-type-missing-one', {
2139
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2140
+ '.xdrs/my-scope/index.md': [
2141
+ '---', 'scope-type: customa, missing-type', 'name: my-scope',
2142
+ 'description: One missing type.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2143
+ '# my-scope Scope Overview', '', 'One missing.', '', '[ADRs](adrs/index.md)', ''
2144
+ ].join('\n'),
2145
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2146
+ ...coreWithStructuredScopeTypes(['customa']),
2147
+ });
2148
+
2149
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2150
+
2151
+ expect(result.errors.join('\n')).toContain('missing-type');
2152
+ expect(result.errors.join('\n')).toContain('has no corresponding');
2153
+ });
2154
+
2155
+ test('detects cross-type cycle in multi-type array via parent chain', () => {
2156
+ // typea's parent is typeb, typeb's parent is typea — cross-type cycle via array
2157
+ const workspaceRoot = createWorkspace('multi-type-cross-cycle', {
2158
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2159
+ '.xdrs/my-scope/index.md': [
2160
+ '---', 'scope-type: typea, typeb', 'name: my-scope',
2161
+ 'description: Cross-cycle scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2162
+ '# my-scope Scope Overview', '', 'Cross cycle.', '', '[ADRs](adrs/index.md)', ''
2163
+ ].join('\n'),
2164
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2165
+ '.xdrs/_core/index.md': [
2166
+ '---', 'scope-type: core', 'name: _core', 'description: Core scope.',
2167
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '', '# _core', '', '[ADRs](adrs/index.md)', ''
2168
+ ].join('\n'),
2169
+ '.xdrs/_core/adrs/index.md': [
2170
+ '# _core ADR Index', '', 'Core ADRs.', '', '## principles', '',
2171
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
2172
+ '- [002-typea-scope-type](principles/002-typea-scope-type.md) - typea',
2173
+ '- [003-typeb-scope-type](principles/003-typeb-scope-type.md) - typeb',
2174
+ ''
2175
+ ].join('\n'),
2176
+ '.xdrs/_core/adrs/principles/001-core-scope-type.md': structuredScopeTypePolicy('_core', '001', 'core'),
2177
+ // typea declares parent typeb
2178
+ '.xdrs/_core/adrs/principles/002-typea-scope-type.md': [
2179
+ '---', 'name: _core-adr-policy-002-typea-scope-type', 'description: typea.',
2180
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2181
+ '# _core-adr-policy-002: typea scope type', '', '## Context and Problem Statement', '',
2182
+ 'typea.', '', '## Decision Outcome', '', 'Use typea.', '', '### Details', '',
2183
+ '#### 01-parent-scope-type', '',
2184
+ 'Instances inherit all rules from the `typeb` scope type.', ''
2185
+ ].join('\n'),
2186
+ // typeb declares parent typea — creates cycle
2187
+ '.xdrs/_core/adrs/principles/003-typeb-scope-type.md': [
2188
+ '---', 'name: _core-adr-policy-003-typeb-scope-type', 'description: typeb.',
2189
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2190
+ '# _core-adr-policy-003: typeb scope type', '', '## Context and Problem Statement', '',
2191
+ 'typeb.', '', '## Decision Outcome', '', 'Use typeb.', '', '### Details', '',
2192
+ '#### 01-parent-scope-type', '',
2193
+ 'Instances inherit all rules from the `typea` scope type.', ''
2194
+ ].join('\n'),
2195
+ });
2196
+
2197
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2198
+
2199
+ expect(result.errors.join('\n')).toContain('cycle');
2200
+ });
2201
+
2202
+ test('reports scope-type policy missing ### Details section', () => {
2203
+ const workspaceRoot = createWorkspace('scope-type-missing-details', {
2204
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2205
+ '.xdrs/my-scope/index.md': [
2206
+ '---', 'scope-type: customa', 'name: my-scope',
2207
+ 'description: Custom scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2208
+ '# my-scope Scope Overview', '', 'Custom.', '', '[ADRs](adrs/index.md)', ''
2209
+ ].join('\n'),
2210
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2211
+ '.xdrs/_core/index.md': [
2212
+ '---', 'scope-type: core', 'name: _core', 'description: Core scope.',
2213
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '', '# _core', '', '[ADRs](adrs/index.md)', ''
2214
+ ].join('\n'),
2215
+ '.xdrs/_core/adrs/index.md': [
2216
+ '# _core ADR Index', '', 'Core ADRs.', '', '## principles', '',
2217
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
2218
+ '- [002-customa-scope-type](principles/002-customa-scope-type.md) - customa',
2219
+ ''
2220
+ ].join('\n'),
2221
+ '.xdrs/_core/adrs/principles/001-core-scope-type.md': structuredScopeTypePolicy('_core', '001', 'core'),
2222
+ // Missing ### Details section
2223
+ '.xdrs/_core/adrs/principles/002-customa-scope-type.md': [
2224
+ '---', 'name: _core-adr-policy-002-customa-scope-type', 'description: customa.',
2225
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2226
+ '# _core-adr-policy-002: customa scope type', '', '## Context and Problem Statement', '',
2227
+ 'Defines customa.', '', '## Decision Outcome', '', 'Use customa.', ''
2228
+ ].join('\n'),
2229
+ });
2230
+
2231
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2232
+
2233
+ expect(result.errors.join('\n')).toContain('"### Details"');
2234
+ expect(result.errors.join('\n')).toContain('08b-def-scope-type-must-be-structured');
2235
+ });
2236
+
2237
+ test('reports scope-type policy missing at least one #### NN-rulename block', () => {
2238
+ const workspaceRoot = createWorkspace('scope-type-missing-rule-block', {
2239
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2240
+ '.xdrs/my-scope/index.md': [
2241
+ '---', 'scope-type: customa', 'name: my-scope',
2242
+ 'description: Custom scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2243
+ '# my-scope Scope Overview', '', 'Custom.', '', '[ADRs](adrs/index.md)', ''
2244
+ ].join('\n'),
2245
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([]),
2246
+ '.xdrs/_core/index.md': [
2247
+ '---', 'scope-type: core', 'name: _core', 'description: Core scope.',
2248
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '', '# _core', '', '[ADRs](adrs/index.md)', ''
2249
+ ].join('\n'),
2250
+ '.xdrs/_core/adrs/index.md': [
2251
+ '# _core ADR Index', '', 'Core ADRs.', '', '## principles', '',
2252
+ '- [001-core-scope-type](principles/001-core-scope-type.md) - core type',
2253
+ '- [002-customa-scope-type](principles/002-customa-scope-type.md) - customa',
2254
+ ''
2255
+ ].join('\n'),
2256
+ '.xdrs/_core/adrs/principles/001-core-scope-type.md': structuredScopeTypePolicy('_core', '001', 'core'),
2257
+ // Has ### Details but no #### NN-rulename blocks
2258
+ '.xdrs/_core/adrs/principles/002-customa-scope-type.md': [
2259
+ '---', 'name: _core-adr-policy-002-customa-scope-type', 'description: customa.',
2260
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2261
+ '# _core-adr-policy-002: customa scope type', '', '## Context and Problem Statement', '',
2262
+ 'Defines customa.', '', '## Decision Outcome', '', 'Use customa.', '', '### Details', '',
2263
+ 'No rule blocks here — just prose.', ''
2264
+ ].join('\n'),
2265
+ });
2266
+
2267
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2268
+
2269
+ expect(result.errors.join('\n')).toContain('at least one structured rule block');
2270
+ expect(result.errors.join('\n')).toContain('08b-def-scope-type-must-be-structured');
2271
+ });
2272
+
2273
+ test('reports local meta-policy missing ### Details section', () => {
2274
+ // Local meta-policy without structured format
2275
+ const workspaceRoot = createWorkspace('local-meta-policy-unstructured', {
2276
+ '.xdrs/index.md': rootIndex(['[my-scope](my-scope/index.md)']),
2277
+ '.xdrs/my-scope/index.md': [
2278
+ '---', 'scope-type: customa', 'name: my-scope',
2279
+ 'description: Custom scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2280
+ '# my-scope Scope Overview', '', 'Custom.', '', '[ADRs](adrs/index.md)', ''
2281
+ ].join('\n'),
2282
+ '.xdrs/my-scope/adrs/index.md': teamAdrIndex([
2283
+ '- [001-core](principles/001-core.md) - local meta-policy'
2284
+ ]),
2285
+ // Local meta-policy without ### Details
2286
+ '.xdrs/my-scope/adrs/principles/001-core.md': [
2287
+ '---', 'name: my-scope-adr-policy-001-core', 'description: Local meta-policy.',
2288
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2289
+ '# my-scope-adr-policy-001: local meta', '', '## Context and Problem Statement', '',
2290
+ 'Local conventions.', '', '## Decision Outcome', '', 'Use local conventions.', ''
2291
+ ].join('\n'),
2292
+ ...coreWithStructuredScopeTypes(['customa']),
2293
+ });
2294
+
2295
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2296
+
2297
+ expect(result.errors.join('\n')).toContain('"### Details"');
2298
+ expect(result.errors.join('\n')).toContain('12b-local-must-be-structured');
2299
+ });
2300
+
2301
+ test('reports policy in non-_core core-type scope missing structured format', () => {
2302
+ const workspaceRoot = createWorkspace('core-scope-policy-unstructured', {
2303
+ '.xdrs/index.md': rootIndex(['[my-core](my-core/index.md)']),
2304
+ '.xdrs/my-core/index.md': [
2305
+ '---', 'scope-type: core', 'name: my-core',
2306
+ 'description: Core scope.', 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2307
+ '# my-core Scope Overview', '', 'Core.', '', '[ADRs](adrs/index.md)', ''
2308
+ ].join('\n'),
2309
+ '.xdrs/my-core/adrs/index.md': [
2310
+ '# my-core ADR Index', '', 'Core ADRs.', '', '## principles', '',
2311
+ '- [001-standard-scope-type](principles/001-standard-scope-type.md) - standard type',
2312
+ ''
2313
+ ].join('\n'),
2314
+ // Policy without ### Details in a core-type scope that is not _core
2315
+ '.xdrs/my-core/adrs/principles/001-standard-scope-type.md': [
2316
+ '---', 'name: my-core-adr-policy-001-standard-scope-type', 'description: Standard type.',
2317
+ 'apply-to: All', 'valid-from: 2026-01-01', '---', '',
2318
+ '# my-core-adr-policy-001: standard scope type', '', '## Context and Problem Statement', '',
2319
+ 'Defines standard.', '', '## Decision Outcome', '', 'Use standard.', ''
2320
+ ].join('\n'),
2321
+ ...coreWithStructuredScopeTypes([]),
2322
+ });
2323
+
2324
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2325
+
2326
+ expect(result.errors.join('\n')).toContain('"### Details"');
2327
+ expect(result.errors.join('\n')).toContain('08c-def-core-scope-policies-must-be-structured');
2328
+ });
2329
+
2330
+ test('does not require structured format for _core scope policies (exempt)', () => {
2331
+ // _core scope is exempt from rule 08c (all-policies-in-core-type-scope check)
2332
+ // The existing coreWithScopeTypes helper creates unstructured policies in _core
2333
+ // — this test verifies _core is NOT checked for 08c
2334
+ const workspaceRoot = createWorkspace('core-scope-exempt', {
2335
+ '.xdrs/index.md': rootIndex(),
2336
+ ...coreWithScopeTypes([]), // intentionally unstructured - _core is exempt from 08c
2337
+ });
2338
+
2339
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2340
+
2341
+ expect(result.errors.join('\n')).not.toContain('08c-def-core-scope-policies-must-be-structured');
2342
+ });
2343
+
1675
2344
  function localScopeIndex() {
1676
2345
  return [
1677
2346
  '---',