xdrs-core 0.7.1 → 0.9.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/README.md CHANGED
@@ -12,11 +12,14 @@ This project defines a standard for organizing XDRs that satisfies the following
12
12
 
13
13
  ## XDR elements
14
14
 
15
- Every XDR package contains three types of documents:
15
+ Every XDR package contains four types of documents:
16
16
 
17
- - **Decision Records (XDRs)** — Architectural (ADR), Business (BDR), or Engineering (EDR) records that capture a single decision, its rationale, and the rules that follow from it. They are the source of truth.
18
- - **Skills** — Step-by-step procedural guides that can be followed by humans, AI agents, or both. A skill may start as a fully manual procedure and evolve toward partial or full AI automation over time. Co-located with the XDRs they implement.
19
- - **Articles** — Synthetic explanatory texts that combine information from multiple XDRs and Skills around a specific topic or audience. They never replace XDRs as source of truth.
17
+ - **Decision Records (XDRs)** — Architectural (ADR), Business (BDR), or Engineering (EDR) records that capture a single decision, its rationale, and the rules that follow from it. They are the source of truth. An XDR may optionally start with a `Metadata` section for short applicability and validity markers.
18
+ - **Research** — Exploratory documents that capture the problem being investigated, constraints or requirements, findings, and option tradeoffs that back a decision during its lifecycle. One research document may inform multiple downstream decisions, but it is not a replacement for the Decision Record.
19
+ - **Skills** — Step-by-step procedural guides that can be followed by humans, AI agents, or both. Skills are task-based artifacts with a concrete outcome and should include enough detail to verify the task was completed correctly. A skill may start as a fully manual procedure and evolve toward partial or full AI automation over time. Co-located with the XDRs they implement.
20
+ - **Articles** — Synthetic explanatory texts that combine information from multiple XDRs, Research documents, and Skills around a specific topic or audience. They never replace XDRs as source of truth.
21
+
22
+ For simple indications, prefer plain Markdown, tables, or ASCII art. Use local images and other supporting files only when they are materially necessary, and keep them in a sibling `assets/` folder next to the document file.
20
23
 
21
24
  ## Getting started
22
25
 
@@ -51,8 +54,11 @@ The `lint` command reads `./.xdrs/**` from the given workspace path and checks c
51
54
  - XDR numbering uniqueness per `scope/type`
52
55
  - skill numbering uniqueness per `scope/type/subject/skills`
53
56
  - article numbering uniqueness per `scope/type/subject/articles`
57
+ - research numbering uniqueness per `scope/type/subject/researches`
54
58
  - canonical index presence and link consistency
55
59
  - root index coverage for all discovered canonical indexes
60
+ - XDR metadata section placement and `Applied to` / `Validity` field format
61
+ - local image and `assets/` links resolving inside the sibling `assets/` folder for each document
56
62
 
57
63
  Examples:
58
64
 
@@ -106,11 +112,17 @@ This is especially important for BDRs: because business rules govern decisions t
106
112
  index.md # canonical index for this scope+type
107
113
  [subject]/
108
114
  [number]-[short-title].md # individual decision record
115
+ assets/ # optional local resources for subject-level XDR files
116
+ researches/ # optional decision-backing research documents
117
+ [number]-[short-title].md
118
+ assets/
109
119
  skills/ # optional skill packages for humans and AI agents
110
120
  [number]-[skill-name]/
111
121
  SKILL.md
112
- articles/ # optional synthetic views over XDRs and Skills
122
+ assets/
123
+ articles/ # optional synthetic views over XDRs, Research, and Skills
113
124
  [number]-[short-title].md
125
+ assets/
114
126
  ```
115
127
 
116
128
  Document types:
@@ -118,31 +130,32 @@ Document types:
118
130
  - **ADR** - Architectural Decision Record: architectural and technical decisions
119
131
  - **BDR** - Business Decision Record: business process and strategy decisions
120
132
  - **EDR** - Engineering Decision Record: engineering workflow and tooling decisions
121
- - **Skills** - Step-by-step procedural guides that can be followed by humans, AI agents, or both. Must comply with Decision Records, but add the execution detail they lack. A skill may start as a fully manual human procedure and evolve incrementally toward partial or full AI automation without being restructured. Co-located with the XDRs they implement inside `skills/` sub-directories.
122
- - **Articles** - Synthetic views that explain concepts or combine information from multiple Decision Records and Skills into a coherent text for a specific topic or audience. Articles are not the source of truth; Decision Records take precedence when there is a conflict. Useful as navigational indexes that link related DRs and Skills around a specific aspect.
133
+ - **Research** - Exploratory support material used while evaluating or updating a decision. Research captures constraints, findings, options, and proposal tradeoffs, but it is not the source of truth.
134
+ - **Skills** - Step-by-step procedural guides that can be followed by humans, AI agents, or both. Must comply with Decision Records, but add the execution detail they lack. Skills are not mandatory by themselves unless referenced by an XDR or another policy artifact. A skill may start as a fully manual human procedure and evolve incrementally toward partial or full AI automation without being restructured. Co-located with the XDRs they implement inside `skills/` sub-directories.
135
+ - **Articles** - Synthetic views that explain concepts or combine information from multiple Decision Records, Research documents, and Skills into a coherent text for a specific topic or audience. Articles are not the source of truth; Decision Records take precedence when there is a conflict. Useful as navigational indexes that link related artifacts around a specific aspect.
123
136
 
124
137
  See [.xdrs/index.md](.xdrs/index.md) for the full list of active decision records.
125
138
 
126
139
  For a deeper overview of XDRs — objective, structure, guidelines, extension, and usage — see the [XDRs Overview article](.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md).
127
- For packaging guidance on publishing your own reusable scope with DRs, skills, and articles, see the [Create your own xdrs-core extension package article](docs/create-your-own-xdrs-extension-package.md), then compare [examples/basic-usage](examples/basic-usage) and [examples/mydevkit](examples/mydevkit).
140
+ For packaging guidance on publishing your own reusable scope with DRs, Research documents, skills, and articles, see the [Create your own xdrs-core extension package article](docs/create-your-own-xdrs-extension-package.md), then compare [examples/basic-usage](examples/basic-usage) and [examples/mydevkit](examples/mydevkit).
128
141
 
129
142
  ## Flow: Decision -> Distribution -> Usage
130
143
 
131
- XDRs and skills follow a three-stage lifecycle that keeps decision-making decentralized while allowing controlled adoption across projects.
144
+ XDRs, Research documents, and skills follow a three-stage lifecycle that keeps decision-making decentralized while allowing controlled adoption across projects.
132
145
 
133
146
  ### Decision
134
147
 
135
- Each scope manages its own set of XDRs independently. Scope owners discuss and evolve decisions through whatever process fits their team, such as RFCs, pull requests, or architecture review boards. XDRs and skills are authored, reviewed, and merged within the scope's folder in the repository.
148
+ Each scope manages its own set of XDR artifacts independently. Scope owners discuss and evolve decisions through whatever process fits their team, such as RFCs, pull requests, or architecture review boards. Research documents, XDRs, and skills are authored, reviewed, and merged within the scope's folder in the repository.
136
149
 
137
150
  ### Distribution
138
151
 
139
152
  Once a set of decisions is ready to share, scope owners pack the relevant `.xdrs/[scope]/` folder into a versioned npm package using a tool such as [filedist](https://github.com/flaviostutz/filedist) and publish it to an npm registry, either public or a company-internal one. Versioning gives consumers explicit control over which revision of a scope's decisions they adopt, avoiding situations where a single breaking policy change is forced on all consumers at once.
140
153
 
141
- The same applies to skills: because they live alongside XDRs inside the scope folder, they are included in the same package and published together.
154
+ The same applies to Research documents, skills, articles, and any sibling `assets/` folders: because they live alongside XDRs inside the scope folder, they are included in the same package and published together.
142
155
 
143
156
  ### Usage
144
157
 
145
- A project that wants to follow a scope's decisions adds the corresponding npm package as a regular dependency. Using a tool such as [filedist](https://github.com/flaviostutz/filedist), the package contents are unpacked into the project's `.xdrs/` folder at install or update time. Updating the dependency version pulls in the latest XDRs and skills for that scope, keeping the project aligned with the scope owners' current decisions.
158
+ A project that wants to follow a scope's decisions adds the corresponding npm package as a regular dependency. Using a tool such as [filedist](https://github.com/flaviostutz/filedist), the package contents are unpacked into the project's `.xdrs/` folder at install or update time. Updating the dependency version pulls in the latest XDRs, Research documents, and skills for that scope, keeping the project aligned with the scope owners' current decisions.
146
159
 
147
160
  Multiple scope packages can be combined in the same workspace by listing them as separate dependencies. Scope precedence (defined in `.xdrs/index.md`) determines which decisions take effect when scopes overlap.
148
161
 
@@ -152,6 +165,7 @@ Multiple scope packages can be combined in the same workspace by listing them as
152
165
  adrs/ bdrs/ edrs/ adrs/ bdrs/ edrs/
153
166
  [subject]/ [subject]/
154
167
  *.md *.md
168
+ researches/ researches/
155
169
  skills/ skills/
156
170
  articles/ articles/
157
171
  ```
package/lib/lint.js CHANGED
@@ -21,6 +21,9 @@ const RESERVED_SCOPES = new Set(['_core', '_local']);
21
21
  const NUMBERED_FILE_RE = /^(\d{3,})-([a-z0-9-]+)\.md$/;
22
22
  const NUMBERED_DIR_RE = /^(\d{3,})-([a-z0-9-]+)$/;
23
23
  const REQUIRED_ROOT_INDEX_TEXT = 'XDRs in scopes listed last override the ones listed first';
24
+ const SUBJECT_ARTIFACT_DIRS = new Set(['skills', 'articles', 'researches']);
25
+ const RESOURCE_DIR_NAME = 'assets';
26
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
24
27
 
25
28
  function runLintCli(args) {
26
29
  if (args.includes('--help') || args.includes('-h')) {
@@ -190,6 +193,9 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
190
193
  const entryPath = path.join(subjectPath, entry.name);
191
194
 
192
195
  if (entry.isDirectory()) {
196
+ if (entry.name === RESOURCE_DIR_NAME) {
197
+ continue;
198
+ }
193
199
  if (entry.name === 'skills') {
194
200
  artifacts.push(...lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
195
201
  continue;
@@ -198,6 +204,10 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
198
204
  artifacts.push(...lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
199
205
  continue;
200
206
  }
207
+ if (entry.name === 'researches') {
208
+ artifacts.push(...lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
209
+ continue;
210
+ }
201
211
 
202
212
  errors.push(`Unexpected directory under ${scopeName}/${typeName}/${subjectName}: ${toDisplayPath(entryPath)}`);
203
213
  continue;
@@ -240,6 +250,120 @@ function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors
240
250
  if (!firstLine.startsWith(expectedHeader)) {
241
251
  errors.push(`XDR title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
242
252
  }
253
+
254
+ lintXdrMetadata(content, filePath, errors);
255
+ lintDocumentResourceLinks(filePath, errors);
256
+ }
257
+
258
+ function lintXdrMetadata(content, filePath, errors) {
259
+ const lines = content.split(/\r?\n/);
260
+ const ignoredLines = findIgnoredMarkdownLines(lines);
261
+ const metadataLine = findHeadingLine(lines, ignoredLines, '## Metadata');
262
+ const contextLine = findHeadingLine(lines, ignoredLines, '## Context and Problem Statement');
263
+ const headingLines = findHeadingLines(lines, ignoredLines);
264
+
265
+ if (metadataLine !== -1) {
266
+ const nextHeadingLine = headingLines.find((lineIndex) => lineIndex > metadataLine);
267
+ if (nextHeadingLine !== contextLine) {
268
+ errors.push(`XDR metadata section must appear immediately before Context and Problem Statement: ${toDisplayPath(filePath)}`);
269
+ }
270
+ }
271
+
272
+ if (metadataLine !== -1 && contextLine !== -1 && metadataLine > contextLine) {
273
+ errors.push(`XDR metadata section must appear before Context and Problem Statement: ${toDisplayPath(filePath)}`);
274
+ }
275
+
276
+ const metadataRange = metadataLine === -1
277
+ ? null
278
+ : {
279
+ start: metadataLine + 1,
280
+ end: (headingLines.find((lineIndex) => lineIndex > metadataLine) ?? lines.length) - 1
281
+ };
282
+ const appliedLineNumbers = findFieldLines(lines, ignoredLines, 'Applied to:');
283
+ const validityLineNumbers = findFieldLines(lines, ignoredLines, 'Validity:');
284
+
285
+ if (!metadataRange && (appliedLineNumbers.length > 0 || validityLineNumbers.length > 0)) {
286
+ errors.push(`XDR metadata fields require a ## Metadata section immediately before Context and Problem Statement: ${toDisplayPath(filePath)}`);
287
+ return;
288
+ }
289
+
290
+ if (!metadataRange) {
291
+ return;
292
+ }
293
+
294
+ const metadataApplied = appliedLineNumbers.filter((lineIndex) => isLineInsideRange(lineIndex, metadataRange));
295
+ const metadataValidity = validityLineNumbers.filter((lineIndex) => isLineInsideRange(lineIndex, metadataRange));
296
+
297
+ if (metadataApplied.length === 0 && metadataValidity.length === 0) {
298
+ errors.push(`XDR metadata section must be omitted when no metadata fields are defined: ${toDisplayPath(filePath)}`);
299
+ }
300
+
301
+ if (metadataApplied.length > 1) {
302
+ errors.push(`XDR metadata must not repeat Applied to: ${toDisplayPath(filePath)}`);
303
+ }
304
+
305
+ if (metadataValidity.length > 1) {
306
+ errors.push(`XDR metadata must not repeat Validity: ${toDisplayPath(filePath)}`);
307
+ }
308
+
309
+ for (const lineIndex of [...appliedLineNumbers, ...validityLineNumbers]) {
310
+ if (!isLineInsideRange(lineIndex, metadataRange)) {
311
+ errors.push(`XDR metadata fields must be declared inside ## Metadata: ${toDisplayPath(filePath)}`);
312
+ break;
313
+ }
314
+ }
315
+
316
+ for (let lineIndex = metadataRange.start; lineIndex <= metadataRange.end; lineIndex += 1) {
317
+ if (ignoredLines[lineIndex]) {
318
+ continue;
319
+ }
320
+ const trimmed = lines[lineIndex].trim();
321
+ if (trimmed === '') {
322
+ continue;
323
+ }
324
+ if (!trimmed.startsWith('Applied to:') && !trimmed.startsWith('Validity:')) {
325
+ errors.push(`XDR metadata section only supports Applied to: and Validity: fields: ${toDisplayPath(filePath)}`);
326
+ break;
327
+ }
328
+ }
329
+
330
+ if (metadataApplied.length === 1) {
331
+ const value = lines[metadataApplied[0]].slice('Applied to:'.length).trim();
332
+ if (value === '') {
333
+ errors.push(`XDR Applied to: must not be empty: ${toDisplayPath(filePath)}`);
334
+ } else if (countWords(value) >= 40) {
335
+ errors.push(`XDR Applied to: must be under 40 words: ${toDisplayPath(filePath)}`);
336
+ }
337
+ }
338
+
339
+ if (metadataValidity.length === 1) {
340
+ lintValidityField(lines[metadataValidity[0]], filePath, errors);
341
+ }
342
+ }
343
+
344
+ function lintValidityField(line, filePath, errors) {
345
+ const value = line.slice('Validity:'.length).trim().replace(/\.$/, '');
346
+
347
+ if (value === 'Draft' || value === 'Retired') {
348
+ return;
349
+ }
350
+
351
+ const validityMatch = value.match(/^(?:from\s+(\d{4}-\d{2}-\d{2}))?(?:\s+until\s+(\d{4}-\d{2}-\d{2}))?$/);
352
+ if (!validityMatch || (!validityMatch[1] && !validityMatch[2])) {
353
+ errors.push(`XDR Validity: must be Draft, Retired, from YYYY-MM-DD, until YYYY-MM-DD, or from YYYY-MM-DD until YYYY-MM-DD: ${toDisplayPath(filePath)}`);
354
+ return;
355
+ }
356
+
357
+ const fromDate = validityMatch[1];
358
+ const untilDate = validityMatch[2];
359
+ if ((fromDate && !isIsoDate(fromDate)) || (untilDate && !isIsoDate(untilDate))) {
360
+ errors.push(`XDR Validity: must use ISO dates in YYYY-MM-DD format: ${toDisplayPath(filePath)}`);
361
+ return;
362
+ }
363
+
364
+ if (fromDate && untilDate && fromDate > untilDate) {
365
+ errors.push(`XDR Validity: from date must be on or before until date: ${toDisplayPath(filePath)}`);
366
+ }
243
367
  }
244
368
 
245
369
  function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsPath, errors) {
@@ -287,6 +411,8 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
287
411
  } else if (frontmatterName !== entry.name) {
288
412
  errors.push(`Skill frontmatter name must match package directory "${entry.name}": ${toDisplayPath(skillFilePath)}`);
289
413
  }
414
+
415
+ lintDocumentResourceLinks(skillFilePath, errors);
290
416
  }
291
417
 
292
418
  return artifacts;
@@ -299,6 +425,10 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
299
425
 
300
426
  for (const entry of entries) {
301
427
  const entryPath = path.join(articlesPath, entry.name);
428
+ if (entry.isDirectory() && entry.name === RESOURCE_DIR_NAME) {
429
+ continue;
430
+ }
431
+
302
432
  if (!entry.isFile()) {
303
433
  errors.push(`Unexpected directory in articles folder: ${toDisplayPath(entryPath)}`);
304
434
  continue;
@@ -330,6 +460,57 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
330
460
  if (!firstLine.startsWith(expectedHeader)) {
331
461
  errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
332
462
  }
463
+
464
+ lintDocumentResourceLinks(entryPath, errors);
465
+ }
466
+
467
+ return artifacts;
468
+ }
469
+
470
+ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, researchPath, errors) {
471
+ const artifacts = [];
472
+ const researchNumbers = new Map();
473
+ const entries = safeReadDir(researchPath, errors, `read research directory ${scopeName}/${typeName}/${subjectName}/researches`);
474
+
475
+ for (const entry of entries) {
476
+ const entryPath = path.join(researchPath, entry.name);
477
+ if (entry.isDirectory() && entry.name === RESOURCE_DIR_NAME) {
478
+ continue;
479
+ }
480
+
481
+ if (!entry.isFile()) {
482
+ errors.push(`Unexpected directory in researches folder: ${toDisplayPath(entryPath)}`);
483
+ continue;
484
+ }
485
+
486
+ const match = entry.name.match(NUMBERED_FILE_RE);
487
+ if (!match) {
488
+ errors.push(`Invalid research file name: ${toDisplayPath(entryPath)}`);
489
+ continue;
490
+ }
491
+
492
+ artifacts.push(entryPath);
493
+
494
+ const number = match[1];
495
+ const previous = researchNumbers.get(number);
496
+ if (previous) {
497
+ errors.push(`Duplicate research number ${number} in ${scopeName}/${typeName}/${subjectName}/researches: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
498
+ } else {
499
+ researchNumbers.set(number, entryPath);
500
+ }
501
+
502
+ if (entry.name !== entry.name.toLowerCase()) {
503
+ errors.push(`Research file name must be lowercase: ${toDisplayPath(entryPath)}`);
504
+ }
505
+
506
+ const content = fs.readFileSync(entryPath, 'utf8');
507
+ const expectedHeader = `# ${scopeName}-research-${number}:`;
508
+ const firstLine = firstNonEmptyLine(content);
509
+ if (!firstLine.startsWith(expectedHeader)) {
510
+ errors.push(`Research title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
511
+ }
512
+
513
+ lintDocumentResourceLinks(entryPath, errors);
333
514
  }
334
515
 
335
516
  return artifacts;
@@ -356,15 +537,43 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
356
537
  }
357
538
  }
358
539
 
540
+ function lintDocumentResourceLinks(documentPath, errors) {
541
+ const content = fs.readFileSync(documentPath, 'utf8');
542
+ const documentDir = path.dirname(documentPath);
543
+ const resourceDir = path.join(documentDir, RESOURCE_DIR_NAME);
544
+
545
+ for (const link of parseLocalLinkTargets(content, documentDir)) {
546
+ if (!shouldValidateResourceLink(link.rawTarget)) {
547
+ continue;
548
+ }
549
+
550
+ if (!fs.existsSync(link.resolvedPath)) {
551
+ errors.push(`Broken asset link in ${toDisplayPath(documentPath)}: ${link.rawTarget}`);
552
+ continue;
553
+ }
554
+
555
+ if (!isPathInside(resourceDir, link.resolvedPath)) {
556
+ errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget}`);
557
+ }
558
+ }
559
+ }
560
+
359
561
  function parseLocalLinks(markdown, baseDir) {
562
+ return parseLocalLinkTargets(markdown, baseDir).map((link) => link.resolvedPath);
563
+ }
564
+
565
+ function parseLocalLinkTargets(markdown, baseDir) {
360
566
  const links = [];
361
- const linkRe = /\[[^\]]+\]\(([^)]+)\)/g;
567
+ const linkRe = /!?\[[^\]]+\]\(([^)]+)\)/g;
362
568
  let match = linkRe.exec(markdown);
363
569
  while (match) {
364
570
  const rawTarget = match[1].trim();
365
571
  if (isLocalLink(rawTarget)) {
366
572
  const targetWithoutAnchor = rawTarget.split('#')[0];
367
- links.push(path.resolve(baseDir, targetWithoutAnchor));
573
+ links.push({
574
+ rawTarget,
575
+ resolvedPath: path.resolve(baseDir, targetWithoutAnchor)
576
+ });
368
577
  }
369
578
  match = linkRe.exec(markdown);
370
579
  }
@@ -384,6 +593,17 @@ function isCanonicalTypeIndex(filePath, xdrsRoot) {
384
593
  return relative.length === 3 && TYPE_NAMES.has(relative[1]) && relative[2] === 'index.md';
385
594
  }
386
595
 
596
+ function shouldValidateResourceLink(rawTarget) {
597
+ const targetWithoutAnchor = rawTarget.split('#')[0];
598
+ const normalizedTarget = targetWithoutAnchor.replace(/\\/g, '/');
599
+ const extension = path.extname(targetWithoutAnchor).toLowerCase();
600
+
601
+ return normalizedTarget === RESOURCE_DIR_NAME
602
+ || normalizedTarget.startsWith(`${RESOURCE_DIR_NAME}/`)
603
+ || normalizedTarget.includes(`/${RESOURCE_DIR_NAME}/`)
604
+ || IMAGE_EXTENSIONS.has(extension);
605
+ }
606
+
387
607
  function extractFrontmatterName(content) {
388
608
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
389
609
  if (!frontmatterMatch) {
@@ -394,6 +614,82 @@ function extractFrontmatterName(content) {
394
614
  return nameMatch ? nameMatch[1].trim() : null;
395
615
  }
396
616
 
617
+ function findIgnoredMarkdownLines(lines) {
618
+ const ignored = [];
619
+ let inCodeFence = false;
620
+
621
+ for (let index = 0; index < lines.length; index += 1) {
622
+ const trimmed = lines[index].trim();
623
+ if (/^```/.test(trimmed)) {
624
+ ignored[index] = true;
625
+ inCodeFence = !inCodeFence;
626
+ continue;
627
+ }
628
+
629
+ ignored[index] = inCodeFence;
630
+ }
631
+
632
+ return ignored;
633
+ }
634
+
635
+ function findHeadingLine(lines, ignoredLines, headingText) {
636
+ for (let index = 0; index < lines.length; index += 1) {
637
+ if (ignoredLines[index]) {
638
+ continue;
639
+ }
640
+ if (lines[index].trim() === headingText) {
641
+ return index;
642
+ }
643
+ }
644
+ return -1;
645
+ }
646
+
647
+ function findHeadingLines(lines, ignoredLines) {
648
+ const result = [];
649
+ for (let index = 0; index < lines.length; index += 1) {
650
+ if (ignoredLines[index]) {
651
+ continue;
652
+ }
653
+ if (/^##\s+/.test(lines[index].trim())) {
654
+ result.push(index);
655
+ }
656
+ }
657
+ return result;
658
+ }
659
+
660
+ function findFieldLines(lines, ignoredLines, prefix) {
661
+ const result = [];
662
+ for (let index = 0; index < lines.length; index += 1) {
663
+ if (ignoredLines[index]) {
664
+ continue;
665
+ }
666
+ if (lines[index].trim().startsWith(prefix)) {
667
+ result.push(index);
668
+ }
669
+ }
670
+ return result;
671
+ }
672
+
673
+ function isLineInsideRange(lineIndex, range) {
674
+ return lineIndex >= range.start && lineIndex <= range.end;
675
+ }
676
+
677
+ function countWords(value) {
678
+ return value.split(/\s+/).filter(Boolean).length;
679
+ }
680
+
681
+ function isIsoDate(value) {
682
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
683
+ return false;
684
+ }
685
+
686
+ const [year, month, day] = value.split('-').map(Number);
687
+ const date = new Date(Date.UTC(year, month - 1, day));
688
+ return date.getUTCFullYear() === year
689
+ && date.getUTCMonth() === month - 1
690
+ && date.getUTCDate() === day;
691
+ }
692
+
397
693
  function firstNonEmptyLine(content) {
398
694
  const lines = content.split(/\r?\n/);
399
695
  for (const line of lines) {
@@ -452,6 +748,11 @@ function normalizePath(filePath) {
452
748
  return path.normalize(filePath);
453
749
  }
454
750
 
751
+ function isPathInside(parentPath, childPath) {
752
+ const relative = path.relative(parentPath, childPath);
753
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
754
+ }
755
+
455
756
  module.exports = {
456
757
  runLintCli,
457
758
  lintWorkspace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
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",
@@ -14,10 +14,10 @@
14
14
  ],
15
15
  "license": "MIT",
16
16
  "files": [
17
- ".xdrs/**",
17
+ ".xdrs/_core/**",
18
+ ".xdrs/index.md",
18
19
  "package.json",
19
20
  "AGENTS.md",
20
- "AGENTS.local.md",
21
21
  "bin/filedist.js",
22
22
  "lib/**/*.js"
23
23
  ],
@@ -47,8 +47,7 @@
47
47
  {
48
48
  "selector": {
49
49
  "files": [
50
- ".xdrs/index.md",
51
- "AGENTS.local.md"
50
+ ".xdrs/index.md"
52
51
  ]
53
52
  },
54
53
  "output": {
package/AGENTS.local.md DELETED
@@ -1,3 +0,0 @@
1
- # AGENTS.local.md
2
-
3
- No repository specific instructions available. Focus on XDRS instructions only.