xdrs-core 0.52.1 → 0.52.3
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-standards.md +3 -3
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +3 -1
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +5 -5
- package/.xdrs/_core/adrs/principles/011-core-scope-type.md +1 -1
- package/.xdrs/_core/adrs/principles/022-scope-activation.md +12 -9
- package/.xdrs/_core/adrs/principles/skills/.assets/lint-verification.md +0 -1
- package/.xdrs/_core/adrs/principles/skills/compile-scope/SKILL.md +1 -1
- package/lib/lint.js +41 -75
- package/lib/lint.test.js +139 -47
- package/package.json +1 -1
|
@@ -50,7 +50,7 @@ ADR and EDR share the same six technical subject names (see [`_core-adr-policy-0
|
|
|
50
50
|
`[xdrs-root]/[scope]/[type]/[subject]/[number]-[short-title].md`
|
|
51
51
|
where `[xdrs-root]` is the XDRS root folder (default: `.xdrs/`).
|
|
52
52
|
- MUST ignore symlinks paths. MUST NOT create or update documents inside symlinked folders.
|
|
53
|
-
- **Files listed in `.filedist.lock` are external XDRs.** A file whose path appears in the workspace root `.filedist.lock` file was distributed from an external source repository. It MUST NOT be modified locally. To change it, submit the change to the source repository and re-extract the updated package. The `.filedist.lock` format is one entry per line: `<relative-path>|<package>|<version>`. A scope is considered external when any of its files appear in `.filedist.lock
|
|
53
|
+
- **Files listed in `.filedist.lock` are external XDRs.** A file whose path appears in the workspace root `.filedist.lock` file was distributed from an external source repository. It MUST NOT be modified locally. To change it, submit the change to the source repository and re-extract the updated package. The `.filedist.lock` format is one entry per line: `<relative-path>|<package>|<version>`. A scope is considered external when any of its files appear in `.filedist.lock`. The `.filedist.lock` file can also be used to detect which files changed when bumping an external scope to a newer version: compare the version field in `.filedist.lock` entries before and after the upgrade and diff the affected paths to understand what decisions were added, updated, or removed.
|
|
54
54
|
- Optional supporting artifacts under the same subject:
|
|
55
55
|
- `[xdrs-root]/[scope]/[type]/[subject]/researches/[number]-[short-title].md`
|
|
56
56
|
- `[xdrs-root]/[scope]/[type]/[subject]/skills/[skill-name]/SKILL.md`
|
|
@@ -91,8 +91,8 @@ ADR and EDR share the same six technical subject names (see [`_core-adr-policy-0
|
|
|
91
91
|
- `apply-to` (required): Declares in which contexts — teams, systems, codebases, or environments — the decisions in this scope are relevant. Max 30 words.
|
|
92
92
|
- `valid-from` (required): ISO date (YYYY-MM-DD) from which this scope became active.
|
|
93
93
|
- `metadata` (optional): Arbitrary key-value map for additional scope metadata.
|
|
94
|
-
- `follows` (optional): Core scope names whose Policies apply as mandatory governance conventions to this scope, beyond `_core`. Last-listed takes precedence on conflicts (e.g., `follows: myarea-core, shared-standards`).
|
|
95
|
-
- `extends` (optional): Scope names whose **policy documents** (decision records only — not skills, articles, research, or initiatives) are inherited by this scope as if they were authored here. Any scope type except `_local` and `_core` may be referenced. Last-listed extended scope takes precedence on conflicts; the extending scope's own policies always take highest precedence. Depth-first transitive resolution applies: if A extends [B, C] and B extends [D], the effective precedence order is D < B < C < A. `extends:` and `follows:` MUST reference disjoint scope sets.
|
|
94
|
+
- `follows` (optional): Core scope names whose Policies apply as mandatory governance conventions to this scope, beyond `_core`. Last-listed takes precedence on conflicts (e.g., `follows: myarea-core, shared-standards`). A missing scope MUST be fixed in local scopes and makes external scopes READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`).
|
|
95
|
+
- `extends` (optional): Scope names whose **policy documents** (decision records only — not skills, articles, research, or initiatives) are inherited by this scope as if they were authored here. Any scope type except `_local` and `_core` may be referenced. Last-listed extended scope takes precedence on conflicts; the extending scope's own policies always take highest precedence. Depth-first transitive resolution applies: if A extends [B, C] and B extends [D], the effective precedence order is D < B < C < A. `extends:` and `follows:` MUST reference disjoint scope sets. A missing scope MUST be fixed in local scopes and makes external scopes READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`). When an extending scope's policy overrides an inherited policy on the same topic, a `## Conflicts` section MUST document the override. Extended scopes are still linked in the root index; tag them `extends-only` to apply them only via `extends:` (see `_core-adr-policy-022.01-root-index-completeness`). `extends:` takes precedence over root index ordering for conflict resolution between the scopes involved; root index ordering is the fallback for all other scope pairs. See `_core-adr-policy-010` Section D (rules 27–35) and Section E (rule 36) for full normative rules.
|
|
96
96
|
- `extends-only` (optional): See `_core-adr-policy-022.07-extends-only-field`.
|
|
97
97
|
- `license` (optional): SPDX identifier for the scope's content license.
|
|
98
98
|
- **Subjects:** The subject folder MUST be chosen per [`_core-adr-policy-016`](016-policy-subjects.md), which defines allowed subjects, full descriptions, examples, and disambiguation tiebreakers.
|
|
@@ -75,6 +75,8 @@ Examples:
|
|
|
75
75
|
|
|
76
76
|
Multiple skills under the same `[subject]/skills/` folder MAY share common instruction modules through a sibling `.assets/` directory placed directly under `skills/` (not inside any individual skill package) — for example `skills/.assets/shared-check.md`, referenced with a relative link from any skill in that folder. This keeps shared procedures DRY without duplicating instructions across skills.
|
|
77
77
|
|
|
78
|
+
A skill's own `.assets/` folder and the shared `skills/.assets/` folder are exempt from orphan-asset tracking: their files do not need to be linked from `SKILL.md`, because they may hold scripts, templates, installed dependencies (e.g., `node_modules/`), or other runtime resources used by the skill. When `SKILL.md` does link to an asset, the link MUST point either to the skill's own `.assets/` folder or to the shared `../.assets/` folder.
|
|
79
|
+
|
|
78
80
|
A skill MAY optionally be distributed as a self-contained standalone package (for example, to share it outside this repository) using the bundling mechanism defined in [`_core-adr-policy-021`](021-skill-bundling.md). Bundling is optional; skills that are always consumed as part of the full package do not need it.
|
|
79
81
|
|
|
80
82
|
**Directory roles**
|
|
@@ -84,7 +86,7 @@ A skill MAY optionally be distributed as a self-contained standalone package (fo
|
|
|
84
86
|
| `SKILL.md` | Required entry point: frontmatter + instructions. |
|
|
85
87
|
| `scripts/` | Optional executable scripts the agent may run. |
|
|
86
88
|
| `references/` | Optional deep reference material split out of `SKILL.md`. |
|
|
87
|
-
| `.assets/` | Optional images, templates, and other local resource files. |
|
|
89
|
+
| `.assets/` | Optional images, templates, scripts, dependencies, and other local resource files. Not subject to orphan-asset tracking. |
|
|
88
90
|
|
|
89
91
|
**Skill naming**
|
|
90
92
|
|
|
@@ -81,9 +81,9 @@ A scope-type definition policy MAY declare a parent scope type by including a ru
|
|
|
81
81
|
|
|
82
82
|
#### 10-def-valid-iff-policy-exists
|
|
83
83
|
|
|
84
|
-
A `scope-type` value in a scope `index.md` is valid if and only if a policy file whose name ends with `{scope-type}-scope-type` exists in the `principles` subject of any `core`-type scope in the workspace.
|
|
84
|
+
A `scope-type` value in a scope `index.md` is valid if and only if a policy file whose name ends with `{scope-type}-scope-type` exists in the `principles` subject of any `core`-type scope in the workspace. When `scope-type` is an array, this applies to every element.
|
|
85
85
|
|
|
86
|
-
When a scope declares a `scope-type` but any corresponding `{scope-type}-scope-type.md` policy is absent from the workspace or defined only in a `disabled` scope, the scope MUST be treated as READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`). Missing types
|
|
86
|
+
When a scope declares a `scope-type` but any corresponding `{scope-type}-scope-type.md` policy is absent from the workspace or defined only in a `disabled` scope, the scope MUST be treated as READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`). Missing or disabled-only types MUST be fixed in local scopes.
|
|
87
87
|
|
|
88
88
|
#### 11-def-scope-type-must-be-structured
|
|
89
89
|
|
|
@@ -103,7 +103,7 @@ The word `core` as a hyphen-delimited segment in a policy filename title is rese
|
|
|
103
103
|
|
|
104
104
|
#### 14-local-placement
|
|
105
105
|
|
|
106
|
-
Local meta-policy files MUST be placed in the `principles` subject of any type folder within the scope they govern (e.g., `my-team/adrs/principles/001-core.md`). They apply implicitly to that scope — no declaration in `index.md` is required. Local meta-policies MUST NOT be placed in any other subject folder; a file whose title starts with `core` in a non-`principles` subject is
|
|
106
|
+
Local meta-policy files MUST be placed in the `principles` subject of any type folder within the scope they govern (e.g., `my-team/adrs/principles/001-core.md`). They apply implicitly to that scope — no declaration in `index.md` is required. Local meta-policies MUST NOT be placed in any other subject folder; a file whose title starts with `core` in a non-`principles` subject is invalid.
|
|
107
107
|
|
|
108
108
|
#### 15-local-primary-required
|
|
109
109
|
|
|
@@ -180,7 +180,7 @@ When a policy (scope-type definition, local meta-policy, or companion) overrides
|
|
|
180
180
|
- The policy file where the original rule is defined.
|
|
181
181
|
- A short explanation of why the override is required for this scope or type.
|
|
182
182
|
|
|
183
|
-
A `## Conflicts` section MAY also document cross-scope-type incompatibilities for informational purposes. Tools and agents MUST detect undeclared conflicts at review time and surface them as errors. Semantic conflict detection is the responsibility of the authoring agent
|
|
183
|
+
A `## Conflicts` section MAY also document cross-scope-type incompatibilities for informational purposes. Tools and agents MUST detect undeclared conflicts at review time and surface them as errors. Semantic conflict detection is the responsibility of the authoring agent.
|
|
184
184
|
|
|
185
185
|
## References
|
|
186
186
|
|
|
@@ -213,7 +213,7 @@ A scope `extends:` declaration MUST NOT reference the declaring scope itself. To
|
|
|
213
213
|
|
|
214
214
|
#### 32-extends-mandatory-presence
|
|
215
215
|
|
|
216
|
-
Every scope listed in a scope's `extends:` field MUST exist in the workspace with an accessible `index.md`. If any referenced scope is absent or its `index.md` cannot be read,
|
|
216
|
+
Every scope listed in a scope's `extends:` field MUST exist in the workspace with an accessible `index.md`. If any referenced scope is absent or its `index.md` cannot be read, the extending scope MUST be fixed when local and is READ-ONLY when external (`_core-adr-policy-022.06-read-only-dependencies`).
|
|
217
217
|
|
|
218
218
|
#### 33-extends-content-precedence
|
|
219
219
|
|
|
@@ -73,7 +73,7 @@ Distributing `-core` scopes alongside their companion consumable scope to downst
|
|
|
73
73
|
|
|
74
74
|
#### 10-no-type-combination
|
|
75
75
|
|
|
76
|
-
A scope is core-type when its resolved scope-type chain, including parent types (see `_core-adr-policy-010.09-def-parent-scope-type`), contains `core`. A core-type scope MUST declare exactly one scope type; combining a core-type with any other type (e.g., `scope-type: core, standard`) is
|
|
76
|
+
A scope is core-type when its resolved scope-type chain, including parent types (see `_core-adr-policy-010.09-def-parent-scope-type`), contains `core`. A core-type scope MUST declare exactly one scope type; combining a core-type with any other type (e.g., `scope-type: core, standard`) is invalid. Core-type scopes hold meta governance only, so mixing them with consumable types would blur rules 04 and 05.
|
|
77
77
|
|
|
78
78
|
## References
|
|
79
79
|
|
|
@@ -9,21 +9,21 @@ valid-from: 2026-07-20
|
|
|
9
9
|
|
|
10
10
|
## Context and Problem Statement
|
|
11
11
|
|
|
12
|
-
Packages install scope folders into a workspace, but nothing guaranteed that every installed scope was listed in the root `index.md`. Scopes that were meant to be reused only through `extends:` were hidden from the root index, so agents could not discover them, and consumers had no way to switch off a scope they installed but did not want to apply. When a scope depended on another scope that was absent, the result was either a hard
|
|
12
|
+
Packages install scope folders into a workspace, but nothing guaranteed that every installed scope was listed in the root `index.md`. Scopes that were meant to be reused only through `extends:` were hidden from the root index, so agents could not discover them, and consumers had no way to switch off a scope they installed but did not want to apply. When a scope depended on another scope that was absent, the result was either a hard failure or silent misbehaviour.
|
|
13
13
|
|
|
14
14
|
How does a consumer control which installed scopes apply globally, which apply only through `extends:`, and which are ignored, while keeping every scope discoverable?
|
|
15
15
|
|
|
16
16
|
## Decision Outcome
|
|
17
17
|
|
|
18
|
-
**Every installed scope is listed once in the root index; the consumer controls activation with an inline activation tag next to the scope link, and the scope author declares intent with an `extends-only` frontmatter field that
|
|
18
|
+
**Every installed scope is listed once in the root index; the consumer controls activation with an inline activation tag next to the scope link, and the scope author declares intent with an `extends-only` frontmatter field that MUST be coherent with the tag.**
|
|
19
19
|
|
|
20
|
-
Policies of listed scopes apply globally by default. An activation tag narrows a scope to `extends-only` or turns it off with `disabled`. Agents rely only on the tags. Scopes whose dependencies are missing, disabled, or invalid become read-only.
|
|
20
|
+
Policies of listed scopes apply globally by default. An activation tag narrows a scope to `extends-only` or turns it off with `disabled`. Agents rely only on the tags. Scopes whose dependencies are missing, disabled, or invalid become read-only; local scopes MUST NOT stay in that state.
|
|
21
21
|
|
|
22
22
|
### Details
|
|
23
23
|
|
|
24
24
|
#### 01-root-index-completeness
|
|
25
25
|
|
|
26
|
-
Every scope folder under the XDRS root that contains an `index.md`, including scopes installed from external packages, MUST be linked exactly once from the root `index.md`. The `_local` scope MUST NOT be linked (see `_core-adr-policy-001`). A scope folder with no link, or a scope linked more than once, is
|
|
26
|
+
Every scope folder under the XDRS root that contains an `index.md`, including scopes installed from external packages, MUST be linked exactly once from the root `index.md`. The `_local` scope MUST NOT be linked (see `_core-adr-policy-001`). A scope folder with no link, or a scope linked more than once, is invalid. Links that resolve to the same scope index (e.g. `./x/index.md` and `x/index.md`) count as the same link. Links inside fenced code blocks are ignored.
|
|
27
27
|
|
|
28
28
|
#### 02-global-applicability
|
|
29
29
|
|
|
@@ -31,23 +31,26 @@ Policies of a listed scope without an activation tag MUST be applied globally, n
|
|
|
31
31
|
|
|
32
32
|
#### 03-activation-tag-syntax
|
|
33
33
|
|
|
34
|
-
An activation tag is an inline code span placed immediately after a scope link on the same line, optionally separated by spaces or tabs, for example a link to `payments-ref/index.md` followed by `` `extends-only` ``. Allowed values are `extends-only` and `disabled`, lowercase only. A scope link line MUST NOT contain any other inline code: an unknown value, a second tag, or inline code placed elsewhere on the line is
|
|
34
|
+
An activation tag is an inline code span placed immediately after a scope link on the same line, optionally separated by spaces or tabs, for example a link to `payments-ref/index.md` followed by `` `extends-only` ``. Allowed values are `extends-only` and `disabled`, lowercase only. A scope link line MUST NOT contain any other inline code: an unknown value, a second tag, or inline code placed elsewhere on the line is invalid. A tag on the next line is not a tag. `_core` MUST NOT be tagged.
|
|
35
35
|
|
|
36
36
|
#### 04-extends-only
|
|
37
37
|
|
|
38
|
-
Policies of a scope tagged `extends-only` MUST be applied only through the `extends:` chain of an active scope (untagged, including read-only scopes, or `_local`). Reach is transitive: if A extends B and B extends C, both B and C apply to A. The root index position of a tagged scope never matters for conflict resolution; `extends:` precedence applies (see `_core-adr-policy-010.33-extends-content-precedence`). Skills, articles, research, and initiatives of the scope remain usable.
|
|
38
|
+
Policies of a scope tagged `extends-only` MUST be applied only through the `extends:` chain of an active scope (untagged, including read-only scopes, or `_local`). Reach is transitive: if A extends B and B extends C, both B and C apply to A. The root index position of a tagged scope never matters for conflict resolution; `extends:` precedence applies (see `_core-adr-policy-010.33-extends-content-precedence`). Skills, articles, research, and initiatives of the scope remain usable. Every `extends-only` scope MUST be reached by at least one active scope; reach only through a `disabled` scope does not count. Fix it by adding the scope to the `extends:` of an active scope or by tagging it `disabled`.
|
|
39
39
|
|
|
40
40
|
#### 05-disabled
|
|
41
41
|
|
|
42
|
-
All content of a scope tagged `disabled` MUST be ignored by agents: its policies, skills, articles, research, and initiatives are not applied, and nothing is inherited from it through `extends:` or `follows:`. The scope is still
|
|
42
|
+
All content of a scope tagged `disabled` MUST be ignored by agents: its policies, skills, articles, research, and initiatives are not applied, and nothing is inherited from it through `extends:` or `follows:`. The scope is still validated and MAY be edited; a disabled local scope follows rule 06 like any other local scope. Skills of a disabled scope that are symlinked into `.agents/skills` (see `_core-adr-policy-018`) stay loaded by tools and MUST be removed by the consumer.
|
|
43
43
|
|
|
44
44
|
#### 06-read-only-dependencies
|
|
45
45
|
|
|
46
|
-
A scope MUST be treated as READ-ONLY when a scope it follows or extends, or the scope that defines one of its scope types (including parent types), is missing, disabled, or invalid, or when it is an external scope and its own `index.md` is invalid. Content in a READ-ONLY scope MUST NOT be added, changed, or removed.
|
|
46
|
+
A scope MUST be treated as READ-ONLY when a scope it follows or extends, or the scope that defines one of its scope types (including parent types), is missing, disabled, or invalid, or when it is an external scope and its own `index.md` is invalid. Content in a READ-ONLY scope MUST NOT be added, changed, or removed.
|
|
47
|
+
|
|
48
|
+
- **Local scope** (not tracked in `.filedist.lock`, including `_local` and disabled scopes): a read-only local scope is frozen and cannot be maintained, so each cause is invalid and MUST be fixed.
|
|
49
|
+
- **External scope** (tracked in `.filedist.lock`): the scope is READ-ONLY; agents MUST tell the user when they skip a change because of it, naming the cause and fix.
|
|
47
50
|
|
|
48
51
|
#### 07-extends-only-field
|
|
49
52
|
|
|
50
|
-
A scope author MAY declare `extends-only: true` or `extends-only: false` in the scope index frontmatter. Only unquoted lowercase `true` and `false` are valid. An invalid value
|
|
53
|
+
A scope author MAY declare `extends-only: true` or `extends-only: false` in the scope index frontmatter. Only unquoted lowercase `true` and `false` are valid. An invalid value in a local scope MUST be fixed; in an external scope it makes the scope READ-ONLY and the value is treated as absent. `extends-only` (field `true` or tag) MUST NOT be used on `_core`, `_local`, or a core-type scope. Coherence: when the field is `true`, the root index link MUST carry the tag `extends-only` or `disabled`; when the field is `false` or absent, the consumer MAY add either tag (narrowing), and MUST NOT widen a scope beyond what the author declared. Agents MUST NOT use the field for activation (rule 08).
|
|
51
54
|
|
|
52
55
|
#### 08-agent-resolution
|
|
53
56
|
|
|
@@ -9,4 +9,3 @@ This module is shared across all XDRS writing skills. Run these steps after all
|
|
|
9
9
|
npx -y xdrs-core@latest lint
|
|
10
10
|
```
|
|
11
11
|
2. Fix all reported errors before considering the task complete.
|
|
12
|
-
3. Review warnings; fix straightforward ones and note intentional deviations explicitly.
|
|
@@ -138,7 +138,7 @@ After writing each policy:
|
|
|
138
138
|
### Phase 6: Lint
|
|
139
139
|
|
|
140
140
|
1. Run `npx xdrs-core lint` from the workspace root (or `make lint` if a `Makefile` is present with a `lint` target).
|
|
141
|
-
2. Review all reported errors
|
|
141
|
+
2. Review all reported errors.
|
|
142
142
|
3. Fix only mechanical and structural errors (missing required frontmatter fields, naming violations, index link mismatches). Do NOT invent content to satisfy content-level findings.
|
|
143
143
|
4. Re-run lint after fixes until it passes or only unfixable findings remain.
|
|
144
144
|
|
package/lib/lint.js
CHANGED
|
@@ -99,18 +99,6 @@ function runLintCli(args) {
|
|
|
99
99
|
const targetPath = pathArgs[0] || '.';
|
|
100
100
|
const result = lintWorkspace(targetPath, { ignoreExternal: !all });
|
|
101
101
|
|
|
102
|
-
if (result.warnings.length > 0) {
|
|
103
|
-
for (const warning of result.warnings) {
|
|
104
|
-
console.warn(`- [warning] ${warning}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
for (const [scopeName, causes] of result.readOnlyScopes) {
|
|
109
|
-
for (const cause of causes) {
|
|
110
|
-
console.log(`- READ-ONLY scope "${scopeName}": ${cause}; no changes must be made to this scope [_core-adr-policy-022.06-read-only-dependencies]`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
102
|
if (result.errors.length === 0) {
|
|
115
103
|
console.log(`Lint passed for ${toDisplayPath(result.xdrsRoot)}`);
|
|
116
104
|
return 0;
|
|
@@ -147,12 +135,10 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
147
135
|
const resolvedTarget = path.resolve(targetPath);
|
|
148
136
|
const xdrsRoot = resolveXdrsRoot(resolvedTarget);
|
|
149
137
|
const errors = [];
|
|
150
|
-
const warnings = [];
|
|
151
|
-
const readOnlyScopes = new Map(); // scopeName -> [cause, ...] per _core-adr-policy-022.06
|
|
152
138
|
|
|
153
139
|
if (!existsDirectory(xdrsRoot)) {
|
|
154
140
|
errors.push(`Missing XDRS root directory: ${toDisplayPath(xdrsRoot)} [_core-adr-policy-001]`);
|
|
155
|
-
return { xdrsRoot, errors
|
|
141
|
+
return { xdrsRoot, errors };
|
|
156
142
|
}
|
|
157
143
|
|
|
158
144
|
const repoRoot = path.dirname(xdrsRoot);
|
|
@@ -247,13 +233,13 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
247
233
|
}
|
|
248
234
|
}
|
|
249
235
|
|
|
250
|
-
|
|
236
|
+
lintLocalReadOnlyDependencies(xdrsRoot, scopeMeta, activation, externalScopes, knownScopeTypes, scopeTypeDefiningScopes, errors);
|
|
251
237
|
|
|
252
238
|
// Populated during lintScopeIndexFrontmatter: scopeName -> [extendedScopeNames...]
|
|
253
239
|
const extendsRelationships = new Map();
|
|
254
240
|
|
|
255
241
|
for (const scopeEntry of scopeEntries) {
|
|
256
|
-
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors,
|
|
242
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, extendsRelationships, externalScopes);
|
|
257
243
|
}
|
|
258
244
|
|
|
259
245
|
// Detect cycles in extends: chains (DFS)
|
|
@@ -283,13 +269,13 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
283
269
|
if (!existsFile(rootIndexPath)) {
|
|
284
270
|
errors.push('Missing required root index: .xdrs/index.md [_core-adr-policy-001]');
|
|
285
271
|
} else {
|
|
286
|
-
lintRootIndex(rootIndexPath, xdrsRoot, errors,
|
|
272
|
+
lintRootIndex(rootIndexPath, xdrsRoot, errors, effectiveExternalScopes, { activation, scopeMeta, knownScopeTypes });
|
|
287
273
|
}
|
|
288
274
|
|
|
289
|
-
return { xdrsRoot, errors
|
|
275
|
+
return { xdrsRoot, errors };
|
|
290
276
|
}
|
|
291
277
|
|
|
292
|
-
function lintRootIndex(rootIndexPath, xdrsRoot, errors,
|
|
278
|
+
function lintRootIndex(rootIndexPath, xdrsRoot, errors, externalScopes, ctx) {
|
|
293
279
|
const content = fs.readFileSync(rootIndexPath, 'utf8');
|
|
294
280
|
const repoRoot = path.dirname(xdrsRoot);
|
|
295
281
|
|
|
@@ -360,7 +346,7 @@ function lintRootIndex(rootIndexPath, xdrsRoot, errors, warnings, externalScopes
|
|
|
360
346
|
}
|
|
361
347
|
for (const [scopeName, entry] of activation) {
|
|
362
348
|
if (entry.tag === 'extends-only' && !reached.has(scopeName)) {
|
|
363
|
-
|
|
349
|
+
errors.push(`Scope "${scopeName}" is tagged \`extends-only\` but no active scope extends it, so its policies never apply; add it to the extends: of an active scope or tag it \`disabled\`: ${display} [_core-adr-policy-022.04-extends-only]`);
|
|
364
350
|
}
|
|
365
351
|
}
|
|
366
352
|
}
|
|
@@ -443,30 +429,19 @@ function readScopeMeta(scopeIndexPath) {
|
|
|
443
429
|
};
|
|
444
430
|
}
|
|
445
431
|
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
function
|
|
432
|
+
// Reports local scopes that would be READ-ONLY per _core-adr-policy-022.06 because of a disabled or
|
|
433
|
+
// invalid dependency. Missing dependencies are reported by lintScopeIndexFrontmatter; external scopes are never reported.
|
|
434
|
+
function lintLocalReadOnlyDependencies(xdrsRoot, scopeMeta, activation, externalScopes, knownScopeTypes, scopeTypeDefiningScopes, errors) {
|
|
449
435
|
const isDisabled = (scopeName) => (activation.get(scopeName) || {}).tag === 'disabled';
|
|
450
436
|
for (const [scopeName, meta] of scopeMeta) {
|
|
451
|
-
if (!meta) continue;
|
|
452
|
-
const isExternal = externalScopes.has(scopeName);
|
|
437
|
+
if (!meta || externalScopes.has(scopeName)) continue;
|
|
453
438
|
const causes = [];
|
|
454
|
-
if (isExternal && !meta.hasFrontmatter) {
|
|
455
|
-
causes.push('its scope index has no frontmatter; fix it in the source package');
|
|
456
|
-
}
|
|
457
|
-
if (isExternal && meta.extendsOnly !== null && meta.extendsOnly !== 'true' && meta.extendsOnly !== 'false') {
|
|
458
|
-
causes.push(`its scope index has an invalid extends-only value "${meta.extendsOnly}" (treated as absent); fix it in the source package`);
|
|
459
|
-
}
|
|
460
|
-
if (isExternal && meta.hasFrontmatter && meta.scopeTypes.length === 0) {
|
|
461
|
-
causes.push('its scope index has no scope-type; fix it in the source package');
|
|
462
|
-
}
|
|
463
439
|
for (const [field, targets] of [['follows', meta.follows], ['extends', meta.extends]]) {
|
|
464
440
|
for (const target of targets) {
|
|
465
441
|
if (target === scopeName || RESERVED_SCOPES.has(target)) continue;
|
|
466
442
|
const targetMeta = scopeMeta.get(target);
|
|
467
|
-
if (!targetMeta)
|
|
468
|
-
|
|
469
|
-
} else if (isDisabled(target)) {
|
|
443
|
+
if (!targetMeta) continue;
|
|
444
|
+
if (isDisabled(target)) {
|
|
470
445
|
causes.push(`${field}: scope "${target}" is tagged \`disabled\` in the root index; enable it or remove the ${field}: reference`);
|
|
471
446
|
} else if (!targetMeta.hasFrontmatter) {
|
|
472
447
|
causes.push(`${field}: scope "${target}" has an invalid scope index; fix it`);
|
|
@@ -474,18 +449,15 @@ function collectReadOnlyScopes(scopeMeta, activation, externalScopes, knownScope
|
|
|
474
449
|
}
|
|
475
450
|
}
|
|
476
451
|
for (const scopeType of resolveScopeTypeChain(meta.scopeTypes, knownScopeTypes)) {
|
|
477
|
-
if (scopeType === '_local') continue;
|
|
478
|
-
if (!knownScopeTypes.has(scopeType)) {
|
|
479
|
-
if (isExternal) causes.push(`scope type "${scopeType}" has no ${scopeType}-scope-type policy in this workspace; install the scope that defines it`);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
452
|
+
if (scopeType === '_local' || !knownScopeTypes.has(scopeType)) continue;
|
|
482
453
|
const definers = [...scopeTypeDefiningScopes.get(scopeType)].filter((s) => s !== scopeName);
|
|
483
454
|
if (definers.length > 0 && definers.every(isDisabled)) {
|
|
484
455
|
causes.push(`scope type "${scopeType}" is defined only in disabled scope(s) ${definers.map((s) => `"${s}"`).join(', ')}; enable one of them`);
|
|
485
456
|
}
|
|
486
457
|
}
|
|
487
|
-
|
|
488
|
-
|
|
458
|
+
const display = toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'));
|
|
459
|
+
for (const cause of causes) {
|
|
460
|
+
errors.push(`Local scope "${scopeName}" is frozen (read-only) because ${cause}: ${display} [_core-adr-policy-022.06-read-only-dependencies]`);
|
|
489
461
|
}
|
|
490
462
|
}
|
|
491
463
|
}
|
|
@@ -787,8 +759,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
787
759
|
}
|
|
788
760
|
const followedIndexPath = path.join(xdrsRoot, entry, 'index.md');
|
|
789
761
|
if (!existsFile(followedIndexPath)) {
|
|
790
|
-
//
|
|
791
|
-
// (collected by collectReadOnlyScopes per _core-adr-policy-022.06), not a lint error.
|
|
762
|
+
// A missing dependency of an external scope makes it READ-ONLY (_core-adr-policy-022.06); lint does not report it.
|
|
792
763
|
if (!isExternal) {
|
|
793
764
|
errors.push(`Scope index frontmatter follows references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
794
765
|
}
|
|
@@ -858,7 +829,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
858
829
|
}
|
|
859
830
|
|
|
860
831
|
|
|
861
|
-
function lintScopeDirectory(xdrsRoot, scopeName, errors,
|
|
832
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), extendsRelationships = new Map(), trackedExternalScopes = new Set()) {
|
|
862
833
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
863
834
|
|
|
864
835
|
if (ignoreExternal && externalScopes.has(scopeName)) {
|
|
@@ -897,7 +868,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, warnings, actualTypeInd
|
|
|
897
868
|
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
898
869
|
continue;
|
|
899
870
|
}
|
|
900
|
-
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors,
|
|
871
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes, externalScopes, isCoreTypeNonExempt, isCompiledScope);
|
|
901
872
|
const typeIndexPath = path.join(entryPath, 'index.md');
|
|
902
873
|
if (existsFile(typeIndexPath)) {
|
|
903
874
|
typeIndexesInScope.push(typeIndexPath);
|
|
@@ -1028,7 +999,7 @@ function lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope = fa
|
|
|
1028
999
|
}
|
|
1029
1000
|
}
|
|
1030
1001
|
|
|
1031
|
-
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors,
|
|
1002
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
1032
1003
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
1033
1004
|
const indexPath = path.join(typePath, 'index.md');
|
|
1034
1005
|
const xdrsNumbers = new Map();
|
|
@@ -1055,11 +1026,10 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
1055
1026
|
errors.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
1056
1027
|
continue;
|
|
1057
1028
|
}
|
|
1058
|
-
// All policies are frozen:
|
|
1059
|
-
warnings.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001] [exempt: freeze-reference]`);
|
|
1029
|
+
// All policies are frozen (_core-adr-policy-002.01): skip the check, keep content-level checks.
|
|
1060
1030
|
}
|
|
1061
1031
|
|
|
1062
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors,
|
|
1032
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrsNumbers, errors, externalScopes, enforceStructured, isCompiledScope));
|
|
1063
1033
|
}
|
|
1064
1034
|
|
|
1065
1035
|
if (existsFile(indexPath)) {
|
|
@@ -1067,7 +1037,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, warnings, actu
|
|
|
1067
1037
|
}
|
|
1068
1038
|
}
|
|
1069
1039
|
|
|
1070
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors,
|
|
1040
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNumbers, errors, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
1071
1041
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
1072
1042
|
const artifacts = [];
|
|
1073
1043
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -1106,7 +1076,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
1106
1076
|
}
|
|
1107
1077
|
|
|
1108
1078
|
artifacts.push(entryPath);
|
|
1109
|
-
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors,
|
|
1079
|
+
lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, entryPath, xdrsNumbers, errors, externalScopes, enforceStructured, isCompiledScope);
|
|
1110
1080
|
}
|
|
1111
1081
|
|
|
1112
1082
|
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
@@ -1116,7 +1086,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
1116
1086
|
return artifacts;
|
|
1117
1087
|
}
|
|
1118
1088
|
|
|
1119
|
-
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors,
|
|
1089
|
+
function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePath, xdrsNumbers, errors, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
|
|
1120
1090
|
const baseName = path.basename(filePath);
|
|
1121
1091
|
const match = baseName.match(NUMBERED_FILE_RE);
|
|
1122
1092
|
if (!match) {
|
|
@@ -1136,26 +1106,23 @@ function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePat
|
|
|
1136
1106
|
}
|
|
1137
1107
|
|
|
1138
1108
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
1139
|
-
lintPolicyNumberRange(number, subjectName, content, filePath, errors
|
|
1109
|
+
lintPolicyNumberRange(number, subjectName, content, filePath, errors);
|
|
1140
1110
|
const { freezeReference } = extractFrontmatter(content);
|
|
1141
1111
|
const expectedHeader = `# ${scopeName}-${TYPE_TO_ID[typeName]}-${number}:`;
|
|
1142
1112
|
const firstLine = firstNonEmptyLine(stripFrontmatter(content));
|
|
1143
1113
|
if (!freezeReference && !firstLine.startsWith(expectedHeader)) {
|
|
1144
1114
|
errors.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
1145
|
-
} else if (freezeReference && !firstLine.startsWith(expectedHeader)) {
|
|
1146
|
-
warnings.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)} [_core-adr-policy-002] [exempt: freeze-reference]`);
|
|
1147
1115
|
}
|
|
1148
1116
|
|
|
1149
1117
|
// Enforce 'core' word reservation: only allowed as first word of filename title in principles/
|
|
1150
1118
|
const titleParts = match[2].split('-');
|
|
1151
|
-
if (titleParts.includes('core') && !(subjectName === 'principles' && titleParts[0] === 'core')) {
|
|
1152
|
-
|
|
1153
|
-
if (freezeReference) { warnings.push(`${msg} [exempt: freeze-reference]`); } else { errors.push(msg); }
|
|
1119
|
+
if (!freezeReference && titleParts.includes('core') && !(subjectName === 'principles' && titleParts[0] === 'core')) {
|
|
1120
|
+
errors.push(`Policy filename must not use "core" as a name word unless it is a local meta-policy in "principles/" starting with "core" (e.g., "NNN-core.md" or "NNN-core-{qualifier}.md"): ${toDisplayPath(filePath)} [_core-adr-policy-010.13-local-naming]`);
|
|
1154
1121
|
}
|
|
1155
1122
|
|
|
1156
1123
|
const expectedName = extractExpectedXdrsNameFromHeading(firstLine)
|
|
1157
1124
|
|| `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
|
|
1158
|
-
lintXdrsElementFrontmatter(content, expectedName, filePath, errors,
|
|
1125
|
+
lintXdrsElementFrontmatter(content, expectedName, filePath, errors, freezeReference);
|
|
1159
1126
|
if (enforceStructured) {
|
|
1160
1127
|
validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-010.12-def-core-scope-policies-must-be-structured');
|
|
1161
1128
|
} else if (isCompiledScope) {
|
|
@@ -1198,7 +1165,7 @@ function slugifyTitle(title) {
|
|
|
1198
1165
|
.replace(/--+/g, '-');
|
|
1199
1166
|
}
|
|
1200
1167
|
|
|
1201
|
-
function lintXdrsElementFrontmatter(content, expectedName, filePath, errors,
|
|
1168
|
+
function lintXdrsElementFrontmatter(content, expectedName, filePath, errors, freezeReference = false) {
|
|
1202
1169
|
const fm = extractFrontmatter(content);
|
|
1203
1170
|
if (!fm.present) {
|
|
1204
1171
|
errors.push(`Policy must start with a YAML frontmatter block: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
@@ -1207,9 +1174,8 @@ function lintXdrsElementFrontmatter(content, expectedName, filePath, errors, war
|
|
|
1207
1174
|
if (!fm.name) {
|
|
1208
1175
|
errors.push(`Policy frontmatter must include a non-empty name field: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
1209
1176
|
} else {
|
|
1210
|
-
if (fm.name !== expectedName) {
|
|
1211
|
-
|
|
1212
|
-
if (freezeReference) { warnings.push(`${msg} [exempt: freeze-reference]`); } else { errors.push(msg); }
|
|
1177
|
+
if (fm.name !== expectedName && !freezeReference) {
|
|
1178
|
+
errors.push(`Policy frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
1213
1179
|
}
|
|
1214
1180
|
if (fm.name.endsWith('-')) {
|
|
1215
1181
|
errors.push(`Policy frontmatter name must not end with a hyphen: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
@@ -1351,7 +1317,6 @@ function lintSkillsDirectory(xdrsRoot, scopeName, skillsPath, errors, externalSc
|
|
|
1351
1317
|
lintNoEmojis(skillContent, skillFilePath, 'Skill', errors);
|
|
1352
1318
|
lintWordCount(skillContent, skillFilePath, 'Skill', SKILL_MAX_WORDS, errors);
|
|
1353
1319
|
lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors, externalScopes);
|
|
1354
|
-
lintOrphanAssets(path.join(entryPath, RESOURCE_DIR_NAME), [skillFilePath], xdrsRoot, errors);
|
|
1355
1320
|
}
|
|
1356
1321
|
|
|
1357
1322
|
return artifacts;
|
|
@@ -1916,19 +1881,17 @@ function hasAnyNonFrozenPolicy(dirPath) {
|
|
|
1916
1881
|
return false;
|
|
1917
1882
|
}
|
|
1918
1883
|
|
|
1919
|
-
function lintPolicyNumberRange(number, subjectName, content, filePath, errors
|
|
1920
|
-
|
|
1884
|
+
function lintPolicyNumberRange(number, subjectName, content, filePath, errors) {
|
|
1885
|
+
if (extractFrontmatter(content).freezeReference) return;
|
|
1921
1886
|
const num = parseInt(number, 10);
|
|
1922
1887
|
if (num >= RESERVED_NUMBER_RANGE_MIN && num <= RESERVED_NUMBER_RANGE_MAX) {
|
|
1923
|
-
|
|
1924
|
-
if (fm.freezeReference) { warnings.push(`${msg} [exempt: freeze-reference]`); } else { errors.push(msg); }
|
|
1888
|
+
errors.push(`Policy number ${number} is in the reserved range (801–900) which MUST NOT be assigned: ${toDisplayPath(filePath)} [_core-adr-policy-017.05-reserved-gap-must-not-be-assigned]`);
|
|
1925
1889
|
return;
|
|
1926
1890
|
}
|
|
1927
1891
|
if (num < OVERFLOW_RANGE_MIN) {
|
|
1928
1892
|
const range = SUBJECT_NUMBER_RANGES[subjectName];
|
|
1929
1893
|
if (range && (num < range[0] || num > range[1])) {
|
|
1930
|
-
|
|
1931
|
-
if (fm.freezeReference) { warnings.push(`${msg} [exempt: freeze-reference]`); } else { errors.push(msg); }
|
|
1894
|
+
errors.push(`Policy number ${number} is outside the ${range[0]}–${range[1]} block reserved for subject "${subjectName}": ${toDisplayPath(filePath)} [_core-adr-policy-017.01-numbering-blocks-per-subject]`);
|
|
1932
1895
|
}
|
|
1933
1896
|
}
|
|
1934
1897
|
}
|
|
@@ -2238,6 +2201,8 @@ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalSc
|
|
|
2238
2201
|
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
2239
2202
|
const documentDir = path.dirname(documentPath);
|
|
2240
2203
|
const resourceDir = path.join(documentDir, RESOURCE_DIR_NAME);
|
|
2204
|
+
const isSkillFile = path.basename(documentPath) === 'SKILL.md' && path.basename(path.dirname(documentDir)) === 'skills';
|
|
2205
|
+
const sharedSkillResourceDir = isSkillFile ? path.join(path.dirname(documentDir), RESOURCE_DIR_NAME) : null;
|
|
2241
2206
|
const localScopePath = normalizePath(path.join(xdrsRoot, '_local'));
|
|
2242
2207
|
const repoRoot = path.dirname(xdrsRoot);
|
|
2243
2208
|
|
|
@@ -2267,7 +2232,8 @@ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalSc
|
|
|
2267
2232
|
errors.push(`Non-_local document must not link into _local scope in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
2268
2233
|
}
|
|
2269
2234
|
|
|
2270
|
-
if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)
|
|
2235
|
+
if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)
|
|
2236
|
+
&& !(sharedSkillResourceDir && isPathInside(sharedSkillResourceDir, link.resolvedPath))) {
|
|
2271
2237
|
errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
2272
2238
|
}
|
|
2273
2239
|
|
package/lib/lint.test.js
CHANGED
|
@@ -2245,22 +2245,35 @@ test('reports error when follows references a scope not in workspace', () => {
|
|
|
2245
2245
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
2246
2246
|
|
|
2247
2247
|
expect(result.errors.join('\n')).toContain('follows references scope "nonexistent-core" which does not exist in the workspace');
|
|
2248
|
-
expect(result.readOnlyScopes.has('myteam')).toBe(false);
|
|
2249
2248
|
});
|
|
2250
2249
|
|
|
2251
|
-
test('
|
|
2252
|
-
const workspaceRoot = createWorkspace(
|
|
2250
|
+
test.each([[true], [false]])('does not report a missing follows target of an external scope (ignoreExternal=%s) [022.06]', (ignoreExternal) => {
|
|
2251
|
+
const workspaceRoot = createWorkspace(`follows-missing-scope-external-${ignoreExternal}`, {
|
|
2253
2252
|
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
2254
2253
|
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nfollows: nonexistent-core\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
2255
2254
|
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
2256
2255
|
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
2257
|
-
'.filedist.lock': '.xdrs/myteam/adrs/principles/001-team.md|some-package|1.0.0\n',
|
|
2256
|
+
'.filedist.lock': '.xdrs/myteam/index.md|some-package|1.0.0\n.xdrs/myteam/adrs/index.md|some-package|1.0.0\n.xdrs/myteam/adrs/principles/001-team.md|some-package|1.0.0\n',
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal });
|
|
2260
|
+
|
|
2261
|
+
expect(result.errors.join('\n')).not.toContain('nonexistent-core');
|
|
2262
|
+
expect(result.errors.join('\n')).not.toContain('022.06');
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
test('does not report a missing extends target of an external scope [022.06]', () => {
|
|
2266
|
+
const workspaceRoot = createWorkspace('extends-missing-scope-external', {
|
|
2267
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
2268
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nextends: ghost-lib\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
2269
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
2270
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
2271
|
+
'.filedist.lock': '.xdrs/myteam/index.md|some-package|1.0.0\n',
|
|
2258
2272
|
});
|
|
2259
2273
|
|
|
2260
2274
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
2261
2275
|
|
|
2262
|
-
expect(result.errors.join('\n')).not.toContain('
|
|
2263
|
-
expect(result.readOnlyScopes.has('myteam')).toBe(true);
|
|
2276
|
+
expect(result.errors.join('\n')).not.toContain('ghost-lib');
|
|
2264
2277
|
});
|
|
2265
2278
|
|
|
2266
2279
|
test('accepts follows scope that exists in workspace', () => {
|
|
@@ -2525,7 +2538,6 @@ test('does not warn when extends-referenced scope also appears in root index', (
|
|
|
2525
2538
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
2526
2539
|
|
|
2527
2540
|
expect(result.errors.join('\n')).not.toContain('[_core-adr-policy-010.35-extends-index]');
|
|
2528
|
-
expect(result.warnings.join('\n')).not.toContain('[_core-adr-policy-010.35-extends-index]');
|
|
2529
2541
|
expect(result.errors.join('\n')).not.toContain('_core-adr-policy-022');
|
|
2530
2542
|
});
|
|
2531
2543
|
|
|
@@ -2664,6 +2676,56 @@ test('reports orphan asset in articles .assets directory', () => {
|
|
|
2664
2676
|
expect(result.errors.join('\n')).not.toContain('used.png');
|
|
2665
2677
|
});
|
|
2666
2678
|
|
|
2679
|
+
test('does not report orphan asset errors for unlinked files in a skill package .assets directory', () => {
|
|
2680
|
+
const workspaceRoot = createWorkspace('skill-assets-not-orphan', {
|
|
2681
|
+
'.xdrs/index.md': rootIndex(),
|
|
2682
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2683
|
+
'- [check-links](principles/skills/check-links/SKILL.md) - Check links'
|
|
2684
|
+
]),
|
|
2685
|
+
'.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
|
|
2686
|
+
'.xdrs/_local/adrs/principles/skills/check-links/.assets/template.json': '{}',
|
|
2687
|
+
'.xdrs/_local/adrs/principles/skills/check-links/.assets/run.sh': 'echo run\n',
|
|
2688
|
+
'.xdrs/_local/adrs/principles/skills/check-links/.assets/node_modules/pkg/index.js': 'module.exports = {};\n',
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2692
|
+
|
|
2693
|
+
expect(result.errors.join('\n')).not.toContain('Orphan asset file');
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
test('allows SKILL.md links to the shared skills .assets directory', () => {
|
|
2697
|
+
const workspaceRoot = createWorkspace('skill-shared-assets-link', {
|
|
2698
|
+
'.xdrs/index.md': rootIndex(),
|
|
2699
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2700
|
+
'- [check-links](principles/skills/check-links/SKILL.md) - Check links'
|
|
2701
|
+
]),
|
|
2702
|
+
'.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Follow [shared check](../.assets/shared-check.md).'),
|
|
2703
|
+
'.xdrs/_local/adrs/principles/skills/.assets/shared-check.md': '# Shared check\n',
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2707
|
+
|
|
2708
|
+
expect(result.errors.join('\n')).not.toContain('Asset links in');
|
|
2709
|
+
expect(result.errors.join('\n')).not.toContain('Broken asset link');
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
test('reports SKILL.md links to another skill package .assets directory', () => {
|
|
2713
|
+
const workspaceRoot = createWorkspace('skill-other-assets-link', {
|
|
2714
|
+
'.xdrs/index.md': rootIndex(),
|
|
2715
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
2716
|
+
'- [check-links](principles/skills/check-links/SKILL.md) - Check links',
|
|
2717
|
+
'- [other-skill](principles/skills/other-skill/SKILL.md) - Other skill'
|
|
2718
|
+
]),
|
|
2719
|
+
'.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Use [template](../other-skill/.assets/template.md).'),
|
|
2720
|
+
'.xdrs/_local/adrs/principles/skills/other-skill/SKILL.md': skillDocument('Test instructions.').replace('name: check-links', 'name: other-skill'),
|
|
2721
|
+
'.xdrs/_local/adrs/principles/skills/other-skill/.assets/template.md': '# Template\n',
|
|
2722
|
+
});
|
|
2723
|
+
|
|
2724
|
+
const result = lintWorkspace(workspaceRoot);
|
|
2725
|
+
|
|
2726
|
+
expect(result.errors.join('\n')).toContain('Asset links in');
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2667
2729
|
test('reports unexpected directories inside a skill package', () => {
|
|
2668
2730
|
const workspaceRoot = createWorkspace('unexpected-skill-package-directory', {
|
|
2669
2731
|
'.xdrs/index.md': rootIndex(),
|
|
@@ -5513,7 +5575,6 @@ function scopeFiles(name, extraFields = '', scopeType = 'standard') {
|
|
|
5513
5575
|
}
|
|
5514
5576
|
|
|
5515
5577
|
const activationErrors = (result) => result.errors.filter((e) => e.includes('_core-adr-policy-022')).join('\n');
|
|
5516
|
-
const activationWarnings = (result) => result.warnings.filter((e) => e.includes('_core-adr-policy-022')).join('\n');
|
|
5517
5578
|
|
|
5518
5579
|
test('passes activation checks when only _core is installed and linked (smoke)', () => {
|
|
5519
5580
|
const workspaceRoot = createWorkspace('activation-smoke', {
|
|
@@ -5522,7 +5583,7 @@ test('passes activation checks when only _core is installed and linked (smoke)',
|
|
|
5522
5583
|
});
|
|
5523
5584
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5524
5585
|
expect(activationErrors(result)).toBe('');
|
|
5525
|
-
expect(result.
|
|
5586
|
+
expect(Object.keys(result).sort()).toEqual(['errors', 'xdrsRoot']);
|
|
5526
5587
|
});
|
|
5527
5588
|
|
|
5528
5589
|
test('reports unlisted scope without type indexes with a paste-ready line [022.01]', () => {
|
|
@@ -5641,7 +5702,6 @@ test('accepts consumer narrowing: extends-only: false with an extends-only tag [
|
|
|
5641
5702
|
});
|
|
5642
5703
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5643
5704
|
expect(activationErrors(result)).toBe('');
|
|
5644
|
-
expect(activationWarnings(result)).toBe('');
|
|
5645
5705
|
});
|
|
5646
5706
|
|
|
5647
5707
|
test.each([['quoted', '"true"'], ['capitalised', 'True']])('reports %s extends-only value in a local scope [022.07]', (_label, value) => {
|
|
@@ -5653,7 +5713,7 @@ test.each([['quoted', '"true"'], ['capitalised', 'True']])('reports %s extends-o
|
|
|
5653
5713
|
expect(activationErrors(result)).toContain('extends-only must be unquoted lowercase true or false');
|
|
5654
5714
|
});
|
|
5655
5715
|
|
|
5656
|
-
test('
|
|
5716
|
+
test('does not report an invalid extends-only value in an external scope [022.07]', () => {
|
|
5657
5717
|
const workspaceRoot = createWorkspace('activation-field-invalid-external', {
|
|
5658
5718
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md)']),
|
|
5659
5719
|
...scopeFiles('lib', 'extends-only: yes\n'),
|
|
@@ -5661,7 +5721,6 @@ test('marks external scope with invalid extends-only value read-only, treated as
|
|
|
5661
5721
|
});
|
|
5662
5722
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5663
5723
|
expect(activationErrors(result)).toBe('');
|
|
5664
|
-
expect(result.readOnlyScopes.get('lib').join('\n')).toContain('invalid extends-only value "yes"');
|
|
5665
5724
|
});
|
|
5666
5725
|
|
|
5667
5726
|
test('reports extends-only on core-type and _local scopes [022.07]', () => {
|
|
@@ -5678,17 +5737,18 @@ test('reports extends-only on core-type and _local scopes [022.07]', () => {
|
|
|
5678
5737
|
expect(errors).toContain('_local/index.md [_core-adr-policy-022.07-extends-only-field]');
|
|
5679
5738
|
});
|
|
5680
5739
|
|
|
5681
|
-
test('
|
|
5740
|
+
test('reports an error when no active scope extends an extends-only scope [022.04]', () => {
|
|
5682
5741
|
const workspaceRoot = createWorkspace('activation-unused-extends-only', {
|
|
5683
5742
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `extends-only`', ACTIVATION_LEGEND]),
|
|
5684
5743
|
...scopeFiles('lib'),
|
|
5685
5744
|
});
|
|
5686
5745
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5687
|
-
expect(activationErrors(result)).
|
|
5688
|
-
expect(
|
|
5746
|
+
expect(activationErrors(result)).toContain('Scope "lib" is tagged `extends-only` but no active scope extends it');
|
|
5747
|
+
expect(activationErrors(result)).toContain('add it to the extends: of an active scope or tag it `disabled`');
|
|
5748
|
+
expect(activationErrors(result)).toContain('[_core-adr-policy-022.04-extends-only]');
|
|
5689
5749
|
});
|
|
5690
5750
|
|
|
5691
|
-
test('
|
|
5751
|
+
test('reports an extends-only scope reached only through a disabled scope, and the frozen extender [022.04, 022.06]', () => {
|
|
5692
5752
|
const workspaceRoot = createWorkspace('activation-reach-through-disabled', {
|
|
5693
5753
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `extends-only`', '[View scope mid](mid/index.md) `disabled`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
|
|
5694
5754
|
...scopeFiles('lib'),
|
|
@@ -5696,11 +5756,11 @@ test('warns when an extends-only scope is reached only through a disabled scope
|
|
|
5696
5756
|
...scopeFiles('myteam', 'extends: mid\n'),
|
|
5697
5757
|
});
|
|
5698
5758
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5699
|
-
expect(
|
|
5700
|
-
expect(result.
|
|
5759
|
+
expect(activationErrors(result)).toContain('Scope "lib" is tagged `extends-only`');
|
|
5760
|
+
expect(activationErrors(result)).toContain('Local scope "myteam" is frozen (read-only) because extends: scope "mid" is tagged `disabled`');
|
|
5701
5761
|
});
|
|
5702
5762
|
|
|
5703
|
-
test('
|
|
5763
|
+
test('reports a local scope following a disabled scope as frozen while it still reaches extends-only scopes [022.04, 022.06]', () => {
|
|
5704
5764
|
const workspaceRoot = createWorkspace('activation-read-only-reaches', {
|
|
5705
5765
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope area-core](area-core/index.md) `disabled`', '[View scope lib](lib/index.md) `extends-only`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
|
|
5706
5766
|
...scopeFiles('area-core', '', 'core'),
|
|
@@ -5708,22 +5768,24 @@ test('makes a scope following a disabled scope read-only while it still reaches
|
|
|
5708
5768
|
...scopeFiles('myteam', 'follows: area-core\nextends: lib\n'),
|
|
5709
5769
|
});
|
|
5710
5770
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5711
|
-
|
|
5712
|
-
expect(
|
|
5771
|
+
const errors = activationErrors(result);
|
|
5772
|
+
expect(errors).not.toContain('022.04');
|
|
5773
|
+
expect(errors).toContain('Local scope "myteam" is frozen (read-only) because follows: scope "area-core" is tagged `disabled` in the root index; enable it or remove the follows: reference: ');
|
|
5774
|
+
expect(errors).toContain('myteam/index.md [_core-adr-policy-022.06-read-only-dependencies]');
|
|
5713
5775
|
});
|
|
5714
5776
|
|
|
5715
|
-
test('
|
|
5777
|
+
test('does not report an external scope extending a disabled scope [022.06]', () => {
|
|
5716
5778
|
const workspaceRoot = createWorkspace('activation-external-extends-disabled', {
|
|
5717
5779
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope vendor](vendor/index.md)', ACTIVATION_LEGEND]),
|
|
5718
5780
|
...scopeFiles('lib'),
|
|
5719
5781
|
...scopeFiles('vendor', 'extends: lib\n'),
|
|
5720
5782
|
'.filedist.lock': '.xdrs/vendor/index.md|some-package|1.0.0\n',
|
|
5721
5783
|
});
|
|
5722
|
-
|
|
5723
|
-
expect(
|
|
5784
|
+
expect(activationErrors(lintWorkspace(workspaceRoot))).toBe('');
|
|
5785
|
+
expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).toBe('');
|
|
5724
5786
|
});
|
|
5725
5787
|
|
|
5726
|
-
test('
|
|
5788
|
+
test('reports _local extending a disabled scope as frozen [022.06]', () => {
|
|
5727
5789
|
const workspaceRoot = createWorkspace('activation-local-extends-disabled', {
|
|
5728
5790
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5729
5791
|
...scopeFiles('lib'),
|
|
@@ -5731,10 +5793,10 @@ test('makes _local extending a disabled scope read-only [022.06]', () => {
|
|
|
5731
5793
|
...coreWithScopeTypes(['standard', '_local']),
|
|
5732
5794
|
});
|
|
5733
5795
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5734
|
-
expect(result.
|
|
5796
|
+
expect(activationErrors(result)).toContain('Local scope "_local" is frozen (read-only)');
|
|
5735
5797
|
});
|
|
5736
5798
|
|
|
5737
|
-
test('
|
|
5799
|
+
test('reports a local scope whose type is defined only in a disabled scope as frozen [022.06]', () => {
|
|
5738
5800
|
const workspaceRoot = createWorkspace('activation-type-in-disabled-scope', {
|
|
5739
5801
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope widgets-core](widgets-core/index.md) `disabled`', '[View scope shop](shop/index.md)', ACTIVATION_LEGEND]),
|
|
5740
5802
|
'.xdrs/widgets-core/index.md': scopeIndexWith('widgets-core', '', 'core'),
|
|
@@ -5745,10 +5807,22 @@ test('makes a scope whose type is defined only in a disabled scope read-only [02
|
|
|
5745
5807
|
});
|
|
5746
5808
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5747
5809
|
expect(result.errors.join('\n')).not.toContain('has no corresponding widget-scope-type policy');
|
|
5748
|
-
expect(result
|
|
5810
|
+
expect(activationErrors(result)).toContain('Local scope "shop" is frozen (read-only) because scope type "widget" is defined only in disabled scope(s) "widgets-core"');
|
|
5811
|
+
});
|
|
5812
|
+
|
|
5813
|
+
test('reports missing dependencies of a disabled local scope as errors [022.05]', () => {
|
|
5814
|
+
const workspaceRoot = createWorkspace('activation-disabled-local-missing-deps', {
|
|
5815
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5816
|
+
...scopeFiles('myteam', 'follows: ghost-core\nextends: ghost-lib\n', 'phantom'),
|
|
5817
|
+
...coreWithScopeTypes(['standard']),
|
|
5818
|
+
});
|
|
5819
|
+
const errors = lintWorkspace(workspaceRoot, { ignoreExternal: false }).errors.join('\n');
|
|
5820
|
+
expect(errors).toContain('follows references scope "ghost-core" which does not exist in the workspace');
|
|
5821
|
+
expect(errors).toContain('ghost-lib');
|
|
5822
|
+
expect(errors).toContain('phantom');
|
|
5749
5823
|
});
|
|
5750
5824
|
|
|
5751
|
-
test('prints
|
|
5825
|
+
test('prints frozen local scopes as errors and never prints warning or READ-ONLY lines [022.06]', () => {
|
|
5752
5826
|
const workspaceRoot = createWorkspace('activation-cli-read-only', {
|
|
5753
5827
|
'.xdrs/index.md': rootIndex(['[View scope _core](_core/index.md)', '[View scope area-core](area-core/index.md) `disabled`', '[View scope lib](lib/index.md) `disabled`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
|
|
5754
5828
|
...scopeFiles('area-core', '', 'core'),
|
|
@@ -5756,22 +5830,40 @@ test('prints one read-only line per cause citing 022.06', () => {
|
|
|
5756
5830
|
...scopeFiles('myteam', 'follows: area-core\nextends: lib\n'),
|
|
5757
5831
|
});
|
|
5758
5832
|
const { runLintCli } = require('./lint');
|
|
5759
|
-
const
|
|
5760
|
-
const
|
|
5761
|
-
const
|
|
5762
|
-
const
|
|
5833
|
+
const out = [];
|
|
5834
|
+
const errOut = [];
|
|
5835
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation((msg) => out.push(msg));
|
|
5836
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation((msg) => errOut.push(msg));
|
|
5837
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation((msg) => out.push(msg));
|
|
5838
|
+
let code;
|
|
5763
5839
|
try {
|
|
5764
|
-
runLintCli(['--all', workspaceRoot]);
|
|
5840
|
+
code = runLintCli(['--all', workspaceRoot]);
|
|
5765
5841
|
} finally {
|
|
5766
5842
|
logSpy.mockRestore();
|
|
5767
5843
|
errSpy.mockRestore();
|
|
5768
5844
|
warnSpy.mockRestore();
|
|
5769
5845
|
}
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
expect(
|
|
5774
|
-
expect(
|
|
5846
|
+
expect(code).toBe(1);
|
|
5847
|
+
const frozenLines = errOut.filter((l) => l.includes('Local scope "myteam" is frozen (read-only)'));
|
|
5848
|
+
expect(frozenLines).toHaveLength(2);
|
|
5849
|
+
expect(frozenLines.every((l) => l.includes('[_core-adr-policy-022.06-read-only-dependencies]'))).toBe(true);
|
|
5850
|
+
expect([...out, ...errOut].join('\n')).not.toMatch(/\[warning\]|READ-ONLY/);
|
|
5851
|
+
});
|
|
5852
|
+
|
|
5853
|
+
test('silently skips freeze-reference exempt checks [002.01]', () => {
|
|
5854
|
+
const frozen = (name) => teamXdrDocument('Frozen decision.')
|
|
5855
|
+
.replace(/^---\n/, `---\nfreeze-reference: true\n`)
|
|
5856
|
+
.replace(/^name: .*$/m, `name: ${name}`);
|
|
5857
|
+
const workspaceRoot = createWorkspace('freeze-reference-silent', {
|
|
5858
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md)']),
|
|
5859
|
+
'.xdrs/myteam/index.md': scopeIndexWith('myteam'),
|
|
5860
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [850-frozen](principles/850-frozen.md) - Frozen']),
|
|
5861
|
+
'.xdrs/myteam/adrs/principles/850-frozen.md': frozen('wrong-name'),
|
|
5862
|
+
});
|
|
5863
|
+
const errors = lintWorkspace(workspaceRoot, { ignoreExternal: false }).errors.join('\n');
|
|
5864
|
+
expect(errors).not.toContain('017.05');
|
|
5865
|
+
expect(errors).not.toContain('Policy frontmatter name must be');
|
|
5866
|
+
expect(errors).not.toContain('Policy title must start with');
|
|
5775
5867
|
});
|
|
5776
5868
|
|
|
5777
5869
|
test('dry-run workspace: disabled core, extends-only ref, unlisted legacy scope', () => {
|
|
@@ -5784,10 +5876,10 @@ test('dry-run workspace: disabled core, extends-only ref, unlisted legacy scope'
|
|
|
5784
5876
|
});
|
|
5785
5877
|
const disabledIndexBefore = fs.readFileSync(path.join(workspaceRoot, '.xdrs/ecomm-core/index.md'), 'utf8');
|
|
5786
5878
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5787
|
-
|
|
5788
|
-
expect(
|
|
5789
|
-
expect(
|
|
5790
|
-
expect(
|
|
5879
|
+
const errors = activationErrors(result).split('\n');
|
|
5880
|
+
expect(errors).toHaveLength(2);
|
|
5881
|
+
expect(errors.join('\n')).toContain('[View scope legacy](legacy/index.md)');
|
|
5882
|
+
expect(errors.join('\n')).toContain('Local scope "checkout" is frozen (read-only) because follows: scope "ecomm-core" is tagged `disabled`');
|
|
5791
5883
|
expect(fs.readFileSync(path.join(workspaceRoot, '.xdrs/ecomm-core/index.md'), 'utf8')).toBe(disabledIndexBefore);
|
|
5792
5884
|
});
|
|
5793
5885
|
|
|
@@ -5822,7 +5914,7 @@ test('treats a type whose parent chain reaches core as core-type (naming, follow
|
|
|
5822
5914
|
expect(errors).toContain('_core-adr-policy-010.12-def-core-scope-policies-must-be-structured');
|
|
5823
5915
|
});
|
|
5824
5916
|
|
|
5825
|
-
test('reports missing scope-type for local scopes
|
|
5917
|
+
test('reports missing scope-type for local scopes only [010.10, 022.06]', () => {
|
|
5826
5918
|
const noType = (name) => `---\nname: ${name}\ndescription: Scope ${name}.\napply-to: Test teams\nvalid-from: 2026-01-01\n---\n\n# ${name}\n`;
|
|
5827
5919
|
const workspaceRoot = createWorkspace('scope-type-missing-local-external', {
|
|
5828
5920
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope localone](localone/index.md)', '[View scope vendor](vendor/index.md)']),
|
|
@@ -5835,10 +5927,10 @@ test('reports missing scope-type for local scopes and marks external ones read-o
|
|
|
5835
5927
|
const missingTypeErrors = result.errors.filter((e) => e.includes('must include a scope-type field'));
|
|
5836
5928
|
expect(missingTypeErrors.join('\n')).toContain('localone/index.md');
|
|
5837
5929
|
expect(missingTypeErrors.join('\n')).not.toContain('vendor/index.md');
|
|
5838
|
-
expect(result.
|
|
5930
|
+
expect(result.errors.join('\n')).not.toContain('022.06');
|
|
5839
5931
|
});
|
|
5840
5932
|
|
|
5841
|
-
test('reports missing parent type for local scopes
|
|
5933
|
+
test('reports missing parent type for local scopes only [010.09, 022.06]', () => {
|
|
5842
5934
|
const workspaceRoot = createWorkspace('scope-type-missing-parent', {
|
|
5843
5935
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope localone](localone/index.md)', '[View scope vendor](vendor/index.md)']),
|
|
5844
5936
|
...scopeFiles('localone', '', 'widget'),
|
|
@@ -5850,7 +5942,7 @@ test('reports missing parent type for local scopes and marks external ones read-
|
|
|
5850
5942
|
const parentErrors = result.errors.filter((e) => e.includes('declares parent "ghost"'));
|
|
5851
5943
|
expect(parentErrors).toHaveLength(1);
|
|
5852
5944
|
expect(parentErrors[0]).toContain('localone/index.md');
|
|
5853
|
-
expect(result.
|
|
5945
|
+
expect(result.errors.join('\n')).not.toMatch(/vendor\/index\.md.*ghost/);
|
|
5854
5946
|
});
|
|
5855
5947
|
|
|
5856
5948
|
test('accepts compiled combined with a non-core type [011.10]', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xdrs-core",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.3",
|
|
4
4
|
"description": "XDRS (eXtensible Decision Record System): a framework to structure, compile and distribute Architectural (ADR), Business (BDR), and Engineering (EDR) decision records contents so that AI agents and humans can reliably find and use them with hierarchical scopes and controlled rollout in the format of distributable versioned packages.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|