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.
@@ -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`, and tools (such as `xdrs-core lint`) will skip external scopes by default. 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.
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`). All referenced scopes MUST exist in the workspace; if any is absent, READ/WRITE/REVIEW operations MUST fail.
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. All referenced scopes MUST exist in the workspace; if any is absent, READ/WRITE/REVIEW operations MUST fail. 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.
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. Tools (such as `xdrs-core lint`) MUST enforce this for every element when `scope-type` is an array.
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 are a lint error for local scopes.
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 a lint error.
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, not the lint tool.
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, all READ, WRITE, and REVIEW operations on the extending scope MUST fail immediately. Tools MUST surface a clear message naming the missing scope and citing code `_core-adr-policy-010.32-extends-mandatory-presence`.
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 a lint error. Core-type scopes hold meta governance only, so mixing them with consumable types would blur rules 04 and 05.
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 lint error or silent misbehaviour.
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 lint checks for coherence.**
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 a lint error. 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.
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 a lint error. A tag on the next line is not a tag. `_core` MUST NOT be tagged.
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. Tools SHOULD warn when no active scope reaches an `extends-only` scope; reach only through a `disabled` scope does not count.
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 linted and MAY be edited. 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.
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. For local scopes, missing dependencies and invalid indexes remain lint errors (see `_core-adr-policy-010.32-extends-mandatory-presence`); a disabled dependency only makes the scope read-only. Tools MUST report each cause and how to fix it, citing this rule.
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 is a lint error in a local scope; in an external scope it makes the scope read-only and the value is treated as absent. `extends-only` (field `true` or tag) on `_core`, `_local`, or a core-type scope is a lint error. 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. The field is a lint aid only.
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 and warnings.
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, warnings, readOnlyScopes };
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
- collectReadOnlyScopes(scopeMeta, activation, externalScopes, knownScopeTypes, scopeTypeDefiningScopes, readOnlyScopes);
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, warnings, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, extendsRelationships, externalScopes);
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, warnings, effectiveExternalScopes, { activation, scopeMeta, knownScopeTypes });
272
+ lintRootIndex(rootIndexPath, xdrsRoot, errors, effectiveExternalScopes, { activation, scopeMeta, knownScopeTypes });
287
273
  }
288
274
 
289
- return { xdrsRoot, errors, warnings, readOnlyScopes };
275
+ return { xdrsRoot, errors };
290
276
  }
291
277
 
292
- function lintRootIndex(rootIndexPath, xdrsRoot, errors, warnings, externalScopes, ctx) {
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
- warnings.push(`Scope "${scopeName}" is tagged \`extends-only\` but no active scope extends it, so its policies never apply: ${display} [_core-adr-policy-022.04-extends-only]`);
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
- // Computes read-only scopes and their causes per _core-adr-policy-022.06. Missing dependencies of
447
- // local scopes are reported as lint errors elsewhere; here they only make external scopes read-only.
448
- function collectReadOnlyScopes(scopeMeta, activation, externalScopes, knownScopeTypes, scopeTypeDefiningScopes, readOnlyScopes) {
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
- if (isExternal) causes.push(`${field}: scope "${target}" is not present in this workspace; install it`);
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
- if (causes.length > 0) {
488
- readOnlyScopes.set(scopeName, causes);
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
- // Missing -core is expected for an external companion scope: it becomes read-only
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, warnings, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), extendsRelationships = new Map(), trackedExternalScopes = new Set()) {
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, warnings, actualTypeIndexes, externalScopes, isCoreTypeNonExempt, isCompiledScope);
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, warnings, actualTypeIndexes, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
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: emit a warning and still process for content-level checks.
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, warnings, externalScopes, enforceStructured, isCompiledScope));
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, warnings, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
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, warnings, externalScopes, enforceStructured, isCompiledScope);
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, warnings, externalScopes = new Set(), enforceStructured = false, isCompiledScope = false) {
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, warnings);
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
- const msg = `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]`;
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, warnings, freezeReference);
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, warnings, freezeReference = false) {
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
- const msg = `Policy frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`;
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, warnings) {
1920
- const fm = extractFrontmatter(content);
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
- const msg = `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]`;
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
- const msg = `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]`;
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('marks scope read-only instead of erroring when follows references a missing scope for an external scope', () => {
2252
- const workspaceRoot = createWorkspace('follows-missing-scope-external', {
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('follows references scope "nonexistent-core" which does not exist in the workspace');
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.readOnlyScopes.size).toBe(0);
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('marks external scope with invalid extends-only value read-only, treated as absent [022.06]', () => {
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('warns when no active scope extends an extends-only scope [022.04]', () => {
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)).toBe('');
5688
- expect(activationWarnings(result)).toContain('Scope "lib" is tagged `extends-only` but no active scope extends it');
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('warns when an extends-only scope is reached only through a disabled scope [022.04]', () => {
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(activationWarnings(result)).toContain('Scope "lib" is tagged `extends-only`');
5700
- expect(result.readOnlyScopes.get('myteam').join('\n')).toContain('extends: scope "mid" is tagged `disabled`');
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('makes a scope following a disabled scope read-only while it still reaches extends-only scopes [022.04, 022.06]', () => {
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
- expect(activationWarnings(result)).toBe('');
5712
- expect(result.readOnlyScopes.get('myteam').join('\n')).toContain('follows: scope "area-core" is tagged `disabled` in the root index; enable it or remove the follows: reference');
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('makes an external scope extending a disabled scope read-only [022.06]', () => {
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
- const result = lintWorkspace(workspaceRoot);
5723
- expect(result.readOnlyScopes.has('vendor')).toBe(true);
5784
+ expect(activationErrors(lintWorkspace(workspaceRoot))).toBe('');
5785
+ expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).toBe('');
5724
5786
  });
5725
5787
 
5726
- test('makes _local extending a disabled scope read-only [022.06]', () => {
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.readOnlyScopes.has('_local')).toBe(true);
5796
+ expect(activationErrors(result)).toContain('Local scope "_local" is frozen (read-only)');
5735
5797
  });
5736
5798
 
5737
- test('makes a scope whose type is defined only in a disabled scope read-only [022.06]', () => {
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.readOnlyScopes.get('shop').join('\n')).toContain('scope type "widget" is defined only in disabled scope(s) "widgets-core"');
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 one read-only line per cause citing 022.06', () => {
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 logs = [];
5760
- const logSpy = jest.spyOn(console, 'log').mockImplementation((msg) => logs.push(msg));
5761
- const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
5762
- const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
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
- const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
5771
- expect(result.readOnlyScopes.get('myteam')).toHaveLength(2);
5772
- const readOnlyLines = logs.filter((l) => l.includes('READ-ONLY scope "myteam"'));
5773
- expect(readOnlyLines).toHaveLength(2);
5774
- expect(readOnlyLines.every((l) => l.includes('[_core-adr-policy-022.06-read-only-dependencies]'))).toBe(true);
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
- expect(result.readOnlyScopes.get('checkout')).toHaveLength(1);
5788
- expect(activationWarnings(result)).toBe('');
5789
- expect(activationErrors(result)).toContain('[View scope legacy](legacy/index.md)');
5790
- expect(activationErrors(result).split('\n')).toHaveLength(1);
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 and marks external ones read-only [010.10, 022.06]', () => {
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.readOnlyScopes.get('vendor').join('\n')).toContain('no scope-type');
5930
+ expect(result.errors.join('\n')).not.toContain('022.06');
5839
5931
  });
5840
5932
 
5841
- test('reports missing parent type for local scopes and marks external ones read-only [010.09, 022.06]', () => {
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.readOnlyScopes.get('vendor').join('\n')).toContain('scope type "ghost"');
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.1",
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",