xdrs-core 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,11 +51,12 @@ Collectively, these are referred to as XDRs.
51
51
  - `_core-adr-006` defines research standards
52
52
  - `_core-adr-007` defines plan standards
53
53
  - For simple structures, flows, layout, or relationship indications, documents SHOULD prefer plain Markdown, tables, Mermaid.js (sequence, state, activity, entity diagrams) or ASCII art instead of external assets.
54
- - Images and other supporting files SHOULD be used only when they are materially necessary to preserve clarity, fidelity, or evidence. When used, they MUST live in a sibling `.assets/` folder next to the document.
54
+ - Any non-Markdown supporting files referenced by a document (schemas, JSON examples, images, diagrams, binaries, or any other data files) SHOULD be used only when they are materially necessary to preserve clarity, fidelity, or evidence. When used, they MUST live in a sibling `.assets/` folder next to the document.
55
55
  - XDRs in the subject root use `[xdrs-root]/[scope]/[type]/[subject]/.assets/`
56
56
  - Articles use `[xdrs-root]/[scope]/[type]/[subject]/articles/.assets/`
57
57
  - Research uses `[xdrs-root]/[scope]/[type]/[subject]/researches/.assets/`
58
58
  - Skills use `[xdrs-root]/[scope]/[type]/[subject]/skills/[number]-[skill-name]/.assets/`
59
+ - Sub-directories inside `.assets/` are allowed to keep related files organized only when that `.assets/` folder already has more than 10 files. Otherwise, keep files flat in `.assets/`.
59
60
  - **Scopes:**
60
61
  - Short name that defines a group or a package of xdrs
61
62
  - examples: `business-x`, `business-y`, `team-43`, `_core`
@@ -25,8 +25,8 @@ XDR documents are the authoritative policy for their scope, type, and subject. T
25
25
  |---|---|---|
26
26
  | `name` | Yes | 1-64 characters. Lowercase letters, numbers, hyphens, and leading underscores only. Must not end with a hyphen. Must not contain consecutive hyphens. Must match the document identifier from the heading: `[scope]-[type]-[number]-[short-title]`. |
27
27
  | `description` | Yes | 1-1024 characters. Describes what this decision is about and when to use it. Should include keywords that help agents identify when to apply it. |
28
- | `applyTo` | No | Short description of contexts this decision is applicable to. Keep it under 40 words. If omitted, the decision applies to all logically applicable elements. ONLY use this section if the usage is very specific to a specific case. Examples: `Only frontend code`, `JavaScript projects`. |
29
- | `validFrom` | No | ISO date (`YYYY-MM-DD`) indicating from when this decision must be enforced. Before this date it should be used everywhere possible, but compliance is not enforced during reviews until after this date. |
28
+ | `apply-to` | No | Short description of contexts this decision is applicable to. Keep it under 40 words. If omitted, the decision applies to all logically applicable elements. ONLY use this section if the usage is very specific to a specific case. Examples: `Only frontend code`, `JavaScript projects`. |
29
+ | `valid-from` | No | ISO date (`YYYY-MM-DD`) indicating from when this decision must be enforced. Before this date it should be used everywhere possible, but compliance is not enforced during reviews until after this date. |
30
30
  | `license` | No | SPDX license expression (e.g. `MIT`, `Apache-2.0`, `CC-BY-4.0`). Indicates the license under which the document content is shared. If omitted, the license is governed by the repository or package defaults. |
31
31
  | `metadata` | No | Arbitrary key-value map for additional properties not defined by this spec. |
32
32
 
@@ -42,23 +42,23 @@ XDR documents are the authoritative policy for their scope, type, and subject. T
42
42
  ---
43
43
  name: _core-adr-002-xdr-standards
44
44
  description: Defines how XDR documents should be written. Use when writing or reviewing any XDR.
45
- applyTo: All XDR scopes
46
- validFrom: 2026-06-01
45
+ apply-to: All XDR scopes
46
+ valid-from: 2026-06-01
47
47
  metadata:
48
48
  author: example-org
49
49
  ---
50
50
  ```
51
51
  - All documents present in the collection are considered active. There is no status field. When a decision is no longer relevant, valid or active, it must be removed from the collection. Historical versions are available via versioned packages or git history.
52
52
  - Before using, enforcing, or citing an XDR as a current rule, humans and AI agents MUST decide whether the decision is applicable for the current case.
53
- - Check `validFrom:` first. If a date is present and has not yet been reached, the decision SHOULD be adopted for new implementations but is not enforced during reviews.
54
- - Check `applyTo:` next to determine whether the decision fits the current codebase, system, workflow, or audience.
53
+ - Check `valid-from:` first. If a date is present and has not yet been reached, the decision SHOULD be adopted for new implementations but is not enforced during reviews.
54
+ - Check `apply-to:` next to determine whether the decision fits the current codebase, system, workflow, or audience.
55
55
  - Check the decision context and implementation details last to determine any additional boundaries, exceptions, or qualifiers that metadata alone cannot express.
56
56
  - Research documents MAY be added under the same subject to capture the exploration, findings, and proposals that backed a decision. Research is useful during elaboration, discussion, and updates of XDRs, but the XDR document remains the source of truth.
57
57
  - **XDR Id:** [scope]-[type]-[xdr number] (numbers are scoped per type+scope combination and must not be reused within that combination; always use lowercase)
58
58
  - Types in IDs: `adr`, `bdr`, `edr`
59
59
  - Define the next number of an XDR by checking what is the highest number present in the type+scope. Don't fill numbering gaps, as they might be old deleted XDRs and we should never reuse numbers of different documents/decisions. Numbering gaps are expected.
60
60
  - Decisions MUST be concise and reference other XDRs to avoid duplication.
61
- - The `### Details` section SHOULD state relevant boundaries or exceptions and what a reader should do or avoid in common cases. Use the frontmatter fields `applyTo` and `validFrom` as the first-pass filter for applicability, then keep nuanced boundaries in the decision text.
61
+ - The `### Details` section SHOULD state relevant boundaries or exceptions and what a reader should do or avoid in common cases. Use the frontmatter fields `apply-to` and `valid-from` as the first-pass filter for applicability, then keep nuanced boundaries in the decision text.
62
62
  - Use concise rules, examples, `Allowed` / `Disallowed` lists or checklists with required items to help the reader apply the decision correctly. Keep them short and decision-specific.
63
63
  - When the decision defines strong policies or rules that should be stated explicitly as stable rule blocks, or when other documents, skills, or agents need to cite those rules individually by identifier, the XDR MUST follow the extension [_core-adr-008 - XDR standards - structured](008-xdr-standards-structured.md) instead of using plain bullet lists for those rules.
64
64
  - Conflict handling applies to XDR documents:
@@ -67,6 +67,8 @@ XDR documents are the authoritative policy for their scope, type, and subject. T
67
67
  - When research exists for a decision, the XDR SHOULD mention the related research documents after the `## Considered Options` list.
68
68
  - Never use emojis in contents.
69
69
  - Always use file names with lowercase.
70
+ - Any non-Markdown files referenced by an XDR (schemas, JSON examples, images, diagrams, binaries, or any other data files) SHOULD be used only when they are materially necessary and MUST live in `[xdrs-root]/[scope]/[type]/[subject]/.assets/`.
71
+ - Sub-directories inside this `.assets/` folder are allowed only when it already has more than 10 files. Otherwise, keep files flat.
70
72
  - Avoid using lengthy instructions on the XDR. If there are long and detailed instructions related to the XDR, or instructions that are outside the decision, create another file with a guide. If the guide is small, keep it in the XDR itself.
71
73
  - XDRs should be under 1300 words long as a rule of thumb.
72
74
  - This is important to make them focused on a clear decision
@@ -81,8 +83,8 @@ All XDRs MUST follow this template
81
83
  ---
82
84
  name: [scope]-[type]-[number]-[short-title]
83
85
  description: [What this decision is about and when to use it]
84
- applyTo: [Optional. Contexts this decision applies to, under 40 words]
85
- validFrom: [Optional. ISO date YYYY-MM-DD from when enforcement begins]
86
+ apply-to: [Optional. Contexts this decision applies to, under 40 words]
87
+ valid-from: [Optional. ISO date YYYY-MM-DD from when enforcement begins]
86
88
  license: [Optional. SPDX license expression]
87
89
  metadata:
88
90
  [optional-key]: [optional-value]
@@ -137,8 +139,8 @@ Question: In the end, state explicitly the question that needs to be answered. E
137
139
 
138
140
  **Examples:**
139
141
  - Frontmatter examples:
140
- - `validFrom: 2026-03-01`
141
- - `applyTo: JavaScript projects`
142
+ - `valid-from: 2026-03-01`
143
+ - `apply-to: JavaScript projects`
142
144
 
143
145
  **XDR ID Examples:**
144
146
  - `business-x-adr-001` (not `ADR-business-x-001` or `business-x-adr-1`)
@@ -36,7 +36,7 @@ Skills are procedures, XDRs are guardrails and decisions, Research documents cap
36
36
  Always create links back and forth between skills <-> XDRs when the relationship is direct, and link to related Research or Articles when they provide important context.
37
37
  - Skills are task-based artifacts. They should have a clear starting trigger, an expected end result, and enough detail for a human or agent to verify that the task finished correctly.
38
38
  - A skill is not policy by itself. If following a skill is mandatory, that obligation must come from an XDR or another explicit policy that references the skill.
39
- - When a skill reads, operationalizes, or enforces XDRs, it MUST evaluate the XDR metadata first. `validFrom:` determines the convergence date for adoption, `applyTo:` determines whether the decision fits the current task context, and the decision text itself determines any remaining boundaries. All documents present in the collection are considered active. Skills must not treat out-of-window or out-of-scope XDRs as current requirements.
39
+ - When a skill reads, operationalizes, or enforces XDRs, it MUST evaluate the XDR metadata first. `valid-from:` determines the convergence date for adoption, `apply-to:` determines whether the decision fits the current task context, and the decision text itself determines any remaining boundaries. All documents present in the collection are considered active. Skills must not treat out-of-window or out-of-scope XDRs as current requirements.
40
40
  - Skills and XDRs have a many-to-many relationship: one skill may operationalize multiple XDRs, and one XDR may be executed through multiple skills in different contexts.
41
41
 
42
42
  Place a skill under the XDR type that matches the nature of the activity the skill performs:
@@ -114,7 +114,8 @@ Rules:
114
114
  - `## Overview` SHOULD state the task objective, expected outcome, and relevant prerequisites or tools when they matter.
115
115
  - `## Instructions` SHOULD include verification steps or acceptance criteria at the end of the task, or at the end of major phases when partial validation matters.
116
116
  - For simple structure, flow, layout, or relationship indications, `SKILL.md` SHOULD prefer plain Markdown, tables, or ASCII art instead of external assets.
117
- - Images and other local resource files referenced from `SKILL.md` SHOULD be used only when they are materially necessary and MUST live in `.assets/` inside the same skill package.
117
+ - Any non-Markdown files referenced from `SKILL.md` (schemas, JSON examples, images, diagrams, binaries, or any other data files) SHOULD be used only when they are materially necessary and MUST live in `.assets/` inside the same skill package.
118
+ - Sub-directories inside this `.assets/` folder are allowed only when it already has more than 10 files. Otherwise, keep files flat.
118
119
  - Keep `SKILL.md` under 6500 words. Move lengthy reference material to `references/`.
119
120
  - Use relative paths for all links; never use absolute paths starting with `/`.
120
121
  - Always use lowercase file names.
@@ -24,14 +24,15 @@ Articles are Markdown documents placed inside a subject folder alongside decisio
24
24
  - Articles must reference the XDRs, Research documents, and Skills they synthesize. Never duplicate decision content; link back to the authoritative sources.
25
25
  - Articles may serve as indexes, combining related artifacts on a specific topic into a single navigable document.
26
26
  - In more complex cases, an article may be an index of links to other articles, grouping related documentation into a single entry point that guides readers across a set of related topics.
27
- - When an article tells readers which decisions to follow, it SHOULD check `validFrom:` first to determine the convergence date, `applyTo:` second to determine context fit, and the decision text itself last. All documents present in the collection are considered active; articles must not present out-of-window or out-of-scope XDRs as current rules for the discussed context.
27
+ - When an article tells readers which decisions to follow, it SHOULD check `valid-from:` first to determine the convergence date, `apply-to:` second to determine context fit, and the decision text itself last. All documents present in the collection are considered active; articles must not present out-of-window or out-of-scope XDRs as current rules for the discussed context.
28
28
  - Articles must remain consistent with the XDRs, Research documents, and Skills they reference. When a referenced artifact changes, the article must be reviewed and updated.
29
29
  - Place an article in the subject folder that best matches its topic using the required list of subjects per type defined in `_core-adr-001`. If an article spans more than one subject, place it in `principles`.
30
30
  - For simple structure, flow, layout, or relationship indications, articles SHOULD prefer plain Markdown, tables, or ASCII art instead of external assets.
31
- - Images and other local resource files referenced by an article SHOULD be used only when they are materially necessary and MUST live in `articles/.assets/` next to the article files.
31
+ - Any non-Markdown files referenced by an article (schemas, JSON examples, images, diagrams, binaries, or any other data files) SHOULD be used only when they are materially necessary and MUST live in `articles/.assets/` next to the article files.
32
+ - Sub-directories inside this `.assets/` folder are allowed only when it already has more than 10 files. Otherwise, keep files flat.
32
33
  - Always use lowercase file names.
33
34
  - Never use emojis in article content.
34
- - Articles should be kept under 1950 words. Move detailed content to referenced XDRs or Skills.
35
+ - Articles should be kept under 5000 words. Move detailed content to referenced XDRs or Skills.
35
36
 
36
37
  **Folder layout**
37
38
 
@@ -48,7 +48,8 @@ Research documents are Markdown files placed inside a subject folder alongside d
48
48
  - Research documents SHOULD link in `## References` to the XDRs, skills, articles, discussions, and external references relevant to the subject or that later cite the work.
49
49
  - A 1:1 relationship between one research document and one decision will likely be common in practice, but it is not required.
50
50
  - One research document MAY also be referenced by multiple XDRs, including a mix of ADRs, BDRs, and EDRs, when the same investigation remains relevant across several decisions.
51
- - Images and other local resource files referenced by a research document SHOULD be used only when they are materially necessary and MUST live in `researches/.assets/` next to the research files.
51
+ - Any non-Markdown files referenced by a research document (schemas, JSON examples, images, diagrams, binaries, or any other data files) SHOULD be used only when they are materially necessary and MUST live in `researches/.assets/` next to the research files.
52
+ - Sub-directories inside this `.assets/` folder are allowed only when it already has more than 10 files. Otherwise, keep files flat.
52
53
  - Research file names MUST be lowercase. Never use emojis.
53
54
  - A research document MAY exist before the related XDR is written, or remain after the XDR changes, as long as its status and references stay clear.
54
55
 
@@ -31,7 +31,8 @@ Plans are Markdown documents placed inside a subject folder alongside decision r
31
31
  - Plans MUST include an `Expected end date:` field in ISO format (YYYY-MM-DD) inside the `## Proposed Solution` section.
32
32
  - Always use lowercase file names.
33
33
  - Never use emojis in plan content.
34
- - Images and other local resource files referenced by a plan MUST live in `plans/.assets/` next to the plan files.
34
+ - Any non-Markdown files referenced by a plan (schemas, JSON examples, images, diagrams, binaries, or any other data files) SHOULD be used only when they are materially necessary and MUST live in `plans/.assets/` next to the plan files.
35
+ - Sub-directories inside this `.assets/` folder are allowed only when it already has more than 10 files. Otherwise, keep files flat.
35
36
 
36
37
  **Folder layout**
37
38
 
@@ -26,8 +26,8 @@ Performs a structured review of code changes or files against the XDRs in the re
26
26
  - XDR scopes are controlled by nested folders; some are broad, others domain-specific.
27
27
  - Extract frontmatter first to decide whether each XDR should be used for the current review context.
28
28
  - All documents present in the collection are considered active.
29
- - Check `validFrom:` first. If a date is present and has not yet been reached, the decision SHOULD be adopted for new implementations but is not enforced during reviews.
30
- - Check `applyTo:` second. Keep only XDRs whose stated scope fits the files, systems, or workflows under review.
29
+ - Check `valid-from:` first. If a date is present and has not yet been reached, the decision SHOULD be adopted for new implementations but is not enforced during reviews.
30
+ - Check `apply-to:` second. Keep only XDRs whose stated scope fits the files, systems, or workflows under review.
31
31
  - Check the decision text itself last for additional boundaries or exceptions that metadata does not encode.
32
32
  2. Filter relevance based on file types, domains, and architectural patterns in scope.
33
33
 
@@ -58,7 +58,7 @@ Choose a title that clearly states the question this XDR answers, not the answer
58
58
  ### Phase 4: Research Related XDRs
59
59
 
60
60
  1. Read all existing XDRs relevant to the topic across all scopes listed in the XDR root `index.md`.
61
- 2. Evaluate XDR metadata before treating any decision as a current constraint. All documents present in the collection are considered active. `validFrom:` determines the convergence date for adoption, `applyTo:` determines whether it fits the current topic, and the decision text defines any remaining boundaries. Treat out-of-window or out-of-scope XDRs as background only when assessing overlaps and conflicts.
61
+ 2. Evaluate XDR metadata before treating any decision as a current constraint. All documents present in the collection are considered active. `valid-from:` determines the convergence date for adoption, `apply-to:` determines whether it fits the current topic, and the decision text defines any remaining boundaries. Treat out-of-window or out-of-scope XDRs as background only when assessing overlaps and conflicts.
62
62
  3. Identify decisions that already address the topic (full or partial overlap).
63
63
  4. Note decisions that might conflict with the intended outcome.
64
64
  5. Read related `researches/` documents when they exist, especially if they contain constraints, findings, or option tradeoffs that should influence the decision.
@@ -107,8 +107,8 @@ Refer to `_core-adr-008-xdr-standards-structured` for full requirements and cita
107
107
  ---
108
108
  name: [scope]-[type]-[number]-[short-title]
109
109
  description: [What this decision is about and when to use it]
110
- applyTo: [Optional. Contexts this decision applies to, under 40 words]
111
- validFrom: [Optional. ISO date YYYY-MM-DD from when enforcement begins]
110
+ apply-to: [Optional. Contexts this decision applies to, under 40 words]
111
+ valid-from: [Optional. ISO date YYYY-MM-DD from when enforcement begins]
112
112
  ---
113
113
 
114
114
  # [scope]-[type]-[number]: [Short Title]
@@ -132,10 +132,10 @@ validFrom: [Optional. ISO date YYYY-MM-DD from when enforcement begins]
132
132
  ```
133
133
 
134
134
  Mandatory rules to apply while drafting:
135
- - Include frontmatter `applyTo:` only when it adds value by narrowing the decision scope; omit it when the decision applies broadly.
136
- - Include frontmatter `validFrom:` only when there is a specific future enforcement date; omit it when the decision is immediately effective.
137
- - Keep `applyTo:` under 40 words and use `validFrom:` only with `YYYY-MM-DD` ISO format.
138
- - When frontmatter metadata is present, write it so a reader can decide whether the XDR should be used for the current case without guessing. `validFrom:` sets a convergence date for adoption, `applyTo:` narrows the contexts where the decision applies, and the decision text defines any remaining boundaries.
135
+ - Include frontmatter `apply-to:` only when it adds value by narrowing the decision scope; omit it when the decision applies broadly.
136
+ - Include frontmatter `valid-from:` only when there is a specific future enforcement date; omit it when the decision is immediately effective.
137
+ - Keep `apply-to:` under 40 words and use `valid-from:` only with `YYYY-MM-DD` ISO format.
138
+ - When frontmatter metadata is present, write it so a reader can decide whether the XDR should be used for the current case without guessing. `valid-from:` sets a convergence date for adoption, `apply-to:` narrows the contexts where the decision applies, and the decision text defines any remaining boundaries.
139
139
  - Use mandatory language ("must", "always", "never") only for hard requirements; use advisory language ("should", "recommended") for guidance.
140
140
  - Do not duplicate content already in referenced XDRs — link instead.
141
141
  - Keep the decision itself authoritative in the XDR. Supporting artifacts may elaborate, but they should not restate the full decision when a short reference is enough.
@@ -152,7 +152,7 @@ Mandatory rules to apply while drafting:
152
152
  Check every item before finalizing:
153
153
 
154
154
  1. **Length**: Is it under 1300 words? Trim verbose explanations. Move detailed skills to a separate file and link.
155
- 2. **Frontmatter**: Are `applyTo:` and `validFrom:` present only when they add value, omitted entirely when not needed, and specific enough for a reader to decide whether the XDR is currently valid and applicable?
155
+ 2. **Frontmatter**: Are `apply-to:` and `valid-from:` present only when they add value, omitted entirely when not needed, and specific enough for a reader to decide whether the XDR is currently valid and applicable?
156
156
  3. **Originality**: Does every sentence add value that cannot be found in a generic web search? Remove obvious advice. Keep only the project-specific decision.
157
157
  4. **Clarity**: Is the chosen option unambiguous? Is the "why" clear in one reading?
158
158
  5. **Redundancy**: Is the XDR the primary source for the decision itself, with related documents linked instead of duplicated wherever possible?
@@ -51,7 +51,7 @@ Quick test:
51
51
 
52
52
  1. List `.xdrs/[scope]/[type]/[subject]/skills/` for existing skills. If one already covers the goal, extend or reference it instead of creating a duplicate.
53
53
  2. Read all XDRs relevant to the skill's domain to collect rules and cross-references.
54
- 3. Evaluate XDR metadata before operationalizing those rules. All documents present in the collection are considered active. `validFrom:` determines the convergence date for adoption, `applyTo:` determines whether the decision fits the intended task context, and the decision text defines any remaining boundaries. Keep out-of-window or out-of-scope XDRs as background only.
54
+ 3. Evaluate XDR metadata before operationalizing those rules. All documents present in the collection are considered active. `valid-from:` determines the convergence date for adoption, `apply-to:` determines whether the decision fits the intended task context, and the decision text defines any remaining boundaries. Keep out-of-window or out-of-scope XDRs as background only.
55
55
  4. Decide whether the skill is merely guidance or is being referenced by an XDR as a mandatory procedure. Do not encode policy in the skill unless it comes from a referenced XDR.
56
56
 
57
57
  ### Phase 4: Write the SKILL.md
@@ -63,7 +63,7 @@ If the article spans more than one subject, place it in `principles`.
63
63
  ### Phase 4: Research XDRs and Skills to Synthesize
64
64
 
65
65
  1. Read all XDRs, Research documents, and Skills relevant to the article topic across all scopes listed in the XDR root `index.md`.
66
- 2. Evaluate XDR metadata before synthesizing guidance. All documents present in the collection are considered active. Use `validFrom:` to determine the convergence date for adoption, `applyTo:` to determine whether the decision fits the audience or context being discussed, and the decision text itself for any remaining applicability boundaries.
66
+ 2. Evaluate XDR metadata before synthesizing guidance. All documents present in the collection are considered active. Use `valid-from:` to determine the convergence date for adoption, `apply-to:` to determine whether the decision fits the audience or context being discussed, and the decision text itself for any remaining applicability boundaries.
67
67
  3. Identify the key points a reader needs to understand the topic end-to-end.
68
68
  4. Collect XDR IDs and file paths for cross-references. Never copy decision text verbatim; link to it.
69
69
 
@@ -50,7 +50,7 @@ Consult `001-xdrs-core` while making each choice in this phase. The summaries be
50
50
  ### Phase 3: Research Existing Artifacts
51
51
 
52
52
  1. Read relevant XDRs across all scopes listed in the XDR root `index.md`.
53
- 2. Evaluate XDR metadata before treating any decision as current context. All documents present in the collection are considered active. `validFrom:` determines the convergence date for adoption, `applyTo:` determines whether the decision fits the intended task context, and the decision text defines any remaining boundaries. Keep out-of-window or out-of-scope XDRs as background only.
53
+ 2. Evaluate XDR metadata before treating any decision as current context. All documents present in the collection are considered active. `valid-from:` determines the convergence date for adoption, `apply-to:` determines whether the decision fits the intended task context, and the decision text defines any remaining boundaries. Keep out-of-window or out-of-scope XDRs as background only.
54
54
  3. Read existing research documents in the same or overlapping subjects to avoid duplicating the same study.
55
55
  4. Read related skills or articles if they contain context, implementation limits, or terminology that must be reflected.
56
56
  5. Collect links that should appear in the final `## References` section.
@@ -53,7 +53,7 @@ Consult `001-xdrs-core` while making each choice in this phase. The summaries be
53
53
  ### Phase 4: Research Related Artifacts
54
54
 
55
55
  1. Read all XDRs, Research documents, Skills, and existing Plans relevant to the plan topic across all scopes listed in the XDR root `index.md`.
56
- 2. Evaluate XDR metadata before treating any decision as current context. All documents present in the collection are considered active. `validFrom:` determines the convergence date for adoption, `applyTo:` determines whether the decision fits the intended context, and the decision text defines any remaining boundaries.
56
+ 2. Evaluate XDR metadata before treating any decision as current context. All documents present in the collection are considered active. `valid-from:` determines the convergence date for adoption, `apply-to:` determines whether the decision fits the intended context, and the decision text defines any remaining boundaries.
57
57
  3. Identify Decisions that this plan implements, Research that informs the planning, and any existing Plans that overlap.
58
58
  4. Collect artifact IDs and file paths for cross-references.
59
59
 
@@ -19,7 +19,7 @@ XDR decisions are the authoritative source of mandatory policy. Skills are execu
19
19
 
20
20
  ### Details
21
21
 
22
- - Before treating any XDR as a current requirement, evaluate applicability in order: `validFrom`, `applyTo`, then the decision text.
22
+ - Before treating any XDR as a current requirement, evaluate applicability in order: `valid-from`, `apply-to`, then the decision text.
23
23
  - The set of policies that an agent or human must comply with for a given operational context must be declared in BDRs.
24
24
  - If a policy set becomes too large or too mixed in purpose to review clearly in one record, it must be split into multiple focused BDRs.
25
25
  - Specific work instructions that operationalize BDR policies must be structured as skills when the procedure is detailed enough to benefit from a dedicated operational document.
package/README.md CHANGED
@@ -66,7 +66,7 @@ The folder layout, file naming, and document format are designed so that AI agen
66
66
  - Each XDR is a small, focused Markdown file (target under 1300 words), covering one decision.
67
67
  - The canonical index per scope and type lists all XDRs with short descriptions, enabling agents to identify relevant records without reading every file.
68
68
  - The root index at `.xdrs/index.md` provides a single entry point for discovery.
69
- - XDR metadata gives agents a first-pass filter: check `validFrom` for the convergence date, then check `applyTo`, and finally the decision text itself to confirm the decision should be used in the current context. All documents present in the collection are considered active.
69
+ - XDR metadata gives agents a first-pass filter: check `valid-from` for the convergence date, then check `apply-to`, and finally the decision text itself to confirm the decision should be used in the current context. All documents present in the collection are considered active.
70
70
  - Decisions cross-reference each other by XDR ID rather than duplicating content, keeping individual files concise.
71
71
  - Subject folders reduce the search space when a query maps to a known domain.
72
72
 
@@ -166,7 +166,7 @@ The `lint` command reads `./.xdrs/**` from the given workspace path and checks c
166
166
  - plan `Expected end date:` field presence and ISO date format
167
167
  - canonical index presence and link consistency
168
168
  - root index coverage for all discovered canonical indexes
169
- - XDR metadata section placement and `validFrom` / `applyTo` field format
169
+ - XDR metadata section placement and `valid-from` / `apply-to` field format
170
170
  - local markdown links between XDR documents, skills, articles, researches, and plans (excluding fenced code blocks)
171
171
  - local image and `.assets/` links resolving inside the sibling `.assets/` folder for each document
172
172
 
package/lib/lint.js CHANGED
@@ -23,6 +23,10 @@ const NUMBERED_DIR_RE = /^(\d{3,})-([a-z0-9-]+)$/;
23
23
  const REQUIRED_ROOT_INDEX_TEXT = 'XDRs in scopes listed last override the ones listed first';
24
24
  const SUBJECT_ARTIFACT_DIRS = new Set(['skills', 'articles', 'researches', 'plans']);
25
25
  const RESOURCE_DIR_NAME = '.assets';
26
+ const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_DIR_NAME]);
27
+
28
+ const XDR_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata']);
29
+ const SKILL_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'license', 'metadata', 'compatibility', 'allowed-tools']);
26
30
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
27
31
 
28
32
  function runLintCli(args) {
@@ -371,15 +375,20 @@ function lintXdrFrontmatter(content, expectedName, filePath, errors) {
371
375
  }
372
376
  if (fm.validFrom !== undefined) {
373
377
  if (!isIsoDate(fm.validFrom)) {
374
- errors.push(`XDR frontmatter validFrom must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)}`);
378
+ errors.push(`XDR frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)}`);
375
379
  }
376
380
  }
377
381
  if (fm.appliedTo !== undefined) {
378
382
  const words = countWords(fm.appliedTo);
379
383
  if (words === 0) {
380
- errors.push(`XDR frontmatter applyTo must not be empty: ${toDisplayPath(filePath)}`);
384
+ errors.push(`XDR frontmatter apply-to must not be empty: ${toDisplayPath(filePath)}`);
381
385
  } else if (words >= 40) {
382
- errors.push(`XDR frontmatter applyTo must be under 40 words: ${toDisplayPath(filePath)}`);
386
+ errors.push(`XDR frontmatter apply-to must be under 40 words: ${toDisplayPath(filePath)}`);
387
+ }
388
+ }
389
+ for (const key of fm.topLevelKeys) {
390
+ if (!XDR_ALLOWED_FRONTMATTER_KEYS.has(key)) {
391
+ errors.push(`XDR frontmatter has unknown field "${key}": ${toDisplayPath(filePath)}`);
383
392
  }
384
393
  }
385
394
  }
@@ -415,6 +424,21 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
415
424
  }
416
425
 
417
426
  const skillFilePath = path.join(entryPath, 'SKILL.md');
427
+ const packageEntries = safeReadDir(entryPath, errors, `read skill package ${toDisplayPath(entryPath)}`);
428
+
429
+ for (const packageEntry of packageEntries) {
430
+ const packageEntryPath = path.join(entryPath, packageEntry.name);
431
+
432
+ if (!packageEntry.isDirectory()) {
433
+ continue;
434
+ }
435
+
436
+ if (SKILL_PACKAGE_OPTIONAL_DIRS.has(packageEntry.name)) {
437
+ continue;
438
+ }
439
+
440
+ errors.push(`Unexpected directory in skill package: ${toDisplayPath(packageEntryPath)}`);
441
+ }
418
442
 
419
443
  if (!existsFile(skillFilePath)) {
420
444
  errors.push(`Missing SKILL.md in skill package: ${toDisplayPath(entryPath)}`);
@@ -437,6 +461,11 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
437
461
  if (!skillFm.description) {
438
462
  errors.push(`SKILL.md frontmatter must include a non-empty description field: ${toDisplayPath(skillFilePath)}`);
439
463
  }
464
+ for (const key of skillFm.topLevelKeys) {
465
+ if (!SKILL_ALLOWED_FRONTMATTER_KEYS.has(key)) {
466
+ errors.push(`SKILL.md frontmatter has unknown field "${key}": ${toDisplayPath(skillFilePath)}`);
467
+ }
468
+ }
440
469
  }
441
470
 
442
471
  lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors, externalScopes);
@@ -673,17 +702,15 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
673
702
  return;
674
703
  }
675
704
 
676
- const entries = safeReadDir(assetsDir, errors, `read assets directory ${toDisplayPath(assetsDir)}`);
677
- if (entries.length === 0) {
705
+ const assetTree = collectAssetTree(assetsDir, errors);
706
+ if (assetTree.files.length === 0 && assetTree.directories.length === 0) {
678
707
  return;
679
708
  }
680
709
 
681
- const assetFiles = entries
682
- .filter((entry) => entry.isFile())
683
- .map((entry) => normalizePath(path.join(assetsDir, entry.name)));
684
-
685
- if (assetFiles.length === 0) {
686
- return;
710
+ if (assetTree.directories.length > 0 && assetTree.files.length <= 10) {
711
+ for (const directoryPath of assetTree.directories) {
712
+ errors.push(`.assets directory must be flat unless it already contains more than 10 files: ${toDisplayPath(directoryPath)}`);
713
+ }
687
714
  }
688
715
 
689
716
  const repoRoot = path.dirname(xdrsRoot);
@@ -701,13 +728,37 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
701
728
  }
702
729
  }
703
730
 
704
- for (const assetPath of assetFiles) {
731
+ for (const assetPath of assetTree.files) {
705
732
  if (!referencedAssets.has(assetPath)) {
706
733
  errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)}`);
707
734
  }
708
735
  }
709
736
  }
710
737
 
738
+ function collectAssetTree(dirPath, errors) {
739
+ const files = [];
740
+ const directories = [];
741
+ const entries = safeReadDir(dirPath, errors, `read assets directory ${toDisplayPath(dirPath)}`);
742
+
743
+ for (const entry of entries) {
744
+ const entryPath = path.join(dirPath, entry.name);
745
+
746
+ if (entry.isDirectory()) {
747
+ directories.push(normalizePath(entryPath));
748
+ const nestedTree = collectAssetTree(entryPath, errors);
749
+ directories.push(...nestedTree.directories);
750
+ files.push(...nestedTree.files);
751
+ continue;
752
+ }
753
+
754
+ if (entry.isFile()) {
755
+ files.push(normalizePath(entryPath));
756
+ }
757
+ }
758
+
759
+ return { files, directories };
760
+ }
761
+
711
762
  function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalScopes = new Set()) {
712
763
  const lines = fs.readFileSync(documentPath, 'utf8').split(/\r?\n/);
713
764
  const ignoredLines = findIgnoredMarkdownLines(lines);
@@ -834,18 +885,24 @@ function shouldValidateResourceLink(rawTarget) {
834
885
  function extractFrontmatter(content) {
835
886
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
836
887
  if (!match) {
837
- return { present: false, name: null, description: false, validFrom: undefined, appliedTo: undefined };
888
+ return { present: false, name: null, description: false, validFrom: undefined, appliedTo: undefined, topLevelKeys: [] };
838
889
  }
839
890
  const block = match[1];
840
891
  const nameMatch = block.match(/^name:\s*(.+)$/m);
841
- const validFromMatch = block.match(/^validFrom:\s*(.+)$/m);
842
- const appliedToMatch = block.match(/^applyTo:\s*(.+)$/m);
892
+ const validFromMatch = block.match(/^valid-from:\s*(.+)$/m);
893
+ const appliedToMatch = block.match(/^apply-to:\s*(.+)$/m);
894
+ const topLevelKeys = [];
895
+ for (const line of block.split('\n')) {
896
+ const keyMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*/);
897
+ if (keyMatch) topLevelKeys.push(keyMatch[1]);
898
+ }
843
899
  return {
844
900
  present: true,
845
901
  name: nameMatch ? nameMatch[1].trim() || null : null,
846
902
  description: /^description:\s*\S/m.test(block),
847
903
  validFrom: validFromMatch ? validFromMatch[1].trim() : undefined,
848
904
  appliedTo: appliedToMatch ? appliedToMatch[1].trim() : undefined,
905
+ topLevelKeys,
849
906
  };
850
907
  }
851
908
 
package/lib/lint.test.js CHANGED
@@ -168,6 +168,134 @@ test('skips entire external scope when only some of its files are in filedist',
168
168
  expect(result.errors.join('\n')).not.toContain('Broken local link in');
169
169
  });
170
170
 
171
+ test('reports unknown frontmatter fields in XDR documents', () => {
172
+ const workspaceRoot = createWorkspace('xdr-unknown-frontmatter', {
173
+ '.xdrs/index.md': rootIndex(),
174
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
175
+ '- [001-main](principles/001-main.md) - Main decision'
176
+ ]),
177
+ '.xdrs/_local/adrs/principles/001-main.md': [
178
+ '---',
179
+ 'name: _local-adr-001-main',
180
+ 'description: Test XDR document',
181
+ 'unknown-field: some value',
182
+ '---',
183
+ '',
184
+ '# _local-adr-001: Main decision',
185
+ '',
186
+ '## Context and Problem Statement',
187
+ '',
188
+ 'Test body.',
189
+ '',
190
+ '## Decision Outcome',
191
+ '',
192
+ 'Test decision outcome.',
193
+ ''
194
+ ].join('\n'),
195
+ });
196
+
197
+ const result = lintWorkspace(workspaceRoot);
198
+
199
+ expect(result.errors.join('\n')).toContain('XDR frontmatter has unknown field "unknown-field"');
200
+ });
201
+
202
+ test('accepts all known frontmatter fields in XDR documents', () => {
203
+ const workspaceRoot = createWorkspace('xdr-known-frontmatter', {
204
+ '.xdrs/index.md': rootIndex(),
205
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
206
+ '- [001-main](principles/001-main.md) - Main decision'
207
+ ]),
208
+ '.xdrs/_local/adrs/principles/001-main.md': [
209
+ '---',
210
+ 'name: _local-adr-001-main',
211
+ 'description: Test XDR document',
212
+ 'apply-to: All projects',
213
+ 'valid-from: 2026-01-01',
214
+ 'license: MIT',
215
+ 'metadata:',
216
+ ' author: test',
217
+ '---',
218
+ '',
219
+ '# _local-adr-001: Main decision',
220
+ '',
221
+ '## Context and Problem Statement',
222
+ '',
223
+ 'Test body.',
224
+ '',
225
+ '## Decision Outcome',
226
+ '',
227
+ 'Test decision outcome.',
228
+ ''
229
+ ].join('\n'),
230
+ });
231
+
232
+ const result = lintWorkspace(workspaceRoot);
233
+
234
+ expect(result.errors.join('\n')).not.toContain('unknown field');
235
+ });
236
+
237
+ test('reports unknown frontmatter fields in SKILL.md files', () => {
238
+ const workspaceRoot = createWorkspace('skill-unknown-frontmatter', {
239
+ '.xdrs/index.md': rootIndex(),
240
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
241
+ '- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
242
+ ]),
243
+ '.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': [
244
+ '---',
245
+ 'name: 001-check-links',
246
+ 'description: Test skill document',
247
+ 'unknown-field: some value',
248
+ '---',
249
+ '',
250
+ '## Overview',
251
+ '',
252
+ 'Test skill overview.',
253
+ '',
254
+ '## Instructions',
255
+ '',
256
+ 'Test instructions.',
257
+ ''
258
+ ].join('\n'),
259
+ });
260
+
261
+ const result = lintWorkspace(workspaceRoot);
262
+
263
+ expect(result.errors.join('\n')).toContain('SKILL.md frontmatter has unknown field "unknown-field"');
264
+ });
265
+
266
+ test('accepts all known frontmatter fields in SKILL.md files', () => {
267
+ const workspaceRoot = createWorkspace('skill-known-frontmatter', {
268
+ '.xdrs/index.md': rootIndex(),
269
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
270
+ '- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check links'
271
+ ]),
272
+ '.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': [
273
+ '---',
274
+ 'name: 001-check-links',
275
+ 'description: Test skill document',
276
+ 'license: MIT',
277
+ 'metadata:',
278
+ ' version: "1.0"',
279
+ 'compatibility: node >= 18',
280
+ 'allowed-tools: read_file grep_search',
281
+ '---',
282
+ '',
283
+ '## Overview',
284
+ '',
285
+ 'Test skill overview.',
286
+ '',
287
+ '## Instructions',
288
+ '',
289
+ 'Test instructions.',
290
+ ''
291
+ ].join('\n'),
292
+ });
293
+
294
+ const result = lintWorkspace(workspaceRoot);
295
+
296
+ expect(result.errors.join('\n')).not.toContain('unknown field');
297
+ });
298
+
171
299
  test('derives expected frontmatter name from the markdown heading title', () => {
172
300
  const workspaceRoot = createWorkspace('heading-name-match', {
173
301
  '.xdrs/index.md': rootIndex(),
@@ -514,6 +642,100 @@ test('reports orphan asset in articles .assets directory', () => {
514
642
  expect(result.errors.join('\n')).not.toContain('used.png');
515
643
  });
516
644
 
645
+ test('reports unexpected directories inside a skill package', () => {
646
+ const workspaceRoot = createWorkspace('unexpected-skill-package-directory', {
647
+ '.xdrs/index.md': rootIndex(),
648
+ '.xdrs/_local/index.md': '# _local Scope Overview\n\nOverview.\n\n[ADRs](adrs/index.md)\n',
649
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
650
+ '- [001-check-links](principles/skills/001-check-links/SKILL.md) - Check local links'
651
+ ]),
652
+ '.xdrs/_local/adrs/principles/skills/001-check-links/SKILL.md': skillDocument('Skill body.'),
653
+ '.xdrs/_local/adrs/principles/skills/001-check-links/extras/note.txt': 'unexpected',
654
+ });
655
+
656
+ const result = lintWorkspace(workspaceRoot);
657
+
658
+ expect(result.errors.join('\n')).toContain('Unexpected directory in skill package');
659
+ expect(result.errors.join('\n')).toContain('extras');
660
+ });
661
+
662
+ test('reports nested directories in .assets when it has 10 files or fewer', () => {
663
+ const workspaceRoot = createWorkspace('asset-subdir-too-small', {
664
+ '.xdrs/index.md': rootIndex(),
665
+ '.xdrs/_local/index.md': '# _local Scope Overview\n\nOverview.\n\n[ADRs](adrs/index.md)\n',
666
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
667
+ '- [001-main](principles/001-main.md) - Main decision'
668
+ ]),
669
+ '.xdrs/_local/adrs/principles/001-main.md': [
670
+ '---',
671
+ 'name: _local-adr-001-main-decision',
672
+ 'description: Test XDR document',
673
+ '---',
674
+ '',
675
+ '# _local-adr-001: Main decision',
676
+ '',
677
+ '## Context and Problem Statement',
678
+ '',
679
+ 'See ![img](.assets/used.png).',
680
+ '',
681
+ '## Decision Outcome',
682
+ '',
683
+ 'Test decision outcome.',
684
+ ''
685
+ ].join('\n'),
686
+ '.xdrs/_local/adrs/principles/.assets/used.png': Buffer.alloc(0),
687
+ '.xdrs/_local/adrs/principles/.assets/grouped/extra.png': Buffer.alloc(0),
688
+ });
689
+
690
+ const result = lintWorkspace(workspaceRoot);
691
+
692
+ expect(result.errors.join('\n')).toContain('.assets directory must be flat unless it already contains more than 10 files');
693
+ expect(result.errors.join('\n')).toContain('.assets/grouped');
694
+ });
695
+
696
+ test('allows nested directories in .assets when it has more than 10 files', () => {
697
+ const workspaceRoot = createWorkspace('asset-subdir-large-enough', {
698
+ '.xdrs/index.md': rootIndex(),
699
+ '.xdrs/_local/index.md': '# _local Scope Overview\n\nOverview.\n\n[ADRs](adrs/index.md)\n',
700
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
701
+ '- [001-main](principles/001-main.md) - Main decision'
702
+ ]),
703
+ '.xdrs/_local/adrs/principles/001-main.md': [
704
+ '---',
705
+ 'name: _local-adr-001-main-decision',
706
+ 'description: Test XDR document',
707
+ '---',
708
+ '',
709
+ '# _local-adr-001: Main decision',
710
+ '',
711
+ '## Context and Problem Statement',
712
+ '',
713
+ 'See ![img](.assets/used.png).',
714
+ '',
715
+ '## Decision Outcome',
716
+ '',
717
+ 'Test decision outcome.',
718
+ ''
719
+ ].join('\n'),
720
+ '.xdrs/_local/adrs/principles/.assets/used.png': Buffer.alloc(0),
721
+ '.xdrs/_local/adrs/principles/.assets/01.png': Buffer.alloc(0),
722
+ '.xdrs/_local/adrs/principles/.assets/02.png': Buffer.alloc(0),
723
+ '.xdrs/_local/adrs/principles/.assets/03.png': Buffer.alloc(0),
724
+ '.xdrs/_local/adrs/principles/.assets/04.png': Buffer.alloc(0),
725
+ '.xdrs/_local/adrs/principles/.assets/05.png': Buffer.alloc(0),
726
+ '.xdrs/_local/adrs/principles/.assets/06.png': Buffer.alloc(0),
727
+ '.xdrs/_local/adrs/principles/.assets/07.png': Buffer.alloc(0),
728
+ '.xdrs/_local/adrs/principles/.assets/08.png': Buffer.alloc(0),
729
+ '.xdrs/_local/adrs/principles/.assets/09.png': Buffer.alloc(0),
730
+ '.xdrs/_local/adrs/principles/.assets/10.png': Buffer.alloc(0),
731
+ '.xdrs/_local/adrs/principles/.assets/grouped/11.png': Buffer.alloc(0),
732
+ });
733
+
734
+ const result = lintWorkspace(workspaceRoot);
735
+
736
+ expect(result.errors.join('\n')).not.toContain('.assets directory must be flat unless it already contains more than 10 files');
737
+ });
738
+
517
739
  function teamAdrIndex(entries) {
518
740
  return [
519
741
  '# myteam ADR Index',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "A standard way to organize Decision Records (XDRs) across scopes, subjects, and teams so that AI agents can reliably query and follow them.",
5
5
  "repository": {
6
6
  "type": "git",