xdrs-core 0.22.0 → 0.23.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/principles/004-article-standards.md +1 -1
- package/lib/lint.js +105 -5
- package/lib/lint.test.js +398 -1
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ Articles are Markdown documents placed inside a subject folder alongside decisio
|
|
|
32
32
|
- Sub-directories inside this `.assets/` folder are allowed only when it already has more than 10 files. Otherwise, keep files flat.
|
|
33
33
|
- Always use lowercase file names.
|
|
34
34
|
- Never use emojis in article content.
|
|
35
|
-
- Articles should be kept under 5000 words. Move
|
|
35
|
+
- Articles should be kept under 5000 words. Move or point to detailed contents referenced in XDRs decisions, researches, plans or skills.
|
|
36
36
|
|
|
37
37
|
**Folder layout**
|
|
38
38
|
|
package/lib/lint.js
CHANGED
|
@@ -28,6 +28,11 @@ const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_D
|
|
|
28
28
|
const XDR_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
|
+
const EMOJI_RE = /\p{Extended_Pictographic}/u;
|
|
32
|
+
const XDR_MAX_WORDS = 2600;
|
|
33
|
+
const ARTICLE_MAX_WORDS = 5000;
|
|
34
|
+
const RESEARCH_MAX_WORDS = 5000;
|
|
35
|
+
const SKILL_MAX_WORDS = 6500;
|
|
31
36
|
|
|
32
37
|
function runLintCli(args) {
|
|
33
38
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -331,6 +336,8 @@ function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors
|
|
|
331
336
|
|| `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
|
|
332
337
|
lintXdrFrontmatter(content, expectedName, filePath, errors);
|
|
333
338
|
lintRequiredSections(content, filePath, XDR_REQUIRED_SECTIONS, 'XDR', errors);
|
|
339
|
+
lintNoEmojis(content, filePath, 'XDR', errors);
|
|
340
|
+
lintWordCount(content, filePath, 'XDR', XDR_MAX_WORDS, errors);
|
|
334
341
|
lintDocumentLinks(filePath, xdrsRoot, scopeName, errors, externalScopes);
|
|
335
342
|
}
|
|
336
343
|
|
|
@@ -367,11 +374,18 @@ function lintXdrFrontmatter(content, expectedName, filePath, errors) {
|
|
|
367
374
|
}
|
|
368
375
|
if (!fm.name) {
|
|
369
376
|
errors.push(`XDR frontmatter must include a non-empty name field: ${toDisplayPath(filePath)}`);
|
|
370
|
-
} else
|
|
371
|
-
|
|
377
|
+
} else {
|
|
378
|
+
if (fm.name !== expectedName) {
|
|
379
|
+
errors.push(`XDR frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)}`);
|
|
380
|
+
}
|
|
381
|
+
if (fm.name.length > 64) {
|
|
382
|
+
errors.push(`XDR frontmatter name must be 64 characters or fewer: ${toDisplayPath(filePath)}`);
|
|
383
|
+
}
|
|
372
384
|
}
|
|
373
385
|
if (!fm.description) {
|
|
374
386
|
errors.push(`XDR frontmatter must include a non-empty description field: ${toDisplayPath(filePath)}`);
|
|
387
|
+
} else if (fm.descriptionText && fm.descriptionText.length > 1024) {
|
|
388
|
+
errors.push(`XDR frontmatter description must be 1024 characters or fewer: ${toDisplayPath(filePath)}`);
|
|
375
389
|
}
|
|
376
390
|
if (fm.validFrom !== undefined) {
|
|
377
391
|
if (!isIsoDate(fm.validFrom)) {
|
|
@@ -455,11 +469,18 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
455
469
|
} else {
|
|
456
470
|
if (!skillFm.name) {
|
|
457
471
|
errors.push(`SKILL.md frontmatter must include a non-empty name field: ${toDisplayPath(skillFilePath)}`);
|
|
458
|
-
} else
|
|
459
|
-
|
|
472
|
+
} else {
|
|
473
|
+
if (skillFm.name !== entry.name) {
|
|
474
|
+
errors.push(`Skill frontmatter name must match package directory "${entry.name}": ${toDisplayPath(skillFilePath)}`);
|
|
475
|
+
}
|
|
476
|
+
if (skillFm.name.length > 64) {
|
|
477
|
+
errors.push(`SKILL.md frontmatter name must be 64 characters or fewer: ${toDisplayPath(skillFilePath)}`);
|
|
478
|
+
}
|
|
460
479
|
}
|
|
461
480
|
if (!skillFm.description) {
|
|
462
481
|
errors.push(`SKILL.md frontmatter must include a non-empty description field: ${toDisplayPath(skillFilePath)}`);
|
|
482
|
+
} else if (skillFm.descriptionText && skillFm.descriptionText.length > 1024) {
|
|
483
|
+
errors.push(`SKILL.md frontmatter description must be 1024 characters or fewer: ${toDisplayPath(skillFilePath)}`);
|
|
463
484
|
}
|
|
464
485
|
for (const key of skillFm.topLevelKeys) {
|
|
465
486
|
if (!SKILL_ALLOWED_FRONTMATTER_KEYS.has(key)) {
|
|
@@ -468,6 +489,8 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
468
489
|
}
|
|
469
490
|
}
|
|
470
491
|
|
|
492
|
+
lintNoEmojis(skillContent, skillFilePath, 'Skill', errors);
|
|
493
|
+
lintWordCount(skillContent, skillFilePath, 'Skill', SKILL_MAX_WORDS, errors);
|
|
471
494
|
lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors, externalScopes);
|
|
472
495
|
lintOrphanAssets(path.join(entryPath, RESOURCE_DIR_NAME), [skillFilePath], xdrsRoot, errors);
|
|
473
496
|
}
|
|
@@ -519,6 +542,8 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
519
542
|
}
|
|
520
543
|
|
|
521
544
|
lintRequiredSections(content, entryPath, ARTICLE_REQUIRED_SECTIONS, 'Article', errors);
|
|
545
|
+
lintNoEmojis(content, entryPath, 'Article', errors);
|
|
546
|
+
lintWordCount(content, entryPath, 'Article', ARTICLE_MAX_WORDS, errors);
|
|
522
547
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors, externalScopes);
|
|
523
548
|
}
|
|
524
549
|
|
|
@@ -571,6 +596,9 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
571
596
|
}
|
|
572
597
|
|
|
573
598
|
lintRequiredSections(content, entryPath, RESEARCH_REQUIRED_SECTIONS, 'Research', errors);
|
|
599
|
+
lintResearchIntroductionQuestion(content, entryPath, errors);
|
|
600
|
+
lintNoEmojis(content, entryPath, 'Research', errors);
|
|
601
|
+
lintWordCount(content, entryPath, 'Research', RESEARCH_MAX_WORDS, errors);
|
|
574
602
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors, externalScopes);
|
|
575
603
|
}
|
|
576
604
|
|
|
@@ -624,6 +652,7 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
624
652
|
|
|
625
653
|
lintRequiredSections(content, entryPath, PLAN_REQUIRED_SECTIONS, 'Plan', errors);
|
|
626
654
|
lintPlanExpectedEndDate(content, entryPath, errors);
|
|
655
|
+
lintNoEmojis(content, entryPath, 'Plan', errors);
|
|
627
656
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors, externalScopes);
|
|
628
657
|
}
|
|
629
658
|
|
|
@@ -632,6 +661,52 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
632
661
|
return artifacts;
|
|
633
662
|
}
|
|
634
663
|
|
|
664
|
+
function lintNoEmojis(content, filePath, docType, errors) {
|
|
665
|
+
const lines = content.split(/\r?\n/);
|
|
666
|
+
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
667
|
+
for (let i = 0; i < lines.length; i++) {
|
|
668
|
+
if (ignoredLines[i]) continue;
|
|
669
|
+
if (EMOJI_RE.test(lines[i])) {
|
|
670
|
+
errors.push(`${docType} must not contain emojis: ${toDisplayPath(filePath)}:${i + 1}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function lintWordCount(content, filePath, docType, maxWords, errors) {
|
|
676
|
+
const body = stripFrontmatter(content);
|
|
677
|
+
const wordCount = countWords(body);
|
|
678
|
+
if (wordCount > maxWords) {
|
|
679
|
+
errors.push(`${docType} exceeds maximum word count of ${maxWords} (${wordCount} words): ${toDisplayPath(filePath)}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function lintResearchIntroductionQuestion(content, filePath, errors) {
|
|
684
|
+
const lines = content.split(/\r?\n/);
|
|
685
|
+
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
686
|
+
const introStart = findHeadingLine(lines, ignoredLines, '## Introduction');
|
|
687
|
+
if (introStart === -1) return; // Already caught by lintRequiredSections
|
|
688
|
+
|
|
689
|
+
let introEnd = lines.length;
|
|
690
|
+
for (let i = introStart + 1; i < lines.length; i++) {
|
|
691
|
+
if (!ignoredLines[i] && /^## /.test(lines[i].trim())) {
|
|
692
|
+
introEnd = i;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let hasQuestion = false;
|
|
698
|
+
for (let i = introStart + 1; i < introEnd; i++) {
|
|
699
|
+
if (!ignoredLines[i] && lines[i].trim().startsWith('Question:')) {
|
|
700
|
+
hasQuestion = true;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (!hasQuestion) {
|
|
706
|
+
errors.push(`Research ## Introduction must contain a "Question:" line: ${toDisplayPath(filePath)}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
635
710
|
const XDR_REQUIRED_SECTIONS = ['## Context and Problem Statement', '## Decision Outcome'];
|
|
636
711
|
const ARTICLE_REQUIRED_SECTIONS = ['## Overview', '## Content', '## References'];
|
|
637
712
|
const RESEARCH_REQUIRED_SECTIONS = ['## Abstract', '## Introduction', '## Methods', '## Results', '## Discussion', '## Conclusion', '## References'];
|
|
@@ -882,10 +957,33 @@ function shouldValidateResourceLink(rawTarget) {
|
|
|
882
957
|
|| IMAGE_EXTENSIONS.has(extension);
|
|
883
958
|
}
|
|
884
959
|
|
|
960
|
+
function extractDescriptionText(block) {
|
|
961
|
+
const lines = block.split(/\r?\n/);
|
|
962
|
+
for (let i = 0; i < lines.length; i++) {
|
|
963
|
+
const m = lines[i].match(/^description:\s*(.*)$/);
|
|
964
|
+
if (!m) continue;
|
|
965
|
+
const inlineValue = m[1].trim();
|
|
966
|
+
if (inlineValue && inlineValue !== '>' && inlineValue !== '|') {
|
|
967
|
+
return inlineValue;
|
|
968
|
+
}
|
|
969
|
+
// Block scalar: collect following indented lines
|
|
970
|
+
const bodyLines = [];
|
|
971
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
972
|
+
if (lines[j] === '' || /^\s+/.test(lines[j])) {
|
|
973
|
+
bodyLines.push(lines[j].trim());
|
|
974
|
+
} else {
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return bodyLines.join(' ').trim() || null;
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
|
|
885
983
|
function extractFrontmatter(content) {
|
|
886
984
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
887
985
|
if (!match) {
|
|
888
|
-
return { present: false, name: null, description: false, validFrom: undefined, appliedTo: undefined, topLevelKeys: [] };
|
|
986
|
+
return { present: false, name: null, description: false, descriptionText: null, validFrom: undefined, appliedTo: undefined, topLevelKeys: [] };
|
|
889
987
|
}
|
|
890
988
|
const block = match[1];
|
|
891
989
|
const nameMatch = block.match(/^name:\s*(.+)$/m);
|
|
@@ -896,10 +994,12 @@ function extractFrontmatter(content) {
|
|
|
896
994
|
const keyMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*/);
|
|
897
995
|
if (keyMatch) topLevelKeys.push(keyMatch[1]);
|
|
898
996
|
}
|
|
997
|
+
const descriptionText = extractDescriptionText(block);
|
|
899
998
|
return {
|
|
900
999
|
present: true,
|
|
901
1000
|
name: nameMatch ? nameMatch[1].trim() || null : null,
|
|
902
1001
|
description: /^description:\s*\S/m.test(block),
|
|
1002
|
+
descriptionText,
|
|
903
1003
|
validFrom: validFromMatch ? validFromMatch[1].trim() : undefined,
|
|
904
1004
|
appliedTo: appliedToMatch ? appliedToMatch[1].trim() : undefined,
|
|
905
1005
|
topLevelKeys,
|
package/lib/lint.test.js
CHANGED
|
@@ -919,4 +919,401 @@ test('falls back to .xdrs subdirectory when given path has no index.md', () => {
|
|
|
919
919
|
|
|
920
920
|
expect(result.errors).toHaveLength(0);
|
|
921
921
|
expect(result.xdrsRoot).toBe(path.join(workspaceRoot, '.xdrs'));
|
|
922
|
-
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// ─── Emoji checks ─────────────────────────────────────────────────────────────
|
|
925
|
+
|
|
926
|
+
test('reports emoji in XDR document body', () => {
|
|
927
|
+
const workspaceRoot = createWorkspace('xdr-emoji-body', {
|
|
928
|
+
'.xdrs/index.md': rootIndex(),
|
|
929
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
930
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
931
|
+
]),
|
|
932
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('This is great \uD83C\uDF89'),
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const result = lintWorkspace(workspaceRoot);
|
|
936
|
+
|
|
937
|
+
expect(result.errors.join('\n')).toContain('XDR must not contain emojis');
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test('does not report emoji inside a code block in XDR documents', () => {
|
|
941
|
+
const workspaceRoot = createWorkspace('xdr-emoji-in-code-block', {
|
|
942
|
+
'.xdrs/index.md': rootIndex(),
|
|
943
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
944
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
945
|
+
]),
|
|
946
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('```\necho "hello \uD83C\uDF89"\n```'),
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
const result = lintWorkspace(workspaceRoot);
|
|
950
|
+
|
|
951
|
+
expect(result.errors.join('\n')).not.toContain('must not contain emojis');
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test('reports emoji in SKILL.md', () => {
|
|
955
|
+
const workspaceRoot = createWorkspace('skill-emoji', {
|
|
956
|
+
'.xdrs/index.md': rootIndex(),
|
|
957
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
958
|
+
'- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
|
|
959
|
+
]),
|
|
960
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': skillDocument('Step 1: do something \u2705'),
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
const result = lintWorkspace(workspaceRoot);
|
|
964
|
+
|
|
965
|
+
expect(result.errors.join('\n')).toContain('Skill must not contain emojis');
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test('reports emoji in article document', () => {
|
|
969
|
+
const workspaceRoot = createWorkspace('article-emoji', {
|
|
970
|
+
'.xdrs/index.md': rootIndex(),
|
|
971
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
972
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide'
|
|
973
|
+
]),
|
|
974
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': articleDocument('Nice overview \uD83D\uDE80'),
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const result = lintWorkspace(workspaceRoot);
|
|
978
|
+
|
|
979
|
+
expect(result.errors.join('\n')).toContain('Article must not contain emojis');
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test('reports emoji in research document', () => {
|
|
983
|
+
const workspaceRoot = createWorkspace('research-emoji', {
|
|
984
|
+
'.xdrs/index.md': rootIndex(),
|
|
985
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
986
|
+
'- [001-study](principles/researches/001-study.md) - Study'
|
|
987
|
+
]),
|
|
988
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': researchDocument('Some finding \uD83D\uDD2C', 'Question: Is this right?'),
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const result = lintWorkspace(workspaceRoot);
|
|
992
|
+
|
|
993
|
+
expect(result.errors.join('\n')).toContain('Research must not contain emojis');
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
test('reports emoji in plan document', () => {
|
|
997
|
+
const workspaceRoot = createWorkspace('plan-emoji', {
|
|
998
|
+
'.xdrs/index.md': rootIndex(),
|
|
999
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1000
|
+
'- [001-myplan](principles/plans/001-myplan.md) - My plan'
|
|
1001
|
+
]),
|
|
1002
|
+
'.xdrs/_local/adrs/principles/plans/001-myplan.md': planDocument('Great plan \uD83C\uDFAF'),
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1006
|
+
|
|
1007
|
+
expect(result.errors.join('\n')).toContain('Plan must not contain emojis');
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// ─── Name / description length ────────────────────────────────────────────────
|
|
1011
|
+
|
|
1012
|
+
test('reports XDR frontmatter name exceeding 64 characters', () => {
|
|
1013
|
+
const longName = '_local-adr-001-' + 'a'.repeat(50);
|
|
1014
|
+
const workspaceRoot = createWorkspace('xdr-name-too-long', {
|
|
1015
|
+
'.xdrs/index.md': rootIndex(),
|
|
1016
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1017
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
1018
|
+
]),
|
|
1019
|
+
'.xdrs/_local/adrs/principles/001-main.md': [
|
|
1020
|
+
'---',
|
|
1021
|
+
`name: ${longName}`,
|
|
1022
|
+
'description: Test XDR document',
|
|
1023
|
+
'---',
|
|
1024
|
+
'',
|
|
1025
|
+
'# _local-adr-001: Main decision',
|
|
1026
|
+
'',
|
|
1027
|
+
'## Context and Problem Statement',
|
|
1028
|
+
'',
|
|
1029
|
+
'Test body.',
|
|
1030
|
+
'',
|
|
1031
|
+
'## Decision Outcome',
|
|
1032
|
+
'',
|
|
1033
|
+
'Test decision outcome.',
|
|
1034
|
+
''
|
|
1035
|
+
].join('\n'),
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1039
|
+
|
|
1040
|
+
expect(result.errors.join('\n')).toContain('XDR frontmatter name must be 64 characters or fewer');
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test('reports XDR frontmatter description exceeding 1024 characters', () => {
|
|
1044
|
+
const workspaceRoot = createWorkspace('xdr-description-too-long', {
|
|
1045
|
+
'.xdrs/index.md': rootIndex(),
|
|
1046
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1047
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
1048
|
+
]),
|
|
1049
|
+
'.xdrs/_local/adrs/principles/001-main.md': [
|
|
1050
|
+
'---',
|
|
1051
|
+
'name: _local-adr-001-main',
|
|
1052
|
+
`description: ${'x'.repeat(1025)}`,
|
|
1053
|
+
'---',
|
|
1054
|
+
'',
|
|
1055
|
+
'# _local-adr-001: Main decision',
|
|
1056
|
+
'',
|
|
1057
|
+
'## Context and Problem Statement',
|
|
1058
|
+
'',
|
|
1059
|
+
'Test body.',
|
|
1060
|
+
'',
|
|
1061
|
+
'## Decision Outcome',
|
|
1062
|
+
'',
|
|
1063
|
+
'Test decision outcome.',
|
|
1064
|
+
''
|
|
1065
|
+
].join('\n'),
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1069
|
+
|
|
1070
|
+
expect(result.errors.join('\n')).toContain('XDR frontmatter description must be 1024 characters or fewer');
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
test('reports SKILL.md frontmatter name exceeding 64 characters', () => {
|
|
1074
|
+
const longName = '001-check-links-' + 'a'.repeat(50);
|
|
1075
|
+
const workspaceRoot = createWorkspace('skill-name-too-long', {
|
|
1076
|
+
'.xdrs/index.md': rootIndex(),
|
|
1077
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1078
|
+
'- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
|
|
1079
|
+
]),
|
|
1080
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': [
|
|
1081
|
+
'---',
|
|
1082
|
+
`name: ${longName}`,
|
|
1083
|
+
'description: Test skill document',
|
|
1084
|
+
'---',
|
|
1085
|
+
'',
|
|
1086
|
+
'## Overview',
|
|
1087
|
+
'',
|
|
1088
|
+
'Overview.',
|
|
1089
|
+
'',
|
|
1090
|
+
'## Instructions',
|
|
1091
|
+
'',
|
|
1092
|
+
'Instructions.',
|
|
1093
|
+
''
|
|
1094
|
+
].join('\n'),
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1098
|
+
|
|
1099
|
+
expect(result.errors.join('\n')).toContain('SKILL.md frontmatter name must be 64 characters or fewer');
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test('reports SKILL.md frontmatter description exceeding 1024 characters', () => {
|
|
1103
|
+
const workspaceRoot = createWorkspace('skill-description-too-long', {
|
|
1104
|
+
'.xdrs/index.md': rootIndex(),
|
|
1105
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1106
|
+
'- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
|
|
1107
|
+
]),
|
|
1108
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': [
|
|
1109
|
+
'---',
|
|
1110
|
+
'name: 001-check-links',
|
|
1111
|
+
`description: ${'x'.repeat(1025)}`,
|
|
1112
|
+
'---',
|
|
1113
|
+
'',
|
|
1114
|
+
'## Overview',
|
|
1115
|
+
'',
|
|
1116
|
+
'Overview.',
|
|
1117
|
+
'',
|
|
1118
|
+
'## Instructions',
|
|
1119
|
+
'',
|
|
1120
|
+
'Instructions.',
|
|
1121
|
+
''
|
|
1122
|
+
].join('\n'),
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1126
|
+
|
|
1127
|
+
expect(result.errors.join('\n')).toContain('SKILL.md frontmatter description must be 1024 characters or fewer');
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// ─── Word count ───────────────────────────────────────────────────────────────
|
|
1131
|
+
|
|
1132
|
+
test('reports XDR document exceeding 2600 word limit', () => {
|
|
1133
|
+
const longBody = ('word '.repeat(2601)).trimEnd();
|
|
1134
|
+
const workspaceRoot = createWorkspace('xdr-too-many-words', {
|
|
1135
|
+
'.xdrs/index.md': rootIndex(),
|
|
1136
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1137
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
1138
|
+
]),
|
|
1139
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument(longBody),
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1143
|
+
|
|
1144
|
+
expect(result.errors.join('\n')).toContain('XDR exceeds maximum word count of 2600');
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
test('reports article exceeding 5000 word limit', () => {
|
|
1148
|
+
const longBody = ('word '.repeat(5001)).trimEnd();
|
|
1149
|
+
const workspaceRoot = createWorkspace('article-too-many-words', {
|
|
1150
|
+
'.xdrs/index.md': rootIndex(),
|
|
1151
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1152
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide'
|
|
1153
|
+
]),
|
|
1154
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': articleDocument(longBody),
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1158
|
+
|
|
1159
|
+
expect(result.errors.join('\n')).toContain('Article exceeds maximum word count of 5000');
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
test('reports research exceeding 5000 word limit', () => {
|
|
1163
|
+
const longBody = ('word '.repeat(5001)).trimEnd();
|
|
1164
|
+
const workspaceRoot = createWorkspace('research-too-many-words', {
|
|
1165
|
+
'.xdrs/index.md': rootIndex(),
|
|
1166
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1167
|
+
'- [001-study](principles/researches/001-study.md) - Study'
|
|
1168
|
+
]),
|
|
1169
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': researchDocument(longBody, 'Question: What is the answer?'),
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1173
|
+
|
|
1174
|
+
expect(result.errors.join('\n')).toContain('Research exceeds maximum word count of 5000');
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test('reports SKILL.md exceeding 6500 word limit', () => {
|
|
1178
|
+
const longBody = ('word '.repeat(6501)).trimEnd();
|
|
1179
|
+
const workspaceRoot = createWorkspace('skill-too-many-words', {
|
|
1180
|
+
'.xdrs/index.md': rootIndex(),
|
|
1181
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1182
|
+
'- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
|
|
1183
|
+
]),
|
|
1184
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': skillDocument(longBody),
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1188
|
+
|
|
1189
|
+
expect(result.errors.join('\n')).toContain('Skill exceeds maximum word count of 6500');
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// ─── Files outside .assets in skill package root ────────────────────────────
|
|
1193
|
+
|
|
1194
|
+
test('passes when extra files exist at skill package root', () => {
|
|
1195
|
+
const workspaceRoot = createWorkspace('extra-skill-file', {
|
|
1196
|
+
'.xdrs/index.md': rootIndex(),
|
|
1197
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1198
|
+
'- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
|
|
1199
|
+
]),
|
|
1200
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': skillDocument('Body.'),
|
|
1201
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/extra.md': 'Allowed extra file',
|
|
1202
|
+
'.xdrs/_local/adrs/principles/skills/001-check-links/schema.json': '{"type":"object"}',
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1206
|
+
|
|
1207
|
+
expect(result.errors.join('\n')).not.toContain('extra.md');
|
|
1208
|
+
expect(result.errors.join('\n')).not.toContain('schema.json');
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// ─── Research Question: in Introduction ──────────────────────────────────────
|
|
1212
|
+
|
|
1213
|
+
test('reports research ## Introduction missing Question: line', () => {
|
|
1214
|
+
const workspaceRoot = createWorkspace('research-no-question', {
|
|
1215
|
+
'.xdrs/index.md': rootIndex(),
|
|
1216
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1217
|
+
'- [001-study](principles/researches/001-study.md) - Study'
|
|
1218
|
+
]),
|
|
1219
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': researchDocument('No question here.', null),
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1223
|
+
|
|
1224
|
+
expect(result.errors.join('\n')).toContain('Research ## Introduction must contain a "Question:" line');
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
test('passes when research ## Introduction contains Question: line', () => {
|
|
1228
|
+
const workspaceRoot = createWorkspace('research-with-question', {
|
|
1229
|
+
'.xdrs/index.md': rootIndex(),
|
|
1230
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
1231
|
+
'- [001-study](principles/researches/001-study.md) - Study'
|
|
1232
|
+
]),
|
|
1233
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': researchDocument('Some context.', 'Question: Is this the right approach?'),
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
const result = lintWorkspace(workspaceRoot);
|
|
1237
|
+
|
|
1238
|
+
expect(result.errors.join('\n')).not.toContain('Research ## Introduction must contain');
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// ─── New helpers ──────────────────────────────────────────────────────────────
|
|
1242
|
+
|
|
1243
|
+
function articleDocument(body) {
|
|
1244
|
+
return [
|
|
1245
|
+
'# _local-article-001: Guide',
|
|
1246
|
+
'',
|
|
1247
|
+
'## Overview',
|
|
1248
|
+
'',
|
|
1249
|
+
'Overview text.',
|
|
1250
|
+
'',
|
|
1251
|
+
'## Content',
|
|
1252
|
+
'',
|
|
1253
|
+
body,
|
|
1254
|
+
'',
|
|
1255
|
+
'## References',
|
|
1256
|
+
'',
|
|
1257
|
+
'- No references.',
|
|
1258
|
+
''
|
|
1259
|
+
].join('\n');
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function researchDocument(introBody, questionLine) {
|
|
1263
|
+
const introSection = questionLine
|
|
1264
|
+
? `${introBody}\n\n${questionLine}`
|
|
1265
|
+
: introBody;
|
|
1266
|
+
return [
|
|
1267
|
+
'# _local-research-001: Study',
|
|
1268
|
+
'',
|
|
1269
|
+
'## Abstract',
|
|
1270
|
+
'',
|
|
1271
|
+
'Single paragraph abstract.',
|
|
1272
|
+
'',
|
|
1273
|
+
'## Introduction',
|
|
1274
|
+
'',
|
|
1275
|
+
introSection,
|
|
1276
|
+
'',
|
|
1277
|
+
'## Methods',
|
|
1278
|
+
'',
|
|
1279
|
+
'Study methods.',
|
|
1280
|
+
'',
|
|
1281
|
+
'## Results',
|
|
1282
|
+
'',
|
|
1283
|
+
'Study results.',
|
|
1284
|
+
'',
|
|
1285
|
+
'## Discussion',
|
|
1286
|
+
'',
|
|
1287
|
+
'Discussion.',
|
|
1288
|
+
'',
|
|
1289
|
+
'## Conclusion',
|
|
1290
|
+
'',
|
|
1291
|
+
'Conclusion.',
|
|
1292
|
+
'',
|
|
1293
|
+
'## References',
|
|
1294
|
+
'',
|
|
1295
|
+
'- No references.',
|
|
1296
|
+
''
|
|
1297
|
+
].join('\n');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function planDocument(body) {
|
|
1301
|
+
return [
|
|
1302
|
+
'# _local-plan-001: My plan',
|
|
1303
|
+
'',
|
|
1304
|
+
'## Executive Summary',
|
|
1305
|
+
'',
|
|
1306
|
+
'Summary.',
|
|
1307
|
+
'',
|
|
1308
|
+
'## Context and Problem Statement',
|
|
1309
|
+
'',
|
|
1310
|
+
body,
|
|
1311
|
+
'',
|
|
1312
|
+
'## Proposed Solution',
|
|
1313
|
+
'',
|
|
1314
|
+
'We will fix it.',
|
|
1315
|
+
'',
|
|
1316
|
+
'Expected end date: 2026-12-31',
|
|
1317
|
+
''
|
|
1318
|
+
].join('\n');
|
|
1319
|
+
}
|
package/package.json
CHANGED