xdrs-core 0.24.1 → 0.26.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 +19 -19
- package/.xdrs/_core/adrs/principles/001-xdrs-core.md +37 -37
- package/.xdrs/_core/adrs/principles/002-policy-standards.md +153 -0
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +23 -22
- package/.xdrs/_core/adrs/principles/004-article-standards.md +23 -23
- package/.xdrs/_core/adrs/principles/{005-semantic-versioning-for-xdr-packages.md → 005-semantic-versioning-for-xdrs-packages.md} +11 -11
- package/.xdrs/_core/adrs/principles/006-research-standards.md +24 -24
- package/.xdrs/_core/adrs/principles/007-plan-standards.md +14 -14
- package/.xdrs/_core/adrs/principles/008-xdr-standards-structured.md +19 -19
- package/.xdrs/_core/adrs/principles/009-presentation-standards.md +11 -11
- package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +61 -50
- package/.xdrs/_core/adrs/principles/skills/{001-lint/001-lint.test.int.js → 001-review/001-review.test.int.js} +4 -4
- package/.xdrs/_core/adrs/principles/skills/{001-lint/001-lint.test.int.report → 001-review/001-review.test.int.report} +1 -1
- package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +94 -0
- package/.xdrs/_core/adrs/principles/skills/002-write-policy/002-write-policy.test.int.js +24 -0
- package/.xdrs/_core/adrs/principles/skills/{002-write-xdr/002-write-xdr.test.int.report → 002-write-policy/002-write-policy.test.int.report} +2 -2
- package/.xdrs/_core/adrs/principles/skills/{002-write-xdr → 002-write-policy}/SKILL.md +40 -40
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +18 -18
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +32 -32
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +25 -25
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +17 -17
- package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +14 -14
- package/.xdrs/_core/index.md +21 -26
- package/.xdrs/index.md +4 -4
- package/AGENTS.md +12 -12
- package/README.md +53 -40
- package/lib/lint.js +53 -48
- package/lib/lint.test.js +96 -96
- package/package.json +3 -3
- package/.xdrs/_core/adrs/principles/002-xdr-standards.md +0 -158
- package/.xdrs/_core/adrs/principles/skills/001-lint/SKILL.md +0 -94
- package/.xdrs/_core/adrs/principles/skills/002-write-xdr/002-write-xdr.test.int.js +0 -24
- package/.xdrs/_core/bdrs/index.md +0 -9
- package/.xdrs/_core/bdrs/principles/001-xdr-decisions-and-skills-usage.md +0 -52
package/lib/lint.js
CHANGED
|
@@ -5,9 +5,9 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const TYPE_TO_ID = {
|
|
8
|
-
adrs: 'adr',
|
|
9
|
-
bdrs: 'bdr',
|
|
10
|
-
edrs: 'edr'
|
|
8
|
+
adrs: 'adr-policy',
|
|
9
|
+
bdrs: 'bdr-policy',
|
|
10
|
+
edrs: 'edr-policy'
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const ALLOWED_SUBJECTS = {
|
|
@@ -20,18 +20,18 @@ const TYPE_NAMES = new Set(Object.keys(TYPE_TO_ID));
|
|
|
20
20
|
const RESERVED_SCOPES = new Set(['_core', '_local']);
|
|
21
21
|
const NUMBERED_FILE_RE = /^(\d{3,})-([a-z0-9-]+)\.md$/;
|
|
22
22
|
const NUMBERED_DIR_RE = /^(\d{3,})-([a-z0-9-]+)$/;
|
|
23
|
-
const REQUIRED_ROOT_INDEX_TEXT = '
|
|
23
|
+
const REQUIRED_ROOT_INDEX_TEXT = 'XDRS scopes listed last override the ones listed first';
|
|
24
24
|
const SUBJECT_ARTIFACT_DIRS = new Set(['skills', 'articles', 'researches', 'plans']);
|
|
25
25
|
const RESOURCE_DIR_NAME = '.assets';
|
|
26
26
|
const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_DIR_NAME]);
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const POLICY_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata']);
|
|
29
29
|
const SKILL_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'license', 'metadata', 'compatibility', 'allowed-tools']);
|
|
30
30
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
|
|
31
31
|
const SLIDE_FILE_RE = /^.+-slides(?:-[a-z0-9-]+)?\.md$/;
|
|
32
32
|
const SLIDE_MAX_NAME_LENGTH = 64;
|
|
33
33
|
const EMOJI_RE = /\p{Extended_Pictographic}/u;
|
|
34
|
-
const
|
|
34
|
+
const POLICY_MAX_WORDS = 2600;
|
|
35
35
|
const ARTICLE_MAX_WORDS = 5000;
|
|
36
36
|
const RESEARCH_MAX_WORDS = 5000;
|
|
37
37
|
const SKILL_MAX_WORDS = 6500;
|
|
@@ -62,14 +62,14 @@ function runLintCli(args) {
|
|
|
62
62
|
|
|
63
63
|
function printHelp() {
|
|
64
64
|
console.log('Usage: xdrs-core lint [options] [path]\n');
|
|
65
|
-
console.log('Lint the
|
|
65
|
+
console.log('Lint the XDRS tree rooted at [path] when [path] contains an index.md, or at [path]/.xdrs by default.');
|
|
66
66
|
console.log('\nOptions:');
|
|
67
67
|
console.log(' --all Check all files, including files from external scopes distributed via .filedist (default: skip external scopes)');
|
|
68
68
|
console.log('\nAll other commands continue to be delegated to the bundled filedist CLI.');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function resolveXdrsRoot(resolvedTarget) {
|
|
72
|
-
// If the path itself contains an index.md, treat it as the
|
|
72
|
+
// If the path itself contains an index.md, treat it as the XDRS root directly.
|
|
73
73
|
// This allows any folder name to serve as the root, not only ".xdrs".
|
|
74
74
|
if (existsFile(path.join(resolvedTarget, 'index.md'))) {
|
|
75
75
|
return resolvedTarget;
|
|
@@ -85,7 +85,7 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
85
85
|
const errors = [];
|
|
86
86
|
|
|
87
87
|
if (!existsDirectory(xdrsRoot)) {
|
|
88
|
-
errors.push(`Missing
|
|
88
|
+
errors.push(`Missing XDRS root directory: ${toDisplayPath(xdrsRoot)}`);
|
|
89
89
|
return { xdrsRoot, errors };
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -95,7 +95,7 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
95
95
|
const effectiveExternalScopes = ignoreExternal ? externalScopes : new Set();
|
|
96
96
|
|
|
97
97
|
const actualTypeIndexes = [];
|
|
98
|
-
const rootEntries = safeReadDir(xdrsRoot, errors, 'read
|
|
98
|
+
const rootEntries = safeReadDir(xdrsRoot, errors, 'read XDRS root directory');
|
|
99
99
|
const scopeEntries = rootEntries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
100
100
|
|
|
101
101
|
for (const entry of rootEntries) {
|
|
@@ -227,7 +227,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
227
227
|
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes, externalScopes = new Set()) {
|
|
228
228
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
229
229
|
const indexPath = path.join(typePath, 'index.md');
|
|
230
|
-
const
|
|
230
|
+
const xdrsNumbers = new Map();
|
|
231
231
|
const artifacts = [];
|
|
232
232
|
|
|
233
233
|
if (!existsFile(indexPath)) {
|
|
@@ -251,7 +251,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
251
251
|
continue;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name,
|
|
254
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, externalScopes));
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
if (existsFile(indexPath)) {
|
|
@@ -259,7 +259,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName,
|
|
262
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, externalScopes = new Set()) {
|
|
263
263
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
264
264
|
const artifacts = [];
|
|
265
265
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -293,22 +293,22 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
if (!NUMBERED_FILE_RE.test(entry.name)) {
|
|
296
|
-
errors.push(`Invalid
|
|
296
|
+
errors.push(`Invalid Policy file name: ${toDisplayPath(entryPath)}`);
|
|
297
297
|
continue;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
artifacts.push(entryPath);
|
|
301
|
-
|
|
301
|
+
lintXdrsElementFile(xdrsRoot, scopeName, typeName, entryPath, xdrsNumbers, errors, externalScopes);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
305
|
-
const
|
|
306
|
-
lintOrphanAssets(subjectAssetsDir,
|
|
305
|
+
const xdrsDocsInSubject = artifacts.filter((p) => path.dirname(p) === subjectPath);
|
|
306
|
+
lintOrphanAssets(subjectAssetsDir, xdrsDocsInSubject, xdrsRoot, errors);
|
|
307
307
|
|
|
308
308
|
return artifacts;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
function
|
|
311
|
+
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, filePath, xdrsNumbers, errors, externalScopes = new Set()) {
|
|
312
312
|
const baseName = path.basename(filePath);
|
|
313
313
|
const match = baseName.match(NUMBERED_FILE_RE);
|
|
314
314
|
if (!match) {
|
|
@@ -316,35 +316,35 @@ function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
const number = match[1];
|
|
319
|
-
const previous =
|
|
319
|
+
const previous = xdrsNumbers.get(number);
|
|
320
320
|
if (previous) {
|
|
321
|
-
errors.push(`Duplicate
|
|
321
|
+
errors.push(`Duplicate Policy number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)}`);
|
|
322
322
|
} else {
|
|
323
|
-
|
|
323
|
+
xdrsNumbers.set(number, filePath);
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
if (baseName !== baseName.toLowerCase()) {
|
|
327
|
-
errors.push(`
|
|
327
|
+
errors.push(`Policy file name must be lowercase: ${toDisplayPath(filePath)}`);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
331
331
|
const expectedHeader = `# ${scopeName}-${TYPE_TO_ID[typeName]}-${number}:`;
|
|
332
332
|
const firstLine = firstNonEmptyLine(stripFrontmatter(content));
|
|
333
333
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
334
|
-
errors.push(`
|
|
334
|
+
errors.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
const expectedName =
|
|
337
|
+
const expectedName = extractExpectedXdrsNameFromHeading(firstLine)
|
|
338
338
|
|| `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
|
|
339
|
-
|
|
340
|
-
lintRequiredSections(content, filePath,
|
|
341
|
-
lintNoEmojis(content, filePath, '
|
|
342
|
-
lintWordCount(content, filePath, '
|
|
339
|
+
lintXdrsElementFrontmatter(content, expectedName, filePath, errors);
|
|
340
|
+
lintRequiredSections(content, filePath, XDRS_REQUIRED_SECTIONS, 'Policy', errors);
|
|
341
|
+
lintNoEmojis(content, filePath, 'Policy', errors);
|
|
342
|
+
lintWordCount(content, filePath, 'Policy', POLICY_MAX_WORDS, errors);
|
|
343
343
|
lintDocumentLinks(filePath, xdrsRoot, scopeName, errors, externalScopes);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
function
|
|
347
|
-
const match = headingLine.match(/^#\s+([a-z0-9_]+-(?:adr|bdr|edr)-\d{3,}):\s+(.+?)\s*$/);
|
|
346
|
+
function extractExpectedXdrsNameFromHeading(headingLine) {
|
|
347
|
+
const match = headingLine.match(/^#\s+([a-z0-9_]+-(?:adr-policy|bdr-policy|edr-policy)-\d{3,}):\s+(.+?)\s*$/);
|
|
348
348
|
if (!match) {
|
|
349
349
|
return null;
|
|
350
350
|
}
|
|
@@ -368,43 +368,43 @@ function slugifyTitle(title) {
|
|
|
368
368
|
.replace(/--+/g, '-');
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
function
|
|
371
|
+
function lintXdrsElementFrontmatter(content, expectedName, filePath, errors) {
|
|
372
372
|
const fm = extractFrontmatter(content);
|
|
373
373
|
if (!fm.present) {
|
|
374
|
-
errors.push(`
|
|
374
|
+
errors.push(`Policy must start with a YAML frontmatter block: ${toDisplayPath(filePath)}`);
|
|
375
375
|
return;
|
|
376
376
|
}
|
|
377
377
|
if (!fm.name) {
|
|
378
|
-
errors.push(`
|
|
378
|
+
errors.push(`Policy frontmatter must include a non-empty name field: ${toDisplayPath(filePath)}`);
|
|
379
379
|
} else {
|
|
380
380
|
if (fm.name !== expectedName) {
|
|
381
|
-
errors.push(`
|
|
381
|
+
errors.push(`Policy frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)}`);
|
|
382
382
|
}
|
|
383
383
|
if (fm.name.length > 64) {
|
|
384
|
-
errors.push(`
|
|
384
|
+
errors.push(`Policy frontmatter name must be 64 characters or fewer: ${toDisplayPath(filePath)}`);
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
if (!fm.description) {
|
|
388
|
-
errors.push(`
|
|
388
|
+
errors.push(`Policy frontmatter must include a non-empty description field: ${toDisplayPath(filePath)}`);
|
|
389
389
|
} else if (fm.descriptionText && fm.descriptionText.length > 1024) {
|
|
390
|
-
errors.push(`
|
|
390
|
+
errors.push(`Policy frontmatter description must be 1024 characters or fewer: ${toDisplayPath(filePath)}`);
|
|
391
391
|
}
|
|
392
392
|
if (fm.validFrom !== undefined) {
|
|
393
393
|
if (!isIsoDate(fm.validFrom)) {
|
|
394
|
-
errors.push(`
|
|
394
|
+
errors.push(`Policy frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)}`);
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
if (fm.appliedTo !== undefined) {
|
|
398
398
|
const words = countWords(fm.appliedTo);
|
|
399
399
|
if (words === 0) {
|
|
400
|
-
errors.push(`
|
|
400
|
+
errors.push(`Policy frontmatter apply-to must not be empty: ${toDisplayPath(filePath)}`);
|
|
401
401
|
} else if (words >= 40) {
|
|
402
|
-
errors.push(`
|
|
402
|
+
errors.push(`Policy frontmatter apply-to must be under 40 words: ${toDisplayPath(filePath)}`);
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
for (const key of fm.topLevelKeys) {
|
|
406
|
-
if (!
|
|
407
|
-
errors.push(`
|
|
406
|
+
if (!POLICY_ALLOWED_FRONTMATTER_KEYS.has(key)) {
|
|
407
|
+
errors.push(`Policy frontmatter has unknown field "${key}": ${toDisplayPath(filePath)}`);
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
}
|
|
@@ -472,8 +472,10 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
472
472
|
if (!skillFm.name) {
|
|
473
473
|
errors.push(`SKILL.md frontmatter must include a non-empty name field: ${toDisplayPath(skillFilePath)}`);
|
|
474
474
|
} else {
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
|
|
476
|
+
const expectedSkillName = `${scopeName}-${typeId}-skill-${number}-${match[2]}`;
|
|
477
|
+
if (skillFm.name !== expectedSkillName) {
|
|
478
|
+
errors.push(`Skill frontmatter name must be "${expectedSkillName}": ${toDisplayPath(skillFilePath)}`);
|
|
477
479
|
}
|
|
478
480
|
if (skillFm.name.length > 64) {
|
|
479
481
|
errors.push(`SKILL.md frontmatter name must be 64 characters or fewer: ${toDisplayPath(skillFilePath)}`);
|
|
@@ -537,7 +539,8 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
537
539
|
}
|
|
538
540
|
|
|
539
541
|
const content = fs.readFileSync(entryPath, 'utf8');
|
|
540
|
-
const
|
|
542
|
+
const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
|
|
543
|
+
const expectedHeader = `# ${scopeName}-${typeId}-article-${number}:`;
|
|
541
544
|
const firstLine = firstNonEmptyLine(content);
|
|
542
545
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
543
546
|
errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
@@ -591,7 +594,8 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
591
594
|
}
|
|
592
595
|
|
|
593
596
|
const content = fs.readFileSync(entryPath, 'utf8');
|
|
594
|
-
const
|
|
597
|
+
const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
|
|
598
|
+
const expectedHeader = `# ${scopeName}-${typeId}-research-${number}:`;
|
|
595
599
|
const firstLine = firstNonEmptyLine(content);
|
|
596
600
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
597
601
|
errors.push(`Research title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
@@ -646,7 +650,8 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
646
650
|
}
|
|
647
651
|
|
|
648
652
|
const content = fs.readFileSync(entryPath, 'utf8');
|
|
649
|
-
const
|
|
653
|
+
const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
|
|
654
|
+
const expectedHeader = `# ${scopeName}-${typeId}-plan-${number}:`;
|
|
650
655
|
const firstLine = firstNonEmptyLine(content);
|
|
651
656
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
652
657
|
errors.push(`Plan title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
@@ -709,7 +714,7 @@ function lintResearchIntroductionQuestion(content, filePath, errors) {
|
|
|
709
714
|
}
|
|
710
715
|
}
|
|
711
716
|
|
|
712
|
-
const
|
|
717
|
+
const XDRS_REQUIRED_SECTIONS = ['## Context and Problem Statement', '## Decision Outcome'];
|
|
713
718
|
const ARTICLE_REQUIRED_SECTIONS = ['## Overview', '## Content', '## References'];
|
|
714
719
|
const RESEARCH_REQUIRED_SECTIONS = ['## Abstract', '## Introduction', '## Methods', '## Results', '## Discussion', '## Conclusion', '## References'];
|
|
715
720
|
const PLAN_REQUIRED_SECTIONS = ['## Executive Summary', '## Context and Problem Statement', '## Proposed Solution'];
|