xdrs-core 0.43.1 → 0.45.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 +2 -0
- package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +6 -4
- package/.xdrs/_core/adrs/principles/002-policy-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +96 -48
- package/.xdrs/_core/adrs/principles/018-external-path-symlinks.md +1 -1
- package/.xdrs/_core/adrs/principles/019-compiled-scope-type.md +117 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/lint-verification.md +12 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/meta-policy-compliance.md +12 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/prerequisites-gate.md +19 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/scope-placement.md +47 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/scope-selection.md +25 -0
- package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +9 -3
- package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +7 -23
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +7 -18
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +5 -15
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +6 -17
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +6 -17
- package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +2 -13
- package/.xdrs/_core/adrs/principles/skills/008-write-xdrs-doc/SKILL.md +1 -8
- package/.xdrs/_core/adrs/principles/skills/009-compile-scope/SKILL.md +121 -0
- package/.xdrs/_core/index.md +1 -1
- package/README.md +15 -2
- package/lib/lint.js +101 -38
- package/lib/lint.test.js +156 -41
- package/package.json +1 -1
package/lib/lint.js
CHANGED
|
@@ -27,7 +27,7 @@ const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_D
|
|
|
27
27
|
|
|
28
28
|
const POLICY_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'freeze-reference']);
|
|
29
29
|
const SKILL_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'license', 'metadata', 'compatibility', 'allowed-tools']);
|
|
30
|
-
const SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS = new Set(['scope-type', 'name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'follows', '
|
|
30
|
+
const SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS = new Set(['scope-type', 'name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'follows', 'extends']);
|
|
31
31
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
|
|
32
32
|
const SLIDE_FILE_RE = /^.+-slides(?:-[a-z0-9-]+)?\.md$/;
|
|
33
33
|
const SLIDE_MAX_NAME_LENGTH = 64;
|
|
@@ -220,21 +220,48 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// Populated during lintScopeIndexFrontmatter: scopeName -> [extendedScopeNames...]
|
|
224
|
+
const extendsRelationships = new Map();
|
|
225
|
+
|
|
223
226
|
for (const scopeEntry of scopeEntries) {
|
|
224
|
-
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, warnings, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, readOnlyScopes);
|
|
227
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, warnings, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, readOnlyScopes, extendsRelationships);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Detect cycles in extends: chains (DFS)
|
|
231
|
+
const visitedCycle = new Set();
|
|
232
|
+
const inStackCycle = new Set();
|
|
233
|
+
const detectExtendsCycles = (scope, stack) => {
|
|
234
|
+
if (inStackCycle.has(scope)) {
|
|
235
|
+
const cycleStart = stack.indexOf(scope);
|
|
236
|
+
const cycle = stack.slice(cycleStart).concat(scope).join(' -> ');
|
|
237
|
+
errors.push(`Circular extends chain detected: ${cycle} [_core-adr-policy-010.31-extends-no-cycle]`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (visitedCycle.has(scope)) return;
|
|
241
|
+
visitedCycle.add(scope);
|
|
242
|
+
inStackCycle.add(scope);
|
|
243
|
+
stack.push(scope);
|
|
244
|
+
for (const target of (extendsRelationships.get(scope) || [])) {
|
|
245
|
+
detectExtendsCycles(target, stack);
|
|
246
|
+
}
|
|
247
|
+
stack.pop();
|
|
248
|
+
inStackCycle.delete(scope);
|
|
249
|
+
};
|
|
250
|
+
for (const scope of extendsRelationships.keys()) {
|
|
251
|
+
detectExtendsCycles(scope, []);
|
|
225
252
|
}
|
|
226
253
|
|
|
227
254
|
const rootIndexPath = path.join(xdrsRoot, 'index.md');
|
|
228
255
|
if (!existsFile(rootIndexPath)) {
|
|
229
256
|
errors.push('Missing required root index: .xdrs/index.md [_core-adr-policy-001]');
|
|
230
257
|
} else {
|
|
231
|
-
lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, effectiveExternalScopes);
|
|
258
|
+
lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, warnings, effectiveExternalScopes, extendsRelationships);
|
|
232
259
|
}
|
|
233
260
|
|
|
234
261
|
return { xdrsRoot, errors, warnings, readOnlyScopes };
|
|
235
262
|
}
|
|
236
263
|
|
|
237
|
-
function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, externalScopes = new Set()) {
|
|
264
|
+
function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, warnings = [], externalScopes = new Set(), extendsRelationships = new Map()) {
|
|
238
265
|
const content = fs.readFileSync(rootIndexPath, 'utf8');
|
|
239
266
|
const repoRoot = path.dirname(xdrsRoot);
|
|
240
267
|
|
|
@@ -263,6 +290,12 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
263
290
|
}
|
|
264
291
|
}
|
|
265
292
|
|
|
293
|
+
// Build set of all scopes referenced via any extends: chain
|
|
294
|
+
const extendsReferencedScopes = new Set();
|
|
295
|
+
for (const entries of extendsRelationships.values()) {
|
|
296
|
+
for (const e of entries) extendsReferencedScopes.add(e);
|
|
297
|
+
}
|
|
298
|
+
|
|
266
299
|
// Collect non-_local scopes that have type indexes and check their scope index is linked
|
|
267
300
|
const scopesWithTypeIndexes = new Set();
|
|
268
301
|
for (const indexPath of actualTypeIndexes) {
|
|
@@ -275,7 +308,16 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
275
308
|
for (const scopeName of scopesWithTypeIndexes) {
|
|
276
309
|
const scopeIndexPath = normalizePath(path.join(xdrsRoot, scopeName, 'index.md'));
|
|
277
310
|
if (!linkedSet.has(scopeIndexPath)) {
|
|
311
|
+
// Exempt scopes that are only reachable via extends: chains
|
|
312
|
+
if (extendsReferencedScopes.has(scopeName)) {
|
|
313
|
+
// Scope is extends-only (not in root index) — exempt from mandatory link requirement
|
|
314
|
+
// No warning here: absence from root index is expected and valid for extends-only scopes
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
278
317
|
errors.push(`Root index is missing scope index link: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))} [_core-adr-policy-001]`);
|
|
318
|
+
} else if (extendsReferencedScopes.has(scopeName)) {
|
|
319
|
+
// Scope is in BOTH root index and extends chain — ambiguous dual precedence
|
|
320
|
+
warnings.push(`Scope "${scopeName}" appears in both the root index and an extends: chain; precedence may be ambiguous for consumers that do not follow extends declarations: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))} [_core-adr-policy-010.35-extends-index]`);
|
|
279
321
|
}
|
|
280
322
|
}
|
|
281
323
|
}
|
|
@@ -316,7 +358,7 @@ function extractParentScopeType(policyFilePath) {
|
|
|
316
358
|
return bodyMatch ? bodyMatch[1] : null;
|
|
317
359
|
}
|
|
318
360
|
|
|
319
|
-
function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), readOnlyScopes = new Set()) {
|
|
361
|
+
function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), readOnlyScopes = new Set(), extendsRelationships = new Map()) {
|
|
320
362
|
const content = fs.readFileSync(scopeIndexPath, 'utf8');
|
|
321
363
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
322
364
|
if (!fmMatch) {
|
|
@@ -528,24 +570,27 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
528
570
|
}
|
|
529
571
|
}
|
|
530
572
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
573
|
+
// Parse extends: field
|
|
574
|
+
let extendsEntries = [];
|
|
575
|
+
const extendsLineMatch = block.match(/^extends:[ \t]*(.*)/m);
|
|
576
|
+
if (extendsLineMatch !== null) {
|
|
577
|
+
const extendsValue = extendsLineMatch[1].trim();
|
|
534
578
|
const scopeNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
579
|
+
const commaSeparatedPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*,\s*[a-zA-Z_][a-zA-Z0-9_-]*)+$/;
|
|
580
|
+
if (extendsValue) {
|
|
581
|
+
if (!scopeNamePattern.test(extendsValue) && !commaSeparatedPattern.test(extendsValue)) {
|
|
582
|
+
errors.push(`Scope index frontmatter extends must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
538
583
|
}
|
|
539
584
|
} else {
|
|
540
|
-
const
|
|
585
|
+
const extendsStart = block.indexOf(extendsLineMatch[0]) + extendsLineMatch[0].length;
|
|
541
586
|
const listItems = [];
|
|
542
587
|
let hasInvalidEntry = false;
|
|
543
|
-
for (const line of block.slice(
|
|
588
|
+
for (const line of block.slice(extendsStart).split('\n')) {
|
|
544
589
|
const itemMatch = line.match(/^\s+-\s*(\S.*)?$/);
|
|
545
590
|
if (itemMatch) {
|
|
546
591
|
const item = (itemMatch[1] || '').trim();
|
|
547
592
|
if (!item || !scopeNamePattern.test(item)) {
|
|
548
|
-
errors.push(`Scope index frontmatter
|
|
593
|
+
errors.push(`Scope index frontmatter extends entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
549
594
|
hasInvalidEntry = true;
|
|
550
595
|
break;
|
|
551
596
|
}
|
|
@@ -555,45 +600,63 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
555
600
|
}
|
|
556
601
|
}
|
|
557
602
|
if (!hasInvalidEntry && listItems.length === 0) {
|
|
558
|
-
errors.push(`Scope index frontmatter
|
|
603
|
+
errors.push(`Scope index frontmatter extends must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
559
604
|
}
|
|
560
605
|
}
|
|
561
|
-
|
|
562
|
-
|
|
606
|
+
const rawEntries = extendsValue
|
|
607
|
+
? (commaSeparatedPattern.test(extendsValue)
|
|
608
|
+
? extendsValue.split(',').map((s) => s.trim())
|
|
609
|
+
: (scopeNamePattern.test(extendsValue) ? [extendsValue] : []))
|
|
610
|
+
: (() => {
|
|
563
611
|
const items = [];
|
|
564
|
-
const
|
|
565
|
-
for (const line of block.slice(
|
|
612
|
+
const extendsStart = block.indexOf(extendsLineMatch[0]) + extendsLineMatch[0].length;
|
|
613
|
+
for (const line of block.slice(extendsStart).split('\n')) {
|
|
566
614
|
const itemMatch = line.match(/^\s+-\s*(\S.*)?$/);
|
|
567
615
|
if (itemMatch && itemMatch[1]) items.push(itemMatch[1].trim());
|
|
568
616
|
else if (line.trim() && !/^\s/.test(line)) break;
|
|
569
617
|
}
|
|
570
618
|
return items;
|
|
571
619
|
})();
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
620
|
+
const seenExtends = new Set();
|
|
621
|
+
for (const entry of rawEntries) {
|
|
622
|
+
if (seenExtends.has(entry)) {
|
|
623
|
+
errors.push(`Scope index frontmatter extends has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
seenExtends.add(entry);
|
|
627
|
+
if (!scopeNamePattern.test(entry)) continue;
|
|
628
|
+
if (entry === scopeName) {
|
|
629
|
+
errors.push(`Scope index frontmatter extends must not reference the scope itself: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.30-extends-no-self]`);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (entry === '_local' || entry === '_core') {
|
|
633
|
+
errors.push(`Scope index frontmatter extends must not reference reserved scope "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.29-extends-reserved-scopes]`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
extendsEntries.push(entry);
|
|
637
|
+
}
|
|
638
|
+
if (xdrsRoot) {
|
|
639
|
+
const seenValid = new Set();
|
|
640
|
+
for (const entry of extendsEntries) {
|
|
641
|
+
if (seenValid.has(entry)) continue;
|
|
642
|
+
seenValid.add(entry);
|
|
643
|
+
if (followsEntries.includes(entry)) {
|
|
644
|
+
errors.push(`Scope index frontmatter extends must not repeat scope "${entry}" already declared in follows: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.28-extends-disjoint]`);
|
|
645
|
+
continue;
|
|
576
646
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (entry === scopeName) {
|
|
580
|
-
errors.push(`Scope index frontmatter related-scopes must not reference the scope itself: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
if (followsEntries.includes(entry)) {
|
|
584
|
-
errors.push(`Scope index frontmatter related-scopes must not repeat scope "${entry}" already declared in follows: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
if (!existsFile(path.join(xdrsRoot, entry, 'index.md'))) {
|
|
588
|
-
errors.push(`Scope index frontmatter related-scopes references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
589
|
-
}
|
|
647
|
+
if (!existsFile(path.join(xdrsRoot, entry, 'index.md'))) {
|
|
648
|
+
errors.push(`Scope index frontmatter extends references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.32-extends-mandatory-presence]`);
|
|
590
649
|
}
|
|
591
650
|
}
|
|
651
|
+
if (extendsEntries.length > 0) {
|
|
652
|
+
extendsRelationships.set(scopeName, extendsEntries.filter((e) => existsFile(path.join(xdrsRoot, e, 'index.md'))));
|
|
653
|
+
}
|
|
592
654
|
}
|
|
593
655
|
}
|
|
594
656
|
}
|
|
595
657
|
|
|
596
|
-
|
|
658
|
+
|
|
659
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), readOnlyScopes = new Set(), extendsRelationships = new Map()) {
|
|
597
660
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
598
661
|
|
|
599
662
|
if (ignoreExternal && externalScopes.has(scopeName)) {
|
|
@@ -653,7 +716,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
653
716
|
errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
654
717
|
} else {
|
|
655
718
|
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors, externalScopes);
|
|
656
|
-
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes);
|
|
719
|
+
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes, extendsRelationships);
|
|
657
720
|
}
|
|
658
721
|
lintLocalMetaPolicies(xdrsRoot, scopeName, errors);
|
|
659
722
|
}
|
package/lib/lint.test.js
CHANGED
|
@@ -1733,83 +1733,198 @@ test('reports scope index description exceeding 40 words', () => {
|
|
|
1733
1733
|
expect(result.errors.join('\n')).toContain('Scope index frontmatter description must be 40 words or fewer');
|
|
1734
1734
|
});
|
|
1735
1735
|
|
|
1736
|
-
test('
|
|
1737
|
-
const workspaceRoot = createWorkspace('
|
|
1738
|
-
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)'
|
|
1739
|
-
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nrelated-scopes
|
|
1736
|
+
test('reports related-scopes as unknown frontmatter field in scope index (field removed)', () => {
|
|
1737
|
+
const workspaceRoot = createWorkspace('scope-related-scopes-unknown-field', {
|
|
1738
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1739
|
+
'.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',
|
|
1740
1740
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1741
1741
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1742
|
-
'.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',
|
|
1743
|
-
'.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1744
|
-
'.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1745
|
-
'.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',
|
|
1746
|
-
'.xdrs/parent-domain/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1747
|
-
'.xdrs/parent-domain/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1748
1742
|
});
|
|
1749
1743
|
|
|
1750
1744
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1751
1745
|
|
|
1752
|
-
expect(result.errors.join('\n')).
|
|
1746
|
+
expect(result.errors.join('\n')).toContain('unknown field "related-scopes"');
|
|
1753
1747
|
});
|
|
1754
1748
|
|
|
1755
|
-
test('accepts
|
|
1756
|
-
const workspaceRoot = createWorkspace('
|
|
1757
|
-
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[
|
|
1758
|
-
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\
|
|
1749
|
+
test('accepts extends: single scope name in scope index frontmatter', () => {
|
|
1750
|
+
const workspaceRoot = createWorkspace('extends-single-scope', {
|
|
1751
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[shared](shared/index.md)']),
|
|
1752
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: shared\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1759
1753
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1760
1754
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1761
|
-
'.xdrs/
|
|
1762
|
-
'.xdrs/
|
|
1763
|
-
'.xdrs/
|
|
1755
|
+
'.xdrs/shared/index.md': '---\nscope-type: standard\nname: shared\ndescription: Shared policies.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1756
|
+
'.xdrs/shared/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Shared decision']),
|
|
1757
|
+
'.xdrs/shared/adrs/principles/001-team.md': teamXdrDocument('Shared decision.'),
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1761
|
+
|
|
1762
|
+
expect(result.errors.filter((e) => /\[_core-adr-policy-010\.\d+-extends-/.test(e)).join('\n')).toBe('');
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
test('accepts extends: multiple scopes comma-separated in scope index frontmatter', () => {
|
|
1766
|
+
const workspaceRoot = createWorkspace('extends-multiple-scopes', {
|
|
1767
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[shared-a](shared-a/index.md)', '[shared-b](shared-b/index.md)']),
|
|
1768
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: shared-a, shared-b\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1769
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1770
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1771
|
+
'.xdrs/shared-a/index.md': '---\nscope-type: standard\nname: shared-a\ndescription: Shared A.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared-a Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1772
|
+
'.xdrs/shared-a/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Shared A decision']),
|
|
1773
|
+
'.xdrs/shared-a/adrs/principles/001-team.md': teamXdrDocument('Shared A decision.'),
|
|
1774
|
+
'.xdrs/shared-b/index.md': '---\nscope-type: standard\nname: shared-b\ndescription: Shared B.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared-b Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1775
|
+
'.xdrs/shared-b/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Shared B decision']),
|
|
1776
|
+
'.xdrs/shared-b/adrs/principles/001-team.md': teamXdrDocument('Shared B decision.'),
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1780
|
+
|
|
1781
|
+
expect(result.errors.filter((e) => /\[_core-adr-policy-010\.\d+-extends-/.test(e)).join('\n')).toBe('');
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
test('accepts extends: YAML list format in scope index frontmatter', () => {
|
|
1785
|
+
const workspaceRoot = createWorkspace('extends-yaml-list', {
|
|
1786
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[shared-a](shared-a/index.md)', '[shared-b](shared-b/index.md)']),
|
|
1787
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends:\n - shared-a\n - shared-b\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1788
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1789
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1790
|
+
'.xdrs/shared-a/index.md': '---\nscope-type: standard\nname: shared-a\ndescription: Shared A.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared-a Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1791
|
+
'.xdrs/shared-a/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Shared A decision']),
|
|
1792
|
+
'.xdrs/shared-a/adrs/principles/001-team.md': teamXdrDocument('Shared A decision.'),
|
|
1793
|
+
'.xdrs/shared-b/index.md': '---\nscope-type: standard\nname: shared-b\ndescription: Shared B.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared-b Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1794
|
+
'.xdrs/shared-b/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Shared B decision']),
|
|
1795
|
+
'.xdrs/shared-b/adrs/principles/001-team.md': teamXdrDocument('Shared B decision.'),
|
|
1764
1796
|
});
|
|
1765
1797
|
|
|
1766
1798
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1767
1799
|
|
|
1768
|
-
expect(result.errors.join('\n')).
|
|
1800
|
+
expect(result.errors.filter((e) => /\[_core-adr-policy-010\.\d+-extends-/.test(e)).join('\n')).toBe('');
|
|
1769
1801
|
});
|
|
1770
1802
|
|
|
1771
|
-
test('reports
|
|
1772
|
-
const workspaceRoot = createWorkspace('
|
|
1803
|
+
test('reports extends: referencing a missing scope', () => {
|
|
1804
|
+
const workspaceRoot = createWorkspace('extends-missing-scope', {
|
|
1773
1805
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1774
|
-
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\
|
|
1806
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: nonexistent-scope\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1775
1807
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1776
1808
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1777
1809
|
});
|
|
1778
1810
|
|
|
1779
1811
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1780
1812
|
|
|
1781
|
-
expect(result.errors.join('\n')).toContain('
|
|
1813
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.32-extends-mandatory-presence]');
|
|
1814
|
+
expect(result.errors.join('\n')).toContain('nonexistent-scope');
|
|
1782
1815
|
});
|
|
1783
1816
|
|
|
1784
|
-
test('reports
|
|
1785
|
-
const workspaceRoot = createWorkspace('
|
|
1817
|
+
test('reports extends: self-reference in scope index frontmatter', () => {
|
|
1818
|
+
const workspaceRoot = createWorkspace('extends-self-reference', {
|
|
1786
1819
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1787
|
-
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\
|
|
1820
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: myteam\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1788
1821
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1789
1822
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1790
1823
|
});
|
|
1791
1824
|
|
|
1792
1825
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1793
1826
|
|
|
1794
|
-
expect(result.errors.join('\n')).toContain('
|
|
1827
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.30-extends-no-self]');
|
|
1795
1828
|
});
|
|
1796
1829
|
|
|
1797
|
-
test('
|
|
1798
|
-
const workspaceRoot = createWorkspace('
|
|
1799
|
-
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)'
|
|
1800
|
-
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\
|
|
1830
|
+
test('reports extends: referencing reserved scope _local', () => {
|
|
1831
|
+
const workspaceRoot = createWorkspace('extends-reserved-local', {
|
|
1832
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1833
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: _local\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1801
1834
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1802
1835
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1803
|
-
'.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',
|
|
1804
|
-
'.xdrs/sibling-team/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1805
|
-
'.xdrs/sibling-team/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1806
1836
|
});
|
|
1807
1837
|
|
|
1808
1838
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1809
1839
|
|
|
1810
|
-
expect(result.errors.join('\n')).
|
|
1840
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.29-extends-reserved-scopes]');
|
|
1811
1841
|
});
|
|
1812
1842
|
|
|
1843
|
+
test('reports extends: referencing reserved scope _core', () => {
|
|
1844
|
+
const workspaceRoot = createWorkspace('extends-reserved-core', {
|
|
1845
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1846
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: _core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1847
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1848
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1852
|
+
|
|
1853
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.29-extends-reserved-scopes]');
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
test('reports extends: cycle between two scopes', () => {
|
|
1857
|
+
const workspaceRoot = createWorkspace('extends-cycle', {
|
|
1858
|
+
'.xdrs/index.md': rootIndex(['[scope-a](scope-a/index.md)', '[scope-b](scope-b/index.md)']),
|
|
1859
|
+
'.xdrs/scope-a/index.md': '---\nscope-type: standard\nname: scope-a\ndescription: Scope A.\napply-to: Scope A\nvalid-from: 2026-01-01\nextends: scope-b\n---\n\n# scope-a Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1860
|
+
'.xdrs/scope-a/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - A decision']),
|
|
1861
|
+
'.xdrs/scope-a/adrs/principles/001-team.md': teamXdrDocument('A decision.'),
|
|
1862
|
+
'.xdrs/scope-b/index.md': '---\nscope-type: standard\nname: scope-b\ndescription: Scope B.\napply-to: Scope B\nvalid-from: 2026-01-01\nextends: scope-a\n---\n\n# scope-b Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1863
|
+
'.xdrs/scope-b/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - B decision']),
|
|
1864
|
+
'.xdrs/scope-b/adrs/principles/001-team.md': teamXdrDocument('B decision.'),
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1868
|
+
|
|
1869
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.31-extends-no-cycle]');
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
test('reports extends: and follows: referencing same scope (disjoint violation)', () => {
|
|
1873
|
+
const workspaceRoot = createWorkspace('extends-follows-overlap', {
|
|
1874
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
|
|
1875
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\nextends: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1876
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1877
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1878
|
+
'.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',
|
|
1879
|
+
'.xdrs/myarea-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Core decision']),
|
|
1880
|
+
'.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Core decision.'),
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1884
|
+
|
|
1885
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.28-extends-disjoint]');
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
test('extends-only scope not in root index is exempt from missing link error', () => {
|
|
1889
|
+
const workspaceRoot = createWorkspace('extends-only-scope-not-in-index', {
|
|
1890
|
+
// shared-policies is NOT linked in the root index — it is only referenced via extends:
|
|
1891
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[myteam](myteam/index.md)']),
|
|
1892
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: shared-policies\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1893
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1894
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1895
|
+
'.xdrs/shared-policies/index.md': '---\nscope-type: standard\nname: shared-policies\ndescription: Shared policies.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared-policies Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1896
|
+
'.xdrs/shared-policies/adrs/index.md': teamAdrIndex(['- [001-shared](principles/001-shared.md) - Shared decision']),
|
|
1897
|
+
'.xdrs/shared-policies/adrs/principles/001-shared.md': teamXdrDocument('Shared decision.'),
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1901
|
+
|
|
1902
|
+
// shared-policies should NOT trigger "missing root index link" since it is extends-only
|
|
1903
|
+
expect(result.errors.filter((e) => e.includes('Root index is missing scope index link') && e.includes('shared-policies')).length).toBe(0);
|
|
1904
|
+
// No extends: validation errors should fire for the extends: field
|
|
1905
|
+
expect(result.errors.filter((e) => /\[_core-adr-policy-010\.\d+-extends-/.test(e)).join('\n')).toBe('');
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
test('warns when extends-referenced scope also appears in root index', () => {
|
|
1909
|
+
const workspaceRoot = createWorkspace('extends-scope-in-root-index', {
|
|
1910
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[myteam](myteam/index.md)', '[shared-policies](shared-policies/index.md)']),
|
|
1911
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: shared-policies\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1912
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
1913
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
1914
|
+
'.xdrs/shared-policies/index.md': '---\nscope-type: standard\nname: shared-policies\ndescription: Shared policies.\napply-to: All teams\nvalid-from: 2026-01-01\n---\n\n# shared-policies Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
1915
|
+
'.xdrs/shared-policies/adrs/index.md': teamAdrIndex(['- [001-shared](principles/001-shared.md) - Shared decision']),
|
|
1916
|
+
'.xdrs/shared-policies/adrs/principles/001-shared.md': teamXdrDocument('Shared decision.'),
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1920
|
+
|
|
1921
|
+
expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.35-extends-index]');
|
|
1922
|
+
expect(result.warnings.join('\n')).toContain('[_core-adr-policy-010.35-extends-index]');
|
|
1923
|
+
expect(result.warnings.join('\n')).toContain('shared-policies');
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
|
|
1813
1928
|
test('reports duplicate entries in follows list', () => {
|
|
1814
1929
|
const workspaceRoot = createWorkspace('follows-duplicate-entries', {
|
|
1815
1930
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
|
|
@@ -1842,7 +1957,7 @@ test('reports duplicate entries in follows comma-separated list', () => {
|
|
|
1842
1957
|
expect(result.errors.join('\n')).toContain('follows has duplicate entry "myarea-core"');
|
|
1843
1958
|
});
|
|
1844
1959
|
|
|
1845
|
-
test('reports related-scopes that references the scope itself', () => {
|
|
1960
|
+
test('reports related-scopes that references the scope itself (field now removed, reports unknown field)', () => {
|
|
1846
1961
|
const workspaceRoot = createWorkspace('related-scopes-self-reference', {
|
|
1847
1962
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1848
1963
|
'.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',
|
|
@@ -1852,10 +1967,10 @@ test('reports related-scopes that references the scope itself', () => {
|
|
|
1852
1967
|
|
|
1853
1968
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1854
1969
|
|
|
1855
|
-
expect(result.errors.join('\n')).toContain('related-scopes
|
|
1970
|
+
expect(result.errors.join('\n')).toContain('unknown field "related-scopes"');
|
|
1856
1971
|
});
|
|
1857
1972
|
|
|
1858
|
-
test('reports duplicate entries in related-scopes list', () => {
|
|
1973
|
+
test('reports duplicate entries in related-scopes list (field now removed, reports unknown field)', () => {
|
|
1859
1974
|
const workspaceRoot = createWorkspace('related-scopes-duplicate-entries', {
|
|
1860
1975
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[sibling-team](sibling-team/index.md)']),
|
|
1861
1976
|
'.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',
|
|
@@ -1868,10 +1983,10 @@ test('reports duplicate entries in related-scopes list', () => {
|
|
|
1868
1983
|
|
|
1869
1984
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1870
1985
|
|
|
1871
|
-
expect(result.errors.join('\n')).toContain('
|
|
1986
|
+
expect(result.errors.join('\n')).toContain('unknown field "related-scopes"');
|
|
1872
1987
|
});
|
|
1873
1988
|
|
|
1874
|
-
test('reports related-scopes entry
|
|
1989
|
+
test('reports related-scopes entry already in follows (field now removed, reports unknown field)', () => {
|
|
1875
1990
|
const workspaceRoot = createWorkspace('related-scopes-follows-overlap', {
|
|
1876
1991
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
|
|
1877
1992
|
'.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',
|
|
@@ -1884,7 +1999,7 @@ test('reports related-scopes entry that is already declared in follows', () => {
|
|
|
1884
1999
|
|
|
1885
2000
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1886
2001
|
|
|
1887
|
-
expect(result.errors.join('\n')).toContain('
|
|
2002
|
+
expect(result.errors.join('\n')).toContain('unknown field "related-scopes"');
|
|
1888
2003
|
});
|
|
1889
2004
|
|
|
1890
2005
|
test('reports orphan asset files not referenced by any document', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xdrs-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "A framework to structure, compile and distribute Architectural (ADR), Business (BDR), and Engineering (EDR) decision records contents so that AI agents and humans can reliably find and use them with hierarchical scopes and controlled rollout in the format of distributable versioned packages.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|