xdrs-core 0.41.1 → 0.43.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 +2 -1
- package/.xdrs/_core/adrs/principles/{001-xdrs-core.md → 001-xdrs-standards.md} +4 -4
- package/.xdrs/_core/adrs/principles/002-policy-standards.md +2 -2
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/006-research-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/007-plan-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/008-policy-structured-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/009-presentation-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +76 -27
- package/.xdrs/_core/adrs/principles/011-core-scope-type.md +3 -3
- package/.xdrs/_core/adrs/principles/012-reference-scope-type.md +1 -1
- package/.xdrs/_core/adrs/principles/013-platform-scope-type.md +1 -1
- package/.xdrs/_core/adrs/principles/014-standard-scope-type.md +1 -1
- package/.xdrs/_core/adrs/principles/015-local-scope-type.md +1 -1
- package/.xdrs/_core/adrs/principles/016-policy-subjects.md +1 -1
- package/.xdrs/_core/adrs/principles/017-policy-numbering-ranges.md +1 -1
- package/.xdrs/_core/adrs/principles/018-external-path-symlinks.md +49 -0
- package/.xdrs/_core/adrs/principles/articles/.assets/001-xdrs-overview-slides.md +1 -1
- package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +3 -3
- package/.xdrs/_core/adrs/principles/skills/.assets/conflict-detection.md +76 -0
- package/.xdrs/_core/adrs/principles/skills/001-review/001-review.test.int.js +2 -2
- package/.xdrs/_core/adrs/principles/skills/001-review/001-review.test.int.report +2 -2
- package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +5 -4
- package/.xdrs/_core/adrs/principles/skills/002-write-policy/SKILL.md +12 -9
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +9 -6
- package/.xdrs/_core/adrs/principles/skills/004-write-article/004-write-article.test.int.js +1 -1
- package/.xdrs/_core/adrs/principles/skills/004-write-article/004-write-article.test.int.report +1 -1
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +9 -6
- package/.xdrs/_core/adrs/principles/skills/005-write-research/005-write-research.test.int.report +1 -1
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +9 -6
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +9 -6
- package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +7 -4
- package/.xdrs/_core/adrs/principles/skills/008-write-xdrs-doc/SKILL.md +5 -2
- package/.xdrs/_core/index.md +5 -3
- package/README.md +32 -2
- package/lib/lint.js +207 -47
- package/lib/lint.test.js +694 -25
- package/package.json +1 -1
package/lib/lint.js
CHANGED
|
@@ -149,9 +149,13 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
149
149
|
const scopeEntries = rootEntries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
150
150
|
|
|
151
151
|
// Pass 1: collect known scope-type names from NNN-{scope-type}-scope-type.md files in any scope
|
|
152
|
-
const knownScopeTypes = new Map(); // scopeTypeName -> filePath
|
|
152
|
+
const knownScopeTypes = new Map(); // scopeTypeName -> filePath (first primary found, for downstream validation)
|
|
153
|
+
const scopeTypePrimaryKeys = new Set(); // "scopeName|typeName|scopeTypeName" for companion co-location checks
|
|
154
|
+
const scopeTypePrimaryByScope = new Map(); // "scopeName|scopeTypeName" -> [typeName, ...] for duplicate detection
|
|
155
|
+
const scopeTypeCompanions = []; // [{scopeTypeName, scopeName, typeName, filePath}]
|
|
153
156
|
for (const scopeEntry of scopeEntries) {
|
|
154
|
-
const
|
|
157
|
+
const scopeName = scopeEntry.name;
|
|
158
|
+
const scopePath = path.join(xdrsRoot, scopeName);
|
|
155
159
|
for (const typeName of Object.keys(TYPE_TO_ID)) {
|
|
156
160
|
const principlesDir = path.join(scopePath, typeName, 'principles');
|
|
157
161
|
if (!existsDirectory(principlesDir)) continue;
|
|
@@ -162,17 +166,54 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
162
166
|
const m = entry.name.match(NUMBERED_FILE_RE);
|
|
163
167
|
if (!m) continue;
|
|
164
168
|
const shortTitle = m[2];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const filePath = path.join(principlesDir, entry.name);
|
|
170
|
+
if (shortTitle.endsWith('-scope-type')) {
|
|
171
|
+
// Primary scope-type definition file
|
|
172
|
+
const rawScopeTypeName = shortTitle.slice(0, -'-scope-type'.length);
|
|
173
|
+
// Special-case: 'local' maps to '_local' (underscore not allowed in NUMBERED_FILE_RE)
|
|
174
|
+
const scopeTypeName = rawScopeTypeName === 'local' ? '_local' : rawScopeTypeName;
|
|
175
|
+
if (!knownScopeTypes.has(scopeTypeName)) {
|
|
176
|
+
knownScopeTypes.set(scopeTypeName, filePath);
|
|
177
|
+
}
|
|
178
|
+
scopeTypePrimaryKeys.add(`${scopeName}|${typeName}|${scopeTypeName}`);
|
|
179
|
+
const scopeKey = `${scopeName}|${scopeTypeName}`;
|
|
180
|
+
if (!scopeTypePrimaryByScope.has(scopeKey)) {
|
|
181
|
+
scopeTypePrimaryByScope.set(scopeKey, []);
|
|
182
|
+
}
|
|
183
|
+
scopeTypePrimaryByScope.get(scopeKey).push(typeName);
|
|
184
|
+
validateStructuredFormat(filePath, errors, '_core-adr-policy-010.08b-def-scope-type-must-be-structured');
|
|
185
|
+
} else {
|
|
186
|
+
// Check if it's a companion file: title contains '-scope-type-' followed by a qualifier
|
|
187
|
+
const stIdx = shortTitle.indexOf('-scope-type-');
|
|
188
|
+
if (stIdx > 0) {
|
|
189
|
+
const rawScopeTypeName = shortTitle.slice(0, stIdx);
|
|
190
|
+
// Special-case: 'local' maps to '_local'
|
|
191
|
+
const scopeTypeName = rawScopeTypeName === 'local' ? '_local' : rawScopeTypeName;
|
|
192
|
+
scopeTypeCompanions.push({ scopeTypeName, scopeName, typeName, filePath });
|
|
193
|
+
validateStructuredFormat(filePath, errors, '_core-adr-policy-010.08b-def-scope-type-must-be-structured');
|
|
194
|
+
}
|
|
171
195
|
}
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
}
|
|
175
199
|
|
|
200
|
+
// Validate: no duplicate primaries across type folders in the same scope
|
|
201
|
+
for (const [scopeKey, typeNames] of scopeTypePrimaryByScope) {
|
|
202
|
+
if (typeNames.length > 1) {
|
|
203
|
+
const pipeIdx = scopeKey.indexOf('|');
|
|
204
|
+
const sName = scopeKey.slice(0, pipeIdx);
|
|
205
|
+
const stName = scopeKey.slice(pipeIdx + 1);
|
|
206
|
+
errors.push(`Scope type "${stName}" has primary definitions in multiple type folders [${typeNames.join(', ')}] in scope "${sName}": only one primary is allowed per scope [_core-adr-policy-010.02-def-naming]`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate: each companion must have a corresponding primary in the same scope and type folder
|
|
211
|
+
for (const { scopeTypeName, scopeName, typeName, filePath } of scopeTypeCompanions) {
|
|
212
|
+
if (!scopeTypePrimaryKeys.has(`${scopeName}|${typeName}|${scopeTypeName}`)) {
|
|
213
|
+
errors.push(`Companion scope-type file has no corresponding primary "${scopeTypeName}-scope-type" policy in the same scope "${scopeName}" and type folder "${typeName}": ${toDisplayPath(filePath)} [_core-adr-policy-010.02b-companion-requires-primary]`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
176
217
|
for (const entry of rootEntries) {
|
|
177
218
|
if (entry.isFile() && entry.name !== 'index.md') {
|
|
178
219
|
errors.push(`Unexpected file at .xdrs root: ${entry.name} [_core-adr-policy-001]`);
|
|
@@ -292,29 +333,64 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
292
333
|
}
|
|
293
334
|
|
|
294
335
|
const scopeTypeMatch = block.match(/^scope-type:\s*(.+)$/m);
|
|
295
|
-
const
|
|
296
|
-
if (!
|
|
336
|
+
const scopeTypeRaw = scopeTypeMatch ? scopeTypeMatch[1].trim() : null;
|
|
337
|
+
if (!scopeTypeRaw) {
|
|
297
338
|
errors.push(`Scope index frontmatter must include a scope-type field: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.08-def-valid-iff-policy-exists]`);
|
|
298
339
|
return;
|
|
299
340
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
341
|
+
|
|
342
|
+
// Parse scope-type: supports single string or comma-separated list (e.g. "compiled, internal-docs")
|
|
343
|
+
const commaSeparatedScopeTypePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*,\s*[a-zA-Z_][a-zA-Z0-9_-]*)+$/;
|
|
344
|
+
let scopeTypes;
|
|
345
|
+
if (commaSeparatedScopeTypePattern.test(scopeTypeRaw)) {
|
|
346
|
+
scopeTypes = scopeTypeRaw.split(',').map((s) => s.trim());
|
|
347
|
+
} else {
|
|
348
|
+
scopeTypes = [scopeTypeRaw];
|
|
303
349
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
350
|
+
|
|
351
|
+
// Validate each element: no duplicates, valid chars, no reserved prefix
|
|
352
|
+
const seenTypes = new Set();
|
|
353
|
+
let hasElementError = false;
|
|
354
|
+
const SCOPE_TYPE_ELEMENT_RE = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
355
|
+
for (const scopeType of scopeTypes) {
|
|
356
|
+
if (!SCOPE_TYPE_ELEMENT_RE.test(scopeType)) {
|
|
357
|
+
errors.push(`Scope type element "${scopeType}" contains invalid characters (must match [a-zA-Z_][a-zA-Z0-9_-]*): ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.06-def-no-underscore-prefix]`);
|
|
358
|
+
hasElementError = true;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (scopeType.startsWith('_') && scopeType !== '_local') {
|
|
362
|
+
errors.push(`Scope type "${scopeType}" uses a reserved "_" prefix; only "_local" is a valid underscore-prefixed scope type: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.06-def-no-underscore-prefix]`);
|
|
363
|
+
hasElementError = true;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (seenTypes.has(scopeType)) {
|
|
367
|
+
errors.push(`Scope type list contains duplicate value "${scopeType}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.05b-def-multi-type-validation]`);
|
|
368
|
+
hasElementError = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
seenTypes.add(scopeType);
|
|
307
372
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
373
|
+
if (hasElementError) return;
|
|
374
|
+
|
|
375
|
+
// Use first element for single-value checks that only apply to the primary type
|
|
376
|
+
const scopeType = scopeTypes[0];
|
|
377
|
+
|
|
378
|
+
// Validate each type exists in knownScopeTypes and check parent chains with global cycle detection
|
|
379
|
+
const globalVisited = new Set(scopeTypes);
|
|
380
|
+
for (const st of scopeTypes) {
|
|
381
|
+
if (st === '_local') continue;
|
|
382
|
+
if (!knownScopeTypes.has(st)) {
|
|
383
|
+
errors.push(`Scope index scope-type "${st}" has no corresponding ${st}-scope-type policy in the principles of any scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.08-def-valid-iff-policy-exists]`);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// Validate parent chain
|
|
387
|
+
let currentType = st;
|
|
312
388
|
while (currentType) {
|
|
313
389
|
const policyPath = knownScopeTypes.get(currentType);
|
|
314
390
|
if (!policyPath) break;
|
|
315
391
|
const parentType = extractParentScopeType(policyPath);
|
|
316
392
|
if (!parentType) break;
|
|
317
|
-
if (
|
|
393
|
+
if (globalVisited.has(parentType)) {
|
|
318
394
|
errors.push(`Scope type "${currentType}" declares parent "${parentType}" which creates a cycle in the scope-type inheritance chain: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.07-def-parent-scope-type]`);
|
|
319
395
|
break;
|
|
320
396
|
}
|
|
@@ -322,7 +398,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
322
398
|
errors.push(`Scope type "${currentType}" declares parent "${parentType}" but no ${parentType}-scope-type policy exists in any scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.07-def-parent-scope-type]`);
|
|
323
399
|
break;
|
|
324
400
|
}
|
|
325
|
-
|
|
401
|
+
globalVisited.add(parentType);
|
|
326
402
|
currentType = parentType;
|
|
327
403
|
}
|
|
328
404
|
}
|
|
@@ -332,16 +408,16 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
332
408
|
if (scopeName === '_local' && scopeType !== '_local') {
|
|
333
409
|
errors.push(`Scope "_local" must have scope-type "_local": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-015.01-scope-type-name]`);
|
|
334
410
|
}
|
|
335
|
-
if (
|
|
411
|
+
if (scopeTypes.includes('core') && !scopeName.includes('core')) {
|
|
336
412
|
errors.push(`Scope with type "core" must have "core" in its name: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-011.03-naming-convention]`);
|
|
337
413
|
}
|
|
338
|
-
if (
|
|
414
|
+
if (scopeTypes.includes('_local') && scopeName !== '_local') {
|
|
339
415
|
errors.push(`Scope type "_local" is reserved for the "_local" scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-015.03-exclusive-reservation]`);
|
|
340
416
|
}
|
|
341
|
-
if (
|
|
417
|
+
if (scopeTypes.includes('reference') && !scopeName.includes('-ref-')) {
|
|
342
418
|
errors.push(`Scope with type "reference" must follow the naming pattern {domain}-ref-{name} (e.g. "security-ref-baseline"): ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-012.03-naming-convention]`);
|
|
343
419
|
}
|
|
344
|
-
if (
|
|
420
|
+
if (scopeTypes.includes('platform') && !scopeName.includes('-plat-')) {
|
|
345
421
|
errors.push(`Scope with type "platform" must follow the naming pattern {domain}-plat-{name} (e.g. "cloud-plat-aws"): ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-013.03-naming-convention]`);
|
|
346
422
|
}
|
|
347
423
|
|
|
@@ -528,6 +604,26 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
528
604
|
errors.push(`Invalid scope name: ${toDisplayPath(scopePath)} [_core-adr-policy-001]`);
|
|
529
605
|
}
|
|
530
606
|
|
|
607
|
+
// Determine if this scope is a core-type scope (but not _core which is exempt)
|
|
608
|
+
// Used to enforce structured format on all policies in core-type scopes
|
|
609
|
+
let isCoreTypeNonExempt = false;
|
|
610
|
+
if (scopeName !== '_core') {
|
|
611
|
+
const scopeIndexPath = path.join(scopePath, 'index.md');
|
|
612
|
+
if (existsFile(scopeIndexPath)) {
|
|
613
|
+
try {
|
|
614
|
+
const idxContent = fs.readFileSync(scopeIndexPath, 'utf8');
|
|
615
|
+
const stMatch = idxContent.match(/^scope-type:\s*(.+)$/m);
|
|
616
|
+
if (stMatch) {
|
|
617
|
+
const stRaw = stMatch[1].trim();
|
|
618
|
+
const stList = stRaw.startsWith('[') && stRaw.endsWith(']')
|
|
619
|
+
? stRaw.slice(1, -1).split(',').map((s) => s.trim())
|
|
620
|
+
: [stRaw];
|
|
621
|
+
isCoreTypeNonExempt = stList.includes('core');
|
|
622
|
+
}
|
|
623
|
+
} catch { /* ignore read errors; will be caught later */ }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
531
627
|
const typeIndexesInScope = [];
|
|
532
628
|
const entries = safeReadDir(scopePath, errors, `read scope directory ${scopeName}`);
|
|
533
629
|
for (const entry of entries) {
|
|
@@ -537,7 +633,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
537
633
|
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
538
634
|
continue;
|
|
539
635
|
}
|
|
540
|
-
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, warnings, actualTypeIndexes, externalScopes);
|
|
636
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, warnings, actualTypeIndexes, externalScopes, isCoreTypeNonExempt);
|
|
541
637
|
const typeIndexPath = path.join(entryPath, 'index.md');
|
|
542
638
|
if (existsFile(typeIndexPath)) {
|
|
543
639
|
typeIndexesInScope.push(typeIndexPath);
|
|
@@ -559,37 +655,62 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
559
655
|
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors, externalScopes);
|
|
560
656
|
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes);
|
|
561
657
|
}
|
|
562
|
-
|
|
658
|
+
lintLocalMetaPolicies(xdrsRoot, scopeName, errors);
|
|
563
659
|
}
|
|
564
660
|
|
|
565
|
-
function
|
|
661
|
+
function lintLocalMetaPolicies(xdrsRoot, scopeName, errors) {
|
|
566
662
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
567
|
-
|
|
568
|
-
const
|
|
663
|
+
// Track qualifiers across all type folders: qualifier ('' for primary) -> filePath
|
|
664
|
+
const seenQualifiers = new Map();
|
|
569
665
|
for (const typeName of Object.keys(TYPE_TO_ID)) {
|
|
570
666
|
const principlesDir = path.join(scopePath, typeName, 'principles');
|
|
571
667
|
if (!existsDirectory(principlesDir)) continue;
|
|
572
668
|
let principlesEntries;
|
|
573
669
|
try { principlesEntries = fs.readdirSync(principlesDir, { withFileTypes: true }); } catch { continue; }
|
|
670
|
+
// Collect primary and companions per type folder
|
|
671
|
+
let primaryInFolder = null;
|
|
672
|
+
const companionsInFolder = [];
|
|
574
673
|
for (const entry of principlesEntries) {
|
|
575
674
|
if (!entry.isFile()) continue;
|
|
576
|
-
|
|
675
|
+
const m = entry.name.match(NUMBERED_FILE_RE);
|
|
676
|
+
if (!m) continue;
|
|
677
|
+
const title = m[2];
|
|
678
|
+
// Exclude scope-type definition files
|
|
679
|
+
if (title.endsWith('-scope-type')) continue;
|
|
680
|
+
const titleParts = title.split('-');
|
|
681
|
+
if (titleParts[0] !== 'core') continue;
|
|
682
|
+
// Determine qualifier ('') for primary, qualifier string for companions
|
|
683
|
+
const qualifier = titleParts.length === 1 ? '' : titleParts.slice(1).join('-');
|
|
577
684
|
const filePath = path.join(principlesDir, entry.name);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
foundPolicies.push(filePath);
|
|
685
|
+
if (qualifier === '') {
|
|
686
|
+
primaryInFolder = { filePath, qualifier };
|
|
687
|
+
} else {
|
|
688
|
+
companionsInFolder.push({ filePath, qualifier });
|
|
583
689
|
}
|
|
584
690
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
691
|
+
// Error: companion exists without primary in same type folder
|
|
692
|
+
for (const companion of companionsInFolder) {
|
|
693
|
+
if (!primaryInFolder) {
|
|
694
|
+
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
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Check qualifier uniqueness across all type folders
|
|
698
|
+
const allInFolder = primaryInFolder ? [primaryInFolder, ...companionsInFolder] : companionsInFolder;
|
|
699
|
+
for (const { filePath, qualifier } of allInFolder) {
|
|
700
|
+
const existing = seenQualifiers.get(qualifier);
|
|
701
|
+
if (existing) {
|
|
702
|
+
const label = qualifier === '' ? 'primary (no qualifier)' : `qualifier "${qualifier}"`;
|
|
703
|
+
errors.push(`Local meta-policy ${label} appears in multiple type folders in scope "${scopeName}": ${toDisplayPath(existing)} and ${toDisplayPath(filePath)} [_core-adr-policy-010.10c-local-unique-qualifier]`);
|
|
704
|
+
} else {
|
|
705
|
+
seenQualifiers.set(qualifier, filePath);
|
|
706
|
+
}
|
|
707
|
+
// Validate structured format for all local meta-policy files
|
|
708
|
+
validateStructuredFormat(filePath, errors, '_core-adr-policy-010.12b-local-must-be-structured');
|
|
709
|
+
}
|
|
589
710
|
}
|
|
590
711
|
}
|
|
591
712
|
|
|
592
|
-
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actualTypeIndexes, externalScopes = new Set()) {
|
|
713
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false) {
|
|
593
714
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
594
715
|
const indexPath = path.join(typePath, 'index.md');
|
|
595
716
|
const xdrsNumbers = new Map();
|
|
@@ -620,7 +741,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
620
741
|
warnings.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001] [exempt: freeze-reference]`);
|
|
621
742
|
}
|
|
622
743
|
|
|
623
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, warnings, externalScopes));
|
|
744
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, warnings, externalScopes, enforceStructured));
|
|
624
745
|
}
|
|
625
746
|
|
|
626
747
|
if (existsFile(indexPath)) {
|
|
@@ -628,7 +749,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
628
749
|
}
|
|
629
750
|
}
|
|
630
751
|
|
|
631
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, warnings, externalScopes = new Set()) {
|
|
752
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false) {
|
|
632
753
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
633
754
|
const artifacts = [];
|
|
634
755
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -667,7 +788,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
667
788
|
}
|
|
668
789
|
|
|
669
790
|
artifacts.push(entryPath);
|
|
670
|
-
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, warnings, externalScopes);
|
|
791
|
+
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, warnings, externalScopes, enforceStructured);
|
|
671
792
|
}
|
|
672
793
|
|
|
673
794
|
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
@@ -677,7 +798,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
677
798
|
return artifacts;
|
|
678
799
|
}
|
|
679
800
|
|
|
680
|
-
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, warnings, externalScopes = new Set()) {
|
|
801
|
+
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, warnings, externalScopes = new Set(), enforceStructured = false) {
|
|
681
802
|
const baseName = path.basename(filePath);
|
|
682
803
|
const match = baseName.match(NUMBERED_FILE_RE);
|
|
683
804
|
if (!match) {
|
|
@@ -707,10 +828,21 @@ function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePat
|
|
|
707
828
|
warnings.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)} [_core-adr-policy-002] [exempt: freeze-reference]`);
|
|
708
829
|
}
|
|
709
830
|
|
|
831
|
+
// Enforce 'core' word reservation: only allowed as first word of filename title in principles/
|
|
832
|
+
const titleParts = match[2].split('-');
|
|
833
|
+
if (titleParts.includes('core') && !(subjectName === 'principles' && titleParts[0] === 'core')) {
|
|
834
|
+
const msg = `Policy filename must not use "core" as a name word unless it is a local meta-policy in "principles/" starting with "core" (e.g., "NNN-core.md" or "NNN-core-{qualifier}.md"): ${toDisplayPath(filePath)} [_core-adr-policy-010.09-local-naming]`;
|
|
835
|
+
if (freezeReference) { warnings.push(`${msg} [exempt: freeze-reference]`); } else { errors.push(msg); }
|
|
836
|
+
}
|
|
837
|
+
|
|
710
838
|
const expectedName = extractExpectedXdrsNameFromHeading(firstLine)
|
|
711
839
|
|| `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
|
|
712
840
|
lintXdrsElementFrontmatter(content, expectedName, filePath, errors, warnings, freezeReference);
|
|
713
|
-
|
|
841
|
+
if (enforceStructured) {
|
|
842
|
+
validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-010.08c-def-core-scope-policies-must-be-structured');
|
|
843
|
+
} else {
|
|
844
|
+
lintStructuredRuleBlocks(content, filePath, errors);
|
|
845
|
+
}
|
|
714
846
|
lintRequiredSections(content, filePath, XDRS_REQUIRED_SECTIONS, 'Policy', errors);
|
|
715
847
|
lintNoEmojis(content, filePath, 'Policy', errors);
|
|
716
848
|
lintWordCount(content, filePath, 'Policy', POLICY_MAX_WORDS, errors);
|
|
@@ -801,6 +933,11 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
801
933
|
continue;
|
|
802
934
|
}
|
|
803
935
|
|
|
936
|
+
// .assets is allowed as a shared resources directory within skills/
|
|
937
|
+
if (entry.name === RESOURCE_DIR_NAME) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
|
|
804
941
|
const match = entry.name.match(NUMBERED_DIR_RE);
|
|
805
942
|
if (!match) {
|
|
806
943
|
errors.push(`Invalid skill package name: ${toDisplayPath(entryPath)} [_core-adr-policy-003]`);
|
|
@@ -1485,6 +1622,29 @@ function stripFrontmatter(content) {
|
|
|
1485
1622
|
return match ? content.slice(match[0].length) : content;
|
|
1486
1623
|
}
|
|
1487
1624
|
|
|
1625
|
+
/**
|
|
1626
|
+
* Validate that a policy file follows the structured format defined in policy-008:
|
|
1627
|
+
* must have a "### Details" section and at least one "#### NN-rulename" rule block.
|
|
1628
|
+
*/
|
|
1629
|
+
function validateStructuredFormat(filePath, errors, errorCode) {
|
|
1630
|
+
let content;
|
|
1631
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch { return; }
|
|
1632
|
+
validateStructuredFormatContent(content, filePath, errors, errorCode);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function validateStructuredFormatContent(content, filePath, errors, errorCode) {
|
|
1636
|
+
const body = stripFrontmatter(content);
|
|
1637
|
+
if (!/^### Details\s*$/m.test(body)) {
|
|
1638
|
+
errors.push(`Policy must have a "### Details" section: ${toDisplayPath(filePath)} [${errorCode}]`);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (!/^#### \d{2}-[a-z][a-z0-9-]*$/m.test(body)) {
|
|
1642
|
+
errors.push(`Policy must have at least one structured rule block (#### NN-rulename) in ### Details: ${toDisplayPath(filePath)} [${errorCode}]`);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
lintStructuredRuleBlocks(content, filePath, errors);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1488
1648
|
function findIgnoredMarkdownLines(lines) {
|
|
1489
1649
|
const ignored = [];
|
|
1490
1650
|
let inCodeFence = false;
|