xdrs-core 0.50.0 → 0.52.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/lib/lint.test.js CHANGED
@@ -1590,7 +1590,7 @@ test('accepts companion scope-type file when primary exists in same type folder'
1590
1590
 
1591
1591
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1592
1592
 
1593
- expect(result.errors.join('\n')).not.toContain('companion-requires-primary');
1593
+ expect(result.errors.join('\n')).not.toContain('def-companion-files');
1594
1594
  expect(result.errors.join('\n')).not.toContain('has no corresponding');
1595
1595
  });
1596
1596
 
@@ -1610,7 +1610,7 @@ test('reports companion scope-type file with no corresponding primary', () => {
1610
1610
 
1611
1611
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1612
1612
 
1613
- expect(result.errors.join('\n')).toContain('companion-requires-primary');
1613
+ expect(result.errors.join('\n')).toContain('def-companion-files');
1614
1614
  expect(result.errors.join('\n')).toContain('custom-scope-type');
1615
1615
  });
1616
1616
 
@@ -1638,7 +1638,7 @@ test('reports companion scope-type file when primary is in a different type fold
1638
1638
 
1639
1639
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1640
1640
 
1641
- expect(result.errors.join('\n')).toContain('companion-requires-primary');
1641
+ expect(result.errors.join('\n')).toContain('def-companion-files');
1642
1642
  expect(result.errors.join('\n')).toContain('custom-scope-type');
1643
1643
  });
1644
1644
 
@@ -1807,8 +1807,8 @@ test('accepts multiple local meta-policies in same scope', () => {
1807
1807
 
1808
1808
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1809
1809
 
1810
- expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.10');
1811
- expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.09-local-naming]');
1810
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.15');
1811
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.13-local-naming]');
1812
1812
  });
1813
1813
 
1814
1814
  test('accepts single local meta-policy in a scope', () => {
@@ -1843,8 +1843,8 @@ test('accepts single local meta-policy in a scope', () => {
1843
1843
 
1844
1844
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1845
1845
 
1846
- expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.10');
1847
- expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.09-local-naming]');
1846
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.15');
1847
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.13-local-naming]');
1848
1848
  });
1849
1849
 
1850
1850
  test('errors when local meta-policy companion exists without primary in same type folder', () => {
@@ -1879,7 +1879,7 @@ test('errors when local meta-policy companion exists without primary in same typ
1879
1879
 
1880
1880
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1881
1881
 
1882
- expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.10b-local-primary-required]');
1882
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.15-local-primary-required]');
1883
1883
  });
1884
1884
 
1885
1885
  test('errors when local meta-policy same qualifier appears in two type folders', () => {
@@ -1946,7 +1946,7 @@ test('errors when local meta-policy same qualifier appears in two type folders',
1946
1946
 
1947
1947
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1948
1948
 
1949
- expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.10c-local-unique-qualifier]');
1949
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.16-local-unique-qualifier]');
1950
1950
  });
1951
1951
 
1952
1952
  test('errors when policy filename contains core as word outside principles', () => {
@@ -1977,7 +1977,7 @@ test('errors when policy filename contains core as word outside principles', ()
1977
1977
 
1978
1978
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
1979
1979
 
1980
- expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.09-local-naming]');
1980
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.13-local-naming]');
1981
1981
  });
1982
1982
 
1983
1983
  test('errors when policy filename contains core as non-first word in principles', () => {
@@ -2010,7 +2010,7 @@ test('errors when policy filename contains core as non-first word in principles'
2010
2010
 
2011
2011
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2012
2012
 
2013
- expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.09-local-naming]');
2013
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.13-local-naming]');
2014
2014
  });
2015
2015
 
2016
2016
  test('accepts NNN-core-scope-type.md in principles without triggering core-word error', () => {
@@ -2060,7 +2060,7 @@ test('accepts NNN-core-scope-type.md in principles without triggering core-word
2060
2060
 
2061
2061
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2062
2062
 
2063
- expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.09-local-naming]');
2063
+ expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.13-local-naming]');
2064
2064
  });
2065
2065
 
2066
2066
  test('reports missing name field in scope index frontmatter', () => {
@@ -2248,6 +2248,21 @@ test('reports error when follows references a scope not in workspace', () => {
2248
2248
  expect(result.readOnlyScopes.has('myteam')).toBe(false);
2249
2249
  });
2250
2250
 
2251
+ test('marks scope read-only instead of erroring when follows references a missing scope for an external scope', () => {
2252
+ const workspaceRoot = createWorkspace('follows-missing-scope-external', {
2253
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
2254
+ '.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',
2255
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
2256
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
2257
+ '.filedist.lock': '.xdrs/myteam/adrs/principles/001-team.md|some-package|1.0.0\n',
2258
+ });
2259
+
2260
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2261
+
2262
+ expect(result.errors.join('\n')).not.toContain('follows references scope "nonexistent-core" which does not exist in the workspace');
2263
+ expect(result.readOnlyScopes.has('myteam')).toBe(true);
2264
+ });
2265
+
2251
2266
  test('accepts follows scope that exists in workspace', () => {
2252
2267
  const workspaceRoot = createWorkspace('follows-existing-scope', {
2253
2268
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
@@ -2280,6 +2295,23 @@ test('reports follows scope that is not core-type', () => {
2280
2295
  expect(result.errors.join('\n')).toContain('follows references scope "myarea-domain" which is not a core-type scope');
2281
2296
  });
2282
2297
 
2298
+ test('reports core scope-type combined with another type [011.10]', () => {
2299
+ const workspaceRoot = createWorkspace('follows-multi-type-core-scope', {
2300
+ '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)', '[myarea-core](myarea-core/index.md)']),
2301
+ '.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: myarea-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
2302
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
2303
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
2304
+ '.xdrs/myarea-core/index.md': '---\nscope-type: core, standard\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',
2305
+ '.xdrs/myarea-core/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
2306
+ '.xdrs/myarea-core/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
2307
+ });
2308
+
2309
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2310
+
2311
+ expect(result.errors.join('\n')).not.toContain('follows references scope');
2312
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-011.10-no-type-combination]');
2313
+ });
2314
+
2283
2315
  test('reports scope index apply-to exceeding 30 words', () => {
2284
2316
  const workspaceRoot = createWorkspace('scope-apply-to-too-long', {
2285
2317
  '.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
@@ -2458,7 +2490,7 @@ test('reports extends: and follows: referencing same scope (disjoint violation)'
2458
2490
  expect(result.errors.join('\n')).toContain('[_core-adr-policy-010.28-extends-disjoint]');
2459
2491
  });
2460
2492
 
2461
- test('extends-only scope not in root index is exempt from missing link error', () => {
2493
+ test('reports extends-referenced scope missing from root index [022.01]', () => {
2462
2494
  const workspaceRoot = createWorkspace('extends-only-scope-not-in-index', {
2463
2495
  // shared-policies is NOT linked in the root index — it is only referenced via extends:
2464
2496
  '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[myteam](myteam/index.md)']),
@@ -2472,13 +2504,14 @@ test('extends-only scope not in root index is exempt from missing link error', (
2472
2504
 
2473
2505
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2474
2506
 
2475
- // shared-policies should NOT trigger "missing root index link" since it is extends-only
2476
- expect(result.errors.filter((e) => e.includes('Root index is missing scope index link') && e.includes('shared-policies')).length).toBe(0);
2507
+ // shared-policies must still be linked in the root index (no extends exemption)
2508
+ expect(result.errors.filter((e) => e.includes('Root index is missing scope index link') && e.includes('shared-policies')).join('\n')).toContain('[_core-adr-policy-022.01-root-index-completeness]');
2509
+ expect(result.errors.join('\n')).toContain('add the line: [View scope shared-policies](shared-policies/index.md)');
2477
2510
  // No extends: validation errors should fire for the extends: field
2478
2511
  expect(result.errors.filter((e) => /\[_core-adr-policy-010\.\d+-extends-/.test(e)).join('\n')).toBe('');
2479
2512
  });
2480
2513
 
2481
- test('warns when extends-referenced scope also appears in root index', () => {
2514
+ test('does not warn when extends-referenced scope also appears in root index', () => {
2482
2515
  const workspaceRoot = createWorkspace('extends-scope-in-root-index', {
2483
2516
  '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[myteam](myteam/index.md)', '[shared-policies](shared-policies/index.md)']),
2484
2517
  '.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',
@@ -2492,8 +2525,8 @@ test('warns when extends-referenced scope also appears in root index', () => {
2492
2525
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2493
2526
 
2494
2527
  expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.35-extends-index]');
2495
- expect(result.warnings.join('\n')).toContain('[_core-adr-policy-010.35-extends-index]');
2496
- expect(result.warnings.join('\n')).toContain('shared-policies');
2528
+ expect(result.warnings.join('\n')).not.toContain('[_core-adr-policy-010.35-extends-index]');
2529
+ expect(result.errors.join('\n')).not.toContain('_core-adr-policy-022');
2497
2530
  });
2498
2531
 
2499
2532
 
@@ -2919,7 +2952,7 @@ test('reports scope-type policy missing ### Details section', () => {
2919
2952
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2920
2953
 
2921
2954
  expect(result.errors.join('\n')).toContain('"### Details"');
2922
- expect(result.errors.join('\n')).toContain('08b-def-scope-type-must-be-structured');
2955
+ expect(result.errors.join('\n')).toContain('11-def-scope-type-must-be-structured');
2923
2956
  });
2924
2957
 
2925
2958
  test('reports scope-type policy missing at least one #### NN-rulename block', () => {
@@ -2955,7 +2988,7 @@ test('reports scope-type policy missing at least one #### NN-rulename block', ()
2955
2988
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2956
2989
 
2957
2990
  expect(result.errors.join('\n')).toContain('at least one structured rule block');
2958
- expect(result.errors.join('\n')).toContain('08b-def-scope-type-must-be-structured');
2991
+ expect(result.errors.join('\n')).toContain('11-def-scope-type-must-be-structured');
2959
2992
  });
2960
2993
 
2961
2994
  test('reports local meta-policy missing ### Details section', () => {
@@ -2983,7 +3016,7 @@ test('reports local meta-policy missing ### Details section', () => {
2983
3016
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
2984
3017
 
2985
3018
  expect(result.errors.join('\n')).toContain('"### Details"');
2986
- expect(result.errors.join('\n')).toContain('12b-local-must-be-structured');
3019
+ expect(result.errors.join('\n')).toContain('19-local-must-be-structured');
2987
3020
  });
2988
3021
 
2989
3022
  test('reports policy in non-_core core-type scope missing structured format', () => {
@@ -3012,7 +3045,7 @@ test('reports policy in non-_core core-type scope missing structured format', ()
3012
3045
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
3013
3046
 
3014
3047
  expect(result.errors.join('\n')).toContain('"### Details"');
3015
- expect(result.errors.join('\n')).toContain('08c-def-core-scope-policies-must-be-structured');
3048
+ expect(result.errors.join('\n')).toContain('12-def-core-scope-policies-must-be-structured');
3016
3049
  });
3017
3050
 
3018
3051
  test('does not require structured format for _core scope policies (exempt)', () => {
@@ -3026,7 +3059,7 @@ test('does not require structured format for _core scope policies (exempt)', ()
3026
3059
 
3027
3060
  const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
3028
3061
 
3029
- expect(result.errors.join('\n')).not.toContain('08c-def-core-scope-policies-must-be-structured');
3062
+ expect(result.errors.join('\n')).not.toContain('12-def-core-scope-policies-must-be-structured');
3030
3063
  });
3031
3064
 
3032
3065
  // ─── compiled scope-type checks ───────────────────────────────────────────────
@@ -3122,11 +3155,15 @@ function validCompiledMetaPolicy() {
3122
3155
  '',
3123
3156
  '## Sources',
3124
3157
  '',
3125
- '- [web] https://example.com/standards',
3158
+ '- [web] example-standards: https://example.com/standards',
3126
3159
  '',
3127
3160
  '## Selectors',
3128
3161
  '',
3129
3162
  '- Include all requirement sections.',
3163
+ '',
3164
+ '## Sync Settings',
3165
+ '',
3166
+ '- Source re-sync period days: 30',
3130
3167
  ''
3131
3168
  ].join('\n');
3132
3169
  }
@@ -3416,6 +3453,331 @@ test('reports compiled scope meta-policy missing ## Selectors section', () => {
3416
3453
  expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3417
3454
  });
3418
3455
 
3456
+ test('reports compiled scope meta-policy missing ## Sync Settings section', () => {
3457
+ const workspaceRoot = createWorkspace('compiled-meta-missing-sync-settings', {
3458
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3459
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3460
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3461
+ '- [001-core](principles/001-core.md) - meta-policy',
3462
+ '- [002-a01](principles/002-a01.md) - A01'
3463
+ ]),
3464
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3465
+ '---',
3466
+ 'name: my-compiled-adr-policy-001-core',
3467
+ 'description: Compilation meta-policy.',
3468
+ 'apply-to: Compilation process',
3469
+ 'valid-from: 2026-01-01',
3470
+ '---',
3471
+ '',
3472
+ '# my-compiled-adr-policy-001: core',
3473
+ '',
3474
+ '## Context and Problem Statement',
3475
+ '',
3476
+ 'Defines compilation config.',
3477
+ '',
3478
+ '## Decision Outcome',
3479
+ '',
3480
+ 'Use configured sources.',
3481
+ '',
3482
+ '### Details',
3483
+ '',
3484
+ '#### 01-rule',
3485
+ '',
3486
+ 'MUST use configured sources.',
3487
+ '',
3488
+ '## Sources',
3489
+ '',
3490
+ '- [web] example-standards: https://example.com/standards',
3491
+ '',
3492
+ '## Selectors',
3493
+ '',
3494
+ '- Include requirement sections.',
3495
+ ''
3496
+ ].join('\n'),
3497
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3498
+ '---',
3499
+ 'name: my-compiled-adr-policy-002-a01',
3500
+ 'description: Compiled test policy.',
3501
+ 'apply-to: All teams',
3502
+ 'valid-from: 2026-01-01',
3503
+ '---',
3504
+ '',
3505
+ '# my-compiled-adr-policy-002: A01',
3506
+ '',
3507
+ '## Context and Problem Statement',
3508
+ '',
3509
+ 'External source.',
3510
+ '',
3511
+ '## Decision Outcome',
3512
+ '',
3513
+ 'Follow rule.',
3514
+ '',
3515
+ '### Details',
3516
+ '',
3517
+ '#### 01-rule',
3518
+ '',
3519
+ 'MUST comply.',
3520
+ '',
3521
+ '## Source',
3522
+ '',
3523
+ '- source-1/a01.md',
3524
+ ''
3525
+ ].join('\n'),
3526
+ });
3527
+
3528
+ const result = lintWorkspace(workspaceRoot);
3529
+
3530
+ expect(result.errors.join('\n')).toContain('"## Sync Settings" section');
3531
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3532
+ });
3533
+
3534
+ test('reports missing Source re-sync period days in ## Sync Settings', () => {
3535
+ const workspaceRoot = createWorkspace('compiled-meta-missing-resync-period', {
3536
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3537
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3538
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3539
+ '- [001-core](principles/001-core.md) - meta-policy',
3540
+ '- [002-a01](principles/002-a01.md) - A01'
3541
+ ]),
3542
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3543
+ '---',
3544
+ 'name: my-compiled-adr-policy-001-core',
3545
+ 'description: Compilation meta-policy.',
3546
+ 'apply-to: Compilation process',
3547
+ 'valid-from: 2026-01-01',
3548
+ '---',
3549
+ '',
3550
+ '# my-compiled-adr-policy-001: core',
3551
+ '',
3552
+ '## Context and Problem Statement',
3553
+ '',
3554
+ 'Defines compilation config.',
3555
+ '',
3556
+ '## Decision Outcome',
3557
+ '',
3558
+ 'Use configured sources.',
3559
+ '',
3560
+ '### Details',
3561
+ '',
3562
+ '#### 01-rule',
3563
+ '',
3564
+ 'MUST use configured sources.',
3565
+ '',
3566
+ '## Sources',
3567
+ '',
3568
+ '- [web] example-standards: https://example.com/standards',
3569
+ '',
3570
+ '## Selectors',
3571
+ '',
3572
+ '- Include requirement sections.',
3573
+ '',
3574
+ '## Sync Settings',
3575
+ '',
3576
+ '- Source storage: temporary',
3577
+ ''
3578
+ ].join('\n'),
3579
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3580
+ '---',
3581
+ 'name: my-compiled-adr-policy-002-a01',
3582
+ 'description: Compiled test policy.',
3583
+ 'apply-to: All teams',
3584
+ 'valid-from: 2026-01-01',
3585
+ '---',
3586
+ '',
3587
+ '# my-compiled-adr-policy-002: A01',
3588
+ '',
3589
+ '## Context and Problem Statement',
3590
+ '',
3591
+ 'External source.',
3592
+ '',
3593
+ '## Decision Outcome',
3594
+ '',
3595
+ 'Follow rule.',
3596
+ '',
3597
+ '### Details',
3598
+ '',
3599
+ '#### 01-rule',
3600
+ '',
3601
+ 'MUST comply.',
3602
+ '',
3603
+ '## Source',
3604
+ '',
3605
+ '- source-1/a01.md',
3606
+ ''
3607
+ ].join('\n'),
3608
+ });
3609
+
3610
+ const result = lintWorkspace(workspaceRoot);
3611
+
3612
+ expect(result.errors.join('\n')).toContain('"Source re-sync period days: N" bullet with a positive integer');
3613
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3614
+ });
3615
+
3616
+ test('reports invalid Source storage value in ## Sync Settings', () => {
3617
+ const workspaceRoot = createWorkspace('compiled-meta-invalid-storage-value', {
3618
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3619
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3620
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3621
+ '- [001-core](principles/001-core.md) - meta-policy',
3622
+ '- [002-a01](principles/002-a01.md) - A01'
3623
+ ]),
3624
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3625
+ '---',
3626
+ 'name: my-compiled-adr-policy-001-core',
3627
+ 'description: Compilation meta-policy.',
3628
+ 'apply-to: Compilation process',
3629
+ 'valid-from: 2026-01-01',
3630
+ '---',
3631
+ '',
3632
+ '# my-compiled-adr-policy-001: core',
3633
+ '',
3634
+ '## Context and Problem Statement',
3635
+ '',
3636
+ 'Defines compilation config.',
3637
+ '',
3638
+ '## Decision Outcome',
3639
+ '',
3640
+ 'Use configured sources.',
3641
+ '',
3642
+ '### Details',
3643
+ '',
3644
+ '#### 01-rule',
3645
+ '',
3646
+ 'MUST use configured sources.',
3647
+ '',
3648
+ '## Sources',
3649
+ '',
3650
+ '- [web] example-standards: https://example.com/standards',
3651
+ '',
3652
+ '## Selectors',
3653
+ '',
3654
+ '- Include requirement sections.',
3655
+ '',
3656
+ '## Sync Settings',
3657
+ '',
3658
+ '- Source re-sync period days: 30',
3659
+ '- Source storage: permanent',
3660
+ ''
3661
+ ].join('\n'),
3662
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3663
+ '---',
3664
+ 'name: my-compiled-adr-policy-002-a01',
3665
+ 'description: Compiled test policy.',
3666
+ 'apply-to: All teams',
3667
+ 'valid-from: 2026-01-01',
3668
+ '---',
3669
+ '',
3670
+ '# my-compiled-adr-policy-002: A01',
3671
+ '',
3672
+ '## Context and Problem Statement',
3673
+ '',
3674
+ 'External source.',
3675
+ '',
3676
+ '## Decision Outcome',
3677
+ '',
3678
+ 'Follow rule.',
3679
+ '',
3680
+ '### Details',
3681
+ '',
3682
+ '#### 01-rule',
3683
+ '',
3684
+ 'MUST comply.',
3685
+ '',
3686
+ '## Source',
3687
+ '',
3688
+ '- source-1/a01.md',
3689
+ ''
3690
+ ].join('\n'),
3691
+ });
3692
+
3693
+ const result = lintWorkspace(workspaceRoot);
3694
+
3695
+ expect(result.errors.join('\n')).toContain('"Source storage" value must be "temporary" if present');
3696
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3697
+ });
3698
+
3699
+ test('reports compiled scope ## Sources bullet missing NAME slug', () => {
3700
+ const workspaceRoot = createWorkspace('compiled-meta-sources-missing-name', {
3701
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3702
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3703
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3704
+ '- [001-core](principles/001-core.md) - meta-policy',
3705
+ '- [002-a01](principles/002-a01.md) - A01'
3706
+ ]),
3707
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3708
+ '---',
3709
+ 'name: my-compiled-adr-policy-001-core',
3710
+ 'description: Compilation meta-policy.',
3711
+ 'apply-to: Compilation process',
3712
+ 'valid-from: 2026-01-01',
3713
+ '---',
3714
+ '',
3715
+ '# my-compiled-adr-policy-001: core',
3716
+ '',
3717
+ '## Context and Problem Statement',
3718
+ '',
3719
+ 'Defines compilation config.',
3720
+ '',
3721
+ '## Decision Outcome',
3722
+ '',
3723
+ 'Use configured sources.',
3724
+ '',
3725
+ '### Details',
3726
+ '',
3727
+ '#### 01-rule',
3728
+ '',
3729
+ 'MUST use configured sources.',
3730
+ '',
3731
+ '## Sources',
3732
+ '',
3733
+ '- [web] https://example.com/standards',
3734
+ '',
3735
+ '## Selectors',
3736
+ '',
3737
+ '- Include requirement sections.',
3738
+ '',
3739
+ '## Sync Settings',
3740
+ '',
3741
+ '- Source re-sync period days: 30',
3742
+ ''
3743
+ ].join('\n'),
3744
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3745
+ '---',
3746
+ 'name: my-compiled-adr-policy-002-a01',
3747
+ 'description: Compiled test policy.',
3748
+ 'apply-to: All teams',
3749
+ 'valid-from: 2026-01-01',
3750
+ '---',
3751
+ '',
3752
+ '# my-compiled-adr-policy-002: A01',
3753
+ '',
3754
+ '## Context and Problem Statement',
3755
+ '',
3756
+ 'External source.',
3757
+ '',
3758
+ '## Decision Outcome',
3759
+ '',
3760
+ 'Follow rule.',
3761
+ '',
3762
+ '### Details',
3763
+ '',
3764
+ '#### 01-rule',
3765
+ '',
3766
+ 'MUST comply.',
3767
+ '',
3768
+ '## Source',
3769
+ '',
3770
+ '- source-1/a01.md',
3771
+ ''
3772
+ ].join('\n'),
3773
+ });
3774
+
3775
+ const result = lintWorkspace(workspaceRoot);
3776
+
3777
+ expect(result.errors.join('\n')).toContain('must include a NAME slug');
3778
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3779
+ });
3780
+
3419
3781
  test('accepts valid compiled scope meta-policy with required sections', () => {
3420
3782
  const workspaceRoot = createWorkspace('compiled-meta-valid', {
3421
3783
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
@@ -3476,6 +3838,88 @@ test('skips ## Source check for local meta-policy files in compiled scope', () =
3476
3838
  expect(result.errors.join('\n')).not.toContain('Compiled policy must include a "## Source" section');
3477
3839
  });
3478
3840
 
3841
+ test('does not report orphan asset errors for files under .assets/sources in a compiled scope', () => {
3842
+ const workspaceRoot = createWorkspace('compiled-sources-orphan-exempt', {
3843
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3844
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3845
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3846
+ '- [001-core](principles/001-core.md) - meta-policy'
3847
+ ]),
3848
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3849
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/source.md': [
3850
+ '# Source',
3851
+ '',
3852
+ 'last-fetch-timestamp: 2026-01-01 10:00:00',
3853
+ 'last-compilation-timestamp: 2026-01-01 10:05:00',
3854
+ ''
3855
+ ].join('\n'),
3856
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3857
+ '.xdrs/my-compiled/adrs/principles/.assets/unused.png': Buffer.alloc(0),
3858
+ });
3859
+
3860
+ const result = lintWorkspace(workspaceRoot);
3861
+
3862
+ expect(result.errors.join('\n')).not.toContain('raw-standard.md');
3863
+ expect(result.errors.join('\n')).not.toContain('019.12-source-storage-tracking');
3864
+ expect(result.errors.join('\n')).toContain('Orphan asset file');
3865
+ expect(result.errors.join('\n')).toContain('unused.png');
3866
+ });
3867
+
3868
+ test('reports missing source.md tracking file in a compiled source snapshot folder', () => {
3869
+ const workspaceRoot = createWorkspace('compiled-sources-missing-tracking-file', {
3870
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3871
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3872
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3873
+ '- [001-core](principles/001-core.md) - meta-policy'
3874
+ ]),
3875
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3876
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3877
+ });
3878
+
3879
+ const result = lintWorkspace(workspaceRoot);
3880
+
3881
+ expect(result.errors.join('\n')).toContain('must contain a "source.md" tracking file');
3882
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3883
+ });
3884
+
3885
+ test('reports malformed source.md tracking file content', () => {
3886
+ const workspaceRoot = createWorkspace('compiled-sources-malformed-tracking-file', {
3887
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3888
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3889
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3890
+ '- [001-core](principles/001-core.md) - meta-policy'
3891
+ ]),
3892
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3893
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/source.md': [
3894
+ 'last-fetch-timestamp: not-a-date',
3895
+ ''
3896
+ ].join('\n'),
3897
+ });
3898
+
3899
+ const result = lintWorkspace(workspaceRoot);
3900
+
3901
+ expect(result.errors.join('\n')).toContain('must start with a "# Source" heading');
3902
+ expect(result.errors.join('\n')).toContain('must include a "last-compilation-timestamp:" line');
3903
+ expect(result.errors.join('\n')).toContain('must match "YYYY-MM-DD HH:MM:SS"');
3904
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3905
+ });
3906
+
3907
+ test('reports document linking directly into a compiled-source snapshot path', () => {
3908
+ const workspaceRoot = createWorkspace('source-snapshot-link-forbidden', {
3909
+ '.xdrs/index.md': rootIndex(),
3910
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
3911
+ '- [001-main](principles/001-main.md) - Main decision'
3912
+ ]),
3913
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('See [raw source](.assets/sources/example-standards/raw-standard.md).'),
3914
+ '.xdrs/_local/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3915
+ });
3916
+
3917
+ const result = lintWorkspace(workspaceRoot);
3918
+
3919
+ expect(result.errors.join('\n')).toContain('must not link directly to compiled-source snapshot content');
3920
+ expect(result.errors.join('\n')).toContain('019.14-source-not-authoritative');
3921
+ });
3922
+
3479
3923
  test('enforces compiled scope checks when scope-type is comma-separated with compiled', () => {
3480
3924
  const workspaceRoot = createWorkspace('compiled-multi-type-checks', {
3481
3925
  '.xdrs/index.md': rootIndex(['[my-compiled-scope](my-compiled-scope/index.md)']),
@@ -5017,4 +5461,372 @@ test('error messages include policy references', () => {
5017
5461
 
5018
5462
  expect(result.errors.join('\n')).toContain('[_core-adr-policy-');
5019
5463
  expect(result.errors.every((e) => e.includes('[_core-adr-policy-'))).toBe(true);
5020
- });
5464
+ });
5465
+
5466
+ // ---------------------------------------------------------------------------
5467
+ // Scope activation (_core-adr-policy-022) and scope-type hardening (011.10)
5468
+ // ---------------------------------------------------------------------------
5469
+
5470
+ const ACTIVATION_LEGEND = 'Scopes tagged `disabled` MUST be ignored; `extends-only` scopes apply only via extends (see _core-adr-policy-022)';
5471
+
5472
+ function scopeIndexWith(name, extraFields = '', scopeType = 'standard') {
5473
+ return `---\nscope-type: ${scopeType}\nname: ${name}\ndescription: Scope ${name}.\napply-to: Test teams\nvalid-from: 2026-01-01\n${extraFields}---\n\n# ${name} Scope Overview\n\n[ADRs](adrs/index.md)\n`;
5474
+ }
5475
+
5476
+ function scopeFiles(name, extraFields = '', scopeType = 'standard') {
5477
+ return {
5478
+ [`.xdrs/${name}/index.md`]: scopeIndexWith(name, extraFields, scopeType),
5479
+ [`.xdrs/${name}/adrs/index.md`]: teamAdrIndex(['- [001-team](principles/001-team.md) - Decision']),
5480
+ [`.xdrs/${name}/adrs/principles/001-team.md`]: teamXdrDocument('Decision.'),
5481
+ };
5482
+ }
5483
+
5484
+ const activationErrors = (result) => result.errors.filter((e) => e.includes('_core-adr-policy-022')).join('\n');
5485
+ const activationWarnings = (result) => result.warnings.filter((e) => e.includes('_core-adr-policy-022')).join('\n');
5486
+
5487
+ test('passes activation checks when only _core is installed and linked (smoke)', () => {
5488
+ const workspaceRoot = createWorkspace('activation-smoke', {
5489
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)']),
5490
+ ...coreWithScopeTypes(['standard']),
5491
+ });
5492
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5493
+ expect(activationErrors(result)).toBe('');
5494
+ expect(result.readOnlyScopes.size).toBe(0);
5495
+ });
5496
+
5497
+ test('reports unlisted scope without type indexes with a paste-ready line [022.01]', () => {
5498
+ const workspaceRoot = createWorkspace('activation-unlisted-no-types', {
5499
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)']),
5500
+ '.xdrs/lonely/index.md': scopeIndexWith('lonely'),
5501
+ });
5502
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5503
+ expect(activationErrors(result)).toContain('add the line: [View scope lonely](lonely/index.md) [_core-adr-policy-022.01-root-index-completeness]');
5504
+ });
5505
+
5506
+ test('reports unlisted external scope even when external scopes are skipped [022.01]', () => {
5507
+ const workspaceRoot = createWorkspace('activation-unlisted-external', {
5508
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)']),
5509
+ ...scopeFiles('vendor'),
5510
+ '.filedist.lock': '.xdrs/vendor/index.md|some-package|1.0.0\n',
5511
+ });
5512
+ const result = lintWorkspace(workspaceRoot);
5513
+ expect(activationErrors(result)).toContain('[View scope vendor](vendor/index.md)');
5514
+ });
5515
+
5516
+ test('does not require _local to be listed in the root index', () => {
5517
+ const workspaceRoot = createWorkspace('activation-local-unlisted', {
5518
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)']),
5519
+ '.xdrs/_local/index.md': localScopeIndex(),
5520
+ ...coreWithScopeTypes(['_local']),
5521
+ });
5522
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5523
+ expect(activationErrors(result)).toBe('');
5524
+ });
5525
+
5526
+ test('reports a scope linked twice, including ./x/index.md vs x/index.md [022.01]', () => {
5527
+ const workspaceRoot = createWorkspace('activation-duplicate-link', {
5528
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md)', '[again](./myteam/index.md)']),
5529
+ ...scopeFiles('myteam'),
5530
+ });
5531
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5532
+ expect(activationErrors(result)).toContain('Root index links scope "myteam" more than once');
5533
+ });
5534
+
5535
+ test('ignores scope links inside fenced code blocks [022.01]', () => {
5536
+ const workspaceRoot = createWorkspace('activation-fenced-link', {
5537
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '```', '[View scope myteam](myteam/index.md) `whatever`', '```']),
5538
+ ...scopeFiles('myteam'),
5539
+ });
5540
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5541
+ expect(activationErrors(result)).toContain('missing scope index link');
5542
+ expect(activationErrors(result)).not.toContain('022.03');
5543
+ });
5544
+
5545
+ test('recognises a tag separated by tabs and spaces [022.03]', () => {
5546
+ const workspaceRoot = createWorkspace('activation-tag-whitespace', {
5547
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md) \t `disabled`', ACTIVATION_LEGEND]),
5548
+ ...scopeFiles('myteam'),
5549
+ });
5550
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5551
+ expect(activationErrors(result)).toBe('');
5552
+ });
5553
+
5554
+ test.each([
5555
+ ['uppercase value', '[View scope myteam](myteam/index.md) `DISABLED`'],
5556
+ ['unknown value', '[View scope myteam](myteam/index.md) `optional`'],
5557
+ ['two tags', '[View scope myteam](myteam/index.md) `disabled` `extends-only`'],
5558
+ ['tag placed later on the line', '[View scope myteam](myteam/index.md) - team rules `disabled`'],
5559
+ ])('reports invalid activation tag: %s [022.03]', (_label, linkLine) => {
5560
+ const workspaceRoot = createWorkspace(`activation-invalid-tag-${_label.replace(/\s+/g, '-')}`, {
5561
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', linkLine, ACTIVATION_LEGEND]),
5562
+ ...scopeFiles('myteam'),
5563
+ });
5564
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5565
+ expect(activationErrors(result)).toContain('[_core-adr-policy-022.03-activation-tag-syntax]');
5566
+ });
5567
+
5568
+ test('does not treat inline code on the next line or on other lines as a tag [022.03]', () => {
5569
+ const workspaceRoot = createWorkspace('activation-tag-next-line', {
5570
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md)', '`disabled`']),
5571
+ ...scopeFiles('myteam'),
5572
+ });
5573
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5574
+ expect(activationErrors(result)).toBe('');
5575
+ });
5576
+
5577
+ test('reports an activation tag on _core [022.03]', () => {
5578
+ const workspaceRoot = createWorkspace('activation-tag-core', {
5579
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md) `disabled`', ACTIVATION_LEGEND]),
5580
+ ...coreWithScopeTypes(['standard']),
5581
+ });
5582
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5583
+ expect(activationErrors(result)).toContain('Scope "_core" must not carry an activation tag');
5584
+ });
5585
+
5586
+ test('reports extends-only: true without a tag and prints the exact line [022.07]', () => {
5587
+ const workspaceRoot = createWorkspace('activation-field-no-tag', {
5588
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md)']),
5589
+ ...scopeFiles('lib', 'extends-only: true\n'),
5590
+ });
5591
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5592
+ expect(activationErrors(result)).toContain('change it to: [View scope lib](lib/index.md) `extends-only`');
5593
+ expect(activationErrors(result)).toContain('[_core-adr-policy-022.07-extends-only-field]');
5594
+ });
5595
+
5596
+ test('accepts extends-only: true with a disabled tag [022.07]', () => {
5597
+ const workspaceRoot = createWorkspace('activation-field-disabled', {
5598
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', ACTIVATION_LEGEND]),
5599
+ ...scopeFiles('lib', 'extends-only: true\n'),
5600
+ });
5601
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5602
+ expect(activationErrors(result)).toBe('');
5603
+ });
5604
+
5605
+ test('accepts consumer narrowing: extends-only: false with an extends-only tag [022.07]', () => {
5606
+ const workspaceRoot = createWorkspace('activation-field-narrowing', {
5607
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `extends-only`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
5608
+ ...scopeFiles('lib', 'extends-only: false\n'),
5609
+ ...scopeFiles('myteam', 'extends: lib\n'),
5610
+ });
5611
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5612
+ expect(activationErrors(result)).toBe('');
5613
+ expect(activationWarnings(result)).toBe('');
5614
+ });
5615
+
5616
+ test.each([['quoted', '"true"'], ['capitalised', 'True']])('reports %s extends-only value in a local scope [022.07]', (_label, value) => {
5617
+ const workspaceRoot = createWorkspace(`activation-field-invalid-${_label}`, {
5618
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md)']),
5619
+ ...scopeFiles('lib', `extends-only: ${value}\n`),
5620
+ });
5621
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5622
+ expect(activationErrors(result)).toContain('extends-only must be unquoted lowercase true or false');
5623
+ });
5624
+
5625
+ test('marks external scope with invalid extends-only value read-only, treated as absent [022.06]', () => {
5626
+ const workspaceRoot = createWorkspace('activation-field-invalid-external', {
5627
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md)']),
5628
+ ...scopeFiles('lib', 'extends-only: yes\n'),
5629
+ '.filedist.lock': '.xdrs/lib/index.md|some-package|1.0.0\n',
5630
+ });
5631
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5632
+ expect(activationErrors(result)).toBe('');
5633
+ expect(result.readOnlyScopes.get('lib').join('\n')).toContain('invalid extends-only value "yes"');
5634
+ });
5635
+
5636
+ test('reports extends-only on core-type and _local scopes [022.07]', () => {
5637
+ const workspaceRoot = createWorkspace('activation-field-core-local', {
5638
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope area-core](area-core/index.md) `extends-only`', ACTIVATION_LEGEND]),
5639
+ ...scopeFiles('area-core', 'extends-only: true\n', 'core'),
5640
+ '.xdrs/_local/index.md': localScopeIndex().replace('valid-from: 2026-01-01', 'valid-from: 2026-01-01\nextends-only: true'),
5641
+ ...coreWithScopeTypes(['_local']),
5642
+ });
5643
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5644
+ const errors = activationErrors(result);
5645
+ expect(errors).toContain('Core-type scope "area-core" must not be tagged `extends-only`');
5646
+ expect(errors).toContain('area-core/index.md [_core-adr-policy-022.07-extends-only-field]');
5647
+ expect(errors).toContain('_local/index.md [_core-adr-policy-022.07-extends-only-field]');
5648
+ });
5649
+
5650
+ test('warns when no active scope extends an extends-only scope [022.04]', () => {
5651
+ const workspaceRoot = createWorkspace('activation-unused-extends-only', {
5652
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `extends-only`', ACTIVATION_LEGEND]),
5653
+ ...scopeFiles('lib'),
5654
+ });
5655
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5656
+ expect(activationErrors(result)).toBe('');
5657
+ expect(activationWarnings(result)).toContain('Scope "lib" is tagged `extends-only` but no active scope extends it');
5658
+ });
5659
+
5660
+ test('warns when an extends-only scope is reached only through a disabled scope [022.04]', () => {
5661
+ const workspaceRoot = createWorkspace('activation-reach-through-disabled', {
5662
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `extends-only`', '[View scope mid](mid/index.md) `disabled`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
5663
+ ...scopeFiles('lib'),
5664
+ ...scopeFiles('mid', 'extends: lib\n'),
5665
+ ...scopeFiles('myteam', 'extends: mid\n'),
5666
+ });
5667
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5668
+ expect(activationWarnings(result)).toContain('Scope "lib" is tagged `extends-only`');
5669
+ expect(result.readOnlyScopes.get('myteam').join('\n')).toContain('extends: scope "mid" is tagged `disabled`');
5670
+ });
5671
+
5672
+ test('makes a scope following a disabled scope read-only while it still reaches extends-only scopes [022.04, 022.06]', () => {
5673
+ const workspaceRoot = createWorkspace('activation-read-only-reaches', {
5674
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope area-core](area-core/index.md) `disabled`', '[View scope lib](lib/index.md) `extends-only`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
5675
+ ...scopeFiles('area-core', '', 'core'),
5676
+ ...scopeFiles('lib'),
5677
+ ...scopeFiles('myteam', 'follows: area-core\nextends: lib\n'),
5678
+ });
5679
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5680
+ expect(activationWarnings(result)).toBe('');
5681
+ expect(result.readOnlyScopes.get('myteam').join('\n')).toContain('follows: scope "area-core" is tagged `disabled` in the root index; enable it or remove the follows: reference');
5682
+ });
5683
+
5684
+ test('makes an external scope extending a disabled scope read-only [022.06]', () => {
5685
+ const workspaceRoot = createWorkspace('activation-external-extends-disabled', {
5686
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope vendor](vendor/index.md)', ACTIVATION_LEGEND]),
5687
+ ...scopeFiles('lib'),
5688
+ ...scopeFiles('vendor', 'extends: lib\n'),
5689
+ '.filedist.lock': '.xdrs/vendor/index.md|some-package|1.0.0\n',
5690
+ });
5691
+ const result = lintWorkspace(workspaceRoot);
5692
+ expect(result.readOnlyScopes.has('vendor')).toBe(true);
5693
+ });
5694
+
5695
+ test('makes _local extending a disabled scope read-only [022.06]', () => {
5696
+ const workspaceRoot = createWorkspace('activation-local-extends-disabled', {
5697
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', ACTIVATION_LEGEND]),
5698
+ ...scopeFiles('lib'),
5699
+ '.xdrs/_local/index.md': localScopeIndex().replace('valid-from: 2026-01-01', 'valid-from: 2026-01-01\nextends: lib'),
5700
+ ...coreWithScopeTypes(['standard', '_local']),
5701
+ });
5702
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5703
+ expect(result.readOnlyScopes.has('_local')).toBe(true);
5704
+ });
5705
+
5706
+ test('makes a scope whose type is defined only in a disabled scope read-only [022.06]', () => {
5707
+ const workspaceRoot = createWorkspace('activation-type-in-disabled-scope', {
5708
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope widgets-core](widgets-core/index.md) `disabled`', '[View scope shop](shop/index.md)', ACTIVATION_LEGEND]),
5709
+ '.xdrs/widgets-core/index.md': scopeIndexWith('widgets-core', '', 'core'),
5710
+ '.xdrs/widgets-core/adrs/index.md': teamAdrIndex(['- [001-widget-scope-type](principles/001-widget-scope-type.md) - Widget type']),
5711
+ '.xdrs/widgets-core/adrs/principles/001-widget-scope-type.md': structuredScopeTypePolicy('widgets-core', '001', 'widget'),
5712
+ ...scopeFiles('shop', '', 'widget'),
5713
+ ...coreWithScopeTypes(['standard']),
5714
+ });
5715
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5716
+ expect(result.errors.join('\n')).not.toContain('has no corresponding widget-scope-type policy');
5717
+ expect(result.readOnlyScopes.get('shop').join('\n')).toContain('scope type "widget" is defined only in disabled scope(s) "widgets-core"');
5718
+ });
5719
+
5720
+ test('prints one read-only line per cause citing 022.06', () => {
5721
+ const workspaceRoot = createWorkspace('activation-cli-read-only', {
5722
+ '.xdrs/index.md': rootIndex(['[View scope _core](_core/index.md)', '[View scope area-core](area-core/index.md) `disabled`', '[View scope lib](lib/index.md) `disabled`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
5723
+ ...scopeFiles('area-core', '', 'core'),
5724
+ ...scopeFiles('lib'),
5725
+ ...scopeFiles('myteam', 'follows: area-core\nextends: lib\n'),
5726
+ });
5727
+ const { runLintCli } = require('./lint');
5728
+ const logs = [];
5729
+ const logSpy = jest.spyOn(console, 'log').mockImplementation((msg) => logs.push(msg));
5730
+ const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
5731
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
5732
+ try {
5733
+ runLintCli(['--all', workspaceRoot]);
5734
+ } finally {
5735
+ logSpy.mockRestore();
5736
+ errSpy.mockRestore();
5737
+ warnSpy.mockRestore();
5738
+ }
5739
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5740
+ expect(result.readOnlyScopes.get('myteam')).toHaveLength(2);
5741
+ const readOnlyLines = logs.filter((l) => l.includes('READ-ONLY scope "myteam"'));
5742
+ expect(readOnlyLines).toHaveLength(2);
5743
+ expect(readOnlyLines.every((l) => l.includes('[_core-adr-policy-022.06-read-only-dependencies]'))).toBe(true);
5744
+ });
5745
+
5746
+ test('dry-run workspace: disabled core, extends-only ref, unlisted legacy scope', () => {
5747
+ const workspaceRoot = createWorkspace('activation-dry-run', {
5748
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope ecomm-core](ecomm-core/index.md) `disabled`', '[View scope payments-ref](payments-ref/index.md) `extends-only`', '[View scope checkout](checkout/index.md)', ACTIVATION_LEGEND]),
5749
+ ...scopeFiles('ecomm-core', '', 'core'),
5750
+ ...scopeFiles('payments-ref', 'extends-only: true\n'),
5751
+ ...scopeFiles('checkout', 'follows: ecomm-core\nextends: payments-ref\n'),
5752
+ ...scopeFiles('legacy'),
5753
+ });
5754
+ const disabledIndexBefore = fs.readFileSync(path.join(workspaceRoot, '.xdrs/ecomm-core/index.md'), 'utf8');
5755
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5756
+ expect(result.readOnlyScopes.get('checkout')).toHaveLength(1);
5757
+ expect(activationWarnings(result)).toBe('');
5758
+ expect(activationErrors(result)).toContain('[View scope legacy](legacy/index.md)');
5759
+ expect(activationErrors(result).split('\n')).toHaveLength(1);
5760
+ expect(fs.readFileSync(path.join(workspaceRoot, '.xdrs/ecomm-core/index.md'), 'utf8')).toBe(disabledIndexBefore);
5761
+ });
5762
+
5763
+ test('reports activation tags without the legend line [022.09]', () => {
5764
+ const workspaceRoot = createWorkspace('activation-no-legend', {
5765
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md) `disabled`']),
5766
+ ...scopeFiles('myteam'),
5767
+ });
5768
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5769
+ expect(activationErrors(result)).toContain('[_core-adr-policy-022.09-root-index-ownership]');
5770
+ });
5771
+
5772
+ function coreWithParentType(typeName, parentType) {
5773
+ const files = coreWithScopeTypes(['standard', typeName]);
5774
+ const policyPath = Object.keys(files).find((p) => p.endsWith(`-${typeName}-scope-type.md`));
5775
+ const num = path.basename(policyPath).slice(0, 3);
5776
+ files[policyPath] = `${structuredScopeTypePolicy('_core', num, typeName)}\n#### 02-parent-scope-type\n\nInstances inherit all rules from the \`${parentType}\` scope type.\n`;
5777
+ return files;
5778
+ }
5779
+
5780
+ test('treats a type whose parent chain reaches core as core-type (naming, follows, structured) [011.03]', () => {
5781
+ const workspaceRoot = createWorkspace('scope-type-inherited-core', {
5782
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope gov](gov/index.md)', '[View scope myteam](myteam/index.md)']),
5783
+ ...scopeFiles('gov', '', 'governance'),
5784
+ ...scopeFiles('myteam', 'follows: gov\n'),
5785
+ ...coreWithParentType('governance', 'core'),
5786
+ });
5787
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5788
+ const errors = result.errors.join('\n');
5789
+ expect(errors).toContain('gov/index.md [_core-adr-policy-011.03-naming-convention]');
5790
+ expect(errors).not.toContain('follows references scope "gov" which is not a core-type scope');
5791
+ expect(errors).toContain('_core-adr-policy-010.12-def-core-scope-policies-must-be-structured');
5792
+ });
5793
+
5794
+ test('reports missing scope-type for local scopes and marks external ones read-only [010.10, 022.06]', () => {
5795
+ const noType = (name) => `---\nname: ${name}\ndescription: Scope ${name}.\napply-to: Test teams\nvalid-from: 2026-01-01\n---\n\n# ${name}\n`;
5796
+ const workspaceRoot = createWorkspace('scope-type-missing-local-external', {
5797
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope localone](localone/index.md)', '[View scope vendor](vendor/index.md)']),
5798
+ '.xdrs/localone/index.md': noType('localone'),
5799
+ '.xdrs/vendor/index.md': noType('vendor'),
5800
+ '.filedist.lock': '.xdrs/vendor/index.md|some-package|1.0.0\n',
5801
+ ...coreWithScopeTypes(['standard']),
5802
+ });
5803
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5804
+ const missingTypeErrors = result.errors.filter((e) => e.includes('must include a scope-type field'));
5805
+ expect(missingTypeErrors.join('\n')).toContain('localone/index.md');
5806
+ expect(missingTypeErrors.join('\n')).not.toContain('vendor/index.md');
5807
+ expect(result.readOnlyScopes.get('vendor').join('\n')).toContain('no scope-type');
5808
+ });
5809
+
5810
+ test('reports missing parent type for local scopes and marks external ones read-only [010.09, 022.06]', () => {
5811
+ const workspaceRoot = createWorkspace('scope-type-missing-parent', {
5812
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope localone](localone/index.md)', '[View scope vendor](vendor/index.md)']),
5813
+ ...scopeFiles('localone', '', 'widget'),
5814
+ ...scopeFiles('vendor', '', 'widget'),
5815
+ '.filedist.lock': '.xdrs/vendor/index.md|some-package|1.0.0\n',
5816
+ ...coreWithParentType('widget', 'ghost'),
5817
+ });
5818
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5819
+ const parentErrors = result.errors.filter((e) => e.includes('declares parent "ghost"'));
5820
+ expect(parentErrors).toHaveLength(1);
5821
+ expect(parentErrors[0]).toContain('localone/index.md');
5822
+ expect(result.readOnlyScopes.get('vendor').join('\n')).toContain('scope type "ghost"');
5823
+ });
5824
+
5825
+ test('accepts compiled combined with a non-core type [011.10]', () => {
5826
+ const workspaceRoot = createWorkspace('scope-type-compiled-combo', {
5827
+ '.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope sec-ref-owasp](sec-ref-owasp/index.md)']),
5828
+ ...scopeFiles('sec-ref-owasp', '', 'compiled, reference'),
5829
+ });
5830
+ const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5831
+ expect(result.errors.join('\n')).not.toContain('011.10');
5832
+ });