xdrs-core 0.23.0 → 0.24.1

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.
@@ -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)
@@ -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 six skills that automate the most common framework operations:
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,8 @@ 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 SLIDE_FILE_RE = /^.+-slides(?:-[a-z0-9-]+)?\.md$/;
32
+ const SLIDE_MAX_NAME_LENGTH = 64;
31
33
  const EMOJI_RE = /\p{Extended_Pictographic}/u;
32
34
  const XDR_MAX_WORDS = 2600;
33
35
  const ARTICLE_MAX_WORDS = 5000;
@@ -808,6 +810,51 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
808
810
  errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)}`);
809
811
  }
810
812
  }
813
+
814
+ // Lint slide/presentation files inside .assets
815
+ for (const assetPath of assetTree.files) {
816
+ const fileName = path.basename(assetPath);
817
+ if (SLIDE_FILE_RE.test(fileName)) {
818
+ lintSlideFile(assetPath, documentPaths, xdrsRoot, errors);
819
+ }
820
+ }
821
+ }
822
+
823
+ function lintSlideFile(filePath, documentPaths, xdrsRoot, errors) {
824
+ const fileName = path.basename(filePath);
825
+
826
+ if (fileName !== fileName.toLowerCase()) {
827
+ errors.push(`Slide file name must be lowercase: ${toDisplayPath(filePath)}`);
828
+ }
829
+
830
+ if (fileName.length > SLIDE_MAX_NAME_LENGTH) {
831
+ errors.push(`Slide file name must be ${SLIDE_MAX_NAME_LENGTH} characters or fewer: ${toDisplayPath(filePath)}`);
832
+ }
833
+
834
+ const content = fs.readFileSync(filePath, 'utf8');
835
+
836
+ const fm = extractFrontmatter(content);
837
+ if (!fm.present) {
838
+ errors.push(`Slide file must start with a YAML frontmatter block containing marp: true: ${toDisplayPath(filePath)}`);
839
+ } else {
840
+ const hasMarp = /^marp:\s*true$/m.test(content.match(/^---\r?\n([\s\S]*?)\r?\n---/)[1]);
841
+ if (!hasMarp) {
842
+ errors.push(`Slide frontmatter must include marp: true: ${toDisplayPath(filePath)}`);
843
+ }
844
+ }
845
+
846
+ lintNoEmojis(content, filePath, 'Slide', errors);
847
+
848
+ // Check that the slide links back to at least one parent document
849
+ const repoRoot = path.dirname(xdrsRoot);
850
+ const slideDir = path.dirname(filePath);
851
+ const slideLinks = parseLocalLinks(content, slideDir, repoRoot);
852
+ const slideLinkSet = new Set(slideLinks.map(normalizePath));
853
+
854
+ const linksToParent = documentPaths.some((docPath) => slideLinkSet.has(normalizePath(docPath)));
855
+ if (!linksToParent) {
856
+ errors.push(`Slide file must contain a link back to its parent document: ${toDisplayPath(filePath)}`);
857
+ }
811
858
  }
812
859
 
813
860
  function collectAssetTree(dirPath, errors) {
package/lib/lint.test.js CHANGED
@@ -1238,8 +1238,158 @@ test('passes when research ## Introduction contains Question: line', () => {
1238
1238
  expect(result.errors.join('\n')).not.toContain('Research ## Introduction must contain');
1239
1239
  });
1240
1240
 
1241
+ // ─── Slide / Presentation checks ─────────────────────────────────────────────
1242
+
1243
+ test('passes for valid slide file in .assets with marp frontmatter and backlink', () => {
1244
+ const workspaceRoot = createWorkspace('marp-valid', {
1245
+ '.xdrs/index.md': rootIndex(),
1246
+ '.xdrs/_local/index.md': '# _local Scope Overview\n\n## Content\n\nLocal scope.\n\n- [ADRs](adrs/index.md)\n',
1247
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1248
+ '- [001-main](principles/001-main.md) - Main decision'
1249
+ ]),
1250
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Body.\n\n[Slides](.assets/001-main-slides.md)'),
1251
+ '.xdrs/_local/adrs/principles/.assets/001-main-slides.md': slideDocument('[Parent](../001-main.md)'),
1252
+ });
1253
+
1254
+ const result = lintWorkspace(workspaceRoot);
1255
+
1256
+ const slideErrors = result.errors.filter((e) => /\bSlide\b/.test(e) || /\bSlide\b/i.test(e) && e.includes('slides'));
1257
+ expect(slideErrors).toHaveLength(0);
1258
+ });
1259
+
1260
+ test('reports slide file missing marp: true in frontmatter', () => {
1261
+ const workspaceRoot = createWorkspace('slide-no-marp', {
1262
+ '.xdrs/index.md': rootIndex(),
1263
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1264
+ '- [001-main](principles/001-main.md) - Main decision'
1265
+ ]),
1266
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Body.\n\n[Slides](.assets/001-main-slides.md)'),
1267
+ '.xdrs/_local/adrs/principles/.assets/001-main-slides.md': [
1268
+ '---',
1269
+ 'theme: default',
1270
+ '---',
1271
+ '',
1272
+ '# Slides',
1273
+ '',
1274
+ '[Parent](../001-main.md)',
1275
+ ''
1276
+ ].join('\n'),
1277
+ });
1278
+
1279
+ const result = lintWorkspace(workspaceRoot);
1280
+
1281
+ expect(result.errors.join('\n')).toContain('Slide frontmatter must include marp: true');
1282
+ });
1283
+
1284
+ test('reports slide file without any frontmatter', () => {
1285
+ const workspaceRoot = createWorkspace('slide-no-frontmatter', {
1286
+ '.xdrs/index.md': rootIndex(),
1287
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1288
+ '- [001-main](principles/001-main.md) - Main decision'
1289
+ ]),
1290
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Body.\n\n[Slides](.assets/001-main-slides.md)'),
1291
+ '.xdrs/_local/adrs/principles/.assets/001-main-slides.md': '# Slides\n\n[Parent](../001-main.md)\n',
1292
+ });
1293
+
1294
+ const result = lintWorkspace(workspaceRoot);
1295
+
1296
+ expect(result.errors.join('\n')).toContain('Slide file must start with a YAML frontmatter block containing marp: true');
1297
+ });
1298
+
1299
+ test('reports slide file missing backlink to parent document', () => {
1300
+ const workspaceRoot = createWorkspace('slide-no-backlink', {
1301
+ '.xdrs/index.md': rootIndex(),
1302
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1303
+ '- [001-main](principles/001-main.md) - Main decision'
1304
+ ]),
1305
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Body.\n\n[Slides](.assets/001-main-slides.md)'),
1306
+ '.xdrs/_local/adrs/principles/.assets/001-main-slides.md': slideDocument('No link back here.'),
1307
+ });
1308
+
1309
+ const result = lintWorkspace(workspaceRoot);
1310
+
1311
+ expect(result.errors.join('\n')).toContain('Slide file must contain a link back to its parent document');
1312
+ });
1313
+
1314
+ test('reports slide file name exceeding 64 characters', () => {
1315
+ const longName = '001-main-slides-' + 'a'.repeat(46) + '.md'; // > 64 chars
1316
+ const workspaceRoot = createWorkspace('slide-name-too-long', {
1317
+ '.xdrs/index.md': rootIndex(),
1318
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1319
+ '- [001-main](principles/001-main.md) - Main decision'
1320
+ ]),
1321
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument(`Body.\n\n[Slides](.assets/${longName})`),
1322
+ [`.xdrs/_local/adrs/principles/.assets/${longName}`]: slideDocument('[Parent](../001-main.md)'),
1323
+ });
1324
+
1325
+ const result = lintWorkspace(workspaceRoot);
1326
+
1327
+ expect(result.errors.join('\n')).toContain('Slide file name must be 64 characters or fewer');
1328
+ });
1329
+
1330
+ test('reports emojis in slide files', () => {
1331
+ const workspaceRoot = createWorkspace('slide-emoji', {
1332
+ '.xdrs/index.md': rootIndex(),
1333
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1334
+ '- [001-main](principles/001-main.md) - Main decision'
1335
+ ]),
1336
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Body.\n\n[Slides](.assets/001-main-slides.md)'),
1337
+ '.xdrs/_local/adrs/principles/.assets/001-main-slides.md': slideDocument('Great stuff! \u{1F680}\n\n[Parent](../001-main.md)'),
1338
+ });
1339
+
1340
+ const result = lintWorkspace(workspaceRoot);
1341
+
1342
+ expect(result.errors.join('\n')).toContain('Slide must not contain emojis');
1343
+ });
1344
+
1345
+ test('does not lint non-slide files in .assets as slides', () => {
1346
+ const workspaceRoot = createWorkspace('non-slide-asset', {
1347
+ '.xdrs/index.md': rootIndex(),
1348
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1349
+ '- [001-main](principles/001-main.md) - Main decision'
1350
+ ]),
1351
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Body.\n\n[Schema](.assets/schema.json)'),
1352
+ '.xdrs/_local/adrs/principles/.assets/schema.json': '{"type":"object"}',
1353
+ });
1354
+
1355
+ const result = lintWorkspace(workspaceRoot);
1356
+
1357
+ expect(result.errors.join('\n')).not.toContain('Slide');
1358
+ expect(result.errors.join('\n')).not.toContain('marp');
1359
+ });
1360
+
1361
+ test('passes for slide file in article .assets with backlink', () => {
1362
+ const workspaceRoot = createWorkspace('marp-article-valid', {
1363
+ '.xdrs/index.md': rootIndex(),
1364
+ '.xdrs/_local/index.md': '# _local Scope Overview\n\n## Content\n\nLocal scope.\n\n- [ADRs](adrs/index.md)\n',
1365
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
1366
+ '- [001-guide](principles/articles/001-guide.md) - Guide'
1367
+ ]),
1368
+ '.xdrs/_local/adrs/principles/articles/001-guide.md': articleDocument('Body.\n\n[Slides](.assets/001-guide-slides.md)'),
1369
+ '.xdrs/_local/adrs/principles/articles/.assets/001-guide-slides.md': slideDocument('[Parent](../001-guide.md)'),
1370
+ });
1371
+
1372
+ const result = lintWorkspace(workspaceRoot);
1373
+
1374
+ expect(result.errors.join('\n')).not.toContain('Slide');
1375
+ expect(result.errors.join('\n')).not.toContain('marp');
1376
+ });
1377
+
1241
1378
  // ─── New helpers ──────────────────────────────────────────────────────────────
1242
1379
 
1380
+ function slideDocument(body) {
1381
+ return [
1382
+ '---',
1383
+ 'marp: true',
1384
+ '---',
1385
+ '',
1386
+ '# Presentation',
1387
+ '',
1388
+ body,
1389
+ ''
1390
+ ].join('\n');
1391
+ }
1392
+
1243
1393
  function articleDocument(body) {
1244
1394
  return [
1245
1395
  '# _local-article-001: Guide',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "A standard way to organize Decision Records (XDRs) across scopes, subjects, and teams so that AI agents can reliably query and follow them.",
5
5
  "repository": {
6
6
  "type": "git",