xdrs-core 0.15.4 → 0.16.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.
|
@@ -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.
|
|
@@ -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:
|
|
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.
|
package/.xdrs/index.md
CHANGED
|
@@ -19,4 +19,4 @@ Decisions about how XDRs work
|
|
|
19
19
|
|
|
20
20
|
### _local (reserved)
|
|
21
21
|
|
|
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.
|
|
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. 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