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/.xdrs/_core/adrs/index.md +1 -0
- package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +6 -5
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +11 -10
- package/.xdrs/_core/adrs/principles/011-core-scope-type.md +5 -1
- package/.xdrs/_core/adrs/principles/019-compiled-scope-type.md +38 -6
- package/.xdrs/_core/adrs/principles/022-scope-activation.md +69 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/prerequisites-gate.md +1 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/scope-placement.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/compile-scope/SKILL.md +79 -41
- package/.xdrs/_core/adrs/principles/skills/review/SKILL.md +1 -0
- package/.xdrs/_core/adrs/principles/skills/write-article/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/write-initiative/SKILL.md +1 -1
- package/.xdrs/_core/adrs/principles/skills/write-policy/SKILL.md +9 -9
- package/.xdrs/_core/adrs/principles/skills/write-research/SKILL.md +1 -1
- package/.xdrs/_core/index.md +1 -1
- package/.xdrs/index.md +2 -0
- package/AGENTS.md +1 -1
- package/README.md +26 -2
- package/lib/lint.js +446 -158
- package/lib/lint.test.js +837 -25
- package/lib/testPrompt.js +0 -2
- package/package.json +1 -1
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-
|
|
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-
|
|
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-
|
|
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.
|
|
1811
|
-
expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.
|
|
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.
|
|
1847
|
-
expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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
|
|
2476
|
-
expect(result.errors.filter((e) => e.includes('Root index is missing scope index link') && e.includes('shared-policies')).
|
|
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('
|
|
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.
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
+
});
|