xdrs-core 0.15.3 → 0.15.4

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/lib/lint.js CHANGED
@@ -248,7 +248,6 @@ function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors
248
248
  }
249
249
 
250
250
  const number = match[1];
251
- const shortTitle = match[2];
252
251
  const previous = xdrNumbers.get(number);
253
252
  if (previous) {
254
253
  errors.push(`Duplicate XDR number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)}`);
@@ -267,11 +266,37 @@ function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors
267
266
  errors.push(`XDR title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
268
267
  }
269
268
 
270
- const expectedName = `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${shortTitle}`;
269
+ const expectedName = extractExpectedXdrNameFromHeading(firstLine)
270
+ || `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
271
271
  lintXdrFrontmatter(content, expectedName, filePath, errors);
272
272
  lintDocumentLinks(filePath, errors);
273
273
  }
274
274
 
275
+ function extractExpectedXdrNameFromHeading(headingLine) {
276
+ const match = headingLine.match(/^#\s+([a-z0-9_]+-(?:adr|bdr|edr)-\d{3,}):\s+(.+?)\s*$/);
277
+ if (!match) {
278
+ return null;
279
+ }
280
+
281
+ const identifier = match[1];
282
+ const titleSlug = slugifyTitle(match[2]);
283
+ if (!titleSlug) {
284
+ return null;
285
+ }
286
+
287
+ return `${identifier}-${titleSlug}`;
288
+ }
289
+
290
+ function slugifyTitle(title) {
291
+ return title
292
+ .normalize('NFKD')
293
+ .replace(/[\u0300-\u036f]/g, '')
294
+ .toLowerCase()
295
+ .replace(/[^a-z0-9]+/g, '-')
296
+ .replace(/^-+|-+$/g, '')
297
+ .replace(/--+/g, '-');
298
+ }
299
+
275
300
  function lintXdrFrontmatter(content, expectedName, filePath, errors) {
276
301
  const fm = extractFrontmatter(content);
277
302
  if (!fm.present) {
package/lib/lint.test.js CHANGED
@@ -82,6 +82,32 @@ test('skips read-only files by default and checks them when ignoreReadOnly is fa
82
82
  expect(allResult.errors.join('\n')).toContain('Broken local link in');
83
83
  });
84
84
 
85
+ test('derives expected frontmatter name from the markdown heading title', () => {
86
+ const workspaceRoot = createWorkspace('heading-name-match', {
87
+ '.xdrs/index.md': rootIndex(),
88
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
89
+ '- [002-scope-guidelines](principles/002-xdr-scope-guidelines.md) - Scope guidelines'
90
+ ]),
91
+ '.xdrs/_local/adrs/principles/002-xdr-scope-guidelines.md': [
92
+ '---',
93
+ 'name: _local-adr-002-xdr-scope-guidelines-for-agentme',
94
+ 'description: Test XDR document',
95
+ '---',
96
+ '',
97
+ '# _local-adr-002: XDR scope guidelines for agentme',
98
+ '',
99
+ '## Context and Problem Statement',
100
+ '',
101
+ 'Test body.',
102
+ ''
103
+ ].join('\n'),
104
+ });
105
+
106
+ const result = lintWorkspace(workspaceRoot, { ignoreReadOnly: false });
107
+
108
+ expect(result.errors.join('\n')).not.toContain('XDR frontmatter name must be');
109
+ });
110
+
85
111
  function createWorkspace(name, files) {
86
112
  const workspaceRoot = path.join(tmpRoot, name);
87
113
  fs.mkdirSync(workspaceRoot, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
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",