xdrs-core 0.19.0 → 0.20.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/.xdrs/_core/adrs/principles/001-xdrs-core.md +11 -3
- package/.xdrs/_core/adrs/principles/skills/002-write-xdr/SKILL.md +3 -0
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +3 -0
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +3 -0
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +3 -0
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +9 -0
- package/.xdrs/_core/index.md +56 -0
- package/.xdrs/index.md +1 -2
- package/README.md +1 -1
- package/lib/lint.js +156 -54
- package/lib/lint.test.js +232 -21
- package/package.json +1 -1
|
@@ -36,7 +36,7 @@ Collectively, these are referred to as XDRs.
|
|
|
36
36
|
- ALWAYS use the following folder structure for XDR documents:
|
|
37
37
|
`.xdrs/[scope]/[type]/[subject]/[number]-[short-title].md`
|
|
38
38
|
- ALWAYS ignore symlinks paths. NEVER create or update documents inside symlinked folders.
|
|
39
|
-
- **
|
|
39
|
+
- **Files listed in `.filedist` are external XDRs.** A file whose path appears in the workspace root `.filedist` file was distributed from an external source repository. It must NEVER be modified locally. To change it, submit the change to the source repository and re-extract the updated package. The `.filedist` format is one entry per line: `<relative-path>|<package>|<version>`. A scope is considered external when any of its files appear in `.filedist`, and tools (such as `xdrs-core lint`) will skip external scopes by default.
|
|
40
40
|
- Optional supporting artifacts under the same subject:
|
|
41
41
|
- `.xdrs/[scope]/[type]/[subject]/researches/[number]-[short-title].md`
|
|
42
42
|
- `.xdrs/[scope]/[type]/[subject]/skills/[number]-[skill-name]/SKILL.md`
|
|
@@ -109,14 +109,22 @@ Collectively, these are referred to as XDRs.
|
|
|
109
109
|
- Never use emojis
|
|
110
110
|
- **Links:** Links that reference a parent folder MUST use absolute paths from the repository root with a leading `/` (e.g., `/.xdrs/_core/adrs/principles/001-xdrs-core.md`). Sibling files and child folder references SHOULD use relative paths (e.g., `002-other-doc.md`, `.assets/image.png`, `subdir/file.md`). Never use relative paths that traverse up the directory tree (e.g., `../../.assets/test.png`, `../other.md`); they break when files are moved and are harder to read.
|
|
111
111
|
- **Indexes**
|
|
112
|
-
-
|
|
112
|
+
- Every document in the collection (XDRs, skills, articles, research, and plans) must be reachable through the index chain: root index → scope index → type index → document. A document that exists on disk but is not linked from its canonical type index is considered an orphan and must be added to the index or removed.
|
|
113
|
+
- Keep a canonical type index with all documents of a certain type+scope in `.xdrs/[scope]/[type]/index.md`. The type index must link to every XDR, skill, article, research, and plan under that type+scope.
|
|
113
114
|
- Canonical index requirements:
|
|
114
115
|
- Organize XDR documents by subject for easier navigation
|
|
115
116
|
- Add a short description of what this scope is about (responsibilities, general worries, teams involved, link to discussion process, etc)
|
|
116
117
|
- Add a list of other scope indexes that this scope might be related to (only add scopes that might be overridden). E.g: "business-x-mobileapp" scope could refer to "business-x" and "sensitive-data" scopes in its index list. XDRs in scopes listed last override XDRs in scopes listed first when addressing the same topic.
|
|
117
118
|
- Each XDR element entry in the index MUST include a short description of its content, preferably with an imperative statement or the question it answers, when possible (<15 words). Example: "Use this while planning a new feature", "What communication tone we use with our customers?", "PNPM vs Yarn comparison study"
|
|
118
|
-
- Outside the scopes, keep
|
|
119
|
+
- Outside the scopes, keep a root index in `.xdrs/index.md` that links to each scope index (`.xdrs/[scope]/index.md`). Add the text "XDRs in scopes listed last override the ones listed first". The root index must not link directly to type indexes; readers navigate from the scope index to the type indexes. Use the link text pattern `View scope [scope_name]` for each scope link (e.g. `[View scope myteam] linking to (myteam/index.md)`).
|
|
119
120
|
- Always verify if indexes are up to date after making changes
|
|
121
|
+
- **Scope index**
|
|
122
|
+
- Each scope folder must maintain an `index.md` file at `.xdrs/[scope]/index.md`.
|
|
123
|
+
- The scope index is a short article (under 1000 words) that provides an overview of all XDR contents within that scope. Follow article standards (`_core-adr-004`) when writing this file.
|
|
124
|
+
- The audience for the scope index are engineers, architects, or business analysts who want to check if the scope's contents are useful for them before diving into the specific documents. Write a guided summary that helps them decide whether to explore further.
|
|
125
|
+
- Focus on the most relevant content of the scope: what decisions are covered, what problems they address, and how the scope relates to other scopes.
|
|
126
|
+
- At the end of the scope index, always add links to the canonical type indexes (`adrs/index.md`, `bdrs/index.md`, `edrs/index.md`) that exist within the scope.
|
|
127
|
+
- Whenever the contents of a scope change (new XDRs, skills, articles, research, or plans are added, updated, or removed), evaluate whether the scope index should be updated to reflect the newer contents.
|
|
120
128
|
|
|
121
129
|
**Folder structure examples:**
|
|
122
130
|
- `.xdrs/business-x/edrs/devops/003-required-development-workflow.md`
|
|
@@ -35,6 +35,7 @@ Consult `001-xdrs-core` while making each choice in this phase. The summaries be
|
|
|
35
35
|
- **EDR**: specific tool/library, coding practice, testing strategy, project structure
|
|
36
36
|
|
|
37
37
|
**Scope** — use `_local` unless the user explicitly names another scope.
|
|
38
|
+
- If the user names a scope other than `_local`, check the workspace root `.filedist` file. If any file under `.xdrs/[scope]/` appears in `.filedist`, the scope is external and new documents MUST NOT be created there. Inform the user and ask them to choose a non-external scope.
|
|
38
39
|
|
|
39
40
|
**Subject** — pick one from the allowed list for the chosen type (from `001-xdrs-core`):
|
|
40
41
|
- ADR: `principles`, `application`, `data`, `integration`, `platform`, `controls`, `operations`
|
|
@@ -167,6 +168,7 @@ If any check fails, revise and re-run this phase before proceeding.
|
|
|
167
168
|
3. Add or verify the scope entry in `.xdrs/index.md`.
|
|
168
169
|
4. If significant research was produced or already exists, link it from the XDR `## Considered Options` section.
|
|
169
170
|
5. If concise rules, examples, or do/don't bullets help readers apply the decision correctly, add them inside `### Implementation Details` without turning the XDR into a long procedure.
|
|
171
|
+
6. Evaluate whether the scope index at `.xdrs/[scope]/index.md` should be updated to reflect the new content. If the scope index does not exist, create it following article standards and the scope index rules in `_core-adr-001`.
|
|
170
172
|
|
|
171
173
|
### Phase 9: Verify Package structure with Lint
|
|
172
174
|
|
|
@@ -185,6 +187,7 @@ If any check fails, revise and re-run this phase before proceeding.
|
|
|
185
187
|
- MUST NOT create an XDR that duplicates a decision already captured in another XDR — extend or reference instead.
|
|
186
188
|
- MUST prefer links and short references over repeating the same decision content across related documents.
|
|
187
189
|
- MUST keep scope `_local` unless the user explicitly states otherwise.
|
|
190
|
+
- MUST NOT create documents in external scopes (scopes whose files appear in the workspace root `.filedist`).
|
|
188
191
|
|
|
189
192
|
## References
|
|
190
193
|
|
|
@@ -36,6 +36,7 @@ Quick test:
|
|
|
36
36
|
- "How to execute a business process or policy?" → BDR
|
|
37
37
|
|
|
38
38
|
**Scope** — use `_local` unless the user explicitly names another scope.
|
|
39
|
+
- If the user names a scope other than `_local`, check the workspace root `.filedist` file. If any file under `.xdrs/[scope]/` appears in `.filedist`, the scope is external and new documents MUST NOT be created there. Inform the user and ask them to choose a non-external scope.
|
|
39
40
|
|
|
40
41
|
**Subject** — pick the most specific match for the chosen type (required list per type is in `_core-adr-001`):
|
|
41
42
|
- ADR subjects: `principles`, `application`, `data`, `integration`, `platform`, `controls`, `operations`
|
|
@@ -122,6 +123,7 @@ If any check fails, revise before continuing.
|
|
|
122
123
|
mkdir -p .github/skills/[number]-[skill-name]
|
|
123
124
|
ln -s ../../.xdrs/[scope]/[type]/[subject]/skills/[number]-[skill-name] .github/skills/[number]-[skill-name]
|
|
124
125
|
```
|
|
126
|
+
3. Evaluate whether the scope index at `.xdrs/[scope]/index.md` should be updated to reflect the new skill. If the scope index does not exist, create it following article standards and the scope index rules in `_core-adr-001`.
|
|
125
127
|
|
|
126
128
|
### Constraints
|
|
127
129
|
|
|
@@ -129,6 +131,7 @@ If any check fails, revise before continuing.
|
|
|
129
131
|
- MUST consult `001-xdrs-core` as the canonical source for every core element definition, especially type, scope, subject, numbering, naming, and placement.
|
|
130
132
|
- MUST NOT create a skill that duplicates an existing one — extend or reference it instead.
|
|
131
133
|
- MUST keep scope `_local` unless the user explicitly states otherwise.
|
|
134
|
+
- MUST NOT create documents in external scopes (scopes whose files appear in the workspace root `.filedist`).
|
|
132
135
|
- MUST include a References section linking to `003-skill-standards`.
|
|
133
136
|
|
|
134
137
|
## Examples
|
|
@@ -41,6 +41,7 @@ Do NOT proceed to Phase 1 until you have at minimum a clear **topic** and **audi
|
|
|
41
41
|
Consult `001-xdrs-core` while making each choice in this phase. The summaries below are orientation only; when any detail is unclear, the standard decides.
|
|
42
42
|
|
|
43
43
|
**Scope** — use `_local` unless the user explicitly names another scope.
|
|
44
|
+
- If the user names a scope other than `_local`, check the workspace root `.filedist` file. If any file under `.xdrs/[scope]/` appears in `.filedist`, the scope is external and new documents MUST NOT be created there. Inform the user and ask them to choose a non-external scope.
|
|
44
45
|
|
|
45
46
|
**Type** — match the type of the XDRs the article primarily synthesizes (`adrs`, `bdrs`, or `edrs`).
|
|
46
47
|
If the topic spans multiple types, use `adrs`. Use the same rules as `002-write-xdr` Phase 2:
|
|
@@ -106,6 +107,7 @@ Rules to apply while drafting:
|
|
|
106
107
|
2. Add a link to the article in the canonical index for that scope+type (`.xdrs/[scope]/[type]/index.md`).
|
|
107
108
|
3. Add back-references in the XDRs, Research documents, and Skills that the article synthesizes, under their `## References`
|
|
108
109
|
section.
|
|
110
|
+
4. Evaluate whether the scope index at `.xdrs/[scope]/index.md` should be updated to reflect the new article. If the scope index does not exist, create it following article standards and the scope index rules in `_core-adr-001`.
|
|
109
111
|
|
|
110
112
|
## Examples
|
|
111
113
|
|
|
@@ -136,6 +138,7 @@ Rules to apply while drafting:
|
|
|
136
138
|
- MUST consult `001-xdrs-core` as the canonical source for every core element definition, especially type, scope, subject, numbering, naming, and placement.
|
|
137
139
|
- MUST follow the article template and placement rules from `004-article-standards`.
|
|
138
140
|
- MUST keep scope `_local` unless the user explicitly states otherwise.
|
|
141
|
+
- MUST NOT create documents in external scopes (scopes whose files appear in the workspace root `.filedist`).
|
|
139
142
|
- MUST defer to active and applicable XDRs when article synthesis conflicts with them.
|
|
140
143
|
|
|
141
144
|
## References
|
|
@@ -36,6 +36,7 @@ If the answers from Phase 1 leave scope, type, or subject ambiguous, use `vscode
|
|
|
36
36
|
Consult `001-xdrs-core` while making each choice in this phase. The summaries below are orientation only; when any detail matters, the standard decides.
|
|
37
37
|
|
|
38
38
|
**Scope** — use `_local` unless the user explicitly names another scope.
|
|
39
|
+
- If the user names a scope other than `_local`, check the workspace root `.filedist` file. If any file under `.xdrs/[scope]/` appears in `.filedist`, the scope is external and new documents MUST NOT be created there. Inform the user and ask them to choose a non-external scope.
|
|
39
40
|
|
|
40
41
|
**Type** — match the type of decision this research supports (`adrs`, `bdrs`, or `edrs`). Use the same rules as `002-write-xdr` Phase 2:
|
|
41
42
|
- **BDR**: business process, product policy, strategic rule, operational procedure
|
|
@@ -244,6 +245,7 @@ If any check fails, revise before continuing.
|
|
|
244
245
|
1. Create the research file at `.xdrs/[scope]/[type]/[subject]/researches/[number]-[short-title].md`.
|
|
245
246
|
2. Add an entry to `.xdrs/[scope]/[type]/index.md`.
|
|
246
247
|
3. Add back-references from the related XDR, article, or skill when the relationship is important for discovery.
|
|
248
|
+
4. Evaluate whether the scope index at `.xdrs/[scope]/index.md` should be updated to reflect the new research. If the scope index does not exist, create it following article standards and the scope index rules in `_core-adr-001`.
|
|
247
249
|
|
|
248
250
|
## Examples
|
|
249
251
|
|
|
@@ -276,4 +278,5 @@ If any check fails, revise before continuing.
|
|
|
276
278
|
- MUST consult `001-xdrs-core` as the canonical source for every core element definition, especially type, scope, subject, numbering, naming, and placement.
|
|
277
279
|
- MUST follow the research template and section-goal rules from `006-research-standards`.
|
|
278
280
|
- MUST keep scope `_local` unless the user explicitly states otherwise.
|
|
281
|
+
- MUST NOT create documents in external scopes (scopes whose files appear in the workspace root `.filedist`).
|
|
279
282
|
- MUST keep the document as research rather than turning it into a final decision.
|
|
@@ -33,6 +33,7 @@ Guides the creation of a well-structured plan document by following `_core-adr-0
|
|
|
33
33
|
Consult `001-xdrs-core` while making each choice in this phase. The summaries below are orientation only; when any detail is unclear, the standard decides.
|
|
34
34
|
|
|
35
35
|
**Scope** — use `_local` unless the user explicitly names another scope.
|
|
36
|
+
- If the user names a scope other than `_local`, check the workspace root `.filedist` file. If any file under `.xdrs/[scope]/` appears in `.filedist`, the scope is external and new documents MUST NOT be created there. Inform the user and ask them to choose a non-external scope.
|
|
36
37
|
|
|
37
38
|
**Type** — match the type of the XDRs the plan primarily implements or relates to (`adrs`, `bdrs`, or `edrs`).
|
|
38
39
|
- **BDR**: business process, product policy, strategic rule, operational procedure
|
|
@@ -123,6 +124,7 @@ Rules to apply while drafting:
|
|
|
123
124
|
1. Save the file at `.xdrs/[scope]/[type]/[subject]/plans/[number]-[short-title].md`.
|
|
124
125
|
2. Add a link to the plan in the canonical index for that scope+type (`.xdrs/[scope]/[type]/index.md`).
|
|
125
126
|
3. Add back-references in the XDRs, Research documents, and Skills that the plan relates to, under their `## References` section.
|
|
127
|
+
4. Evaluate whether the scope index at `.xdrs/[scope]/index.md` should be updated to reflect the new plan. If the scope index does not exist, create it following article standards and the scope index rules in `_core-adr-001`.
|
|
126
128
|
|
|
127
129
|
### Phase 7: Verify with Lint
|
|
128
130
|
|
|
@@ -156,3 +158,10 @@ Rules to apply while drafting:
|
|
|
156
158
|
- [_core-adr-001 - XDRs core](/.xdrs/_core/adrs/principles/001-xdrs-core.md)
|
|
157
159
|
- [_core-adr-007 - Plan standards](/.xdrs/_core/adrs/principles/007-plan-standards.md)
|
|
158
160
|
- [_core-adr-002 - XDR standards](/.xdrs/_core/adrs/principles/002-xdr-standards.md)
|
|
161
|
+
|
|
162
|
+
## Constraints
|
|
163
|
+
|
|
164
|
+
- MUST follow the plan template and section-goal rules from `007-plan-standards`.
|
|
165
|
+
- MUST consult `001-xdrs-core` as the canonical source for every core element definition, especially type, scope, subject, numbering, naming, and placement.
|
|
166
|
+
- MUST keep scope `_local` unless the user explicitly states otherwise.
|
|
167
|
+
- MUST NOT create documents in external scopes (scopes whose files appear in the workspace root `.filedist`).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# _core Scope Overview
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `_core` scope defines the XDR framework itself: how decision records, skills, research, articles, and plans are structured, written, versioned, and discovered. This scope is aimed at engineers, architects, and business analysts who build or consume XDR-based documentation.
|
|
6
|
+
|
|
7
|
+
## Content
|
|
8
|
+
|
|
9
|
+
### What this scope covers
|
|
10
|
+
|
|
11
|
+
The `_core` scope is the foundation that all other scopes inherit from. It establishes the rules and conventions that every XDR document, skill, article, research, and plan must follow regardless of which team, product, or domain produces them.
|
|
12
|
+
|
|
13
|
+
If you are evaluating whether to adopt XDRs, setting up a new XDR project, or extending the framework with your own scopes, start here.
|
|
14
|
+
|
|
15
|
+
### Framework structure and organization
|
|
16
|
+
|
|
17
|
+
The core architectural decision [_core-adr-001](adrs/principles/001-xdrs-core.md) defines the fundamental building blocks: three decision types (ADR for architecture, BDR for business, EDR for engineering), scopes as grouping boundaries, subjects as topic categories within each type, and a folder layout that keeps everything discoverable. It also defines the index system (canonical type indexes, scope indexes, and the root index) that ties the collection together.
|
|
18
|
+
|
|
19
|
+
### Document writing standards
|
|
20
|
+
|
|
21
|
+
Each artifact type has its own writing standard:
|
|
22
|
+
|
|
23
|
+
- **XDR documents** follow [_core-adr-002](adrs/principles/002-xdr-standards.md), which defines the mandatory template, frontmatter metadata, applicability rules, conflict handling, and word limits that keep decisions concise and authoritative.
|
|
24
|
+
- **Structured XDRs** with individually referenceable rules follow the extension [_core-adr-008](adrs/principles/008-xdr-standards-structured.md), adding numbered rule blocks and a dot-notation citation syntax.
|
|
25
|
+
- **Skills** follow [_core-adr-003](adrs/principles/003-skill-standards.md), using the agentskills format so they work for both humans and AI agents on an automation gradient from fully manual to fully automated.
|
|
26
|
+
- **Articles** follow [_core-adr-004](adrs/principles/004-article-standards.md), providing synthetic views that combine and link multiple XDRs, research, and skills without replacing them as the source of truth.
|
|
27
|
+
- **Research** follows [_core-adr-006](adrs/principles/006-research-standards.md), using an IMRAD-based structure for studies that back decisions with reproducible evidence.
|
|
28
|
+
- **Plans** follow [_core-adr-007](adrs/principles/007-plan-standards.md), capturing ephemeral execution plans with problem context, proposed solutions, milestones, and deliverables that are deleted after implementation.
|
|
29
|
+
|
|
30
|
+
### Versioning and distribution
|
|
31
|
+
|
|
32
|
+
[_core-adr-005](adrs/principles/005-semantic-versioning-for-xdr-packages.md) defines how XDR packages use semantic versioning to communicate upgrade impact when decisions are shared across repositories or teams.
|
|
33
|
+
|
|
34
|
+
### Usage policy
|
|
35
|
+
|
|
36
|
+
The business decision [_core-bdr-001](bdrs/principles/001-xdr-decisions-and-skills-usage.md) establishes how agents and humans must use XDR decisions and skills, separating policy authority (which lives in XDRs) from execution guidance (which lives in skills).
|
|
37
|
+
|
|
38
|
+
### Available skills
|
|
39
|
+
|
|
40
|
+
The `_core` scope ships with six skills that automate the most common framework operations:
|
|
41
|
+
|
|
42
|
+
- **001-lint** reviews code and files against applicable XDRs
|
|
43
|
+
- **002-write-xdr** guides creation of a new decision record
|
|
44
|
+
- **003-write-skill** guides creation of a new skill package
|
|
45
|
+
- **004-write-article** guides creation of a new article
|
|
46
|
+
- **005-write-research** guides creation of a new research document
|
|
47
|
+
- **006-write-plan** guides creation of a new execution plan
|
|
48
|
+
|
|
49
|
+
### Getting started
|
|
50
|
+
|
|
51
|
+
For a narrative introduction to the framework, including how elements differ, how to decide whether an XDR applies, and how to extend the framework with your own scopes, see the overview article [_core-article-001](adrs/principles/articles/001-xdrs-overview.md).
|
|
52
|
+
|
|
53
|
+
## Type Indexes
|
|
54
|
+
|
|
55
|
+
- [ADRs Index](adrs/index.md) - Architectural decisions about the XDR framework structure and standards
|
|
56
|
+
- [BDRs Index](bdrs/index.md) - Business and operational decisions about framework usage policy
|
package/.xdrs/index.md
CHANGED
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ Multiple scope packages can be combined in the same workspace by listing them as
|
|
|
153
153
|
The published package exposes the `xdrs-core` CLI.
|
|
154
154
|
|
|
155
155
|
- Bootstrap or extract managed XDR files with the existing `filedist`-backed commands such as `npx -y xdrs-core extract` and `npx -y xdrs-core check`.
|
|
156
|
-
- Lint an XDR tree with `npx -y xdrs-core lint .`. By default,
|
|
156
|
+
- Lint an XDR tree with `npx -y xdrs-core lint .`. By default, scopes whose files are listed in the workspace root `.filedist` file are treated as external and skipped; use `--all` to include them.
|
|
157
157
|
|
|
158
158
|
The `lint` command reads `./.xdrs/**` from the given workspace path and checks common consistency rules, including:
|
|
159
159
|
|
package/lib/lint.js
CHANGED
|
@@ -34,7 +34,7 @@ function runLintCli(args) {
|
|
|
34
34
|
const all = args.includes('--all');
|
|
35
35
|
const pathArgs = args.filter((a) => !a.startsWith('--'));
|
|
36
36
|
const targetPath = pathArgs[0] || '.';
|
|
37
|
-
const result = lintWorkspace(targetPath, {
|
|
37
|
+
const result = lintWorkspace(targetPath, { ignoreExternal: !all });
|
|
38
38
|
|
|
39
39
|
if (result.errors.length === 0) {
|
|
40
40
|
console.log(`Lint passed for ${toDisplayPath(result.xdrsRoot)}`);
|
|
@@ -53,12 +53,12 @@ function printHelp() {
|
|
|
53
53
|
console.log('Usage: xdrs-core lint [options] [path]\n');
|
|
54
54
|
console.log('Lint the XDR tree rooted at [path]/.xdrs or at [path] when [path] already points to .xdrs.');
|
|
55
55
|
console.log('\nOptions:');
|
|
56
|
-
console.log(' --all Check all files, including
|
|
56
|
+
console.log(' --all Check all files, including files from external scopes distributed via .filedist (default: skip external scopes)');
|
|
57
57
|
console.log('\nAll other commands continue to be delegated to the bundled filedist CLI.');
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function lintWorkspace(targetPath, options = {}) {
|
|
61
|
-
const {
|
|
61
|
+
const { ignoreExternal = true } = options;
|
|
62
62
|
const resolvedTarget = path.resolve(targetPath);
|
|
63
63
|
const xdrsRoot = path.basename(resolvedTarget) === '.xdrs'
|
|
64
64
|
? resolvedTarget
|
|
@@ -70,6 +70,10 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
70
70
|
return { xdrsRoot, errors };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const repoRoot = path.dirname(xdrsRoot);
|
|
74
|
+
const filedistPaths = loadFiledist(repoRoot);
|
|
75
|
+
const externalScopes = getExternalScopes(filedistPaths, xdrsRoot);
|
|
76
|
+
|
|
73
77
|
const actualTypeIndexes = [];
|
|
74
78
|
const rootEntries = safeReadDir(xdrsRoot, errors, 'read XDR root directory');
|
|
75
79
|
const scopeEntries = rootEntries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
@@ -81,7 +85,7 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
for (const scopeEntry of scopeEntries) {
|
|
84
|
-
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes,
|
|
88
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, externalScopes);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
const rootIndexPath = path.join(xdrsRoot, 'index.md');
|
|
@@ -109,8 +113,8 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors) {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
const
|
|
113
|
-
const linkedSet = new Set(
|
|
116
|
+
const linkedScopeIndexes = links.filter((linkPath) => isScopeIndex(linkPath, xdrsRoot));
|
|
117
|
+
const linkedSet = new Set(linkedScopeIndexes.map(normalizePath));
|
|
114
118
|
const localScopePath = normalizePath(path.join(xdrsRoot, '_local'));
|
|
115
119
|
|
|
116
120
|
for (const linkPath of links) {
|
|
@@ -119,21 +123,46 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors) {
|
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
// Collect non-_local scopes that have type indexes and check their scope index is linked
|
|
127
|
+
const scopesWithTypeIndexes = new Set();
|
|
122
128
|
for (const indexPath of actualTypeIndexes) {
|
|
123
129
|
const scopeName = path.basename(path.dirname(path.dirname(indexPath)));
|
|
124
|
-
if (scopeName
|
|
125
|
-
|
|
130
|
+
if (scopeName !== '_local') {
|
|
131
|
+
scopesWithTypeIndexes.add(scopeName);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const scopeName of scopesWithTypeIndexes) {
|
|
136
|
+
const scopeIndexPath = normalizePath(path.join(xdrsRoot, scopeName, 'index.md'));
|
|
137
|
+
if (!linkedSet.has(scopeIndexPath)) {
|
|
138
|
+
errors.push(`Root index is missing scope index link: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors) {
|
|
144
|
+
const content = fs.readFileSync(scopeIndexPath, 'utf8');
|
|
145
|
+
const repoRoot = path.dirname(xdrsRoot);
|
|
146
|
+
const links = parseLocalLinks(content, path.dirname(scopeIndexPath), repoRoot);
|
|
147
|
+
const linkedSet = new Set(links.map(normalizePath));
|
|
148
|
+
|
|
149
|
+
for (const linkPath of links) {
|
|
150
|
+
if (!fs.existsSync(linkPath)) {
|
|
151
|
+
errors.push(`Broken link in scope index: ${displayPath(scopeIndexPath, linkPath)}`);
|
|
126
152
|
}
|
|
127
|
-
|
|
128
|
-
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const typeIndexPath of typeIndexesInScope) {
|
|
156
|
+
if (!linkedSet.has(normalizePath(typeIndexPath))) {
|
|
157
|
+
errors.push(`Scope index ${toDisplayPath(scopeIndexPath)} is missing link to type index: ${toDisplayPath(typeIndexPath)}`);
|
|
129
158
|
}
|
|
130
159
|
}
|
|
131
160
|
}
|
|
132
161
|
|
|
133
|
-
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes,
|
|
162
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes) {
|
|
134
163
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
135
164
|
|
|
136
|
-
if (
|
|
165
|
+
if (ignoreExternal && externalScopes.has(scopeName)) {
|
|
137
166
|
return;
|
|
138
167
|
}
|
|
139
168
|
|
|
@@ -141,6 +170,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
141
170
|
errors.push(`Invalid scope name: ${toDisplayPath(scopePath)}`);
|
|
142
171
|
}
|
|
143
172
|
|
|
173
|
+
const typeIndexesInScope = [];
|
|
144
174
|
const entries = safeReadDir(scopePath, errors, `read scope directory ${scopeName}`);
|
|
145
175
|
for (const entry of entries) {
|
|
146
176
|
const entryPath = path.join(scopePath, entry.name);
|
|
@@ -149,15 +179,30 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
149
179
|
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
150
180
|
continue;
|
|
151
181
|
}
|
|
152
|
-
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes
|
|
182
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes);
|
|
183
|
+
const typeIndexPath = path.join(entryPath, 'index.md');
|
|
184
|
+
if (existsFile(typeIndexPath)) {
|
|
185
|
+
typeIndexesInScope.push(typeIndexPath);
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (entry.name === 'index.md') {
|
|
153
191
|
continue;
|
|
154
192
|
}
|
|
155
193
|
|
|
156
194
|
errors.push(`Unexpected file under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
157
195
|
}
|
|
196
|
+
|
|
197
|
+
const scopeIndexPath = path.join(scopePath, 'index.md');
|
|
198
|
+
if (!existsFile(scopeIndexPath)) {
|
|
199
|
+
errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)}`);
|
|
200
|
+
} else {
|
|
201
|
+
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors);
|
|
202
|
+
}
|
|
158
203
|
}
|
|
159
204
|
|
|
160
|
-
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes
|
|
205
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes) {
|
|
161
206
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
162
207
|
const indexPath = path.join(typePath, 'index.md');
|
|
163
208
|
const xdrNumbers = new Map();
|
|
@@ -165,7 +210,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
165
210
|
|
|
166
211
|
if (!existsFile(indexPath)) {
|
|
167
212
|
errors.push(`Missing canonical index: ${toDisplayPath(indexPath)}`);
|
|
168
|
-
} else
|
|
213
|
+
} else {
|
|
169
214
|
actualTypeIndexes.push(indexPath);
|
|
170
215
|
}
|
|
171
216
|
|
|
@@ -184,15 +229,15 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
184
229
|
continue;
|
|
185
230
|
}
|
|
186
231
|
|
|
187
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrNumbers, errors
|
|
232
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrNumbers, errors));
|
|
188
233
|
}
|
|
189
234
|
|
|
190
|
-
if (existsFile(indexPath)
|
|
235
|
+
if (existsFile(indexPath)) {
|
|
191
236
|
lintTypeIndex(indexPath, xdrsRoot, artifacts, errors);
|
|
192
237
|
}
|
|
193
238
|
}
|
|
194
239
|
|
|
195
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNumbers, errors
|
|
240
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNumbers, errors) {
|
|
196
241
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
197
242
|
const artifacts = [];
|
|
198
243
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -205,19 +250,19 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
|
|
|
205
250
|
continue;
|
|
206
251
|
}
|
|
207
252
|
if (entry.name === 'skills') {
|
|
208
|
-
artifacts.push(...lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
253
|
+
artifacts.push(...lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
209
254
|
continue;
|
|
210
255
|
}
|
|
211
256
|
if (entry.name === 'articles') {
|
|
212
|
-
artifacts.push(...lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
257
|
+
artifacts.push(...lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
213
258
|
continue;
|
|
214
259
|
}
|
|
215
260
|
if (entry.name === 'researches') {
|
|
216
|
-
artifacts.push(...lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
261
|
+
artifacts.push(...lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
217
262
|
continue;
|
|
218
263
|
}
|
|
219
264
|
if (entry.name === 'plans') {
|
|
220
|
-
artifacts.push(...lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
265
|
+
artifacts.push(...lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
221
266
|
continue;
|
|
222
267
|
}
|
|
223
268
|
|
|
@@ -230,14 +275,14 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
|
|
|
230
275
|
continue;
|
|
231
276
|
}
|
|
232
277
|
|
|
233
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
278
|
artifacts.push(entryPath);
|
|
238
279
|
lintXdrFile(xdrsRoot, scopeName, typeName, entryPath, xdrNumbers, errors);
|
|
239
280
|
}
|
|
240
281
|
|
|
282
|
+
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
283
|
+
const xdrDocsInSubject = artifacts.filter((p) => path.dirname(p) === subjectPath);
|
|
284
|
+
lintOrphanAssets(subjectAssetsDir, xdrDocsInSubject, xdrsRoot, errors);
|
|
285
|
+
|
|
241
286
|
return artifacts;
|
|
242
287
|
}
|
|
243
288
|
|
|
@@ -327,7 +372,7 @@ function lintXdrFrontmatter(content, expectedName, filePath, errors) {
|
|
|
327
372
|
}
|
|
328
373
|
}
|
|
329
374
|
|
|
330
|
-
function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsPath, errors
|
|
375
|
+
function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsPath, errors) {
|
|
331
376
|
const artifacts = [];
|
|
332
377
|
const skillNumbers = new Map();
|
|
333
378
|
const entries = safeReadDir(skillsPath, errors, `read skills directory ${scopeName}/${typeName}/${subjectName}/skills`);
|
|
@@ -364,10 +409,6 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
364
409
|
continue;
|
|
365
410
|
}
|
|
366
411
|
|
|
367
|
-
if (shouldSkipReadOnlyPath(skillFilePath, ignoreReadOnly)) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
412
|
artifacts.push(skillFilePath);
|
|
372
413
|
|
|
373
414
|
const skillContent = fs.readFileSync(skillFilePath, 'utf8');
|
|
@@ -386,12 +427,13 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
386
427
|
}
|
|
387
428
|
|
|
388
429
|
lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors);
|
|
430
|
+
lintOrphanAssets(path.join(entryPath, RESOURCE_DIR_NAME), [skillFilePath], xdrsRoot, errors);
|
|
389
431
|
}
|
|
390
432
|
|
|
391
433
|
return artifacts;
|
|
392
434
|
}
|
|
393
435
|
|
|
394
|
-
function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, articlesPath, errors
|
|
436
|
+
function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, articlesPath, errors) {
|
|
395
437
|
const artifacts = [];
|
|
396
438
|
const articleNumbers = new Map();
|
|
397
439
|
const entries = safeReadDir(articlesPath, errors, `read articles directory ${scopeName}/${typeName}/${subjectName}/articles`);
|
|
@@ -413,10 +455,6 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
413
455
|
continue;
|
|
414
456
|
}
|
|
415
457
|
|
|
416
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
458
|
artifacts.push(entryPath);
|
|
421
459
|
|
|
422
460
|
const number = match[1];
|
|
@@ -441,10 +479,12 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
441
479
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
|
|
442
480
|
}
|
|
443
481
|
|
|
482
|
+
lintOrphanAssets(path.join(articlesPath, RESOURCE_DIR_NAME), artifacts, xdrsRoot, errors);
|
|
483
|
+
|
|
444
484
|
return artifacts;
|
|
445
485
|
}
|
|
446
486
|
|
|
447
|
-
function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, researchPath, errors
|
|
487
|
+
function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, researchPath, errors) {
|
|
448
488
|
const artifacts = [];
|
|
449
489
|
const researchNumbers = new Map();
|
|
450
490
|
const entries = safeReadDir(researchPath, errors, `read research directory ${scopeName}/${typeName}/${subjectName}/researches`);
|
|
@@ -466,10 +506,6 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
466
506
|
continue;
|
|
467
507
|
}
|
|
468
508
|
|
|
469
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
509
|
artifacts.push(entryPath);
|
|
474
510
|
|
|
475
511
|
const number = match[1];
|
|
@@ -494,10 +530,12 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
494
530
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
|
|
495
531
|
}
|
|
496
532
|
|
|
533
|
+
lintOrphanAssets(path.join(researchPath, RESOURCE_DIR_NAME), artifacts, xdrsRoot, errors);
|
|
534
|
+
|
|
497
535
|
return artifacts;
|
|
498
536
|
}
|
|
499
537
|
|
|
500
|
-
function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPath, errors
|
|
538
|
+
function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPath, errors) {
|
|
501
539
|
const artifacts = [];
|
|
502
540
|
const planNumbers = new Map();
|
|
503
541
|
const entries = safeReadDir(plansPath, errors, `read plans directory ${scopeName}/${typeName}/${subjectName}/plans`);
|
|
@@ -519,10 +557,6 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
519
557
|
continue;
|
|
520
558
|
}
|
|
521
559
|
|
|
522
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
560
|
artifacts.push(entryPath);
|
|
527
561
|
|
|
528
562
|
const number = match[1];
|
|
@@ -548,6 +582,8 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
548
582
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
|
|
549
583
|
}
|
|
550
584
|
|
|
585
|
+
lintOrphanAssets(path.join(plansPath, RESOURCE_DIR_NAME), artifacts, xdrsRoot, errors);
|
|
586
|
+
|
|
551
587
|
return artifacts;
|
|
552
588
|
}
|
|
553
589
|
|
|
@@ -599,6 +635,46 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
|
|
|
599
635
|
}
|
|
600
636
|
}
|
|
601
637
|
|
|
638
|
+
function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
|
|
639
|
+
if (!existsDirectory(assetsDir)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const entries = safeReadDir(assetsDir, errors, `read assets directory ${toDisplayPath(assetsDir)}`);
|
|
644
|
+
if (entries.length === 0) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const assetFiles = entries
|
|
649
|
+
.filter((entry) => entry.isFile())
|
|
650
|
+
.map((entry) => normalizePath(path.join(assetsDir, entry.name)));
|
|
651
|
+
|
|
652
|
+
if (assetFiles.length === 0) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const repoRoot = path.dirname(xdrsRoot);
|
|
657
|
+
const referencedAssets = new Set();
|
|
658
|
+
|
|
659
|
+
for (const docPath of documentPaths) {
|
|
660
|
+
if (!existsFile(docPath)) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const content = fs.readFileSync(docPath, 'utf8');
|
|
664
|
+
const docDir = path.dirname(docPath);
|
|
665
|
+
const links = parseLocalLinks(content, docDir, repoRoot);
|
|
666
|
+
for (const linkPath of links) {
|
|
667
|
+
referencedAssets.add(normalizePath(linkPath));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const assetPath of assetFiles) {
|
|
672
|
+
if (!referencedAssets.has(assetPath)) {
|
|
673
|
+
errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
602
678
|
function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors) {
|
|
603
679
|
const lines = fs.readFileSync(documentPath, 'utf8').split(/\r?\n/);
|
|
604
680
|
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
@@ -693,6 +769,11 @@ function isCanonicalTypeIndex(filePath, xdrsRoot) {
|
|
|
693
769
|
return relative.length === 3 && TYPE_NAMES.has(relative[1]) && relative[2] === 'index.md';
|
|
694
770
|
}
|
|
695
771
|
|
|
772
|
+
function isScopeIndex(filePath, xdrsRoot) {
|
|
773
|
+
const relative = relativeFrom(xdrsRoot, filePath).split(path.sep);
|
|
774
|
+
return relative.length === 2 && relative[1] === 'index.md';
|
|
775
|
+
}
|
|
776
|
+
|
|
696
777
|
function shouldValidateResourceLink(rawTarget) {
|
|
697
778
|
const normalizedTargetPath = normalizeLocalLinkTarget(rawTarget);
|
|
698
779
|
if (!normalizedTargetPath) {
|
|
@@ -857,17 +938,38 @@ function existsFile(filePath) {
|
|
|
857
938
|
}
|
|
858
939
|
}
|
|
859
940
|
|
|
860
|
-
function
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
return
|
|
864
|
-
} catch {
|
|
865
|
-
return true;
|
|
941
|
+
function loadFiledist(repoRoot) {
|
|
942
|
+
const filedistPath = path.join(repoRoot, '.filedist');
|
|
943
|
+
if (!existsFile(filedistPath)) {
|
|
944
|
+
return new Set();
|
|
866
945
|
}
|
|
946
|
+
const content = fs.readFileSync(filedistPath, 'utf8');
|
|
947
|
+
const paths = new Set();
|
|
948
|
+
for (const line of content.split(/\r?\n/)) {
|
|
949
|
+
const trimmed = line.trim();
|
|
950
|
+
if (!trimmed) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const parts = trimmed.split('|');
|
|
954
|
+
if (parts.length >= 2) {
|
|
955
|
+
paths.add(normalizePath(path.join(repoRoot, parts[0])));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return paths;
|
|
867
959
|
}
|
|
868
960
|
|
|
869
|
-
function
|
|
870
|
-
|
|
961
|
+
function getExternalScopes(filedistPaths, xdrsRoot) {
|
|
962
|
+
const externalScopes = new Set();
|
|
963
|
+
for (const filePath of filedistPaths) {
|
|
964
|
+
const relative = path.relative(xdrsRoot, filePath);
|
|
965
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
966
|
+
const parts = relative.split(path.sep);
|
|
967
|
+
if (parts.length >= 1 && parts[0]) {
|
|
968
|
+
externalScopes.add(parts[0]);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return externalScopes;
|
|
871
973
|
}
|
|
872
974
|
|
|
873
975
|
function displayPath(indexPath, targetPath) {
|
package/lib/lint.test.js
CHANGED
|
@@ -64,24 +64,92 @@ test('reports broken local document links in skill files', () => {
|
|
|
64
64
|
expect(result.errors.join('\n')).toContain('missing.md');
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
test('skips
|
|
68
|
-
const workspaceRoot = createWorkspace('
|
|
69
|
-
'.xdrs/index.md': rootIndex(),
|
|
70
|
-
'.xdrs/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
test('skips external scopes by default and checks them when ignoreExternal is false', () => {
|
|
68
|
+
const workspaceRoot = createWorkspace('external-default-skip', {
|
|
69
|
+
'.xdrs/index.md': rootIndex(['[extscope](extscope/index.md)']),
|
|
70
|
+
'.xdrs/extscope/index.md': '# extscope Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
71
|
+
'.xdrs/extscope/adrs/index.md': [
|
|
72
|
+
'# extscope ADR Index',
|
|
73
|
+
'',
|
|
74
|
+
'Test.',
|
|
75
|
+
'',
|
|
76
|
+
'## principles',
|
|
77
|
+
'',
|
|
78
|
+
'- [001-ext](principles/001-ext.md) - Ext decision',
|
|
79
|
+
''
|
|
80
|
+
].join('\n'),
|
|
81
|
+
'.xdrs/extscope/adrs/principles/001-ext.md': [
|
|
82
|
+
'---',
|
|
83
|
+
'name: extscope-adr-001-ext',
|
|
84
|
+
'description: External test XDR document',
|
|
85
|
+
'---',
|
|
86
|
+
'',
|
|
87
|
+
'# extscope-adr-001: Ext decision',
|
|
88
|
+
'',
|
|
89
|
+
'## Context and Problem Statement',
|
|
90
|
+
'',
|
|
91
|
+
'See [Missing](002-missing.md).',
|
|
92
|
+
''
|
|
93
|
+
].join('\n'),
|
|
94
|
+
'.filedist': '.xdrs/extscope/adrs/principles/001-ext.md|some-package|1.0.0\n',
|
|
74
95
|
});
|
|
75
|
-
const filePath = path.join(workspaceRoot, '.xdrs/_local/adrs/principles/001-main.md');
|
|
76
|
-
fs.chmodSync(filePath, 0o444);
|
|
77
96
|
|
|
78
97
|
const defaultResult = lintWorkspace(workspaceRoot);
|
|
79
|
-
const allResult = lintWorkspace(workspaceRoot, {
|
|
98
|
+
const allResult = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
80
99
|
|
|
81
100
|
expect(defaultResult.errors.join('\n')).not.toContain('Broken local link in');
|
|
82
101
|
expect(allResult.errors.join('\n')).toContain('Broken local link in');
|
|
83
102
|
});
|
|
84
103
|
|
|
104
|
+
test('skips entire external scope when only some of its files are in filedist', () => {
|
|
105
|
+
const workspaceRoot = createWorkspace('external-scope-partial-filedist', {
|
|
106
|
+
'.xdrs/index.md': rootIndex(['[extscope](extscope/index.md)']),
|
|
107
|
+
'.xdrs/extscope/index.md': '# extscope Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
108
|
+
'.xdrs/extscope/adrs/index.md': [
|
|
109
|
+
'# extscope ADR Index',
|
|
110
|
+
'',
|
|
111
|
+
'Test.',
|
|
112
|
+
'',
|
|
113
|
+
'## principles',
|
|
114
|
+
'',
|
|
115
|
+
'- [001-ext](principles/001-ext.md) - Ext decision',
|
|
116
|
+
'- [002-local](principles/002-local.md) - Local decision',
|
|
117
|
+
''
|
|
118
|
+
].join('\n'),
|
|
119
|
+
'.xdrs/extscope/adrs/principles/001-ext.md': [
|
|
120
|
+
'---',
|
|
121
|
+
'name: extscope-adr-001-ext',
|
|
122
|
+
'description: External test XDR document',
|
|
123
|
+
'---',
|
|
124
|
+
'',
|
|
125
|
+
'# extscope-adr-001: Ext decision',
|
|
126
|
+
'',
|
|
127
|
+
'## Context and Problem Statement',
|
|
128
|
+
'',
|
|
129
|
+
'External body.',
|
|
130
|
+
''
|
|
131
|
+
].join('\n'),
|
|
132
|
+
'.xdrs/extscope/adrs/principles/002-local.md': [
|
|
133
|
+
'---',
|
|
134
|
+
'name: extscope-adr-002-local',
|
|
135
|
+
'description: Second XDR in external scope',
|
|
136
|
+
'---',
|
|
137
|
+
'',
|
|
138
|
+
'# extscope-adr-002: Local decision',
|
|
139
|
+
'',
|
|
140
|
+
'## Context and Problem Statement',
|
|
141
|
+
'',
|
|
142
|
+
'See [Missing](003-missing.md).',
|
|
143
|
+
''
|
|
144
|
+
].join('\n'),
|
|
145
|
+
'.filedist': '.xdrs/extscope/adrs/principles/001-ext.md|some-package|1.0.0\n',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = lintWorkspace(workspaceRoot);
|
|
149
|
+
|
|
150
|
+
expect(result.errors.join('\n')).not.toContain('Broken local link in');
|
|
151
|
+
});
|
|
152
|
+
|
|
85
153
|
test('derives expected frontmatter name from the markdown heading title', () => {
|
|
86
154
|
const workspaceRoot = createWorkspace('heading-name-match', {
|
|
87
155
|
'.xdrs/index.md': rootIndex(),
|
|
@@ -103,18 +171,19 @@ test('derives expected frontmatter name from the markdown heading title', () =>
|
|
|
103
171
|
].join('\n'),
|
|
104
172
|
});
|
|
105
173
|
|
|
106
|
-
const result = lintWorkspace(workspaceRoot, {
|
|
174
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
107
175
|
|
|
108
176
|
expect(result.errors.join('\n')).not.toContain('XDR frontmatter name must be');
|
|
109
177
|
});
|
|
110
178
|
|
|
111
179
|
test('reports non-_local XDR linking to _local scope document', () => {
|
|
112
180
|
const workspaceRoot = createWorkspace('non-local-links-to-local-xdr', {
|
|
113
|
-
'.xdrs/index.md': rootIndex(),
|
|
181
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
114
182
|
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
115
183
|
'- [001-main](principles/001-main.md) - Main decision'
|
|
116
184
|
]),
|
|
117
185
|
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Local decision.'),
|
|
186
|
+
'.xdrs/myteam/index.md': '# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
118
187
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex([
|
|
119
188
|
'- [001-team](principles/001-team.md) - Team decision'
|
|
120
189
|
]),
|
|
@@ -123,7 +192,7 @@ test('reports non-_local XDR linking to _local scope document', () => {
|
|
|
123
192
|
),
|
|
124
193
|
});
|
|
125
194
|
|
|
126
|
-
const result = lintWorkspace(workspaceRoot, {
|
|
195
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
127
196
|
|
|
128
197
|
expect(result.errors.join('\n')).toContain('Non-_local document must not link into _local scope');
|
|
129
198
|
});
|
|
@@ -163,7 +232,7 @@ test('allows _local XDR linking to another _local scope document', () => {
|
|
|
163
232
|
].join('\n'),
|
|
164
233
|
});
|
|
165
234
|
|
|
166
|
-
const result = lintWorkspace(workspaceRoot, {
|
|
235
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
167
236
|
|
|
168
237
|
expect(result.errors.join('\n')).not.toContain('Non-_local document must not link into _local scope');
|
|
169
238
|
expect(result.errors.join('\n')).not.toContain('Broken local link');
|
|
@@ -171,18 +240,19 @@ test('allows _local XDR linking to another _local scope document', () => {
|
|
|
171
240
|
|
|
172
241
|
test('reports non-_local canonical index linking to _local scope document', () => {
|
|
173
242
|
const workspaceRoot = createWorkspace('non-local-type-index-links-to-local', {
|
|
174
|
-
'.xdrs/index.md': rootIndex(),
|
|
243
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
175
244
|
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
176
245
|
'- [001-main](principles/001-main.md) - Main decision'
|
|
177
246
|
]),
|
|
178
247
|
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Local decision.'),
|
|
248
|
+
'.xdrs/myteam/index.md': '# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
179
249
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex([
|
|
180
250
|
'- [_local 001](../../_local/adrs/principles/001-main.md) - Cross-scope link'
|
|
181
251
|
]),
|
|
182
252
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
183
253
|
});
|
|
184
254
|
|
|
185
|
-
const result = lintWorkspace(workspaceRoot, {
|
|
255
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
186
256
|
|
|
187
257
|
expect(result.errors.join('\n')).toContain('Non-_local document must not link into _local scope');
|
|
188
258
|
});
|
|
@@ -273,11 +343,12 @@ test('reports absolute path link that is broken', () => {
|
|
|
273
343
|
|
|
274
344
|
test('reports non-_local XDR linking to _local scope via absolute path', () => {
|
|
275
345
|
const workspaceRoot = createWorkspace('abs-non-local-links-to-local', {
|
|
276
|
-
'.xdrs/index.md': rootIndex(),
|
|
346
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
277
347
|
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
278
348
|
'- [001-main](principles/001-main.md) - Main decision'
|
|
279
349
|
]),
|
|
280
350
|
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Local decision.'),
|
|
351
|
+
'.xdrs/myteam/index.md': '# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
281
352
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex([
|
|
282
353
|
'- [001-team](principles/001-team.md) - Team decision'
|
|
283
354
|
]),
|
|
@@ -286,11 +357,145 @@ test('reports non-_local XDR linking to _local scope via absolute path', () => {
|
|
|
286
357
|
),
|
|
287
358
|
});
|
|
288
359
|
|
|
289
|
-
const result = lintWorkspace(workspaceRoot, {
|
|
360
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
290
361
|
|
|
291
362
|
expect(result.errors.join('\n')).toContain('Non-_local document must not link into _local scope');
|
|
292
363
|
});
|
|
293
364
|
|
|
365
|
+
test('allows index.md at scope level', () => {
|
|
366
|
+
const workspaceRoot = createWorkspace('scope-index-allowed', {
|
|
367
|
+
'.xdrs/index.md': rootIndex(),
|
|
368
|
+
'.xdrs/_local/index.md': '# _local Scope Overview\n\nOverview of local scope.\n\n[ADRs](adrs/index.md)\n',
|
|
369
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
370
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
371
|
+
]),
|
|
372
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Test body.'),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const result = lintWorkspace(workspaceRoot);
|
|
376
|
+
|
|
377
|
+
expect(result.errors.join('\n')).not.toContain('Unexpected file under scope');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('reports scope index missing link to type index', () => {
|
|
381
|
+
const workspaceRoot = createWorkspace('scope-index-missing-type-link', {
|
|
382
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
383
|
+
'.xdrs/myteam/index.md': '# myteam Scope Overview\n\nNo type links here.\n',
|
|
384
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex([
|
|
385
|
+
'- [001-team](principles/001-team.md) - Team decision'
|
|
386
|
+
]),
|
|
387
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
391
|
+
|
|
392
|
+
expect(result.errors.join('\n')).toContain('Scope index');
|
|
393
|
+
expect(result.errors.join('\n')).toContain('is missing link to type index');
|
|
394
|
+
expect(result.errors.join('\n')).toContain('adrs/index.md');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('passes when scope index links to all type indexes', () => {
|
|
398
|
+
const workspaceRoot = createWorkspace('scope-index-links-all-types', {
|
|
399
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
400
|
+
'.xdrs/myteam/index.md': '# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
401
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex([
|
|
402
|
+
'- [001-team](principles/001-team.md) - Team decision'
|
|
403
|
+
]),
|
|
404
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
408
|
+
|
|
409
|
+
expect(result.errors.join('\n')).not.toContain('is missing link to type index');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('reports broken link in scope index', () => {
|
|
413
|
+
const workspaceRoot = createWorkspace('scope-index-broken-link', {
|
|
414
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
415
|
+
'.xdrs/myteam/index.md': '# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n[Missing](missing/index.md)\n',
|
|
416
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex([
|
|
417
|
+
'- [001-team](principles/001-team.md) - Team decision'
|
|
418
|
+
]),
|
|
419
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
423
|
+
|
|
424
|
+
expect(result.errors.join('\n')).toContain('Broken link in scope index');
|
|
425
|
+
expect(result.errors.join('\n')).toContain('missing/index.md');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('reports missing scope index', () => {
|
|
429
|
+
const workspaceRoot = createWorkspace('missing-scope-index', {
|
|
430
|
+
'.xdrs/index.md': rootIndex(),
|
|
431
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
432
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
433
|
+
]),
|
|
434
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('Test body.'),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const result = lintWorkspace(workspaceRoot);
|
|
438
|
+
|
|
439
|
+
expect(result.errors.join('\n')).toContain('Missing required scope index');
|
|
440
|
+
expect(result.errors.join('\n')).toContain('_local/index.md');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('reports orphan asset files not referenced by any document', () => {
|
|
444
|
+
const workspaceRoot = createWorkspace('orphan-asset', {
|
|
445
|
+
'.xdrs/index.md': rootIndex(),
|
|
446
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
447
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
448
|
+
]),
|
|
449
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('See .'),
|
|
450
|
+
'.xdrs/_local/adrs/principles/.assets/used.png': Buffer.alloc(0),
|
|
451
|
+
'.xdrs/_local/adrs/principles/.assets/orphan.png': Buffer.alloc(0),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const result = lintWorkspace(workspaceRoot);
|
|
455
|
+
|
|
456
|
+
expect(result.errors.join('\n')).toContain('Orphan asset file');
|
|
457
|
+
expect(result.errors.join('\n')).toContain('orphan.png');
|
|
458
|
+
expect(result.errors.join('\n')).not.toContain('used.png');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('does not report asset files that are referenced', () => {
|
|
462
|
+
const workspaceRoot = createWorkspace('no-orphan-asset', {
|
|
463
|
+
'.xdrs/index.md': rootIndex(),
|
|
464
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
465
|
+
'- [001-main](principles/001-main.md) - Main decision'
|
|
466
|
+
]),
|
|
467
|
+
'.xdrs/_local/adrs/principles/001-main.md': xdrDocument('See .'),
|
|
468
|
+
'.xdrs/_local/adrs/principles/.assets/diagram.png': Buffer.alloc(0),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const result = lintWorkspace(workspaceRoot);
|
|
472
|
+
|
|
473
|
+
expect(result.errors.join('\n')).not.toContain('Orphan asset file');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('reports orphan asset in articles .assets directory', () => {
|
|
477
|
+
const workspaceRoot = createWorkspace('orphan-article-asset', {
|
|
478
|
+
'.xdrs/index.md': rootIndex(),
|
|
479
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
480
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide article'
|
|
481
|
+
]),
|
|
482
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': [
|
|
483
|
+
'# _local-article-001: Guide',
|
|
484
|
+
'',
|
|
485
|
+
'See .',
|
|
486
|
+
''
|
|
487
|
+
].join('\n'),
|
|
488
|
+
'.xdrs/_local/adrs/principles/articles/.assets/used.png': Buffer.alloc(0),
|
|
489
|
+
'.xdrs/_local/adrs/principles/articles/.assets/unused.jpg': Buffer.alloc(0),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const result = lintWorkspace(workspaceRoot);
|
|
493
|
+
|
|
494
|
+
expect(result.errors.join('\n')).toContain('Orphan asset file');
|
|
495
|
+
expect(result.errors.join('\n')).toContain('unused.jpg');
|
|
496
|
+
expect(result.errors.join('\n')).not.toContain('used.png');
|
|
497
|
+
});
|
|
498
|
+
|
|
294
499
|
function teamAdrIndex(entries) {
|
|
295
500
|
return [
|
|
296
501
|
'# myteam ADR Index',
|
|
@@ -333,18 +538,24 @@ function createWorkspace(name, files) {
|
|
|
333
538
|
return workspaceRoot;
|
|
334
539
|
}
|
|
335
540
|
|
|
336
|
-
function rootIndex() {
|
|
337
|
-
|
|
541
|
+
function rootIndex(extraScopeLinks) {
|
|
542
|
+
const lines = [
|
|
338
543
|
'# XDR Standards Index',
|
|
339
544
|
'',
|
|
340
545
|
'## Scope Indexes',
|
|
341
546
|
'',
|
|
342
547
|
'XDRs in scopes listed last override the ones listed first',
|
|
343
548
|
'',
|
|
549
|
+
];
|
|
550
|
+
if (extraScopeLinks) {
|
|
551
|
+
lines.push(...extraScopeLinks, '');
|
|
552
|
+
}
|
|
553
|
+
lines.push(
|
|
344
554
|
'### _local (reserved)',
|
|
345
555
|
'',
|
|
346
556
|
'Project-local XDRs stay in the workspace tree only.',
|
|
347
|
-
|
|
557
|
+
);
|
|
558
|
+
return lines.join('\n');
|
|
348
559
|
}
|
|
349
560
|
|
|
350
561
|
function localAdrIndex(entries) {
|
package/package.json
CHANGED