xdrs-core 0.22.1 → 0.24.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 -0
- package/.xdrs/_core/adrs/principles/009-presentation-standards.md +52 -0
- package/.xdrs/_core/adrs/principles/skills/007-write-presentation/SKILL.md +169 -0
- package/.xdrs/_core/index.md +6 -1
- package/lib/lint.js +105 -5
- package/lib/lint.test.js +398 -1
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ Foundational standards, principles, and guidelines.
|
|
|
14
14
|
- [_core-adr-006](principles/006-research-standards.md) - **Research standards** — How to structure research documents backing XDR decisions
|
|
15
15
|
- [_core-adr-007](principles/007-plan-standards.md) - **Plan standards** — How to structure ephemeral execution plans that implement decisions
|
|
16
16
|
- [_core-adr-008](principles/008-xdr-standards-structured.md) - **XDR standards - structured** — How to expose individually referenceable numbered rules inside an XDR when external citation by identifier is required
|
|
17
|
+
- [_core-adr-009](principles/009-presentation-standards.md) - **Presentation standards** — How to structure Marp slide presentations that support XDR documents
|
|
17
18
|
|
|
18
19
|
## Skills
|
|
19
20
|
|
|
@@ -25,6 +26,7 @@ Step-by-step procedural guides for humans and AI agents.
|
|
|
25
26
|
- [004-write-article](principles/skills/004-write-article/SKILL.md) - **Write Article** — create a new article document
|
|
26
27
|
- [005-write-research](principles/skills/005-write-research/SKILL.md) - **Write Research** — create a new research document
|
|
27
28
|
- [006-write-plan](principles/skills/006-write-plan/SKILL.md) - **Write Plan** — create a new plan document
|
|
29
|
+
- [007-write-presentation](principles/skills/007-write-presentation/SKILL.md) - **Write Presentation** — create Marp slide presentations for XDR documents
|
|
28
30
|
|
|
29
31
|
## Articles
|
|
30
32
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: _core-adr-009-presentation-standards
|
|
3
|
+
description: Defines how Marp slide presentations are structured, named, placed, and linked within XDR projects. Use when creating, reviewing, or linting slide decks that support XDR documents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# _core-adr-009: Presentation standards
|
|
7
|
+
|
|
8
|
+
## Context and Problem Statement
|
|
9
|
+
|
|
10
|
+
Teams often need slide presentations to communicate decisions, research findings, plans, or article content to different audiences. Without a standard, slides drift from the authoritative documents, use inconsistent formats, and become disconnected from the decision records they support.
|
|
11
|
+
|
|
12
|
+
How should slide presentations be structured, placed, and maintained within the XDR framework so they remain consistent, discoverable, and always traceable to the documents they support?
|
|
13
|
+
|
|
14
|
+
## Decision Outcome
|
|
15
|
+
|
|
16
|
+
**Marp Markdown slides co-located in `.assets/` next to the parent document**
|
|
17
|
+
|
|
18
|
+
Presentations are Markdown files in Marp format, stored in the `.assets/` folder of the document they support, with bidirectional links and strict naming conventions.
|
|
19
|
+
|
|
20
|
+
### Details
|
|
21
|
+
|
|
22
|
+
- Slides are supporting media for XDR documents. They must never exist as standalone artifacts without a parent decision, research, article, or plan.
|
|
23
|
+
- Slides must use the [Marp](https://marp.app/) Markdown format with a minimal YAML frontmatter block at the beginning of the file:
|
|
24
|
+
```
|
|
25
|
+
---
|
|
26
|
+
marp: true
|
|
27
|
+
---
|
|
28
|
+
```
|
|
29
|
+
Additional Marp configuration keys (e.g. `theme`, `paginate`, `header`, `footer`) may be added when needed, but `marp: true` is always required as the first frontmatter key.
|
|
30
|
+
- Slide files must be placed in the `.assets/` folder next to the element they relate to, following the `.assets/` placement rules defined in `_core-adr-001`.
|
|
31
|
+
- The parent document must always contain a link to the slide file. The slide file must always contain a link back to the parent document at the end of the presentation. This bidirectional linking is mandatory.
|
|
32
|
+
- Slide files must use the same base name as the parent file, suffixed with `-slides`. When multiple slide sets exist for the same parent, append a context indicator after `-slides` (e.g. `-slides-overview`, `-slides-executive`).
|
|
33
|
+
- Example: parent `003-naming-conventions.md` produces `003-naming-conventions-slides.md` or `003-naming-conventions-slides-overview.md`.
|
|
34
|
+
- Slide file names must not exceed 64 characters (including the `.md` extension).
|
|
35
|
+
- Slide file names must be lowercase.
|
|
36
|
+
- When slides refer to content from multiple decisions, plans, or research documents, an article explaining the combined view must be written first. The slides then support that article, not the individual documents directly.
|
|
37
|
+
- Slides should contain minimal text. Prefer Mermaid diagrams, short bullet points, ASCII art, key short statements, and tables. Use longer text only when the exact wording must be evaluated by the audience (policies, texts under discussion, controls).
|
|
38
|
+
- Slides should follow a clean, linear storytelling structure (context, problem, solution, actions). Follow the structure of the underlying document, extracting the most important points and stressing central questions, answers, doubts, decisions, and risks.
|
|
39
|
+
- Define the central message or objective of the presentation before creating the slides. If the objective is unclear or there are multiple possible paths, ask the user before proceeding.
|
|
40
|
+
- Always identify the target audience (executives, engineers, specialists, control). If the audience is not clear from the underlying document, ask before creating the slides. Include audience info in the file name when multiple audiences exist (e.g. `005-rail-standards-slides-executive.md`).
|
|
41
|
+
- Keep presentations under 30 slides. Create separate slide sets for different views or audiences when needed.
|
|
42
|
+
- Slides never replace decision records or related document contents. When the underlying document changes, the associated slides must be reviewed and updated to stay consistent.
|
|
43
|
+
- Never use emojis in slide content.
|
|
44
|
+
- Always use lowercase file names.
|
|
45
|
+
|
|
46
|
+
## References
|
|
47
|
+
|
|
48
|
+
- [_core-adr-001 - XDRs core](001-xdrs-core.md) - Framework structure and `.assets/` placement rules
|
|
49
|
+
- [_core-adr-002 - XDR standards](002-xdr-standards.md) - XDR document writing rules
|
|
50
|
+
- [_core-adr-004 - Article standards](004-article-standards.md) - Article standards for multi-document views
|
|
51
|
+
- [007-write-presentation skill](skills/007-write-presentation/SKILL.md) - Skill for creating slide presentations
|
|
52
|
+
- [Marp](https://marp.app/) - Markdown Presentation Ecosystem
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 007-write-presentation
|
|
3
|
+
description: >
|
|
4
|
+
Creates a Marp slide presentation for an existing XDR document (decision, research, article, or plan).
|
|
5
|
+
Activate this skill when the user asks to create slides, a presentation, or a slide deck for an XDR document.
|
|
6
|
+
metadata:
|
|
7
|
+
author: flaviostutz
|
|
8
|
+
version: "1.0"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Guides the creation of a Marp Markdown slide presentation that supports an existing XDR document. The skill ensures the slides follow presentation standards (`_core-adr-009`), are correctly placed in the `.assets/` folder, and maintain bidirectional links with the parent document.
|
|
14
|
+
|
|
15
|
+
## Instructions
|
|
16
|
+
|
|
17
|
+
### Phase 1: Identify the Parent Document
|
|
18
|
+
|
|
19
|
+
1. Read `.xdrs/_core/adrs/principles/009-presentation-standards.md` in full to internalize all presentation rules.
|
|
20
|
+
2. Read `.xdrs/_core/adrs/principles/001-xdrs-core.md` for `.assets/` placement rules and general framework structure.
|
|
21
|
+
3. Identify the parent document (decision, research, article, or plan) that the slides will support. The parent document must already exist. If no parent document exists, inform the user that slides cannot be standalone and suggest creating the parent document first.
|
|
22
|
+
4. If the user wants slides covering content from multiple documents, check whether an article already exists that synthesizes those documents. If not, suggest creating an article first (using the 004-write-article skill) and then creating slides for that article.
|
|
23
|
+
|
|
24
|
+
### Phase 2: Define the Presentation Scope
|
|
25
|
+
|
|
26
|
+
1. Read the parent document in full.
|
|
27
|
+
2. Define the central message or objective of the presentation. If the objective is ambiguous or there are multiple possible paths, ask the user before proceeding. Ask one question at a time.
|
|
28
|
+
3. Identify the target audience. Check the parent document for audience indicators. If unclear, ask the user. Common audiences:
|
|
29
|
+
- **Executives**: high-level, strategic, outcome-focused
|
|
30
|
+
- **Engineers**: technical depth, implementation details, trade-offs
|
|
31
|
+
- **Specialists**: domain-specific depth, compliance, controls
|
|
32
|
+
- **Control**: risk, compliance, audit, governance focus
|
|
33
|
+
4. Determine if multiple slide sets are needed for different audiences. If so, create separate files for each.
|
|
34
|
+
|
|
35
|
+
### Phase 3: Plan the Slide Structure
|
|
36
|
+
|
|
37
|
+
1. Extract the most important points from the parent document.
|
|
38
|
+
2. Organize slides following a linear storytelling structure:
|
|
39
|
+
- **Context**: what is the situation, background, who is impacted
|
|
40
|
+
- **Problem**: what needs to be decided, addressed, or understood
|
|
41
|
+
- **Solution/Decision**: what was decided and why
|
|
42
|
+
- **Actions/Next Steps**: what happens now, who is responsible
|
|
43
|
+
3. For each slide, identify the best format:
|
|
44
|
+
- Mermaid diagrams for flows, relationships, architecture
|
|
45
|
+
- Short bullet points for key decisions and trade-offs
|
|
46
|
+
- Tables for comparisons, options, criteria
|
|
47
|
+
- ASCII art for simple layouts or structures
|
|
48
|
+
- Key short statements for emphasis
|
|
49
|
+
- Longer text only when exact wording must be evaluated (policies, controls)
|
|
50
|
+
4. Keep the total under 30 slides. If more content is needed, plan separate slide sets.
|
|
51
|
+
|
|
52
|
+
### Phase 4: Determine the File Name
|
|
53
|
+
|
|
54
|
+
1. Take the parent document file name (without `.md` extension).
|
|
55
|
+
2. Append `-slides` for the primary set.
|
|
56
|
+
3. If multiple sets exist, append a context indicator: `-slides-overview`, `-slides-executive`, `-slides-technical`, etc.
|
|
57
|
+
4. Verify the final file name (including `.md`) is 64 characters or fewer. If it exceeds 64 characters, shorten the base name while keeping it recognizable.
|
|
58
|
+
5. Ensure the file name is entirely lowercase.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
- Parent: `003-naming-conventions.md` -> `003-naming-conventions-slides.md`
|
|
62
|
+
- Parent: `005-rail-standards.md` (executive audience) -> `005-rail-standards-slides-executive.md`
|
|
63
|
+
- Parent: `001-xdrs-overview.md` (article) -> `001-xdrs-overview-slides.md`
|
|
64
|
+
|
|
65
|
+
### Phase 5: Write the Slides
|
|
66
|
+
|
|
67
|
+
Create the Marp Markdown file with this structure:
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
---
|
|
71
|
+
marp: true
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# [Presentation Title]
|
|
75
|
+
|
|
76
|
+
[Subtitle or context line]
|
|
77
|
+
[Audience and date if relevant]
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
[Slide 2: Context/Background]
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
[Slide 3: Problem Statement]
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
[... additional slides ...]
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# References
|
|
94
|
+
|
|
95
|
+
- [Parent document title](relative/path/to/parent.md)
|
|
96
|
+
- [Other related documents if applicable]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
- The first line of YAML frontmatter must be `marp: true`. Additional Marp keys (`theme`, `paginate`, `header`, `footer`) may be added after.
|
|
101
|
+
- Use `---` as the slide separator (standard Marp syntax).
|
|
102
|
+
- The last slide must contain links back to the parent document and any other related documents.
|
|
103
|
+
- Minimize text per slide. Prefer visual elements and short statements.
|
|
104
|
+
- Stress central questions, answers, doubts, decisions, and risks from the parent document.
|
|
105
|
+
- No emojis.
|
|
106
|
+
- Use relative paths for all links.
|
|
107
|
+
- Follow the audience-appropriate level of detail.
|
|
108
|
+
|
|
109
|
+
### Phase 6: Update the Parent Document
|
|
110
|
+
|
|
111
|
+
1. Add a link to the slide file in the parent document. Place it in the `## References` section or, for XDRs, in the most appropriate section.
|
|
112
|
+
2. Use a descriptive link text such as "Presentation slides" pointing to the slide file in `.assets/`.
|
|
113
|
+
|
|
114
|
+
### Phase 7: Write Files
|
|
115
|
+
|
|
116
|
+
1. Create the slide file at the correct `.assets/` location:
|
|
117
|
+
- XDRs: `[xdrs-root]/[scope]/[type]/[subject]/.assets/[slide-file].md`
|
|
118
|
+
- Articles: `[xdrs-root]/[scope]/[type]/[subject]/articles/.assets/[slide-file].md`
|
|
119
|
+
- Research: `[xdrs-root]/[scope]/[type]/[subject]/researches/.assets/[slide-file].md`
|
|
120
|
+
- Skills: `[xdrs-root]/[scope]/[type]/[subject]/skills/[number]-[skill-name]/.assets/[slide-file].md`
|
|
121
|
+
- Plans: `[xdrs-root]/[scope]/[type]/[subject]/plans/.assets/[slide-file].md`
|
|
122
|
+
2. Update the parent document with the link to the slide file.
|
|
123
|
+
3. Verify that the slide file name is <= 64 characters and lowercase.
|
|
124
|
+
|
|
125
|
+
### Phase 8: Verify with Lint
|
|
126
|
+
|
|
127
|
+
1. Run the CLI lint utility from the repository root:
|
|
128
|
+
```
|
|
129
|
+
npx -y xdrs-core lint .
|
|
130
|
+
```
|
|
131
|
+
2. Fix all reported errors before considering the task complete.
|
|
132
|
+
|
|
133
|
+
### Constraints
|
|
134
|
+
|
|
135
|
+
- MUST follow presentation standards from `_core-adr-009` exactly.
|
|
136
|
+
- MUST NOT create slides without an existing parent document.
|
|
137
|
+
- MUST NOT create standalone slides that reference multiple documents without an article as the parent.
|
|
138
|
+
- MUST maintain bidirectional links between slides and parent document.
|
|
139
|
+
- MUST keep slide file names under 64 characters.
|
|
140
|
+
- MUST include `marp: true` in the slide frontmatter.
|
|
141
|
+
- MUST keep presentations under 30 slides.
|
|
142
|
+
- MUST ask the user about audience and objective when not clear from context.
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
**Input**: "Create slides for our naming conventions decision"
|
|
147
|
+
- Locate the parent XDR (e.g. `003-naming-conventions.md`)
|
|
148
|
+
- Ask: "Who is the target audience for these slides?"
|
|
149
|
+
- Create: `.assets/003-naming-conventions-slides.md`
|
|
150
|
+
- Update: Add link in `003-naming-conventions.md`
|
|
151
|
+
|
|
152
|
+
**Input**: "Create an executive presentation covering our security and data decisions"
|
|
153
|
+
- Check if an article synthesizing security + data decisions exists
|
|
154
|
+
- If not, suggest creating the article first
|
|
155
|
+
- Create slides for the article, not the individual XDRs
|
|
156
|
+
|
|
157
|
+
## Edge Cases
|
|
158
|
+
|
|
159
|
+
- If the parent document does not exist, do not create slides. Inform the user and suggest creating the parent first.
|
|
160
|
+
- If the slide file name would exceed 64 characters, shorten the base name while keeping it recognizable.
|
|
161
|
+
- If the content requires more than 30 slides, split into multiple slide sets with distinct audience or topic focus.
|
|
162
|
+
- If the parent document changes after slides are created, the slides must be reviewed and updated.
|
|
163
|
+
|
|
164
|
+
## References
|
|
165
|
+
|
|
166
|
+
- [_core-adr-009 - Presentation standards](../../009-presentation-standards.md)
|
|
167
|
+
- [_core-adr-001 - XDRs core](../../001-xdrs-core.md)
|
|
168
|
+
- [_core-adr-004 - Article standards](../../004-article-standards.md)
|
|
169
|
+
- [_core-adr-003 - Skill standards](../../003-skill-standards.md)
|
package/.xdrs/_core/index.md
CHANGED
|
@@ -35,9 +35,13 @@ Each artifact type has its own writing standard:
|
|
|
35
35
|
|
|
36
36
|
The business decision [_core-bdr-001](bdrs/principles/001-xdr-decisions-and-skills-usage.md) establishes how agents and humans must use XDR decisions and skills, separating policy authority (which lives in XDRs) from execution guidance (which lives in skills).
|
|
37
37
|
|
|
38
|
+
### Presentation standards
|
|
39
|
+
|
|
40
|
+
Slide presentations that support XDR documents follow [_core-adr-009](adrs/principles/009-presentation-standards.md). Slides use the Marp Markdown format, live in `.assets/` next to the document they support, and must maintain bidirectional links with the parent document.
|
|
41
|
+
|
|
38
42
|
### Available skills
|
|
39
43
|
|
|
40
|
-
The `_core` scope ships with
|
|
44
|
+
The `_core` scope ships with seven skills that automate the most common framework operations:
|
|
41
45
|
|
|
42
46
|
- **001-lint** reviews code and files against applicable XDRs
|
|
43
47
|
- **002-write-xdr** guides creation of a new decision record
|
|
@@ -45,6 +49,7 @@ The `_core` scope ships with six skills that automate the most common framework
|
|
|
45
49
|
- **004-write-article** guides creation of a new article
|
|
46
50
|
- **005-write-research** guides creation of a new research document
|
|
47
51
|
- **006-write-plan** guides creation of a new execution plan
|
|
52
|
+
- **007-write-presentation** guides creation of Marp slide presentations
|
|
48
53
|
|
|
49
54
|
### Getting started
|
|
50
55
|
|
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