xdrs-core 0.44.0 → 0.46.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 +3 -0
- package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +7 -4
- package/.xdrs/_core/adrs/principles/002-policy-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +1 -2
- package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -2
- package/.xdrs/_core/adrs/principles/006-research-standards.md +6 -5
- package/.xdrs/_core/adrs/principles/007-plan-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/009-presentation-standards.md +2 -1
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +64 -16
- package/.xdrs/_core/adrs/principles/019-compiled-scope-type.md +117 -0
- package/.xdrs/_core/adrs/principles/020-media-and-asset-standards.md +48 -0
- package/.xdrs/_core/adrs/principles/articles/.assets/001-xdrs-overview.svg +258 -0
- package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +2 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/lint-verification.md +12 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/meta-policy-compliance.md +12 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/prerequisites-gate.md +19 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/scope-placement.md +47 -0
- package/.xdrs/_core/adrs/principles/skills/.assets/scope-selection.md +25 -0
- package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +7 -1
- package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +8 -24
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +8 -19
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +6 -16
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +7 -18
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +7 -18
- package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +4 -15
- package/.xdrs/_core/adrs/principles/skills/008-write-xdrs-doc/SKILL.md +1 -8
- package/.xdrs/_core/adrs/principles/skills/009-compile-scope/SKILL.md +121 -0
- package/README.md +15 -2
- package/lib/lint.js +160 -47
- package/lib/lint.test.js +659 -41
- package/package.json +1 -1
package/lib/lint.js
CHANGED
|
@@ -27,7 +27,7 @@ const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_D
|
|
|
27
27
|
|
|
28
28
|
const POLICY_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'freeze-reference']);
|
|
29
29
|
const SKILL_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'license', 'metadata', 'compatibility', 'allowed-tools']);
|
|
30
|
-
const SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS = new Set(['scope-type', 'name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'follows', '
|
|
30
|
+
const SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS = new Set(['scope-type', 'name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'follows', 'extends']);
|
|
31
31
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
|
|
32
32
|
const SLIDE_FILE_RE = /^.+-slides(?:-[a-z0-9-]+)?\.md$/;
|
|
33
33
|
const SLIDE_MAX_NAME_LENGTH = 64;
|
|
@@ -220,21 +220,48 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// Populated during lintScopeIndexFrontmatter: scopeName -> [extendedScopeNames...]
|
|
224
|
+
const extendsRelationships = new Map();
|
|
225
|
+
|
|
223
226
|
for (const scopeEntry of scopeEntries) {
|
|
224
|
-
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, warnings, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, readOnlyScopes);
|
|
227
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, warnings, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, readOnlyScopes, extendsRelationships);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Detect cycles in extends: chains (DFS)
|
|
231
|
+
const visitedCycle = new Set();
|
|
232
|
+
const inStackCycle = new Set();
|
|
233
|
+
const detectExtendsCycles = (scope, stack) => {
|
|
234
|
+
if (inStackCycle.has(scope)) {
|
|
235
|
+
const cycleStart = stack.indexOf(scope);
|
|
236
|
+
const cycle = stack.slice(cycleStart).concat(scope).join(' -> ');
|
|
237
|
+
errors.push(`Circular extends chain detected: ${cycle} [_core-adr-policy-010.31-extends-no-cycle]`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (visitedCycle.has(scope)) return;
|
|
241
|
+
visitedCycle.add(scope);
|
|
242
|
+
inStackCycle.add(scope);
|
|
243
|
+
stack.push(scope);
|
|
244
|
+
for (const target of (extendsRelationships.get(scope) || [])) {
|
|
245
|
+
detectExtendsCycles(target, stack);
|
|
246
|
+
}
|
|
247
|
+
stack.pop();
|
|
248
|
+
inStackCycle.delete(scope);
|
|
249
|
+
};
|
|
250
|
+
for (const scope of extendsRelationships.keys()) {
|
|
251
|
+
detectExtendsCycles(scope, []);
|
|
225
252
|
}
|
|
226
253
|
|
|
227
254
|
const rootIndexPath = path.join(xdrsRoot, 'index.md');
|
|
228
255
|
if (!existsFile(rootIndexPath)) {
|
|
229
256
|
errors.push('Missing required root index: .xdrs/index.md [_core-adr-policy-001]');
|
|
230
257
|
} else {
|
|
231
|
-
lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, effectiveExternalScopes);
|
|
258
|
+
lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, warnings, effectiveExternalScopes, extendsRelationships);
|
|
232
259
|
}
|
|
233
260
|
|
|
234
261
|
return { xdrsRoot, errors, warnings, readOnlyScopes };
|
|
235
262
|
}
|
|
236
263
|
|
|
237
|
-
function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, externalScopes = new Set()) {
|
|
264
|
+
function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, warnings = [], externalScopes = new Set(), extendsRelationships = new Map()) {
|
|
238
265
|
const content = fs.readFileSync(rootIndexPath, 'utf8');
|
|
239
266
|
const repoRoot = path.dirname(xdrsRoot);
|
|
240
267
|
|
|
@@ -263,6 +290,12 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
263
290
|
}
|
|
264
291
|
}
|
|
265
292
|
|
|
293
|
+
// Build set of all scopes referenced via any extends: chain
|
|
294
|
+
const extendsReferencedScopes = new Set();
|
|
295
|
+
for (const entries of extendsRelationships.values()) {
|
|
296
|
+
for (const e of entries) extendsReferencedScopes.add(e);
|
|
297
|
+
}
|
|
298
|
+
|
|
266
299
|
// Collect non-_local scopes that have type indexes and check their scope index is linked
|
|
267
300
|
const scopesWithTypeIndexes = new Set();
|
|
268
301
|
for (const indexPath of actualTypeIndexes) {
|
|
@@ -275,7 +308,16 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
275
308
|
for (const scopeName of scopesWithTypeIndexes) {
|
|
276
309
|
const scopeIndexPath = normalizePath(path.join(xdrsRoot, scopeName, 'index.md'));
|
|
277
310
|
if (!linkedSet.has(scopeIndexPath)) {
|
|
311
|
+
// Exempt scopes that are only reachable via extends: chains
|
|
312
|
+
if (extendsReferencedScopes.has(scopeName)) {
|
|
313
|
+
// Scope is extends-only (not in root index) — exempt from mandatory link requirement
|
|
314
|
+
// No warning here: absence from root index is expected and valid for extends-only scopes
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
278
317
|
errors.push(`Root index is missing scope index link: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))} [_core-adr-policy-001]`);
|
|
318
|
+
} else if (extendsReferencedScopes.has(scopeName)) {
|
|
319
|
+
// Scope is in BOTH root index and extends chain — ambiguous dual precedence
|
|
320
|
+
warnings.push(`Scope "${scopeName}" appears in both the root index and an extends: chain; precedence may be ambiguous for consumers that do not follow extends declarations: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))} [_core-adr-policy-010.35-extends-index]`);
|
|
279
321
|
}
|
|
280
322
|
}
|
|
281
323
|
}
|
|
@@ -316,7 +358,7 @@ function extractParentScopeType(policyFilePath) {
|
|
|
316
358
|
return bodyMatch ? bodyMatch[1] : null;
|
|
317
359
|
}
|
|
318
360
|
|
|
319
|
-
function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), readOnlyScopes = new Set()) {
|
|
361
|
+
function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), readOnlyScopes = new Set(), extendsRelationships = new Map()) {
|
|
320
362
|
const content = fs.readFileSync(scopeIndexPath, 'utf8');
|
|
321
363
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
322
364
|
if (!fmMatch) {
|
|
@@ -528,24 +570,27 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
528
570
|
}
|
|
529
571
|
}
|
|
530
572
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
573
|
+
// Parse extends: field
|
|
574
|
+
let extendsEntries = [];
|
|
575
|
+
const extendsLineMatch = block.match(/^extends:[ \t]*(.*)/m);
|
|
576
|
+
if (extendsLineMatch !== null) {
|
|
577
|
+
const extendsValue = extendsLineMatch[1].trim();
|
|
534
578
|
const scopeNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
579
|
+
const commaSeparatedPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*,\s*[a-zA-Z_][a-zA-Z0-9_-]*)+$/;
|
|
580
|
+
if (extendsValue) {
|
|
581
|
+
if (!scopeNamePattern.test(extendsValue) && !commaSeparatedPattern.test(extendsValue)) {
|
|
582
|
+
errors.push(`Scope index frontmatter extends must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
538
583
|
}
|
|
539
584
|
} else {
|
|
540
|
-
const
|
|
585
|
+
const extendsStart = block.indexOf(extendsLineMatch[0]) + extendsLineMatch[0].length;
|
|
541
586
|
const listItems = [];
|
|
542
587
|
let hasInvalidEntry = false;
|
|
543
|
-
for (const line of block.slice(
|
|
588
|
+
for (const line of block.slice(extendsStart).split('\n')) {
|
|
544
589
|
const itemMatch = line.match(/^\s+-\s*(\S.*)?$/);
|
|
545
590
|
if (itemMatch) {
|
|
546
591
|
const item = (itemMatch[1] || '').trim();
|
|
547
592
|
if (!item || !scopeNamePattern.test(item)) {
|
|
548
|
-
errors.push(`Scope index frontmatter
|
|
593
|
+
errors.push(`Scope index frontmatter extends entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
549
594
|
hasInvalidEntry = true;
|
|
550
595
|
break;
|
|
551
596
|
}
|
|
@@ -555,45 +600,63 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
555
600
|
}
|
|
556
601
|
}
|
|
557
602
|
if (!hasInvalidEntry && listItems.length === 0) {
|
|
558
|
-
errors.push(`Scope index frontmatter
|
|
603
|
+
errors.push(`Scope index frontmatter extends must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
559
604
|
}
|
|
560
605
|
}
|
|
561
|
-
|
|
562
|
-
|
|
606
|
+
const rawEntries = extendsValue
|
|
607
|
+
? (commaSeparatedPattern.test(extendsValue)
|
|
608
|
+
? extendsValue.split(',').map((s) => s.trim())
|
|
609
|
+
: (scopeNamePattern.test(extendsValue) ? [extendsValue] : []))
|
|
610
|
+
: (() => {
|
|
563
611
|
const items = [];
|
|
564
|
-
const
|
|
565
|
-
for (const line of block.slice(
|
|
612
|
+
const extendsStart = block.indexOf(extendsLineMatch[0]) + extendsLineMatch[0].length;
|
|
613
|
+
for (const line of block.slice(extendsStart).split('\n')) {
|
|
566
614
|
const itemMatch = line.match(/^\s+-\s*(\S.*)?$/);
|
|
567
615
|
if (itemMatch && itemMatch[1]) items.push(itemMatch[1].trim());
|
|
568
616
|
else if (line.trim() && !/^\s/.test(line)) break;
|
|
569
617
|
}
|
|
570
618
|
return items;
|
|
571
619
|
})();
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
620
|
+
const seenExtends = new Set();
|
|
621
|
+
for (const entry of rawEntries) {
|
|
622
|
+
if (seenExtends.has(entry)) {
|
|
623
|
+
errors.push(`Scope index frontmatter extends has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.27-extends-declaration]`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
seenExtends.add(entry);
|
|
627
|
+
if (!scopeNamePattern.test(entry)) continue;
|
|
628
|
+
if (entry === scopeName) {
|
|
629
|
+
errors.push(`Scope index frontmatter extends must not reference the scope itself: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.30-extends-no-self]`);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (entry === '_local' || entry === '_core') {
|
|
633
|
+
errors.push(`Scope index frontmatter extends must not reference reserved scope "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.29-extends-reserved-scopes]`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
extendsEntries.push(entry);
|
|
637
|
+
}
|
|
638
|
+
if (xdrsRoot) {
|
|
639
|
+
const seenValid = new Set();
|
|
640
|
+
for (const entry of extendsEntries) {
|
|
641
|
+
if (seenValid.has(entry)) continue;
|
|
642
|
+
seenValid.add(entry);
|
|
643
|
+
if (followsEntries.includes(entry)) {
|
|
644
|
+
errors.push(`Scope index frontmatter extends must not repeat scope "${entry}" already declared in follows: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.28-extends-disjoint]`);
|
|
645
|
+
continue;
|
|
576
646
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (entry === scopeName) {
|
|
580
|
-
errors.push(`Scope index frontmatter related-scopes must not reference the scope itself: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
if (followsEntries.includes(entry)) {
|
|
584
|
-
errors.push(`Scope index frontmatter related-scopes must not repeat scope "${entry}" already declared in follows: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
if (!existsFile(path.join(xdrsRoot, entry, 'index.md'))) {
|
|
588
|
-
errors.push(`Scope index frontmatter related-scopes references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
589
|
-
}
|
|
647
|
+
if (!existsFile(path.join(xdrsRoot, entry, 'index.md'))) {
|
|
648
|
+
errors.push(`Scope index frontmatter extends references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.32-extends-mandatory-presence]`);
|
|
590
649
|
}
|
|
591
650
|
}
|
|
651
|
+
if (extendsEntries.length > 0) {
|
|
652
|
+
extendsRelationships.set(scopeName, extendsEntries.filter((e) => existsFile(path.join(xdrsRoot, e, 'index.md'))));
|
|
653
|
+
}
|
|
592
654
|
}
|
|
593
655
|
}
|
|
594
656
|
}
|
|
595
657
|
|
|
596
|
-
|
|
658
|
+
|
|
659
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), readOnlyScopes = new Set(), extendsRelationships = new Map()) {
|
|
597
660
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
598
661
|
|
|
599
662
|
if (ignoreExternal && externalScopes.has(scopeName)) {
|
|
@@ -607,6 +670,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
607
670
|
// Determine if this scope is a core-type scope (but not _core which is exempt)
|
|
608
671
|
// Used to enforce structured format on all policies in core-type scopes
|
|
609
672
|
let isCoreTypeNonExempt = false;
|
|
673
|
+
let isCompiledScope = false;
|
|
610
674
|
if (scopeName !== '_core') {
|
|
611
675
|
const scopeIndexPath = path.join(scopePath, 'index.md');
|
|
612
676
|
if (existsFile(scopeIndexPath)) {
|
|
@@ -615,10 +679,12 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
615
679
|
const stMatch = idxContent.match(/^scope-type:\s*(.+)$/m);
|
|
616
680
|
if (stMatch) {
|
|
617
681
|
const stRaw = stMatch[1].trim();
|
|
682
|
+
// Handle both "[type1, type2]" YAML array format and "type1, type2" inline CSV format
|
|
618
683
|
const stList = stRaw.startsWith('[') && stRaw.endsWith(']')
|
|
619
684
|
? stRaw.slice(1, -1).split(',').map((s) => s.trim())
|
|
620
|
-
:
|
|
685
|
+
: stRaw.split(',').map((s) => s.trim());
|
|
621
686
|
isCoreTypeNonExempt = stList.includes('core');
|
|
687
|
+
isCompiledScope = stList.includes('compiled');
|
|
622
688
|
}
|
|
623
689
|
} catch { /* ignore read errors; will be caught later */ }
|
|
624
690
|
}
|
|
@@ -633,7 +699,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
633
699
|
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
634
700
|
continue;
|
|
635
701
|
}
|
|
636
|
-
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, warnings, actualTypeIndexes, externalScopes, isCoreTypeNonExempt);
|
|
702
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, warnings, actualTypeIndexes, externalScopes, isCoreTypeNonExempt, isCompiledScope);
|
|
637
703
|
const typeIndexPath = path.join(entryPath, 'index.md');
|
|
638
704
|
if (existsFile(typeIndexPath)) {
|
|
639
705
|
typeIndexesInScope.push(typeIndexPath);
|
|
@@ -653,12 +719,12 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
653
719
|
errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
654
720
|
} else {
|
|
655
721
|
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors, externalScopes);
|
|
656
|
-
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes);
|
|
722
|
+
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes, extendsRelationships);
|
|
657
723
|
}
|
|
658
|
-
lintLocalMetaPolicies(xdrsRoot, scopeName, errors);
|
|
724
|
+
lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope);
|
|
659
725
|
}
|
|
660
726
|
|
|
661
|
-
function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
727
|
+
function lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope = false) {
|
|
662
728
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
663
729
|
// Track qualifiers across all type folders: qualifier ('' for primary) -> filePath
|
|
664
730
|
const seenQualifiers = new Map();
|
|
@@ -694,6 +760,21 @@ function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
|
694
760
|
errors.push(`Local meta-policy companion "${path.basename(companion.filePath)}" requires a primary "NNN-core.md" in the same type folder "${typeName}": ${toDisplayPath(companion.filePath)} [_core-adr-policy-010.10b-local-primary-required]`);
|
|
695
761
|
}
|
|
696
762
|
}
|
|
763
|
+
// For compiled scopes: primary meta-policy must include ## Sources and ## Selectors
|
|
764
|
+
if (isCompiledScope && primaryInFolder) {
|
|
765
|
+
let metaContent;
|
|
766
|
+
try { metaContent = fs.readFileSync(primaryInFolder.filePath, 'utf8'); } catch { /* will be caught elsewhere */ }
|
|
767
|
+
if (metaContent !== undefined) {
|
|
768
|
+
const metaLines = metaContent.split(/\r?\n/);
|
|
769
|
+
const metaIgnored = findIgnoredMarkdownLines(metaLines);
|
|
770
|
+
if (findHeadingLine(metaLines, metaIgnored, '## Sources') === -1) {
|
|
771
|
+
errors.push(`Compiled scope meta-policy must include a "## Sources" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
|
|
772
|
+
}
|
|
773
|
+
if (findHeadingLine(metaLines, metaIgnored, '## Selectors') === -1) {
|
|
774
|
+
errors.push(`Compiled scope meta-policy must include a "## Selectors" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
697
778
|
// Check qualifier uniqueness across all type folders
|
|
698
779
|
const allInFolder = primaryInFolder ? [primaryInFolder, ...companionsInFolder] : companionsInFolder;
|
|
699
780
|
for (const { filePath, qualifier } of allInFolder) {
|
|
@@ -710,7 +791,7 @@ function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
|
710
791
|
}
|
|
711
792
|
}
|
|
712
793
|
|
|
713
|
-
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false) {
|
|
794
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
714
795
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
715
796
|
const indexPath = path.join(typePath, 'index.md');
|
|
716
797
|
const xdrsNumbers = new Map();
|
|
@@ -741,7 +822,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
741
822
|
warnings.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001] [exempt: freeze-reference]`);
|
|
742
823
|
}
|
|
743
824
|
|
|
744
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, warnings, externalScopes, enforceStructured));
|
|
825
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, warnings, externalScopes, enforceStructured, isCompiledScope));
|
|
745
826
|
}
|
|
746
827
|
|
|
747
828
|
if (existsFile(indexPath)) {
|
|
@@ -749,7 +830,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
749
830
|
}
|
|
750
831
|
}
|
|
751
832
|
|
|
752
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false) {
|
|
833
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
753
834
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
754
835
|
const artifacts = [];
|
|
755
836
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -788,7 +869,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
788
869
|
}
|
|
789
870
|
|
|
790
871
|
artifacts.push(entryPath);
|
|
791
|
-
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, warnings, externalScopes, enforceStructured);
|
|
872
|
+
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, warnings, externalScopes, enforceStructured, isCompiledScope);
|
|
792
873
|
}
|
|
793
874
|
|
|
794
875
|
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
@@ -798,7 +879,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
798
879
|
return artifacts;
|
|
799
880
|
}
|
|
800
881
|
|
|
801
|
-
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false) {
|
|
882
|
+
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
802
883
|
const baseName = path.basename(filePath);
|
|
803
884
|
const match = baseName.match(NUMBERED_FILE_RE);
|
|
804
885
|
if (!match) {
|
|
@@ -840,9 +921,15 @@ function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePat
|
|
|
840
921
|
lintXdrsElementFrontmatter(content, expectedName, filePath, errors, warnings, freezeReference);
|
|
841
922
|
if (enforceStructured) {
|
|
842
923
|
validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-010.08c-def-core-scope-policies-must-be-structured');
|
|
924
|
+
} else if (isCompiledScope) {
|
|
925
|
+
validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-019.04-structured-format-required');
|
|
843
926
|
} else {
|
|
844
927
|
lintStructuredRuleBlocks(content, filePath, errors);
|
|
845
928
|
}
|
|
929
|
+
const isLocalMetaPolicy = subjectName === 'principles' && titleParts[0] === 'core';
|
|
930
|
+
if (isCompiledScope && !isLocalMetaPolicy) {
|
|
931
|
+
lintCompiledPolicySourceSection(content, filePath, errors);
|
|
932
|
+
}
|
|
846
933
|
lintRequiredSections(content, filePath, XDRS_REQUIRED_SECTIONS, 'Policy', errors);
|
|
847
934
|
lintNoEmojis(content, filePath, 'Policy', errors);
|
|
848
935
|
lintWordCount(content, filePath, 'Policy', POLICY_MAX_WORDS, errors);
|
|
@@ -1645,6 +1732,32 @@ function validateStructuredFormatContent(content, filePath, errors, errorCode) {
|
|
|
1645
1732
|
lintStructuredRuleBlocks(content, filePath, errors);
|
|
1646
1733
|
}
|
|
1647
1734
|
|
|
1735
|
+
function lintCompiledPolicySourceSection(content, filePath, errors) {
|
|
1736
|
+
const lines = content.split(/\r?\n/);
|
|
1737
|
+
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
1738
|
+
const sourceStart = findHeadingLine(lines, ignoredLines, '## Source');
|
|
1739
|
+
if (sourceStart === -1) {
|
|
1740
|
+
errors.push(`Compiled policy must include a "## Source" section: ${toDisplayPath(filePath)} [_core-adr-policy-019.10-source-section]`);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
// Find end of ## Source section (next ## heading or EOF)
|
|
1744
|
+
let sourceEnd = lines.length;
|
|
1745
|
+
for (let i = sourceStart + 1; i < lines.length; i++) {
|
|
1746
|
+
if (!ignoredLines[i] && /^## /.test(lines[i])) {
|
|
1747
|
+
sourceEnd = i;
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
// Paths in ## Source MUST NOT be written as markdown links
|
|
1752
|
+
const MD_LINK_RE = /\[.+?\]\(.+?\)/;
|
|
1753
|
+
for (let i = sourceStart + 1; i < sourceEnd; i++) {
|
|
1754
|
+
if (!ignoredLines[i] && MD_LINK_RE.test(lines[i])) {
|
|
1755
|
+
errors.push(`Compiled policy "## Source" section must not use markdown links (use plain text paths): ${toDisplayPath(filePath)}:${i + 1} [_core-adr-policy-019.10-source-section]`);
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1648
1761
|
function findIgnoredMarkdownLines(lines) {
|
|
1649
1762
|
const ignored = [];
|
|
1650
1763
|
let inCodeFence = false;
|