xdrs-core 0.15.4 → 0.17.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.
@@ -13,6 +13,7 @@ Foundational standards, principles, and guidelines.
13
13
  - [_core-adr-005](principles/005-semantic-versioning-for-xdr-packages.md) - **Semantic versioning for XDR packages** — How to version XDR packages to communicate upgrade impact
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
+ - [_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
16
17
 
17
18
  ## Skills
18
19
 
@@ -57,7 +57,7 @@ Collectively, these are referred to as XDRs.
57
57
  - **Scopes:**
58
58
  - Short name that defines a group or a package of xdrs
59
59
  - examples: `business-x`, `business-y`, `team-43`, `_core`
60
- - `_local` is a reserved scope for XDRs created locally to a specific project or repository. XDRs in `_local` must not be shared with or propagated to other contexts. This scope must always be placed in the lowest position in `.xdrs/index.md` so that its decisions override or extend any decisions from all higher-positioned scopes. Shared `.xdrs/index.md` files MUST NOT link `_local` canonical type indexes because `_local` stays workspace-local and is not distributed with shared packages. Readers, tools, and agents SHOULD still try to discover existing workspace-local `_local` canonical indexes by default, even when the shared root index does not link them.
60
+ - `_local` is a reserved scope for XDRs created locally to a specific project or repository. XDRs in `_local` must not be shared with or propagated to other contexts. This scope must always be placed in the lowest position in `.xdrs/index.md` so that its decisions override or extend any decisions from all higher-positioned scopes. Shared `.xdrs/index.md` files MUST NOT link `_local` canonical type indexes because `_local` stays workspace-local and is not distributed with shared packages. Readers, tools, and agents SHOULD still try to discover existing workspace-local `_local` canonical indexes by default, even when the shared root index does not link them. Documents in non-`_local` scopes MUST NEVER link to any document inside the `_local` scope, because `_local` is workspace-only and such links would break in any consumer workspace. Documents inside `_local` MAY link to other documents inside `_local`.
61
61
  - **Types:** `adrs`, `bdrs`, `edrs`
62
62
  - there can exist sufixes to the standard scope names (e.g: `business-x-mobileapp`, `business-y-servicedesk`)
63
63
  - **Subjects:** MUST be one of the following depending on the type of the XDR. Use the subject to indicate the main concern of the decision.
@@ -25,7 +25,7 @@ XDR documents are the authoritative policy for their scope, type, and subject. T
25
25
  |---|---|---|
26
26
  | `name` | Yes | 1-64 characters. Lowercase letters, numbers, hyphens, and leading underscores only. Must not end with a hyphen. Must not contain consecutive hyphens. Must match the document identifier from the heading: `[scope]-[type]-[number]-[short-title]`. |
27
27
  | `description` | Yes | 1-1024 characters. Describes what this decision is about and when to use it. Should include keywords that help agents identify when to apply it. |
28
- | `applied-to` | No | Short description of contexts this decision is applicable to. Keep it under 40 words. If omitted, the decision applies to all logically applicable elements. Examples: `Only frontend code`, `JavaScript projects`. |
28
+ | `applied-to` | No | Short description of contexts this decision is applicable to. Keep it under 40 words. If omitted, the decision applies to all logically applicable elements. ONLY use this section if the usage is very specific to a specific case. Examples: `Only frontend code`, `JavaScript projects`. |
29
29
  | `valid-from` | No | ISO date (`YYYY-MM-DD`) indicating from when this decision must be enforced. Before this date it should be used everywhere possible, but compliance is not enforced during reviews until after this date. |
30
30
  | `license` | No | SPDX license expression (e.g. `MIT`, `Apache-2.0`, `CC-BY-4.0`). Indicates the license under which the document content is shared. If omitted, the license is governed by the repository or package defaults. |
31
31
  | `metadata` | No | Arbitrary key-value map for additional properties not defined by this spec. |
@@ -60,6 +60,7 @@ XDR documents are the authoritative policy for their scope, type, and subject. T
60
60
  - Decisions MUST be concise and reference other XDRs to avoid duplication.
61
61
  - The `### Implementation Details` section SHOULD state relevant boundaries or exceptions and what a reader should do or avoid in common cases. Use the frontmatter fields `applied-to` and `valid-from` as the first-pass filter for applicability, then keep nuanced boundaries in the decision text.
62
62
  - Use concise rules, examples, `Allowed` / `Disallowed` lists or checklists with required items to help the reader apply the decision correctly. Keep them short and decision-specific.
63
+ - When the decision defines strong policies or rules that should be stated explicitly as stable rule blocks, or when other documents, skills, or agents need to cite those rules individually by identifier, the XDR MUST follow the extension [_core-adr-008 - XDR standards - structured](008-xdr-standards-structured.md) instead of using plain bullet lists for those rules.
63
64
  - Conflict handling applies to XDR documents:
64
65
  - For cross-scope overrides, document the decision conflict in the XDR `## Conflicts` section of the XDR that overrides another scope.
65
66
  - **Within-scope conflicts:** XDRs within the same type+scope must not conflict. If two XDRs appear to conflict, one should be updated, removed, or the conflict resolved through a new XDR.
@@ -107,6 +108,14 @@ Question: In the end, state explicitly the question that needs to be answered. E
107
108
 
108
109
  [Optional section with implementation specifics, applicability boundaries, rules, concise examples, or do/don't guidance. This is the answer to the question in the "Context and Problem Statement". (<1300 words)]
109
110
 
111
+ ## Considered Options
112
+ [this section is present ONLY if there was more than one option to choose from]
113
+
114
+ * (CHOSEN) **Option 2** - Brief description of option 2
115
+ * Reason: Brief description of why this option was accepted, containing the strengths, strategical motivations and it's differential over the other options.
116
+ * (REJECTED) **Option 1** - Brief description of option 1
117
+ * Reason: Brief description why this was rejected with important aspects to be re-checked in the case we want to change this decision
118
+
110
119
  [Related research, if any]
111
120
  - [Research document title](researches/001-example.md) - Brief description of what it informed
112
121
 
@@ -144,3 +153,4 @@ Question: In the end, state explicitly the question that needs to be answered. E
144
153
  - [_core-adr-003 - Skill standards](003-skill-standards.md)
145
154
  - [_core-adr-004 - Article standards](004-article-standards.md)
146
155
  - [_core-adr-006 - Research standards](006-research-standards.md)
156
+ - [_core-adr-008 - XDR standards - structured](008-xdr-standards-structured.md) - Extension for XDRs that expose individually referenceable rules
@@ -121,7 +121,7 @@ Prefer tables, bullets, or ASCII art for simple comparisons. Use external figure
121
121
 
122
122
  ## Considered Options
123
123
 
124
- - Related research: [001-research-and-decision-lifecycle](../../../_local/adrs/principles/researches/001-research-and-decision-lifecycle.md)
124
+ - Related research: `001-research-and-decision-lifecycle` (workspace-local research)
125
125
 
126
126
  * (REJECTED) **Inline long-form analysis inside the XDR** - Put all research and decision text in one file.
127
127
  * Reason: Makes XDRs too long, mixes evidence with the adopted rule set, and hurts fast retrieval by humans and AI agents.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: _core-adr-008-xdr-standards-structured
3
+ description: Extends xdr-standards with a numbered rule format for XDR documents that define strong policies or rules, or need individually referenceable items. Use when an XDR must expose explicit rule blocks that other documents or skills may cite by identifier.
4
+ ---
5
+
6
+ # _core-adr-008: XDR standards - structured
7
+
8
+ ## Context and Problem Statement
9
+
10
+ Some XDR documents define multiple strong policies or rules that must be stated explicitly so they can be applied consistently. In other cases, documents, skills, or agents need to refer to one specific rule without copying its content. Without a standard format, prose references like "see the third bullet in Implementation Details" are fragile and ambiguous.
11
+
12
+ Question: How should an XDR document expose strong or individually referenceable rules or policies so they stay explicit, stable, and easy to cite?
13
+
14
+ ## Decision Outcome
15
+
16
+ **Numbered rule blocks inside Implementation Details with a canonical citation syntax**
17
+
18
+ When an XDR document defines strong rules or policies that should be stated explicitly, or when documents and skills need to cite individual rules precisely, each rule must be placed inside `### Implementation Details` as a numbered heading block. Referencing documents and skills must cite rules using the canonical dot-notation identifier.
19
+
20
+ ### Implementation Details
21
+
22
+ Use this format when the decision defines strong rules or policies that must be stated explicitly as stable rule blocks, or when there is a clear need for external documents, skills, or agents to reference specific items inside an XDR without duplicating the full policy text. Standard XDRs that do not define strong rule sets and do not need item-level citation should follow `_core-adr-002-xdr-standards` without adding numbered rule headings.
23
+
24
+ #### Rule block format
25
+
26
+ Each numbered rule must be written as:
27
+
28
+ ```markdown
29
+ #### [NN]-[short-descriptive-title-in-kebab-case]
30
+ [Body using mandatory or advisory language as defined in _core-adr-001. State the requirement and the situations in which it must or should be followed. Under 500 words.]
31
+ ```
32
+
33
+ Where `NN` is a two-digit zero-padded sequence number (e.g. `01`, `02`, `12`). Numbers must be unique within the document and must never be reused after a rule is removed.
34
+
35
+ Rule bodies must use the mandatory or advisory language terms defined in `_core-adr-001`:
36
+
37
+ - Mandatory: "must", "always", "never", "required", "mandatory"
38
+ - Advisory: "should", "recommended", "advised", "preferably", "possibly", "optionally"
39
+
40
+ #### Citation syntax
41
+
42
+ When another document or skill cites a specific rule, it must use the following dot-notation:
43
+
44
+ ```
45
+ [xdr-name].[NN-short-descriptive-title-in-kebab-case]
46
+ ```
47
+
48
+ Examples:
49
+
50
+ - `_core-adr-008-xdr-standards-structured.[01-use-numbered-rules-for-strong-or-referenceable-policies]`
51
+ - `_local-bdr-003-data-retention-policy.[02-purge-schedule-for-pii]`
52
+
53
+ The `xdr-name` must match the `name` field in the frontmatter of the source document exactly. The rule identifier after the dot must match the heading text exactly, including the two-digit prefix and kebab-case title.
54
+
55
+ #### 01-use-numbered-rules-for-strong-or-referenceable-policies
56
+
57
+ Numbered rule blocks must be added to an XDR when the decision defines strong rules or policies that must be stated explicitly as stable items, or when there is a clear need for other documents, skills, or agents to cite individual rules by identifier. Adding numbered rules only for cosmetic organization is not recommended. Standard XDR documents that do not define strong rule sets and are not expected to be cited at the rule level should follow `_core-adr-002-xdr-standards` without this structured format.
58
+
59
+ #### 02-rule-numbering-must-be-stable
60
+
61
+ Rule numbers must never be reused within the same document. When a rule is removed, its number becomes permanently retired for that document. Gaps in the sequence are expected and must not be filled by renumbering remaining rules, as existing citations depend on number stability.
62
+
63
+ #### 03-rule-body-must-use-normative-language
64
+
65
+ Every rule body must contain at least one mandatory or advisory language term as defined in `_core-adr-001`. Rule bodies without normative language must not be published, as they fail to communicate whether compliance is required or recommended.
66
+
67
+ #### 04-citations-must-use-exact-identifiers
68
+
69
+ Documents and skills that cite a rule must use the exact dot-notation form: `[xdr-name].[NN-short-descriptive-title-in-kebab-case]`. Prose paraphrases such as "see rule 3" or "the third rule in that XDR" must not be used as citations, because they are ambiguous and break when rules are reordered or reworded.
70
+
71
+ ## Considered Options
72
+
73
+ * (REJECTED) **Free-form prose rules with section anchors** — Use markdown heading anchors as citation targets.
74
+ * Reason: Anchors are fragile across editors, rendering tools, and refactors. They do not enforce a stable numbering contract and break silently when headings are reworded.
75
+ * (CHOSEN) **Numbered rule blocks inside Implementation Details** — Prefix each rule heading with a two-digit sequence number and use dot-notation for citations.
76
+ * Reason: Minimal addition to the existing XDR template, stable identifiers independent of heading text, and fully compatible with `_core-adr-002-xdr-standards`.
77
+
78
+ ## References
79
+
80
+ - [_core-adr-001 - XDRs core](001-xdrs-core.md)
81
+ - [_core-adr-002 - XDR standards](002-xdr-standards.md)
@@ -76,6 +76,32 @@ Choose a title that clearly states the question this XDR answers, not the answer
76
76
 
77
77
  Use the mandatory template from `002-xdr-standards`:
78
78
 
79
+ **Check if the decision requires a structured set of rules:**
80
+ If the decision defines strong rules or policies that must be stated explicitly, or if other documents, skills, or agents have a clear need to reference individual rules, you MUST apply the structured rule format from `_core-adr-008-xdr-standards-structured`. This means:
81
+ - Place each rule as a numbered heading block inside `### Implementation Details`.
82
+ - Use the format:
83
+ #### [NN]-[short-descriptive-title-in-kebab-case]
84
+ [Rule body with mandatory/advisory language.]
85
+ - Ensure each rule is uniquely numbered (two digits, zero-padded) and never reuse numbers if a rule is removed.
86
+ - Other documents must cite rules using the canonical dot-notation: `[xdr-name].[NN-short-descriptive-title-in-kebab-case]`.
87
+
88
+ **Example of a structured set of rules:**
89
+
90
+ ```markdown
91
+ ### Implementation Details
92
+
93
+ #### 01-data-must-be-encrypted-at-rest
94
+ All user data must be encrypted at rest using AES-256 or stronger algorithms.
95
+
96
+ #### 02-access-logs-must-be-retained
97
+ Access logs must be retained for at least 90 days and reviewed monthly for suspicious activity.
98
+
99
+ #### 03-external-integrations-should-be-reviewed
100
+ All external integrations should be reviewed annually for compliance with current security standards.
101
+ ```
102
+
103
+ Refer to `_core-adr-008-xdr-standards-structured` for full requirements and citation syntax.
104
+
79
105
  ```
80
106
  ---
81
107
  name: [scope]-[type]-[number]-[short-title]
@@ -0,0 +1,9 @@
1
+ # _core BDRs Index
2
+
3
+ Business and operational decisions about how the XDR framework operates, including policy and process rules for framework users. Owned by the platform team. Propose changes via pull request.
4
+
5
+ ## Principles
6
+
7
+ Foundational business principles and policy decisions that guide all framework users.
8
+
9
+ - [_core-bdr-001](principles/001-xdr-decisions-and-skills-usage.md) - **XDR decisions and skills usage** — How agents and humans must use XDR decisions and skills, separating policy authority from execution guidance
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: _core-bdr-001-xdr-decisions-and-skills-usage
3
+ description: Defines how agents and humans must use XDR decisions and skills, separating policy authority from execution guidance across single-agent and multi-agent workflows.
4
+ ---
5
+
6
+ # _core-bdr-001: XDR decisions and skills usage
7
+
8
+ ## Context and Problem Statement
9
+
10
+ Repositories using the XDR framework contain both decision records (BDRs, ADRs, EDRs) and skills. In multi-agent or multi-role workflows, each actor needs to know which artifact carries policy authority and which carries execution guidance.
11
+
12
+ Question: How must agents and humans use XDR decisions and skills so policy, execution guidance, and role boundaries stay clear?
13
+
14
+ ## Decision Outcome
15
+
16
+ **BDRs define compliance policy; skills operationalize it**
17
+
18
+ XDR decisions are the authoritative source of mandatory policy. Skills are execution artifacts that operationalize those decisions and must never substitute them as policy authority.
19
+
20
+ ### Implementation Details
21
+
22
+ - Before treating any XDR as a current requirement, evaluate applicability in order: `valid-from`, `applied-to`, then the decision text.
23
+ - The set of policies that an agent or human must comply with for a given operational context must be declared in BDRs.
24
+ - If a policy set becomes too large or too mixed in purpose to review clearly in one record, it must be split into multiple focused BDRs.
25
+ - Specific work instructions that operationalize BDR policies must be structured as skills when the procedure is detailed enough to benefit from a dedicated operational document.
26
+ - Every skill that operationalizes or verifies BDR policies must link to the BDRs it implements or checks.
27
+ - If a skill conflicts with an applicable XDR, the XDR prevails. The human or agent must stop relying on the conflicting skill behavior and report the inconsistency.
28
+ - If an applicable BDR exists but no supporting skill exists yet, the human or agent may proceed by following the BDR directly when the decision is actionable, and must report the missing skill as an operational gap.
29
+ - In multi-agent graphs or staged workflows, every participating agent must remain bounded by the same applicable BDR policies even when agents have different local objectives, prompts, or skills.
30
+ - Each agent or human role involved in a workflow must make explicit which applicable BDRs govern its work and which skills it is following.
31
+ - Different documents may be consumed by different actors. For example: one agent uses skills for data acquisition, another for plan execution, and a reviewer reads BDRs directly to validate outcomes.
32
+
33
+ Allowed:
34
+ - using multiple skills to operationalize one BDR in different phases of a workflow;
35
+ - using one skill to operationalize multiple related BDRs when the skill links them clearly;
36
+ - having human-only, agent-only, or mixed human-agent execution for the same skill.
37
+
38
+ Disallowed:
39
+ - treating a skill as policy authority by itself;
40
+ - inventing mandatory policy rules only inside prompts, plans, or skills;
41
+ - allowing one agent in a workflow to bypass an applicable BDR because another agent checked a different subset of rules.
42
+
43
+ ## Considered Options
44
+
45
+ - (REJECTED) **Keep policy and execution guidance mixed inside operational BDRs** — Makes decisions harder to reuse, review, and assign across humans and multiple agent roles.
46
+ - (CHOSEN) **Separate mandatory policy in BDRs from executable guidance in skills** — Preserves a clear source of truth while allowing different humans and agents to execute specialized procedures.
47
+
48
+ ## References
49
+
50
+ - [_core-adr-001](../../adrs/principles/001-xdrs-core.md)
51
+ - [_core-adr-002](../../adrs/principles/002-xdr-standards.md)
52
+ - [_core-adr-003](../../adrs/principles/003-skill-standards.md)
package/.xdrs/index.md CHANGED
@@ -10,6 +10,7 @@ XDRs in scopes listed last override the ones listed first
10
10
 
11
11
  Decisions about how XDRs work
12
12
  [View _core ADRs Index](_core/adrs/index.md)
13
+ [View _core BDRs Index](_core/bdrs/index.md)
13
14
 
14
15
  ---
15
16
 
@@ -19,4 +20,4 @@ Decisions about how XDRs work
19
20
 
20
21
  ### _local (reserved)
21
22
 
22
- Project-local XDRs that must not be shared with other contexts. Always keep this scope last so its decisions override or extend all scopes listed above. Keep `_local` canonical indexes in the workspace tree only; do not link them from this shared index. Readers and tools should still try to discover existing `_local` indexes in the current workspace by default.
23
+ Project-local XDRs that must not be shared with other contexts. Always keep this scope last so its decisions override or extend all scopes listed above. Keep `_local` canonical indexes in the workspace tree only; do not link them from this shared index. Readers and tools should still try to discover existing `_local` indexes in the current workspace by default. Documents in non-`_local` scopes must never link into `_local`; only `_local` documents may link to other `_local` documents.
package/lib/lint.js CHANGED
@@ -269,7 +269,7 @@ function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors
269
269
  const expectedName = extractExpectedXdrNameFromHeading(firstLine)
270
270
  || `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
271
271
  lintXdrFrontmatter(content, expectedName, filePath, errors);
272
- lintDocumentLinks(filePath, errors);
272
+ lintDocumentLinks(filePath, xdrsRoot, scopeName, errors);
273
273
  }
274
274
 
275
275
  function extractExpectedXdrNameFromHeading(headingLine) {
@@ -384,7 +384,7 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
384
384
  }
385
385
  }
386
386
 
387
- lintDocumentLinks(skillFilePath, errors);
387
+ lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors);
388
388
  }
389
389
 
390
390
  return artifacts;
@@ -437,7 +437,7 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
437
437
  errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
438
438
  }
439
439
 
440
- lintDocumentLinks(entryPath, errors);
440
+ lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
441
441
  }
442
442
 
443
443
  return artifacts;
@@ -490,7 +490,7 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
490
490
  errors.push(`Research title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
491
491
  }
492
492
 
493
- lintDocumentLinks(entryPath, errors);
493
+ lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
494
494
  }
495
495
 
496
496
  return artifacts;
@@ -544,7 +544,7 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
544
544
  }
545
545
 
546
546
  lintPlanExpectedEndDate(content, entryPath, errors);
547
- lintDocumentLinks(entryPath, errors);
547
+ lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
548
548
  }
549
549
 
550
550
  return artifacts;
@@ -574,6 +574,8 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
574
574
  const content = fs.readFileSync(indexPath, 'utf8');
575
575
  const localLinks = parseLocalLinks(content, path.dirname(indexPath));
576
576
  const linkedSet = new Set();
577
+ const scopeName = path.relative(xdrsRoot, indexPath).split(path.sep)[0];
578
+ const localScopePath = normalizePath(path.join(xdrsRoot, '_local'));
577
579
 
578
580
  for (const linkPath of localLinks) {
579
581
  if (!fs.existsSync(linkPath)) {
@@ -581,6 +583,10 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
581
583
  continue;
582
584
  }
583
585
 
586
+ if (scopeName !== '_local' && (isPathInside(localScopePath, linkPath) || normalizePath(linkPath) === localScopePath)) {
587
+ errors.push(`Non-_local document must not link into _local scope: ${displayPath(indexPath, linkPath)}`);
588
+ }
589
+
584
590
  linkedSet.add(normalizePath(linkPath));
585
591
  }
586
592
 
@@ -591,11 +597,12 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
591
597
  }
592
598
  }
593
599
 
594
- function lintDocumentLinks(documentPath, errors) {
600
+ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors) {
595
601
  const lines = fs.readFileSync(documentPath, 'utf8').split(/\r?\n/);
596
602
  const ignoredLines = findIgnoredMarkdownLines(lines);
597
603
  const documentDir = path.dirname(documentPath);
598
604
  const resourceDir = path.join(documentDir, RESOURCE_DIR_NAME);
605
+ const localScopePath = normalizePath(path.join(xdrsRoot, '_local'));
599
606
 
600
607
  for (let index = 0; index < lines.length; index += 1) {
601
608
  if (ignoredLines[index]) {
@@ -614,6 +621,10 @@ function lintDocumentLinks(documentPath, errors) {
614
621
  continue;
615
622
  }
616
623
 
624
+ if (scopeName !== '_local' && (isPathInside(localScopePath, link.resolvedPath) || normalizePath(link.resolvedPath) === localScopePath)) {
625
+ errors.push(`Non-_local document must not link into _local scope in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget}`);
626
+ }
627
+
617
628
  if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)) {
618
629
  errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget}`);
619
630
  }
package/lib/lint.test.js CHANGED
@@ -108,6 +108,114 @@ test('derives expected frontmatter name from the markdown heading title', () =>
108
108
  expect(result.errors.join('\n')).not.toContain('XDR frontmatter name must be');
109
109
  });
110
110
 
111
+ test('reports non-_local XDR linking to _local scope document', () => {
112
+ const workspaceRoot = createWorkspace('non-local-links-to-local-xdr', {
113
+ '.xdrs/index.md': rootIndex(),
114
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
115
+ '- [001-main](principles/001-main.md) - Main decision'
116
+ ]),
117
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Local decision.'),
118
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
119
+ '- [001-team](principles/001-team.md) - Team decision'
120
+ ]),
121
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument(
122
+ 'See [local doc](../../../_local/adrs/principles/001-main.md).'
123
+ ),
124
+ });
125
+
126
+ const result = lintWorkspace(workspaceRoot, { ignoreReadOnly: false });
127
+
128
+ expect(result.errors.join('\n')).toContain('Non-_local document must not link into _local scope');
129
+ });
130
+
131
+ test('allows _local XDR linking to another _local scope document', () => {
132
+ const workspaceRoot = createWorkspace('local-links-to-local', {
133
+ '.xdrs/index.md': rootIndex(),
134
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
135
+ '- [001-main](principles/001-main.md) - Main decision',
136
+ '- [002-second](principles/002-second.md) - Second decision'
137
+ ]),
138
+ '.xdrs/_local/adrs/principles/001-main.md': [
139
+ '---',
140
+ 'name: _local-adr-001-main',
141
+ 'description: Test XDR document',
142
+ '---',
143
+ '',
144
+ '# _local-adr-001: Main decision',
145
+ '',
146
+ '## Context and Problem Statement',
147
+ '',
148
+ 'See [second](002-second.md).',
149
+ ''
150
+ ].join('\n'),
151
+ '.xdrs/_local/adrs/principles/002-second.md': [
152
+ '---',
153
+ 'name: _local-adr-002-second',
154
+ 'description: Second test XDR document',
155
+ '---',
156
+ '',
157
+ '# _local-adr-002: Second decision',
158
+ '',
159
+ '## Context and Problem Statement',
160
+ '',
161
+ 'Second body.',
162
+ ''
163
+ ].join('\n'),
164
+ });
165
+
166
+ const result = lintWorkspace(workspaceRoot, { ignoreReadOnly: false });
167
+
168
+ expect(result.errors.join('\n')).not.toContain('Non-_local document must not link into _local scope');
169
+ expect(result.errors.join('\n')).not.toContain('Broken local link');
170
+ });
171
+
172
+ test('reports non-_local canonical index linking to _local scope document', () => {
173
+ const workspaceRoot = createWorkspace('non-local-type-index-links-to-local', {
174
+ '.xdrs/index.md': rootIndex(),
175
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
176
+ '- [001-main](principles/001-main.md) - Main decision'
177
+ ]),
178
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Local decision.'),
179
+ '.xdrs/myteam/adrs/index.md': teamAdrIndex([
180
+ '- [_local 001](../../_local/adrs/principles/001-main.md) - Cross-scope link'
181
+ ]),
182
+ '.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
183
+ });
184
+
185
+ const result = lintWorkspace(workspaceRoot, { ignoreReadOnly: false });
186
+
187
+ expect(result.errors.join('\n')).toContain('Non-_local document must not link into _local scope');
188
+ });
189
+
190
+ function teamAdrIndex(entries) {
191
+ return [
192
+ '# myteam ADR Index',
193
+ '',
194
+ 'Team ADRs for tests.',
195
+ '',
196
+ '## principles',
197
+ '',
198
+ ...entries,
199
+ ''
200
+ ].join('\n');
201
+ }
202
+
203
+ function teamXdrDocument(body) {
204
+ return [
205
+ '---',
206
+ 'name: myteam-adr-001-team',
207
+ 'description: Team test XDR document',
208
+ '---',
209
+ '',
210
+ '# myteam-adr-001: Team decision',
211
+ '',
212
+ '## Context and Problem Statement',
213
+ '',
214
+ body,
215
+ ''
216
+ ].join('\n');
217
+ }
218
+
111
219
  function createWorkspace(name, files) {
112
220
  const workspaceRoot = path.join(tmpRoot, name);
113
221
  fs.mkdirSync(workspaceRoot, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.15.4",
3
+ "version": "0.17.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",