xdrs-core 0.37.2 → 0.38.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/principles/011-core-scope-type.md +8 -0
- package/lib/lint.js +263 -126
- package/lib/lint.test.js +269 -4
- package/package.json +1 -1
package/lib/lint.test.js
CHANGED
|
@@ -116,7 +116,7 @@ test('skips broken links to missing scope directories when ignoring external sco
|
|
|
116
116
|
const result = lintWorkspace(workspaceRoot);
|
|
117
117
|
|
|
118
118
|
expect(result.errors.join('\n')).not.toContain('Broken local link in');
|
|
119
|
-
expect(result.errors.join('\n')).not.toContain('_core');
|
|
119
|
+
expect(result.errors.join('\n')).not.toContain('_core/adrs');
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
test('skips entire external scope when only some of its files are in filedist', () => {
|
|
@@ -1341,7 +1341,7 @@ test('reports invalid follows value in scope index frontmatter', () => {
|
|
|
1341
1341
|
expect(result.errors.join('\n')).toContain('Scope index frontmatter follows must be a core scope name or list of core scope names');
|
|
1342
1342
|
});
|
|
1343
1343
|
|
|
1344
|
-
test('
|
|
1344
|
+
test('marks scope as read-only when follows references a -core scope not in workspace', () => {
|
|
1345
1345
|
const workspaceRoot = createWorkspace('follows-missing-scope', {
|
|
1346
1346
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
1347
1347
|
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: nonexistent-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
@@ -1351,7 +1351,8 @@ test('reports follows scope that does not exist in workspace', () => {
|
|
|
1351
1351
|
|
|
1352
1352
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
1353
1353
|
|
|
1354
|
-
expect(result.errors.join('\n')).toContain('follows references scope "nonexistent-core" which does not exist in the workspace');
|
|
1354
|
+
expect(result.errors.join('\n')).not.toContain('follows references scope "nonexistent-core" which does not exist in the workspace');
|
|
1355
|
+
expect(result.readOnlyScopes.has('myteam')).toBe(true);
|
|
1355
1356
|
});
|
|
1356
1357
|
|
|
1357
1358
|
test('accepts follows scope that exists in workspace', () => {
|
|
@@ -2550,4 +2551,268 @@ function planDocument(body) {
|
|
|
2550
2551
|
'Expected end date: 2026-12-31',
|
|
2551
2552
|
''
|
|
2552
2553
|
].join('\n');
|
|
2553
|
-
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// ─── Tests for Phase 1 new checks ────────────────────────────────────────────
|
|
2557
|
+
|
|
2558
|
+
test('reports root index linking directly to a type index', () => {
|
|
2559
|
+
const workspaceRoot = createWorkspace('root-index-type-link', {
|
|
2560
|
+
'.xdrs/index.md': [
|
|
2561
|
+
'# XDRS Index',
|
|
2562
|
+
'',
|
|
2563
|
+
'## Scope Indexes',
|
|
2564
|
+
'',
|
|
2565
|
+
'XDRS scopes listed last override the ones listed first',
|
|
2566
|
+
'',
|
|
2567
|
+
'[_local ADRs](_local/adrs/index.md)',
|
|
2568
|
+
'',
|
|
2569
|
+
'### _local (reserved)',
|
|
2570
|
+
'',
|
|
2571
|
+
'Read _local scope index at `_local/index.md` when it exists.',
|
|
2572
|
+
].join('\n'),
|
|
2573
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([]),
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2577
|
+
|
|
2578
|
+
expect(result.errors.join('\n')).toContain('Root index must not link directly to type indexes');
|
|
2579
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-001]');
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
test('does not report root index linking to scope indexes', () => {
|
|
2583
|
+
const workspaceRoot = createWorkspace('root-index-scope-link-ok', {
|
|
2584
|
+
'.xdrs/index.md': rootIndex(),
|
|
2585
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([]),
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2588
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2589
|
+
|
|
2590
|
+
expect(result.errors.join('\n')).not.toContain('Root index must not link directly to type indexes');
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
test('reports parent document missing link to its slide file', () => {
|
|
2594
|
+
const workspaceRoot = createWorkspace('parent-missing-slide-link', {
|
|
2595
|
+
'.xdrs/index.md': rootIndex(),
|
|
2596
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2597
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide'
|
|
2598
|
+
]),
|
|
2599
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': articleDocument('No link to slide here.'),
|
|
2600
|
+
'.xdrs/_local/adrs/principles/articles/.assets/001-guide-slides.md': slideDocument('[Parent](../001-guide.md)'),
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2604
|
+
|
|
2605
|
+
expect(result.errors.join('\n')).toContain('Parent document must contain a link to its slide file');
|
|
2606
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-009]');
|
|
2607
|
+
});
|
|
2608
|
+
|
|
2609
|
+
test('does not report parent document when it links to its slide file', () => {
|
|
2610
|
+
const workspaceRoot = createWorkspace('parent-has-slide-link', {
|
|
2611
|
+
'.xdrs/index.md': rootIndex(),
|
|
2612
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2613
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide'
|
|
2614
|
+
]),
|
|
2615
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': articleDocument('Body.\n\n[Slides](.assets/001-guide-slides.md)'),
|
|
2616
|
+
'.xdrs/_local/adrs/principles/articles/.assets/001-guide-slides.md': slideDocument('[Parent](../001-guide.md)'),
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2620
|
+
|
|
2621
|
+
expect(result.errors.join('\n')).not.toContain('Parent document must contain a link to its slide file');
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
test('reports structured rule block with non-two-digit number', () => {
|
|
2625
|
+
const workspaceRoot = createWorkspace('structured-rule-bad-number', {
|
|
2626
|
+
'.xdrs/index.md': rootIndex(),
|
|
2627
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2628
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
2629
|
+
]),
|
|
2630
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument([
|
|
2631
|
+
'### Details',
|
|
2632
|
+
'',
|
|
2633
|
+
'#### 1-my-rule',
|
|
2634
|
+
'',
|
|
2635
|
+
'This rule MUST be followed.',
|
|
2636
|
+
].join('\n')),
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2640
|
+
|
|
2641
|
+
expect(result.errors.join('\n')).toContain('structured rule number must be exactly two digits');
|
|
2642
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.02-rule-numbering-must-be-stable]');
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
test('reports structured rule block with non-kebab-case title', () => {
|
|
2646
|
+
const workspaceRoot = createWorkspace('structured-rule-bad-title', {
|
|
2647
|
+
'.xdrs/index.md': rootIndex(),
|
|
2648
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2649
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
2650
|
+
]),
|
|
2651
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument([
|
|
2652
|
+
'### Details',
|
|
2653
|
+
'',
|
|
2654
|
+
'#### 01-My Rule Title',
|
|
2655
|
+
'',
|
|
2656
|
+
'This rule MUST be followed.',
|
|
2657
|
+
].join('\n')),
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2661
|
+
|
|
2662
|
+
expect(result.errors.join('\n')).toContain('structured rule title must be in kebab-case');
|
|
2663
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.01-always-use-numbered-rules-for-strong-or-referenceable-policies]');
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
test('reports structured rule block with duplicate rule number', () => {
|
|
2667
|
+
const workspaceRoot = createWorkspace('structured-rule-dup-number', {
|
|
2668
|
+
'.xdrs/index.md': rootIndex(),
|
|
2669
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2670
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
2671
|
+
]),
|
|
2672
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument([
|
|
2673
|
+
'### Details',
|
|
2674
|
+
'',
|
|
2675
|
+
'#### 01-first-rule',
|
|
2676
|
+
'',
|
|
2677
|
+
'This rule MUST be followed.',
|
|
2678
|
+
'',
|
|
2679
|
+
'#### 01-second-rule',
|
|
2680
|
+
'',
|
|
2681
|
+
'This rule MUST also be followed.',
|
|
2682
|
+
].join('\n')),
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2686
|
+
|
|
2687
|
+
expect(result.errors.join('\n')).toContain('Duplicate structured rule number "01"');
|
|
2688
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.02-rule-numbering-must-be-stable]');
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
test('reports structured rule block body missing normative language', () => {
|
|
2692
|
+
const workspaceRoot = createWorkspace('structured-rule-no-normative', {
|
|
2693
|
+
'.xdrs/index.md': rootIndex(),
|
|
2694
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2695
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
2696
|
+
]),
|
|
2697
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument([
|
|
2698
|
+
'### Details',
|
|
2699
|
+
'',
|
|
2700
|
+
'#### 01-my-rule',
|
|
2701
|
+
'',
|
|
2702
|
+
'This is a rule without normative keywords.',
|
|
2703
|
+
].join('\n')),
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2707
|
+
|
|
2708
|
+
expect(result.errors.join('\n')).toContain('structured rule body must contain normative language');
|
|
2709
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.03-rule-body-must-use-normative-language]');
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
test('accepts valid structured rule blocks', () => {
|
|
2713
|
+
const workspaceRoot = createWorkspace('structured-rule-valid', {
|
|
2714
|
+
'.xdrs/index.md': rootIndex(),
|
|
2715
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2716
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
2717
|
+
]),
|
|
2718
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument([
|
|
2719
|
+
'### Details',
|
|
2720
|
+
'',
|
|
2721
|
+
'#### 01-my-first-rule',
|
|
2722
|
+
'',
|
|
2723
|
+
'This rule MUST be followed.',
|
|
2724
|
+
'',
|
|
2725
|
+
'#### 02-my-second-rule',
|
|
2726
|
+
'',
|
|
2727
|
+
'Implementors SHOULD consider this.',
|
|
2728
|
+
].join('\n')),
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2732
|
+
|
|
2733
|
+
expect(result.errors.join('\n')).not.toContain('structured rule');
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
test('reports research section exceeding word limit', () => {
|
|
2737
|
+
const body500words = Array(500).fill('word').join(' ');
|
|
2738
|
+
const workspaceRoot = createWorkspace('research-section-over-limit', {
|
|
2739
|
+
'.xdrs/index.md': rootIndex(),
|
|
2740
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2741
|
+
'- [001-study](principles/researches/001-study.md) - Study'
|
|
2742
|
+
]),
|
|
2743
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': researchDocument(
|
|
2744
|
+
'Background text. Question: What is the answer?',
|
|
2745
|
+
null
|
|
2746
|
+
).replace('Single paragraph abstract.', body500words),
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2750
|
+
|
|
2751
|
+
expect(result.errors.join('\n')).toContain('Research ## Abstract section exceeds 200 words');
|
|
2752
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-006]');
|
|
2753
|
+
});
|
|
2754
|
+
|
|
2755
|
+
test('accepts research sections within word limits', () => {
|
|
2756
|
+
const workspaceRoot = createWorkspace('research-section-within-limit', {
|
|
2757
|
+
'.xdrs/index.md': rootIndex(),
|
|
2758
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2759
|
+
'- [001-study](principles/researches/001-study.md) - Study'
|
|
2760
|
+
]),
|
|
2761
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': researchDocument(
|
|
2762
|
+
'Background text.',
|
|
2763
|
+
'Question: What is the answer?'
|
|
2764
|
+
),
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2768
|
+
|
|
2769
|
+
expect(result.errors.join('\n')).not.toContain('section exceeds');
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
test('reports policy frontmatter name ending with hyphen', () => {
|
|
2773
|
+
const workspaceRoot = createWorkspace('policy-name-trailing-hyphen', {
|
|
2774
|
+
'.xdrs/index.md': rootIndex(),
|
|
2775
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2776
|
+
'- [001-my-](principles/001-my-.md) - My decision'
|
|
2777
|
+
]),
|
|
2778
|
+
'.xdrs/_local/adrs/principles/001-my-.md': [
|
|
2779
|
+
'---',
|
|
2780
|
+
'name: _local-adr-policy-001-my-',
|
|
2781
|
+
'description: Test policy',
|
|
2782
|
+
'apply-to: All scopes',
|
|
2783
|
+
'valid-from: 2026-01-01',
|
|
2784
|
+
'---',
|
|
2785
|
+
'',
|
|
2786
|
+
'# _local-adr-policy-001: My decision',
|
|
2787
|
+
'',
|
|
2788
|
+
'## Context and Problem Statement',
|
|
2789
|
+
'',
|
|
2790
|
+
'Body.',
|
|
2791
|
+
'',
|
|
2792
|
+
'## Decision Outcome',
|
|
2793
|
+
'',
|
|
2794
|
+
'Decision.',
|
|
2795
|
+
''
|
|
2796
|
+
].join('\n'),
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2800
|
+
|
|
2801
|
+
expect(result.errors.join('\n')).toContain('Policy frontmatter name must not end with a hyphen');
|
|
2802
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-002]');
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
test('error messages include policy references', () => {
|
|
2806
|
+
const workspaceRoot = createWorkspace('policy-ref-in-errors', {
|
|
2807
|
+
'.xdrs/index.md': rootIndex(),
|
|
2808
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2809
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
2810
|
+
]),
|
|
2811
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('See [Missing](002-missing.md).'),
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2815
|
+
|
|
2816
|
+
expect(result.errors.join('\n')).toContain('[_core-adr-policy-');
|
|
2817
|
+
expect(result.errors.every((e) => e.includes('[_core-adr-policy-'))).toBe(true);
|
|
2818
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xdrs-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|