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.
Files changed (31) hide show
  1. package/.xdrs/_core/adrs/index.md +3 -0
  2. package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +7 -4
  3. package/.xdrs/_core/adrs/principles/002-policy-standards.md +1 -1
  4. package/.xdrs/_core/adrs/principles/003-skill-standards.md +1 -2
  5. package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -2
  6. package/.xdrs/_core/adrs/principles/006-research-standards.md +6 -5
  7. package/.xdrs/_core/adrs/principles/007-plan-standards.md +1 -1
  8. package/.xdrs/_core/adrs/principles/009-presentation-standards.md +2 -1
  9. package/.xdrs/_core/adrs/principles/010-scope-governance.md +64 -16
  10. package/.xdrs/_core/adrs/principles/019-compiled-scope-type.md +117 -0
  11. package/.xdrs/_core/adrs/principles/020-media-and-asset-standards.md +48 -0
  12. package/.xdrs/_core/adrs/principles/articles/.assets/001-xdrs-overview.svg +258 -0
  13. package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +2 -0
  14. package/.xdrs/_core/adrs/principles/skills/.assets/lint-verification.md +12 -0
  15. package/.xdrs/_core/adrs/principles/skills/.assets/meta-policy-compliance.md +12 -0
  16. package/.xdrs/_core/adrs/principles/skills/.assets/prerequisites-gate.md +19 -0
  17. package/.xdrs/_core/adrs/principles/skills/.assets/scope-placement.md +47 -0
  18. package/.xdrs/_core/adrs/principles/skills/.assets/scope-selection.md +25 -0
  19. package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +7 -1
  20. package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +8 -24
  21. package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +8 -19
  22. package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +6 -16
  23. package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +7 -18
  24. package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +7 -18
  25. package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +4 -15
  26. package/.xdrs/_core/adrs/principles/skills/008-write-xdrs-doc/SKILL.md +1 -8
  27. package/.xdrs/_core/adrs/principles/skills/009-compile-scope/SKILL.md +121 -0
  28. package/README.md +15 -2
  29. package/lib/lint.js +160 -47
  30. package/lib/lint.test.js +659 -41
  31. 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', 'related-scopes']);
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
- const relatedScopesLineMatch = block.match(/^related-scopes:[ \t]*(.*)/m);
532
- if (relatedScopesLineMatch !== null) {
533
- const relatedScopesValue = relatedScopesLineMatch[1].trim();
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
- if (relatedScopesValue) {
536
- if (!scopeNamePattern.test(relatedScopesValue)) {
537
- errors.push(`Scope index frontmatter related-scopes must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
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 relatedScopesStart = block.indexOf(relatedScopesLineMatch[0]) + relatedScopesLineMatch[0].length;
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(relatedScopesStart).split('\n')) {
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 related-scopes entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
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 related-scopes must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
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
- if (xdrsRoot) {
562
- const entries = relatedScopesValue ? [relatedScopesValue] : (() => {
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 relatedScopesStart = block.indexOf(relatedScopesLineMatch[0]) + relatedScopesLineMatch[0].length;
565
- for (const line of block.slice(relatedScopesStart).split('\n')) {
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
- const seenRelated = new Set();
573
- for (const entry of entries) {
574
- if (seenRelated.has(entry)) {
575
- errors.push(`Scope index frontmatter related-scopes has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
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
- seenRelated.add(entry);
578
- if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(entry)) {
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
- function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), readOnlyScopes = new Set()) {
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
- : [stRaw];
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;