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.
Files changed (34) hide show
  1. package/.xdrs/_core/adrs/index.md +19 -19
  2. package/.xdrs/_core/adrs/principles/001-xdrs-core.md +37 -37
  3. package/.xdrs/_core/adrs/principles/002-policy-standards.md +153 -0
  4. package/.xdrs/_core/adrs/principles/003-skill-standards.md +23 -22
  5. package/.xdrs/_core/adrs/principles/004-article-standards.md +23 -23
  6. package/.xdrs/_core/adrs/principles/{005-semantic-versioning-for-xdr-packages.md → 005-semantic-versioning-for-xdrs-packages.md} +11 -11
  7. package/.xdrs/_core/adrs/principles/006-research-standards.md +24 -24
  8. package/.xdrs/_core/adrs/principles/007-plan-standards.md +14 -14
  9. package/.xdrs/_core/adrs/principles/008-xdr-standards-structured.md +19 -19
  10. package/.xdrs/_core/adrs/principles/009-presentation-standards.md +11 -11
  11. package/.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md +61 -50
  12. package/.xdrs/_core/adrs/principles/skills/{001-lint/001-lint.test.int.js → 001-review/001-review.test.int.js} +4 -4
  13. package/.xdrs/_core/adrs/principles/skills/{001-lint/001-lint.test.int.report → 001-review/001-review.test.int.report} +1 -1
  14. package/.xdrs/_core/adrs/principles/skills/001-review/SKILL.md +94 -0
  15. package/.xdrs/_core/adrs/principles/skills/002-write-policy/002-write-policy.test.int.js +24 -0
  16. 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
  17. package/.xdrs/_core/adrs/principles/skills/{002-write-xdr → 002-write-policy}/SKILL.md +40 -40
  18. package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +18 -18
  19. package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +32 -32
  20. package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +25 -25
  21. package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +17 -17
  22. package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +14 -14
  23. package/.xdrs/_core/index.md +21 -26
  24. package/.xdrs/index.md +4 -4
  25. package/AGENTS.md +12 -12
  26. package/README.md +53 -40
  27. package/lib/lint.js +53 -48
  28. package/lib/lint.test.js +96 -96
  29. package/package.json +3 -3
  30. package/.xdrs/_core/adrs/principles/002-xdr-standards.md +0 -158
  31. package/.xdrs/_core/adrs/principles/skills/001-lint/SKILL.md +0 -94
  32. package/.xdrs/_core/adrs/principles/skills/002-write-xdr/002-write-xdr.test.int.js +0 -24
  33. package/.xdrs/_core/bdrs/index.md +0 -9
  34. 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 = 'XDRs in scopes listed last override the ones listed first';
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 XDR_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata']);
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 XDR_MAX_WORDS = 2600;
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 XDR tree rooted at [path] when [path] contains an index.md, or at [path]/.xdrs by default.');
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 XDR root directly.
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 XDR root directory: ${toDisplayPath(xdrsRoot)}`);
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 XDR root directory');
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 xdrNumbers = new Map();
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, xdrNumbers, errors, externalScopes));
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, xdrNumbers, errors, externalScopes = new Set()) {
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 XDR file name: ${toDisplayPath(entryPath)}`);
296
+ errors.push(`Invalid Policy file name: ${toDisplayPath(entryPath)}`);
297
297
  continue;
298
298
  }
299
299
 
300
300
  artifacts.push(entryPath);
301
- lintXdrFile(xdrsRoot, scopeName, typeName, entryPath, xdrNumbers, errors, externalScopes);
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 xdrDocsInSubject = artifacts.filter((p) => path.dirname(p) === subjectPath);
306
- lintOrphanAssets(subjectAssetsDir, xdrDocsInSubject, xdrsRoot, errors);
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 lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors, externalScopes = new Set()) {
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 = xdrNumbers.get(number);
319
+ const previous = xdrsNumbers.get(number);
320
320
  if (previous) {
321
- errors.push(`Duplicate XDR number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)}`);
321
+ errors.push(`Duplicate Policy number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)}`);
322
322
  } else {
323
- xdrNumbers.set(number, filePath);
323
+ xdrsNumbers.set(number, filePath);
324
324
  }
325
325
 
326
326
  if (baseName !== baseName.toLowerCase()) {
327
- errors.push(`XDR file name must be lowercase: ${toDisplayPath(filePath)}`);
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(`XDR title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
334
+ errors.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
335
335
  }
336
336
 
337
- const expectedName = extractExpectedXdrNameFromHeading(firstLine)
337
+ const expectedName = extractExpectedXdrsNameFromHeading(firstLine)
338
338
  || `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
339
- lintXdrFrontmatter(content, expectedName, filePath, errors);
340
- lintRequiredSections(content, filePath, XDR_REQUIRED_SECTIONS, 'XDR', errors);
341
- lintNoEmojis(content, filePath, 'XDR', errors);
342
- lintWordCount(content, filePath, 'XDR', XDR_MAX_WORDS, errors);
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 extractExpectedXdrNameFromHeading(headingLine) {
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 lintXdrFrontmatter(content, expectedName, filePath, errors) {
371
+ function lintXdrsElementFrontmatter(content, expectedName, filePath, errors) {
372
372
  const fm = extractFrontmatter(content);
373
373
  if (!fm.present) {
374
- errors.push(`XDR must start with a YAML frontmatter block: ${toDisplayPath(filePath)}`);
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(`XDR frontmatter must include a non-empty name field: ${toDisplayPath(filePath)}`);
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(`XDR frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)}`);
381
+ errors.push(`Policy frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)}`);
382
382
  }
383
383
  if (fm.name.length > 64) {
384
- errors.push(`XDR frontmatter name must be 64 characters or fewer: ${toDisplayPath(filePath)}`);
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(`XDR frontmatter must include a non-empty description field: ${toDisplayPath(filePath)}`);
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(`XDR frontmatter description must be 1024 characters or fewer: ${toDisplayPath(filePath)}`);
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(`XDR frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)}`);
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(`XDR frontmatter apply-to must not be empty: ${toDisplayPath(filePath)}`);
400
+ errors.push(`Policy frontmatter apply-to must not be empty: ${toDisplayPath(filePath)}`);
401
401
  } else if (words >= 40) {
402
- errors.push(`XDR frontmatter apply-to must be under 40 words: ${toDisplayPath(filePath)}`);
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 (!XDR_ALLOWED_FRONTMATTER_KEYS.has(key)) {
407
- errors.push(`XDR frontmatter has unknown field "${key}": ${toDisplayPath(filePath)}`);
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
- if (skillFm.name !== entry.name) {
476
- errors.push(`Skill frontmatter name must match package directory "${entry.name}": ${toDisplayPath(skillFilePath)}`);
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 expectedHeader = `# ${scopeName}-article-${number}:`;
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 expectedHeader = `# ${scopeName}-research-${number}:`;
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 expectedHeader = `# ${scopeName}-plan-${number}:`;
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 XDR_REQUIRED_SECTIONS = ['## Context and Problem Statement', '## Decision Outcome'];
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'];