xdrs-core 0.45.0 → 0.46.1
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 +1 -0
- package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +3 -2
- package/.xdrs/_core/adrs/principles/002-policy-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +1 -2
- package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -2
- package/.xdrs/_core/adrs/principles/006-research-standards.md +6 -5
- package/.xdrs/_core/adrs/principles/007-plan-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/009-presentation-standards.md +2 -1
- package/.xdrs/_core/adrs/principles/020-media-and-asset-standards.md +54 -0
- package/.xdrs/_core/adrs/principles/articles/.assets/001-xdrs-overview.svg +258 -0
- package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +2 -0
- package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +1 -0
- package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +2 -3
- package/.xdrs/_core/index.md +4 -0
- package/README.md +4 -0
- package/lib/lint.js +59 -9
- package/lib/lint.test.js +503 -0
- package/package.json +1 -1
package/.xdrs/_core/index.md
CHANGED
|
@@ -45,6 +45,10 @@ Each artifact type has its own writing standard:
|
|
|
45
45
|
|
|
46
46
|
Slide presentations that support XDRS documents follow [_core-adr-policy-009](adrs/principles/009-presentation-standards.md). Slides use the Marp Markdown format, live in `.assets/` next to the document they support, and must maintain bidirectional links with the parent document.
|
|
47
47
|
|
|
48
|
+
### Media and asset standards
|
|
49
|
+
|
|
50
|
+
[_core-adr-policy-020](adrs/principles/020-media-and-asset-standards.md) defines the canonical preference order for representing visual information across all XDRS document types: Markdown → ASCII → Mermaid → draw.io → SVG → PNG. It also defines where non-Markdown files must be stored (`.assets/` sibling folder) and prohibits policy content from living inside asset files. All document-type policies and write-* skills reference this policy instead of repeating these rules independently.
|
|
51
|
+
|
|
48
52
|
### Standard scope meta governance
|
|
49
53
|
|
|
50
54
|
[_core-adr-policy-010](adrs/principles/010-scope-governance.md) defines the full scope governance model: how to define a custom scope type using `{scope-type}-scope-type` policies, how to define scope-local content standards using local meta-policies (`NNN-core.md` or `NNN-core-{qualifier}.md` in `principles/`), and how all governance mechanisms (`follows:`, scope-type standards, local meta-policies) apply with their precedence chain.
|
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ Policies capture Architectural (ADR), Business (BDR), and Engineering (EDR) deci
|
|
|
12
12
|
|
|
13
13
|
This project defines a standard for organizing XDRS that satisfies the following requirements.
|
|
14
14
|
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
15
19
|
## XDRS elements
|
|
16
20
|
|
|
17
21
|
A traditional Decision Record normally combines several concerns in the same document:
|
package/lib/lint.js
CHANGED
|
@@ -670,6 +670,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
670
670
|
// Determine if this scope is a core-type scope (but not _core which is exempt)
|
|
671
671
|
// Used to enforce structured format on all policies in core-type scopes
|
|
672
672
|
let isCoreTypeNonExempt = false;
|
|
673
|
+
let isCompiledScope = false;
|
|
673
674
|
if (scopeName !== '_core') {
|
|
674
675
|
const scopeIndexPath = path.join(scopePath, 'index.md');
|
|
675
676
|
if (existsFile(scopeIndexPath)) {
|
|
@@ -678,10 +679,12 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
678
679
|
const stMatch = idxContent.match(/^scope-type:\s*(.+)$/m);
|
|
679
680
|
if (stMatch) {
|
|
680
681
|
const stRaw = stMatch[1].trim();
|
|
682
|
+
// Handle both "[type1, type2]" YAML array format and "type1, type2" inline CSV format
|
|
681
683
|
const stList = stRaw.startsWith('[') && stRaw.endsWith(']')
|
|
682
684
|
? stRaw.slice(1, -1).split(',').map((s) => s.trim())
|
|
683
|
-
:
|
|
685
|
+
: stRaw.split(',').map((s) => s.trim());
|
|
684
686
|
isCoreTypeNonExempt = stList.includes('core');
|
|
687
|
+
isCompiledScope = stList.includes('compiled');
|
|
685
688
|
}
|
|
686
689
|
} catch { /* ignore read errors; will be caught later */ }
|
|
687
690
|
}
|
|
@@ -696,7 +699,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
696
699
|
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
697
700
|
continue;
|
|
698
701
|
}
|
|
699
|
-
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, warnings, actualTypeIndexes, externalScopes, isCoreTypeNonExempt);
|
|
702
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, warnings, actualTypeIndexes, externalScopes, isCoreTypeNonExempt, isCompiledScope);
|
|
700
703
|
const typeIndexPath = path.join(entryPath, 'index.md');
|
|
701
704
|
if (existsFile(typeIndexPath)) {
|
|
702
705
|
typeIndexesInScope.push(typeIndexPath);
|
|
@@ -718,10 +721,10 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
718
721
|
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors, externalScopes);
|
|
719
722
|
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes, extendsRelationships);
|
|
720
723
|
}
|
|
721
|
-
lintLocalMetaPolicies(xdrsRoot, scopeName, errors);
|
|
724
|
+
lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope);
|
|
722
725
|
}
|
|
723
726
|
|
|
724
|
-
function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
727
|
+
function lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope = false) {
|
|
725
728
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
726
729
|
// Track qualifiers across all type folders: qualifier ('' for primary) -> filePath
|
|
727
730
|
const seenQualifiers = new Map();
|
|
@@ -757,6 +760,21 @@ function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
|
757
760
|
errors.push(`Local meta-policy companion "${path.basename(companion.filePath)}" requires a primary "NNN-core.md" in the same type folder "${typeName}": ${toDisplayPath(companion.filePath)} [_core-adr-policy-010.10b-local-primary-required]`);
|
|
758
761
|
}
|
|
759
762
|
}
|
|
763
|
+
// For compiled scopes: primary meta-policy must include ## Sources and ## Selectors
|
|
764
|
+
if (isCompiledScope && primaryInFolder) {
|
|
765
|
+
let metaContent;
|
|
766
|
+
try { metaContent = fs.readFileSync(primaryInFolder.filePath, 'utf8'); } catch { /* will be caught elsewhere */ }
|
|
767
|
+
if (metaContent !== undefined) {
|
|
768
|
+
const metaLines = metaContent.split(/\r?\n/);
|
|
769
|
+
const metaIgnored = findIgnoredMarkdownLines(metaLines);
|
|
770
|
+
if (findHeadingLine(metaLines, metaIgnored, '## Sources') === -1) {
|
|
771
|
+
errors.push(`Compiled scope meta-policy must include a "## Sources" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
|
|
772
|
+
}
|
|
773
|
+
if (findHeadingLine(metaLines, metaIgnored, '## Selectors') === -1) {
|
|
774
|
+
errors.push(`Compiled scope meta-policy must include a "## Selectors" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
760
778
|
// Check qualifier uniqueness across all type folders
|
|
761
779
|
const allInFolder = primaryInFolder ? [primaryInFolder, ...companionsInFolder] : companionsInFolder;
|
|
762
780
|
for (const { filePath, qualifier } of allInFolder) {
|
|
@@ -773,7 +791,7 @@ function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
|
773
791
|
}
|
|
774
792
|
}
|
|
775
793
|
|
|
776
|
-
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false) {
|
|
794
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
777
795
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
778
796
|
const indexPath = path.join(typePath, 'index.md');
|
|
779
797
|
const xdrsNumbers = new Map();
|
|
@@ -804,7 +822,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
804
822
|
warnings.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001] [exempt: freeze-reference]`);
|
|
805
823
|
}
|
|
806
824
|
|
|
807
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, warnings, externalScopes, enforceStructured));
|
|
825
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, warnings, externalScopes, enforceStructured, isCompiledScope));
|
|
808
826
|
}
|
|
809
827
|
|
|
810
828
|
if (existsFile(indexPath)) {
|
|
@@ -812,7 +830,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
812
830
|
}
|
|
813
831
|
}
|
|
814
832
|
|
|
815
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false) {
|
|
833
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
816
834
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
817
835
|
const artifacts = [];
|
|
818
836
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -851,7 +869,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
851
869
|
}
|
|
852
870
|
|
|
853
871
|
artifacts.push(entryPath);
|
|
854
|
-
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, warnings, externalScopes, enforceStructured);
|
|
872
|
+
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, warnings, externalScopes, enforceStructured, isCompiledScope);
|
|
855
873
|
}
|
|
856
874
|
|
|
857
875
|
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
@@ -861,7 +879,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
861
879
|
return artifacts;
|
|
862
880
|
}
|
|
863
881
|
|
|
864
|
-
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false) {
|
|
882
|
+
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
865
883
|
const baseName = path.basename(filePath);
|
|
866
884
|
const match = baseName.match(NUMBERED_FILE_RE);
|
|
867
885
|
if (!match) {
|
|
@@ -903,9 +921,15 @@ function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePat
|
|
|
903
921
|
lintXdrsElementFrontmatter(content, expectedName, filePath, errors, warnings, freezeReference);
|
|
904
922
|
if (enforceStructured) {
|
|
905
923
|
validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-010.08c-def-core-scope-policies-must-be-structured');
|
|
924
|
+
} else if (isCompiledScope) {
|
|
925
|
+
validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-019.04-structured-format-required');
|
|
906
926
|
} else {
|
|
907
927
|
lintStructuredRuleBlocks(content, filePath, errors);
|
|
908
928
|
}
|
|
929
|
+
const isLocalMetaPolicy = subjectName === 'principles' && titleParts[0] === 'core';
|
|
930
|
+
if (isCompiledScope && !isLocalMetaPolicy) {
|
|
931
|
+
lintCompiledPolicySourceSection(content, filePath, errors);
|
|
932
|
+
}
|
|
909
933
|
lintRequiredSections(content, filePath, XDRS_REQUIRED_SECTIONS, 'Policy', errors);
|
|
910
934
|
lintNoEmojis(content, filePath, 'Policy', errors);
|
|
911
935
|
lintWordCount(content, filePath, 'Policy', POLICY_MAX_WORDS, errors);
|
|
@@ -1708,6 +1732,32 @@ function validateStructuredFormatContent(content, filePath, errors, errorCode) {
|
|
|
1708
1732
|
lintStructuredRuleBlocks(content, filePath, errors);
|
|
1709
1733
|
}
|
|
1710
1734
|
|
|
1735
|
+
function lintCompiledPolicySourceSection(content, filePath, errors) {
|
|
1736
|
+
const lines = content.split(/\r?\n/);
|
|
1737
|
+
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
1738
|
+
const sourceStart = findHeadingLine(lines, ignoredLines, '## Source');
|
|
1739
|
+
if (sourceStart === -1) {
|
|
1740
|
+
errors.push(`Compiled policy must include a "## Source" section: ${toDisplayPath(filePath)} [_core-adr-policy-019.10-source-section]`);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
// Find end of ## Source section (next ## heading or EOF)
|
|
1744
|
+
let sourceEnd = lines.length;
|
|
1745
|
+
for (let i = sourceStart + 1; i < lines.length; i++) {
|
|
1746
|
+
if (!ignoredLines[i] && /^## /.test(lines[i])) {
|
|
1747
|
+
sourceEnd = i;
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
// Paths in ## Source MUST NOT be written as markdown links
|
|
1752
|
+
const MD_LINK_RE = /\[.+?\]\(.+?\)/;
|
|
1753
|
+
for (let i = sourceStart + 1; i < sourceEnd; i++) {
|
|
1754
|
+
if (!ignoredLines[i] && MD_LINK_RE.test(lines[i])) {
|
|
1755
|
+
errors.push(`Compiled policy "## Source" section must not use markdown links (use plain text paths): ${toDisplayPath(filePath)}:${i + 1} [_core-adr-policy-019.10-source-section]`);
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1711
1761
|
function findIgnoredMarkdownLines(lines) {
|
|
1712
1762
|
const ignored = [];
|
|
1713
1763
|
let inCodeFence = false;
|
package/lib/lint.test.js
CHANGED
|
@@ -2456,6 +2456,509 @@ test('does not require structured format for _core scope policies (exempt)', ()
|
|
|
2456
2456
|
expect(result.errors.join('\n')).not.toContain('08c-def-core-scope-policies-must-be-structured');
|
|
2457
2457
|
});
|
|
2458
2458
|
|
|
2459
|
+
// ─── compiled scope-type checks ───────────────────────────────────────────────
|
|
2460
|
+
|
|
2461
|
+
function compiledScopeIndex() {
|
|
2462
|
+
return [
|
|
2463
|
+
'---',
|
|
2464
|
+
'scope-type: compiled',
|
|
2465
|
+
'name: my-compiled',
|
|
2466
|
+
'description: Compiled scope for tests.',
|
|
2467
|
+
'apply-to: All consumers of compiled content',
|
|
2468
|
+
'valid-from: 2026-01-01',
|
|
2469
|
+
'---',
|
|
2470
|
+
'',
|
|
2471
|
+
'# my-compiled Scope Overview',
|
|
2472
|
+
'',
|
|
2473
|
+
'Compiled scope for tests.',
|
|
2474
|
+
'',
|
|
2475
|
+
'[ADRs](adrs/index.md)',
|
|
2476
|
+
''
|
|
2477
|
+
].join('\n');
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function compiledAdrIndex(entries) {
|
|
2481
|
+
return [
|
|
2482
|
+
'# my-compiled ADR Index',
|
|
2483
|
+
'',
|
|
2484
|
+
'Compiled ADRs for tests.',
|
|
2485
|
+
'',
|
|
2486
|
+
'## principles',
|
|
2487
|
+
'',
|
|
2488
|
+
...entries,
|
|
2489
|
+
''
|
|
2490
|
+
].join('\n');
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function validCompiledPolicy() {
|
|
2494
|
+
return [
|
|
2495
|
+
'---',
|
|
2496
|
+
'name: my-compiled-adr-policy-001-a01',
|
|
2497
|
+
'description: Compiled test policy.',
|
|
2498
|
+
'apply-to: All teams',
|
|
2499
|
+
'valid-from: 2026-01-01',
|
|
2500
|
+
'---',
|
|
2501
|
+
'',
|
|
2502
|
+
'# my-compiled-adr-policy-001: A01',
|
|
2503
|
+
'',
|
|
2504
|
+
'## Context and Problem Statement',
|
|
2505
|
+
'',
|
|
2506
|
+
'External source defines this requirement.',
|
|
2507
|
+
'',
|
|
2508
|
+
'## Decision Outcome',
|
|
2509
|
+
'',
|
|
2510
|
+
'Follow the rule.',
|
|
2511
|
+
'',
|
|
2512
|
+
'### Details',
|
|
2513
|
+
'',
|
|
2514
|
+
'#### 01-rule',
|
|
2515
|
+
'',
|
|
2516
|
+
'MUST comply.',
|
|
2517
|
+
'',
|
|
2518
|
+
'## Source',
|
|
2519
|
+
'',
|
|
2520
|
+
'- source-1/standards/a01.md',
|
|
2521
|
+
''
|
|
2522
|
+
].join('\n');
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function validCompiledMetaPolicy() {
|
|
2526
|
+
return [
|
|
2527
|
+
'---',
|
|
2528
|
+
'name: my-compiled-adr-policy-001-core',
|
|
2529
|
+
'description: Compilation meta-policy for adrs.',
|
|
2530
|
+
'apply-to: Compilation process for this scope',
|
|
2531
|
+
'valid-from: 2026-01-01',
|
|
2532
|
+
'---',
|
|
2533
|
+
'',
|
|
2534
|
+
'# my-compiled-adr-policy-001: core',
|
|
2535
|
+
'',
|
|
2536
|
+
'## Context and Problem Statement',
|
|
2537
|
+
'',
|
|
2538
|
+
'Defines compilation configuration.',
|
|
2539
|
+
'',
|
|
2540
|
+
'## Decision Outcome',
|
|
2541
|
+
'',
|
|
2542
|
+
'Use the configured sources.',
|
|
2543
|
+
'',
|
|
2544
|
+
'### Details',
|
|
2545
|
+
'',
|
|
2546
|
+
'#### 01-rule',
|
|
2547
|
+
'',
|
|
2548
|
+
'MUST use configured sources.',
|
|
2549
|
+
'',
|
|
2550
|
+
'## Sources',
|
|
2551
|
+
'',
|
|
2552
|
+
'- [web] https://example.com/standards',
|
|
2553
|
+
'',
|
|
2554
|
+
'## Selectors',
|
|
2555
|
+
'',
|
|
2556
|
+
'- Include all requirement sections.',
|
|
2557
|
+
''
|
|
2558
|
+
].join('\n');
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
test('reports policy in compiled scope missing structured format', () => {
|
|
2562
|
+
const workspaceRoot = createWorkspace('compiled-scope-policy-unstructured', {
|
|
2563
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2564
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2565
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2566
|
+
'- [001-a01](principles/001-a01.md) - A01'
|
|
2567
|
+
]),
|
|
2568
|
+
'.xdrs/my-compiled/adrs/principles/001-a01.md': [
|
|
2569
|
+
'---',
|
|
2570
|
+
'name: my-compiled-adr-policy-001-a01',
|
|
2571
|
+
'description: Compiled test policy.',
|
|
2572
|
+
'apply-to: All teams',
|
|
2573
|
+
'valid-from: 2026-01-01',
|
|
2574
|
+
'---',
|
|
2575
|
+
'',
|
|
2576
|
+
'# my-compiled-adr-policy-001: A01',
|
|
2577
|
+
'',
|
|
2578
|
+
'## Context and Problem Statement',
|
|
2579
|
+
'',
|
|
2580
|
+
'External source.',
|
|
2581
|
+
'',
|
|
2582
|
+
'## Decision Outcome',
|
|
2583
|
+
'',
|
|
2584
|
+
'Follow rule.',
|
|
2585
|
+
'',
|
|
2586
|
+
'## Source',
|
|
2587
|
+
'',
|
|
2588
|
+
'- source-1/a01.md',
|
|
2589
|
+
''
|
|
2590
|
+
].join('\n'),
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2594
|
+
|
|
2595
|
+
expect(result.errors.join('\n')).toContain('"### Details"');
|
|
2596
|
+
expect(result.errors.join('\n')).toContain('019.04-structured-format-required');
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
test('reports compiled policy missing ## Source section', () => {
|
|
2600
|
+
const workspaceRoot = createWorkspace('compiled-scope-missing-source', {
|
|
2601
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2602
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2603
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2604
|
+
'- [001-a01](principles/001-a01.md) - A01'
|
|
2605
|
+
]),
|
|
2606
|
+
'.xdrs/my-compiled/adrs/principles/001-a01.md': [
|
|
2607
|
+
'---',
|
|
2608
|
+
'name: my-compiled-adr-policy-001-a01',
|
|
2609
|
+
'description: Compiled test policy.',
|
|
2610
|
+
'apply-to: All teams',
|
|
2611
|
+
'valid-from: 2026-01-01',
|
|
2612
|
+
'---',
|
|
2613
|
+
'',
|
|
2614
|
+
'# my-compiled-adr-policy-001: A01',
|
|
2615
|
+
'',
|
|
2616
|
+
'## Context and Problem Statement',
|
|
2617
|
+
'',
|
|
2618
|
+
'External source.',
|
|
2619
|
+
'',
|
|
2620
|
+
'## Decision Outcome',
|
|
2621
|
+
'',
|
|
2622
|
+
'Follow rule.',
|
|
2623
|
+
'',
|
|
2624
|
+
'### Details',
|
|
2625
|
+
'',
|
|
2626
|
+
'#### 01-rule',
|
|
2627
|
+
'',
|
|
2628
|
+
'MUST comply.',
|
|
2629
|
+
''
|
|
2630
|
+
].join('\n'),
|
|
2631
|
+
});
|
|
2632
|
+
|
|
2633
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2634
|
+
|
|
2635
|
+
expect(result.errors.join('\n')).toContain('Compiled policy must include a "## Source" section');
|
|
2636
|
+
expect(result.errors.join('\n')).toContain('019.10-source-section');
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
test('reports compiled policy with markdown links in ## Source section', () => {
|
|
2640
|
+
const workspaceRoot = createWorkspace('compiled-scope-source-md-links', {
|
|
2641
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2642
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2643
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2644
|
+
'- [001-a01](principles/001-a01.md) - A01'
|
|
2645
|
+
]),
|
|
2646
|
+
'.xdrs/my-compiled/adrs/principles/001-a01.md': [
|
|
2647
|
+
'---',
|
|
2648
|
+
'name: my-compiled-adr-policy-001-a01',
|
|
2649
|
+
'description: Compiled test policy.',
|
|
2650
|
+
'apply-to: All teams',
|
|
2651
|
+
'valid-from: 2026-01-01',
|
|
2652
|
+
'---',
|
|
2653
|
+
'',
|
|
2654
|
+
'# my-compiled-adr-policy-001: A01',
|
|
2655
|
+
'',
|
|
2656
|
+
'## Context and Problem Statement',
|
|
2657
|
+
'',
|
|
2658
|
+
'External source.',
|
|
2659
|
+
'',
|
|
2660
|
+
'## Decision Outcome',
|
|
2661
|
+
'',
|
|
2662
|
+
'Follow rule.',
|
|
2663
|
+
'',
|
|
2664
|
+
'### Details',
|
|
2665
|
+
'',
|
|
2666
|
+
'#### 01-rule',
|
|
2667
|
+
'',
|
|
2668
|
+
'MUST comply.',
|
|
2669
|
+
'',
|
|
2670
|
+
'## Source',
|
|
2671
|
+
'',
|
|
2672
|
+
'- [source-1/a01.md](source-1/a01.md)',
|
|
2673
|
+
''
|
|
2674
|
+
].join('\n'),
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2678
|
+
|
|
2679
|
+
expect(result.errors.join('\n')).toContain('"## Source" section must not use markdown links');
|
|
2680
|
+
expect(result.errors.join('\n')).toContain('019.10-source-section');
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
test('accepts valid compiled policy with ## Source section and no markdown links', () => {
|
|
2684
|
+
const workspaceRoot = createWorkspace('compiled-scope-valid', {
|
|
2685
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2686
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2687
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2688
|
+
'- [001-a01](principles/001-a01.md) - A01'
|
|
2689
|
+
]),
|
|
2690
|
+
'.xdrs/my-compiled/adrs/principles/001-a01.md': validCompiledPolicy(),
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2694
|
+
|
|
2695
|
+
expect(result.errors.join('\n')).not.toContain('019');
|
|
2696
|
+
});
|
|
2697
|
+
|
|
2698
|
+
test('reports compiled scope meta-policy missing ## Sources section', () => {
|
|
2699
|
+
const workspaceRoot = createWorkspace('compiled-meta-missing-sources', {
|
|
2700
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2701
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2702
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2703
|
+
'- [001-core](principles/001-core.md) - meta-policy',
|
|
2704
|
+
'- [002-a01](principles/002-a01.md) - A01'
|
|
2705
|
+
]),
|
|
2706
|
+
'.xdrs/my-compiled/adrs/principles/001-core.md': [
|
|
2707
|
+
'---',
|
|
2708
|
+
'name: my-compiled-adr-policy-001-core',
|
|
2709
|
+
'description: Compilation meta-policy.',
|
|
2710
|
+
'apply-to: Compilation process',
|
|
2711
|
+
'valid-from: 2026-01-01',
|
|
2712
|
+
'---',
|
|
2713
|
+
'',
|
|
2714
|
+
'# my-compiled-adr-policy-001: core',
|
|
2715
|
+
'',
|
|
2716
|
+
'## Context and Problem Statement',
|
|
2717
|
+
'',
|
|
2718
|
+
'Defines compilation config.',
|
|
2719
|
+
'',
|
|
2720
|
+
'## Decision Outcome',
|
|
2721
|
+
'',
|
|
2722
|
+
'Use configured sources.',
|
|
2723
|
+
'',
|
|
2724
|
+
'### Details',
|
|
2725
|
+
'',
|
|
2726
|
+
'#### 01-rule',
|
|
2727
|
+
'',
|
|
2728
|
+
'MUST use configured sources.',
|
|
2729
|
+
'',
|
|
2730
|
+
'## Selectors',
|
|
2731
|
+
'',
|
|
2732
|
+
'- Include requirement sections.',
|
|
2733
|
+
''
|
|
2734
|
+
].join('\n'),
|
|
2735
|
+
'.xdrs/my-compiled/adrs/principles/002-a01.md': [
|
|
2736
|
+
'---',
|
|
2737
|
+
'name: my-compiled-adr-policy-002-a01',
|
|
2738
|
+
'description: Compiled test policy.',
|
|
2739
|
+
'apply-to: All teams',
|
|
2740
|
+
'valid-from: 2026-01-01',
|
|
2741
|
+
'---',
|
|
2742
|
+
'',
|
|
2743
|
+
'# my-compiled-adr-policy-002: A01',
|
|
2744
|
+
'',
|
|
2745
|
+
'## Context and Problem Statement',
|
|
2746
|
+
'',
|
|
2747
|
+
'External source.',
|
|
2748
|
+
'',
|
|
2749
|
+
'## Decision Outcome',
|
|
2750
|
+
'',
|
|
2751
|
+
'Follow rule.',
|
|
2752
|
+
'',
|
|
2753
|
+
'### Details',
|
|
2754
|
+
'',
|
|
2755
|
+
'#### 01-rule',
|
|
2756
|
+
'',
|
|
2757
|
+
'MUST comply.',
|
|
2758
|
+
'',
|
|
2759
|
+
'## Source',
|
|
2760
|
+
'',
|
|
2761
|
+
'- source-1/a01.md',
|
|
2762
|
+
''
|
|
2763
|
+
].join('\n'),
|
|
2764
|
+
});
|
|
2765
|
+
|
|
2766
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2767
|
+
|
|
2768
|
+
expect(result.errors.join('\n')).toContain('"## Sources" section');
|
|
2769
|
+
expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
test('reports compiled scope meta-policy missing ## Selectors section', () => {
|
|
2773
|
+
const workspaceRoot = createWorkspace('compiled-meta-missing-selectors', {
|
|
2774
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2775
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2776
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2777
|
+
'- [001-core](principles/001-core.md) - meta-policy',
|
|
2778
|
+
'- [002-a01](principles/002-a01.md) - A01'
|
|
2779
|
+
]),
|
|
2780
|
+
'.xdrs/my-compiled/adrs/principles/001-core.md': [
|
|
2781
|
+
'---',
|
|
2782
|
+
'name: my-compiled-adr-policy-001-core',
|
|
2783
|
+
'description: Compilation meta-policy.',
|
|
2784
|
+
'apply-to: Compilation process',
|
|
2785
|
+
'valid-from: 2026-01-01',
|
|
2786
|
+
'---',
|
|
2787
|
+
'',
|
|
2788
|
+
'# my-compiled-adr-policy-001: core',
|
|
2789
|
+
'',
|
|
2790
|
+
'## Context and Problem Statement',
|
|
2791
|
+
'',
|
|
2792
|
+
'Defines compilation config.',
|
|
2793
|
+
'',
|
|
2794
|
+
'## Decision Outcome',
|
|
2795
|
+
'',
|
|
2796
|
+
'Use configured sources.',
|
|
2797
|
+
'',
|
|
2798
|
+
'### Details',
|
|
2799
|
+
'',
|
|
2800
|
+
'#### 01-rule',
|
|
2801
|
+
'',
|
|
2802
|
+
'MUST use configured sources.',
|
|
2803
|
+
'',
|
|
2804
|
+
'## Sources',
|
|
2805
|
+
'',
|
|
2806
|
+
'- [web] https://example.com/standards',
|
|
2807
|
+
''
|
|
2808
|
+
].join('\n'),
|
|
2809
|
+
'.xdrs/my-compiled/adrs/principles/002-a01.md': [
|
|
2810
|
+
'---',
|
|
2811
|
+
'name: my-compiled-adr-policy-002-a01',
|
|
2812
|
+
'description: Compiled test policy.',
|
|
2813
|
+
'apply-to: All teams',
|
|
2814
|
+
'valid-from: 2026-01-01',
|
|
2815
|
+
'---',
|
|
2816
|
+
'',
|
|
2817
|
+
'# my-compiled-adr-policy-002: A01',
|
|
2818
|
+
'',
|
|
2819
|
+
'## Context and Problem Statement',
|
|
2820
|
+
'',
|
|
2821
|
+
'External source.',
|
|
2822
|
+
'',
|
|
2823
|
+
'## Decision Outcome',
|
|
2824
|
+
'',
|
|
2825
|
+
'Follow rule.',
|
|
2826
|
+
'',
|
|
2827
|
+
'### Details',
|
|
2828
|
+
'',
|
|
2829
|
+
'#### 01-rule',
|
|
2830
|
+
'',
|
|
2831
|
+
'MUST comply.',
|
|
2832
|
+
'',
|
|
2833
|
+
'## Source',
|
|
2834
|
+
'',
|
|
2835
|
+
'- source-1/a01.md',
|
|
2836
|
+
''
|
|
2837
|
+
].join('\n'),
|
|
2838
|
+
});
|
|
2839
|
+
|
|
2840
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2841
|
+
|
|
2842
|
+
expect(result.errors.join('\n')).toContain('"## Selectors" section');
|
|
2843
|
+
expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
test('accepts valid compiled scope meta-policy with required sections', () => {
|
|
2847
|
+
const workspaceRoot = createWorkspace('compiled-meta-valid', {
|
|
2848
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2849
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2850
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2851
|
+
'- [001-core](principles/001-core.md) - meta-policy',
|
|
2852
|
+
'- [002-a01](principles/002-a01.md) - A01'
|
|
2853
|
+
]),
|
|
2854
|
+
'.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
|
|
2855
|
+
'.xdrs/my-compiled/adrs/principles/002-a01.md': [
|
|
2856
|
+
'---',
|
|
2857
|
+
'name: my-compiled-adr-policy-002-a01',
|
|
2858
|
+
'description: Compiled test policy.',
|
|
2859
|
+
'apply-to: All teams',
|
|
2860
|
+
'valid-from: 2026-01-01',
|
|
2861
|
+
'---',
|
|
2862
|
+
'',
|
|
2863
|
+
'# my-compiled-adr-policy-002: A01',
|
|
2864
|
+
'',
|
|
2865
|
+
'## Context and Problem Statement',
|
|
2866
|
+
'',
|
|
2867
|
+
'External source.',
|
|
2868
|
+
'',
|
|
2869
|
+
'## Decision Outcome',
|
|
2870
|
+
'',
|
|
2871
|
+
'Follow rule.',
|
|
2872
|
+
'',
|
|
2873
|
+
'### Details',
|
|
2874
|
+
'',
|
|
2875
|
+
'#### 01-rule',
|
|
2876
|
+
'',
|
|
2877
|
+
'MUST comply.',
|
|
2878
|
+
'',
|
|
2879
|
+
'## Source',
|
|
2880
|
+
'',
|
|
2881
|
+
'- source-1/a01.md',
|
|
2882
|
+
''
|
|
2883
|
+
].join('\n'),
|
|
2884
|
+
});
|
|
2885
|
+
|
|
2886
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2887
|
+
|
|
2888
|
+
expect(result.errors.join('\n')).not.toContain('019');
|
|
2889
|
+
});
|
|
2890
|
+
|
|
2891
|
+
test('skips ## Source check for local meta-policy files in compiled scope', () => {
|
|
2892
|
+
const workspaceRoot = createWorkspace('compiled-meta-no-source-required', {
|
|
2893
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
2894
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
2895
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
|
|
2896
|
+
'- [001-core](principles/001-core.md) - meta-policy'
|
|
2897
|
+
]),
|
|
2898
|
+
'.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2902
|
+
|
|
2903
|
+
expect(result.errors.join('\n')).not.toContain('Compiled policy must include a "## Source" section');
|
|
2904
|
+
});
|
|
2905
|
+
|
|
2906
|
+
test('enforces compiled scope checks when scope-type is comma-separated with compiled', () => {
|
|
2907
|
+
const workspaceRoot = createWorkspace('compiled-multi-type-checks', {
|
|
2908
|
+
'.xdrs/index.md': rootIndex(['[my-compiled-scope](my-compiled-scope/index.md)']),
|
|
2909
|
+
'.xdrs/my-compiled-scope/index.md': [
|
|
2910
|
+
'---',
|
|
2911
|
+
'scope-type: compiled, standard',
|
|
2912
|
+
'name: my-compiled-scope',
|
|
2913
|
+
'description: Compiled standard scope for tests.',
|
|
2914
|
+
'apply-to: All consumers',
|
|
2915
|
+
'valid-from: 2026-01-01',
|
|
2916
|
+
'---',
|
|
2917
|
+
'',
|
|
2918
|
+
'# my-compiled-scope Scope Overview',
|
|
2919
|
+
'',
|
|
2920
|
+
'Compiled standard.',
|
|
2921
|
+
'',
|
|
2922
|
+
'[ADRs](adrs/index.md)',
|
|
2923
|
+
''
|
|
2924
|
+
].join('\n'),
|
|
2925
|
+
'.xdrs/my-compiled-scope/adrs/index.md': [
|
|
2926
|
+
'# my-compiled-scope ADR Index', '', 'ADRs.', '', '## principles', '',
|
|
2927
|
+
'- [001-a01](principles/001-a01.md) - A01', ''
|
|
2928
|
+
].join('\n'),
|
|
2929
|
+
'.xdrs/my-compiled-scope/adrs/principles/001-a01.md': [
|
|
2930
|
+
'---',
|
|
2931
|
+
'name: my-compiled-scope-adr-policy-001-a01',
|
|
2932
|
+
'description: Compiled test policy.',
|
|
2933
|
+
'apply-to: All teams',
|
|
2934
|
+
'valid-from: 2026-01-01',
|
|
2935
|
+
'---',
|
|
2936
|
+
'',
|
|
2937
|
+
'# my-compiled-scope-adr-policy-001: A01',
|
|
2938
|
+
'',
|
|
2939
|
+
'## Context and Problem Statement',
|
|
2940
|
+
'',
|
|
2941
|
+
'External source.',
|
|
2942
|
+
'',
|
|
2943
|
+
'## Decision Outcome',
|
|
2944
|
+
'',
|
|
2945
|
+
'Follow rule.',
|
|
2946
|
+
'',
|
|
2947
|
+
'## Source',
|
|
2948
|
+
'',
|
|
2949
|
+
'- source-1/a01.md',
|
|
2950
|
+
''
|
|
2951
|
+
].join('\n'),
|
|
2952
|
+
...coreWithStructuredScopeTypes(['standard', 'compiled']),
|
|
2953
|
+
});
|
|
2954
|
+
|
|
2955
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2956
|
+
|
|
2957
|
+
expect(result.errors.join('\n')).toContain('"### Details"');
|
|
2958
|
+
expect(result.errors.join('\n')).toContain('019.04-structured-format-required');
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2961
|
+
|
|
2459
2962
|
function localScopeIndex() {
|
|
2460
2963
|
return [
|
|
2461
2964
|
'---',
|