xdrs-core 0.52.0 → 0.52.2

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.
@@ -75,6 +75,8 @@ Examples:
75
75
 
76
76
  Multiple skills under the same `[subject]/skills/` folder MAY share common instruction modules through a sibling `.assets/` directory placed directly under `skills/` (not inside any individual skill package) — for example `skills/.assets/shared-check.md`, referenced with a relative link from any skill in that folder. This keeps shared procedures DRY without duplicating instructions across skills.
77
77
 
78
+ A skill's own `.assets/` folder and the shared `skills/.assets/` folder are exempt from orphan-asset tracking: their files do not need to be linked from `SKILL.md`, because they may hold scripts, templates, installed dependencies (e.g., `node_modules/`), or other runtime resources used by the skill. When `SKILL.md` does link to an asset, the link MUST point either to the skill's own `.assets/` folder or to the shared `../.assets/` folder.
79
+
78
80
  A skill MAY optionally be distributed as a self-contained standalone package (for example, to share it outside this repository) using the bundling mechanism defined in [`_core-adr-policy-021`](021-skill-bundling.md). Bundling is optional; skills that are always consumed as part of the full package do not need it.
79
81
 
80
82
  **Directory roles**
@@ -84,7 +86,7 @@ A skill MAY optionally be distributed as a self-contained standalone package (fo
84
86
  | `SKILL.md` | Required entry point: frontmatter + instructions. |
85
87
  | `scripts/` | Optional executable scripts the agent may run. |
86
88
  | `references/` | Optional deep reference material split out of `SKILL.md`. |
87
- | `.assets/` | Optional images, templates, and other local resource files. |
89
+ | `.assets/` | Optional images, templates, scripts, dependencies, and other local resource files. Not subject to orphan-asset tracking. |
88
90
 
89
91
  **Skill naming**
90
92
 
@@ -164,7 +166,7 @@ Rules:
164
166
  - `description` SHOULD stay near 250 characters when the activation trigger can be stated that tightly, even though the hard cap remains 1024 characters — it is loaded into context on every activation scan.
165
167
  - `## Overview` SHOULD state the task objective, expected outcome, and relevant prerequisites or tools when they matter, and MUST contain nested subsections, in order, for `### Inputs`, `### Outputs`, `### Halt Conditions`, then the optional `### User Interaction` and `### Runtime Requirements`.
166
168
  - `### Inputs` MUST contain `#### Required` (the bare minimum needed to invoke the skill) then `#### Optional` (helpful extra context), in that order.
167
- - `## Instructions` SHOULD state one clearly recommended approach first when multiple are viable, noting alternatives briefly afterward, and SHOULD include verification steps or acceptance criteria at the end of the task or major phases.
169
+ - `## Instructions` is REQUIRED. It SHOULD state one clearly recommended approach first when multiple are viable, noting alternatives briefly afterward, and SHOULD include verification steps or acceptance criteria at the end of the task or major phases.
168
170
  - `## Examples` SHOULD include 2-3 example prompts a user could give as input, alongside a short description of what to expect during execution and as output for each.
169
171
  - `### Outputs` covers only end-objective results, not intermediary items, and MUST contain `#### Contents` (generated files or chat-delivered results) then `#### Changes` (external system mutations that are part of the main objective), in that order.
170
172
  - `### Halt Conditions` MUST list this skill's specific stop-before-completing triggers, grounded in at least missing required input, dubious/ambiguous input, and insufficient agent confidence — distinct from `## Edge Cases` (activation/boundary conditions) and `## Anti-Patterns` (execution mistakes). A partial or empty result the skill still finishes and returns belongs in `### Outputs`, not `### Halt Conditions`.
@@ -49,7 +49,7 @@ A rule title SHOULD include a normative keyword when the rule can be stated clea
49
49
 
50
50
  #### 08-rule-block-must-use-standard-syntax
51
51
 
52
- Each rule MUST be written as a level-4 heading with a two-digit zero-padded sequence number followed by a kebab-case title, and a body paragraph:
52
+ Each rule MUST be written as a level-4 heading with a two-digit zero-padded sequence number followed by a kebab-case title of 12 words or fewer, and a body paragraph:
53
53
 
54
54
  ```markdown
55
55
  #### [NN]-[short-rule-title-in-kebab-case]
package/lib/lint.js CHANGED
@@ -1351,7 +1351,6 @@ function lintSkillsDirectory(xdrsRoot, scopeName, skillsPath, errors, externalSc
1351
1351
  lintNoEmojis(skillContent, skillFilePath, 'Skill', errors);
1352
1352
  lintWordCount(skillContent, skillFilePath, 'Skill', SKILL_MAX_WORDS, errors);
1353
1353
  lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors, externalScopes);
1354
- lintOrphanAssets(path.join(entryPath, RESOURCE_DIR_NAME), [skillFilePath], xdrsRoot, errors);
1355
1354
  }
1356
1355
 
1357
1356
  return artifacts;
@@ -2025,6 +2024,14 @@ function lintInitiativeExpectedEndDate(content, filePath, errors) {
2025
2024
  errors.push(`Initiative must not repeat Expected end date: ${toDisplayPath(filePath)} [_core-adr-policy-007]`);
2026
2025
  }
2027
2026
 
2027
+ const sectionStart = findHeadingLine(lines, ignoredLines, '## Proposed Solution');
2028
+ const sectionEnd = sectionStart === -1
2029
+ ? -1
2030
+ : (findHeadingLines(lines, ignoredLines).find((index) => index > sectionStart) ?? lines.length);
2031
+ if (expectedEndDateLines.some((index) => index <= sectionStart || index >= sectionEnd)) {
2032
+ errors.push(`Initiative Expected end date: field must be inside the "## Proposed Solution" section: ${toDisplayPath(filePath)} [_core-adr-policy-007]`);
2033
+ }
2034
+
2028
2035
  const value = lines[expectedEndDateLines[0]].slice('Expected end date:'.length).trim().replace(/\.$/, '');
2029
2036
  if (!isIsoDate(value)) {
2030
2037
  errors.push(`Initiative Expected end date: must be a valid ISO date in YYYY-MM-DD format: ${toDisplayPath(filePath)} [_core-adr-policy-007]`);
@@ -2123,7 +2130,7 @@ function lintSourceSnapshotTracking(sourcesDir, errors) {
2123
2130
  }
2124
2131
  const trackingFilePath = path.join(sourcesDir, entry.name, SOURCE_TRACKING_FILE_NAME);
2125
2132
  if (!existsFile(trackingFilePath)) {
2126
- errors.push(`Compiled source snapshot folder must contain a "${SOURCE_TRACKING_FILE_NAME}" tracking file: ${toDisplayPath(trackingFilePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
2133
+ errors.push(`Compiled source snapshot folder must contain a "${SOURCE_TRACKING_FILE_NAME}" tracking file: ${toDisplayPath(trackingFilePath)} [_core-adr-policy-019.12-source-storage-and-tracking]`);
2127
2134
  continue;
2128
2135
  }
2129
2136
  validateSourceTrackingFile(trackingFilePath, errors);
@@ -2135,19 +2142,19 @@ function validateSourceTrackingFile(filePath, errors) {
2135
2142
  const lines = content.split(/\r?\n/);
2136
2143
 
2137
2144
  if (firstNonEmptyLine(content) !== '# Source') {
2138
- errors.push(`Source tracking file must start with a "# Source" heading: ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
2145
+ errors.push(`Source tracking file must start with a "# Source" heading: ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-and-tracking]`);
2139
2146
  }
2140
2147
 
2141
2148
  const timestampRe = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
2142
2149
  for (const field of ['last-fetch-timestamp', 'last-compilation-timestamp']) {
2143
2150
  const fieldLine = lines.find((line) => line.trim().startsWith(`${field}:`));
2144
2151
  if (!fieldLine) {
2145
- errors.push(`Source tracking file must include a "${field}:" line: ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
2152
+ errors.push(`Source tracking file must include a "${field}:" line: ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-and-tracking]`);
2146
2153
  continue;
2147
2154
  }
2148
2155
  const value = fieldLine.trim().slice(`${field}:`.length).trim();
2149
2156
  if (!timestampRe.test(value)) {
2150
- errors.push(`Source tracking file "${field}" must match "YYYY-MM-DD HH:MM:SS": ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
2157
+ errors.push(`Source tracking file "${field}" must match "YYYY-MM-DD HH:MM:SS": ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-and-tracking]`);
2151
2158
  }
2152
2159
  }
2153
2160
  }
@@ -2230,6 +2237,8 @@ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalSc
2230
2237
  const ignoredLines = findIgnoredMarkdownLines(lines);
2231
2238
  const documentDir = path.dirname(documentPath);
2232
2239
  const resourceDir = path.join(documentDir, RESOURCE_DIR_NAME);
2240
+ const isSkillFile = path.basename(documentPath) === 'SKILL.md' && path.basename(path.dirname(documentDir)) === 'skills';
2241
+ const sharedSkillResourceDir = isSkillFile ? path.join(path.dirname(documentDir), RESOURCE_DIR_NAME) : null;
2233
2242
  const localScopePath = normalizePath(path.join(xdrsRoot, '_local'));
2234
2243
  const repoRoot = path.dirname(xdrsRoot);
2235
2244
 
@@ -2259,12 +2268,13 @@ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalSc
2259
2268
  errors.push(`Non-_local document must not link into _local scope in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-001]`);
2260
2269
  }
2261
2270
 
2262
- if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)) {
2271
+ if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)
2272
+ && !(sharedSkillResourceDir && isPathInside(sharedSkillResourceDir, link.resolvedPath))) {
2263
2273
  errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget} [_core-adr-policy-001]`);
2264
2274
  }
2265
2275
 
2266
2276
  if (isSourceSnapshotPath(link.resolvedPath)) {
2267
- errors.push(`Document must not link directly to compiled-source snapshot content, which is not authoritative (see _core-adr-policy-019 rule 14) in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-019.14-source-not-authoritative]`);
2277
+ errors.push(`Document must not link directly to compiled-source snapshot content, which is not authoritative (see _core-adr-policy-019 rule 14) in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-019.14-source-documents-not-authoritative]`);
2268
2278
  }
2269
2279
  }
2270
2280
  }
@@ -2724,20 +2734,20 @@ function lintStructuredRuleBlocks(content, filePath, errors) {
2724
2734
  const { lineIndex, number, title } = ruleEntries[r];
2725
2735
 
2726
2736
  if (!/^\d{2}$/.test(number)) {
2727
- errors.push(`Policy structured rule number must be exactly two digits ("${number}" in "${lines[lineIndex].trim()}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.02-rule-numbering-must-be-stable]`);
2737
+ errors.push(`Policy structured rule number must be exactly two digits ("${number}" in "${lines[lineIndex].trim()}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.08-rule-block-must-use-standard-syntax]`);
2728
2738
  }
2729
2739
 
2730
2740
  if (title.length === 0 || !/^[a-z][a-z0-9-]*$/.test(title)) {
2731
2741
  if (/[A-Z]/.test(title)) {
2732
2742
  errors.push(`Policy structured rule title must be all lowercase ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.05-rule-title-must-be-all-lowercase]`);
2733
2743
  } else {
2734
- errors.push(`Policy structured rule title must be in kebab-case ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.01-always-use-numbered-rules-for-strong-or-referenceable-policies]`);
2744
+ errors.push(`Policy structured rule title must be in kebab-case ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.08-rule-block-must-use-standard-syntax]`);
2735
2745
  }
2736
2746
  }
2737
2747
 
2738
2748
  const wordCount = title.split('-').filter(Boolean).length;
2739
2749
  if (wordCount > 12) {
2740
- errors.push(`Policy structured rule title must be 12 words or fewer ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.01-always-use-numbered-rules-for-strong-or-referenceable-policies]`);
2750
+ errors.push(`Policy structured rule title must be 12 words or fewer ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.08-rule-block-must-use-standard-syntax]`);
2741
2751
  }
2742
2752
 
2743
2753
  if (ruleNumbers.has(number)) {
package/lib/lint.test.js CHANGED
@@ -2664,6 +2664,56 @@ test('reports orphan asset in articles .assets directory', () => {
2664
2664
  expect(result.errors.join('\n')).not.toContain('used.png');
2665
2665
  });
2666
2666
 
2667
+ test('does not report orphan asset errors for unlinked files in a skill package .assets directory', () => {
2668
+ const workspaceRoot = createWorkspace('skill-assets-not-orphan', {
2669
+ '.xdrs/index.md': rootIndex(),
2670
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
2671
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
2672
+ ]),
2673
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
2674
+ '.xdrs/_local/adrs/principles/skills/check-links/.assets/template.json': '{}',
2675
+ '.xdrs/_local/adrs/principles/skills/check-links/.assets/run.sh': 'echo run\n',
2676
+ '.xdrs/_local/adrs/principles/skills/check-links/.assets/node_modules/pkg/index.js': 'module.exports = {};\n',
2677
+ });
2678
+
2679
+ const result = lintWorkspace(workspaceRoot);
2680
+
2681
+ expect(result.errors.join('\n')).not.toContain('Orphan asset file');
2682
+ });
2683
+
2684
+ test('allows SKILL.md links to the shared skills .assets directory', () => {
2685
+ const workspaceRoot = createWorkspace('skill-shared-assets-link', {
2686
+ '.xdrs/index.md': rootIndex(),
2687
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
2688
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
2689
+ ]),
2690
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Follow [shared check](../.assets/shared-check.md).'),
2691
+ '.xdrs/_local/adrs/principles/skills/.assets/shared-check.md': '# Shared check\n',
2692
+ });
2693
+
2694
+ const result = lintWorkspace(workspaceRoot);
2695
+
2696
+ expect(result.errors.join('\n')).not.toContain('Asset links in');
2697
+ expect(result.errors.join('\n')).not.toContain('Broken asset link');
2698
+ });
2699
+
2700
+ test('reports SKILL.md links to another skill package .assets directory', () => {
2701
+ const workspaceRoot = createWorkspace('skill-other-assets-link', {
2702
+ '.xdrs/index.md': rootIndex(),
2703
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
2704
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links',
2705
+ '- [other-skill](principles/skills/other-skill/SKILL.md) - Other skill'
2706
+ ]),
2707
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Use [template](../other-skill/.assets/template.md).'),
2708
+ '.xdrs/_local/adrs/principles/skills/other-skill/SKILL.md': skillDocument('Test instructions.').replace('name: check-links', 'name: other-skill'),
2709
+ '.xdrs/_local/adrs/principles/skills/other-skill/.assets/template.md': '# Template\n',
2710
+ });
2711
+
2712
+ const result = lintWorkspace(workspaceRoot);
2713
+
2714
+ expect(result.errors.join('\n')).toContain('Asset links in');
2715
+ });
2716
+
2667
2717
  test('reports unexpected directories inside a skill package', () => {
2668
2718
  const workspaceRoot = createWorkspace('unexpected-skill-package-directory', {
2669
2719
  '.xdrs/index.md': rootIndex(),
@@ -3860,7 +3910,7 @@ test('does not report orphan asset errors for files under .assets/sources in a c
3860
3910
  const result = lintWorkspace(workspaceRoot);
3861
3911
 
3862
3912
  expect(result.errors.join('\n')).not.toContain('raw-standard.md');
3863
- expect(result.errors.join('\n')).not.toContain('019.12-source-storage-tracking');
3913
+ expect(result.errors.join('\n')).not.toContain('019.12-source-storage-and-tracking');
3864
3914
  expect(result.errors.join('\n')).toContain('Orphan asset file');
3865
3915
  expect(result.errors.join('\n')).toContain('unused.png');
3866
3916
  });
@@ -3879,7 +3929,7 @@ test('reports missing source.md tracking file in a compiled source snapshot fold
3879
3929
  const result = lintWorkspace(workspaceRoot);
3880
3930
 
3881
3931
  expect(result.errors.join('\n')).toContain('must contain a "source.md" tracking file');
3882
- expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3932
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-and-tracking');
3883
3933
  });
3884
3934
 
3885
3935
  test('reports malformed source.md tracking file content', () => {
@@ -3901,7 +3951,7 @@ test('reports malformed source.md tracking file content', () => {
3901
3951
  expect(result.errors.join('\n')).toContain('must start with a "# Source" heading');
3902
3952
  expect(result.errors.join('\n')).toContain('must include a "last-compilation-timestamp:" line');
3903
3953
  expect(result.errors.join('\n')).toContain('must match "YYYY-MM-DD HH:MM:SS"');
3904
- expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3954
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-and-tracking');
3905
3955
  });
3906
3956
 
3907
3957
  test('reports document linking directly into a compiled-source snapshot path', () => {
@@ -3917,7 +3967,7 @@ test('reports document linking directly into a compiled-source snapshot path', (
3917
3967
  const result = lintWorkspace(workspaceRoot);
3918
3968
 
3919
3969
  expect(result.errors.join('\n')).toContain('must not link directly to compiled-source snapshot content');
3920
- expect(result.errors.join('\n')).toContain('019.14-source-not-authoritative');
3970
+ expect(result.errors.join('\n')).toContain('019.14-source-documents-not-authoritative');
3921
3971
  });
3922
3972
 
3923
3973
  test('enforces compiled scope checks when scope-type is comma-separated with compiled', () => {
@@ -4603,6 +4653,37 @@ test('reports invalid Expected end date format in initiative document', () => {
4603
4653
  expect(result.errors.join('\n')).toContain('Initiative Expected end date: must be a valid ISO date');
4604
4654
  });
4605
4655
 
4656
+ test('reports Expected end date outside Proposed Solution in initiative document', () => {
4657
+ const workspaceRoot = createWorkspace('initiative-misplaced-end-date', {
4658
+ '.xdrs/index.md': rootIndex(),
4659
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
4660
+ '- [001-myinitiative](principles/initiatives/001-myinitiative.md) - My initiative'
4661
+ ]),
4662
+ '.xdrs/_local/adrs/principles/initiatives/001-myinitiative.md': [
4663
+ '# _local-adr-initiative-001: My initiative',
4664
+ '',
4665
+ '## Executive Summary',
4666
+ '',
4667
+ 'Summary.',
4668
+ '',
4669
+ 'Expected end date: 2026-12-31',
4670
+ '',
4671
+ '## Context and Problem Statement',
4672
+ '',
4673
+ 'Body.',
4674
+ '',
4675
+ '## Proposed Solution',
4676
+ '',
4677
+ 'We will fix it.',
4678
+ ''
4679
+ ].join('\n'),
4680
+ });
4681
+
4682
+ const result = lintWorkspace(workspaceRoot);
4683
+
4684
+ expect(result.errors.join('\n')).toContain('Expected end date: field must be inside the "## Proposed Solution" section');
4685
+ });
4686
+
4606
4687
  // ─── Name / description length ────────────────────────────────────────────────
4607
4688
 
4608
4689
  test('reports XDRS element frontmatter name exceeding 64 characters', () => {
@@ -5175,7 +5256,7 @@ test('reports structured rule block with non-two-digit number', () => {
5175
5256
  const result = lintWorkspace(workspaceRoot);
5176
5257
 
5177
5258
  expect(result.errors.join('\n')).toContain('structured rule number must be exactly two digits');
5178
- expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.02-rule-numbering-must-be-stable]');
5259
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.08-rule-block-must-use-standard-syntax]');
5179
5260
  });
5180
5261
 
5181
5262
  test('reports structured rule block with non-kebab-case title', () => {
@@ -5196,7 +5277,7 @@ test('reports structured rule block with non-kebab-case title', () => {
5196
5277
  const result = lintWorkspace(workspaceRoot);
5197
5278
 
5198
5279
  expect(result.errors.join('\n')).toContain('structured rule title must be in kebab-case');
5199
- expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.01-always-use-numbered-rules-for-strong-or-referenceable-policies]');
5280
+ expect(result.errors.join('\n')).toContain('[_core-adr-policy-008.08-rule-block-must-use-standard-syntax]');
5200
5281
  });
5201
5282
 
5202
5283
  test('reports structured rule block with duplicate rule number', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.52.0",
3
+ "version": "0.52.2",
4
4
  "description": "XDRS (eXtensible Decision Record System): a framework to structure, compile and distribute Architectural (ADR), Business (BDR), and Engineering (EDR) decision records contents so that AI agents and humans can reliably find and use them with hierarchical scopes and controlled rollout in the format of distributable versioned packages.",
5
5
  "repository": {
6
6
  "type": "git",