xdrs-core 0.50.0 → 0.51.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.
@@ -62,12 +62,13 @@ When multiple source entries point to the same content (e.g., a web page and its
62
62
 
63
63
  #### 07-compilation-meta-policy
64
64
 
65
- Before any compilation begins, at least one local meta-policy file named `NNN-core.md` MUST exist in a `[type]/principles/` directory of the target scope (e.g., `adrs/principles/001-core.md`). Compilation tools and agents MUST scan all type folders (`adrs`, `bdrs`, `edrs`) for such meta-policy files; each meta-policy governs compilation for its own type folder.
65
+ Before any compilation begins, at least one local meta-policy file named `NNN-core.md` MUST exist in a `[type]/principles/` directory of the target scope (e.g., `adrs/principles/001-core.md`). Compilation tools and agents MUST scan all type folders (`adrs`, `bdrs`, `edrs`) for such meta-policy files; each meta-policy governs compilation for its own type folder, including every subject compiled under it.
66
66
 
67
67
  The meta-policy MUST contain the following sections:
68
68
 
69
- - `## Sources` — one bullet per source using the format `- [web] https://...`, `- [git] git@...`, or `- [local] ./path`. Each source entry MAY include an inline description after the URL.
69
+ - `## Sources` — one bullet per source using the format `- [web] name: https://...`, `- [git] name: git@...`, or `- [local] name: ./path`. `name` MUST be a lowercase kebab-case slug (`[a-z0-9-]+`) that meaningfully identifies the source's organisation, project, or content (e.g., `github-official-docs`, `spreadsheet-team-123`). It is used as the folder name under `.assets/sources/` (rule 12) and as the path prefix in `## Source` sections (rule 10). Multiple bullets MAY share the same `name` to list alternate access methods for the same logical source — the preference order in rule 06 picks which one is actually fetched. Each source entry MAY include an inline description after the URL.
70
70
  - `## Selectors` — describes which portions of the fetched source to include or exclude, expressed as document sections, filename patterns, topics, or other criteria.
71
+ - `## Sync Settings` — governs re-synchronisation behaviour (rule 13). MUST contain a `Source re-sync period days: N` bullet, where `N` is a positive integer number of days. MAY contain a `Source storage: temporary` bullet; when absent, source storage defaults to persistent (rule 12).
71
72
 
72
73
  The meta-policy MAY also contain:
73
74
 
@@ -85,15 +86,15 @@ There is no fixed position for `compiled`-type scopes in the root `index.md`. Wh
85
86
 
86
87
  #### 10-source-section
87
88
 
88
- Every compiled policy MUST include a `## Source` section placed after the `## References` section (or after `## Decision Outcome` when no `## References` section is present). The section MUST list the relative file path(s) of the source document(s) used to produce the policy as plain text. Paths MUST NOT be written as markdown links, because the source files exist only inside a temporary local directory that is created during the compilation or review process.
89
+ Every compiled policy MUST include a `## Source` section placed after the `## References` section (or after `## Decision Outcome` when no `## References` section is present). The section MUST list the relative file path(s) of the source document(s) used to produce the policy as plain text, relative to the scope's `.assets/sources/` directory. Paths MUST NOT be written as markdown links: when source storage is temporary, the paths only exist for the duration of the compilation or review process; when source storage is persistent, `.assets/sources/` is exempt from orphan-asset tracking and, per rule 14, MUST NOT be linked to directly — these paths identify compilation inputs, not navigable or citable XDRS resources.
89
90
 
90
91
  Example:
91
92
 
92
93
  ```
93
94
  ## Source
94
95
 
95
- - source-1/standards/owasp-top-10-2021/A01-broken-access-control.md
96
- - source-2/docs/security-controls.html
96
+ - owasp-top10/standards/owasp-top-10-2021/A01-broken-access-control.md
97
+ - security-controls-docs/docs/security-controls.html
97
98
  ```
98
99
 
99
100
  #### 11-compilation-notes
@@ -109,6 +110,37 @@ All access control implementations MUST enforce the principle of least privilege
109
110
  **compilation-note:** derived from OWASP A01:2021 "How to Prevent", bullet 3 — "Deny by default".
110
111
  ```
111
112
 
113
+ #### 12-source-storage-and-tracking
114
+
115
+ Unless a meta-policy declares `Source storage: temporary`, fetched and selected source content (rule 13) MUST be persisted under `[type]/principles/.assets/sources/[name]/`, sibling to the governing meta-policy, where `[name]` is the source's slug from its `## Sources` bullet. This location is shared across every subject compiled under that meta-policy's type folder; it is not duplicated per subject. Persisted source content MUST be committed to version control and MUST NOT be gitignored.
116
+
117
+ When `Source storage: temporary` is declared, bulk fetched content MUST instead be written only to the ephemeral compilation working directory and removed at the end of the compilation run.
118
+
119
+ Regardless of storage mode, a tracking file `.assets/sources/[name]/source.md` MUST always be created and kept up to date, with the following format:
120
+
121
+ ```
122
+ # Source
123
+
124
+ last-fetch-timestamp: YYYY-MM-DD HH:MM:SS
125
+ last-compilation-timestamp: YYYY-MM-DD HH:MM:SS
126
+ ```
127
+
128
+ `last-fetch-timestamp` MUST be updated whenever that source is freshly fetched. `last-compilation-timestamp` MUST be updated only after a compilation cycle consuming that source completes lint and review successfully. If a meta-policy's `Source storage` setting changes between compilation runs, the next compilation MUST reconcile storage accordingly: switching to `temporary` MUST remove any previously persisted bulk content under `.assets/sources/[name]/` while keeping `source.md`; switching away from `temporary` MUST resume persisting bulk content going forward.
129
+
130
+ #### 13-selective-persistence-and-resync-optimization
131
+
132
+ Only content matched by `## Selectors` and actually used by at least one compiled policy's `## Source` section MAY be written to `.assets/sources/[name]/`. Content excluded by selectors, or that becomes unused after a re-sync, MUST be removed from persisted storage — `.assets/sources/[name]/` MUST NOT accumulate content beyond what compiled policies currently depend on.
133
+
134
+ Before fetching, compilation tools and agents MUST evaluate whether a re-sync is necessary: compare the elapsed days since `last-fetch-timestamp` against `Source re-sync period days`, and confirm `last-compilation-timestamp` is not older than `last-fetch-timestamp`. If every source governed by a meta-policy is within its re-sync period and already fully compiled, no fetch or recompilation is required unless explicitly requested. When a re-sync does proceed, newly fetched and selected content MUST be compared against the previous `.assets/sources/[name]/` snapshot (for example, by content hash) to scope which existing compiled policies need re-verification and which can be kept unchanged.
135
+
136
+ #### 14-source-documents-not-authoritative
137
+
138
+ By default, content persisted under `.assets/sources/` MUST NOT be treated as a source of truth, cited, or used directly to make architectural, business, or engineering decisions. Only compiled policies — the numbered Policy documents produced from this content — are authoritative for decision-making. Content under `.assets/sources/` exists to support synchronisation, diffing, and recompilation (rules 12 and 13); it is unstructured relative to compiled policies and MAY be misleading, inconsistent, or outdated.
139
+
140
+ An exception MUST be documented in a dedicated `[type]/principles/NNN-direct-source-usage.md` policy, mirroring the dedicated-file convention of rule 08. That policy MUST explicitly state when and how direct use of source documents is permitted. Absent such a policy, direct use of source documents for decisions is forbidden.
141
+
142
+ This rule governs reasoning and usage, not the document-link graph. Independently of this rule, and regardless of whether a `NNN-direct-source-usage.md` policy exists, no document MAY hyperlink directly into `.assets/sources/`; that restriction is enforced mechanically and addresses a separate, structural concern.
143
+
112
144
  ## References
113
145
 
114
146
  - [_core-adr-policy-008 - Policy structured standards](008-policy-structured-standards.md) — numbered rule format required for all policies in compiled scopes
@@ -10,7 +10,7 @@ metadata:
10
10
 
11
11
  ## Overview
12
12
 
13
- Performs a full compilation cycle for any scope declaring `scope-type: compiled`: discovers compilation meta-policies across all type folders, fetches sources into a temporary directory, plans which policies to create, update, or remove, migrates them one at a time with full source traceability, runs lint and review, and documents any inconsistencies found in the source. Works for both initial compilation and re-compilation (updates). Requires `_core-adr-policy-019-compiled-scope-type` to be present in the workspace.
13
+ Performs a full compilation cycle for any scope declaring `scope-type: compiled`: discovers compilation meta-policies across all type folders, checks whether each governed source is already in sync before doing any work, fetches due sources into a temporary staging directory, plans which policies to create, update, or remove, migrates them one at a time with full source traceability, runs lint and review, persists selected source content and updates sync tracking, and documents any inconsistencies found in the source. Works for both initial compilation and re-compilation (updates). By default, fetched source content used to compile policies is persisted under `.assets/sources/[name]/` in the scope (see `_core-adr-policy-019` rules 12–13); this content is never authoritative for decisions (rule 14). Requires `_core-adr-policy-019-compiled-scope-type` to be present in the workspace.
14
14
 
15
15
  ### Inputs
16
16
 
@@ -20,13 +20,13 @@ Performs a full compilation cycle for any scope declaring `scope-type: compiled`
20
20
 
21
21
  #### Optional
22
22
 
23
- - None.
23
+ - Force-refetch flag to bypass the re-sync short-circuit.
24
24
 
25
25
  ### Outputs
26
26
 
27
27
  #### Contents
28
28
 
29
- - Updated compiled scope, linted and reviewed.
29
+ - Updated compiled scope, or a no-op already-in-sync report.
30
30
 
31
31
  #### Changes
32
32
 
@@ -56,39 +56,51 @@ Performs a full compilation cycle for any scope declaring `scope-type: compiled`
56
56
  2. Scan ALL type folders (`adrs/`, `bdrs/`, `edrs/`) inside the target scope for local meta-policy files whose filename matches `NNN-core.md` in `[type]/principles/`. Collect every match.
57
57
  3. If no meta-policy is found in any type folder, output: `FAIL — Compilation cannot proceed: no local meta-policy NNN-core.md found in any [type]/principles/ folder of [scope-name]. Create it first with ## Sources and ## Selectors sections per _core-adr-policy-019 rule 07.`
58
58
  4. Read every found meta-policy fully. For each, extract:
59
- - **Sources**: parse all `- [web] ...`, `- [git] ...`, `- [local] ...` bullet entries from the `## Sources` section.
60
- - **Selectors**: read the `## Selectors` section content (used in Phase 2).
61
- - **Fetch Procedures**: read the `## Fetch Procedures` section if present (used in Phase 1).
62
- - **Compilation Notes**: read if present (used in Phase 2–3).
63
- - **Review Notes**: read if present (used in Phase 6).
64
-
65
- ### Phase 1: Fetch Sources
66
-
67
- 1. Create a temporary directory `.tmp/compilation-[YYYYMMDDHHMMSS]/` in the workspace root.
68
- 2. For each source discovered in Phase 0, fetch in the following preference order (first available wins; if all equivalent URLs are listed, try each in order):
69
- - **Git URL** (`- [git] ...`): run `git clone --depth=1 [url] .tmp/compilation-[ts]/source-N/`. If `git` is not available or the clone fails, fall back to the next option.
70
- - **Local folder** (`- [local] ...`): copy the folder contents to `.tmp/compilation-[ts]/source-N/`.
71
- - **Web URL** (`- [web] ...`): before scraping, search the workspace for a skill specialized in fetching content from this URL or website domain (e.g., a skill whose description or name references the domain, the product, or the content type). Some websites require special handling — SSO, client certificates, shadow DOM, iframes, CAPTCHAs, or other quirks. If a specialized fetch skill is found, follow it to retrieve the content. If no specialized skill exists, fall back to `npx --package=@playwright/cli@latest playwright-cli`; run with `--help` to confirm available commands and save output as `.html` file(s) in `.tmp/compilation-[ts]/source-N/`.
59
+ - **Sources**: parse all `- [web] ...`, `- [git] ...`, `- [local] ...` bullet entries from the `## Sources` section, including each source's `name` slug (per `_core-adr-policy-019` rule 07).
60
+ - **Selectors**: read the `## Selectors` section content (used in Phase 3).
61
+ - **Sync Settings**: parse `Source re-sync period days` and, if present, `Source storage: temporary` from the `## Sync Settings` section (used in Phase 1 and Phase 9).
62
+ - **Fetch Procedures**: read the `## Fetch Procedures` section if present (used in Phase 2).
63
+ - **Compilation Notes**: read if present (used in Phase 3–4).
64
+ - **Review Notes**: read if present (used in Phase 7).
65
+
66
+ ### Phase 1: Check Sync Necessity
67
+
68
+ 1. For each source `name` extracted in Phase 0, look for `.assets/sources/[name]/source.md` in the governed type folder.
69
+ 2. If `source.md` does not exist for a source, that source has never been synced — it is due for Phase 2.
70
+ 3. If `source.md` exists, read its `last-fetch-timestamp` and `last-compilation-timestamp`. Compute the elapsed days since `last-fetch-timestamp`. That source is due for Phase 2 if either is true: the elapsed days exceed the meta-policy's `Source re-sync period days`, or `last-compilation-timestamp` is older than `last-fetch-timestamp` (a previous run fetched but never finished compiling).
71
+ 4. If the input includes an explicit force-refetch/recompile request (see Inputs > Optional), treat every source as due regardless of timestamps.
72
+ 5. If no source across any governed meta-policy is due, output: `OK — [scope-name] is already in sync. No action taken.` and halt — do not proceed to later phases.
73
+ 6. Otherwise, proceed to Phase 2, but scope fetching and compilation to only the meta-policies and sources that are due; meta-policies with no due sources MAY be skipped entirely.
74
+
75
+ ### Phase 2: Fetch Sources
76
+
77
+ 1. Create a temporary directory `.tmp/compilation-[YYYYMMDDHHMMSS]/staging/` in the workspace root.
78
+ 2. For each due source (from Phase 1), fetch in the following preference order (first available wins; if all equivalent URLs are listed, try each in order):
79
+ - **Git URL** (`- [git] ...`): run `git clone --depth=1 [url] .tmp/compilation-[ts]/staging/[name]/`. If `git` is not available or the clone fails, fall back to the next option.
80
+ - **Local folder** (`- [local] ...`): copy the folder contents to `.tmp/compilation-[ts]/staging/[name]/`.
81
+ - **Web URL** (`- [web] ...`): before scraping, search the workspace for a skill specialized in fetching content from this URL or website domain (e.g., a skill whose description or name references the domain, the product, or the content type). Some websites require special handling — SSO, client certificates, shadow DOM, iframes, CAPTCHAs, or other quirks. If a specialized fetch skill is found, follow it to retrieve the content. If no specialized skill exists, fall back to `npx --package=@playwright/cli@latest playwright-cli`; run with `--help` to confirm available commands and save output as `.html` file(s) in `.tmp/compilation-[ts]/staging/[name]/`.
72
82
  3. If any source cannot be fetched via any listed option, report the failure and continue with remaining sources. Do not abort the entire compilation for a single failed source.
73
- 4. Convert non-markdown documents found in the fetched source directories. Use only the format converters actually needed:
83
+ 4. Immediately after a source is fetched successfully, create `.assets/sources/[name]/source.md` if absent and update its `last-fetch-timestamp` to the current time — this happens regardless of the `Source storage` setting, since the tracking file always persists per `_core-adr-policy-019` rule 12.
84
+ 5. Convert non-markdown documents found in the fetched source directories. Use only the format converters actually needed:
74
85
  - PDF files: `uvx markitdown[pdf] <file> -o <output>.md`
75
86
  - XLSX files: `uvx markitdown[xlsx] <file> -o <output>.md`
76
87
  - DOCX files: `uvx markitdown[docx] <file> -o <output>.md`
77
88
  - PPTX files: `uvx markitdown[pptx] <file> -o <output>.md`
78
89
  - HTML files saved from web scraping: `uvx markitdown <file.html> -o <output>.md`
79
90
 
80
- ### Phase 2: Plan Policies
91
+ ### Phase 3: Plan Policies
81
92
 
82
- 1. For each type folder governed by a meta-policy (from Phase 0), apply the `## Selectors` rules to the fetched content to determine which portions to include.
93
+ 1. For each type folder governed by a meta-policy (from Phase 0), apply the `## Selectors` rules to the fetched staging content to determine which portions to include.
83
94
  2. List all existing policies in the scope under each governed type folder.
84
- 3. Analyse the selected content and produce a TODO list showing for each expected policy:
95
+ 3. If `.assets/sources/[name]/` already holds a previous persisted snapshot, diff the newly selected staging content against it (for example, by content hash per file) to pre-seed which policies are likely unaffected.
96
+ 4. Analyse the selected content and produce a TODO list showing for each expected policy:
85
97
  - **CREATE** — new policy to be compiled from source (does not yet exist in scope)
86
98
  - **UPDATE** — existing policy whose source content has changed
87
99
  - **REMOVE** — existing policy whose source content no longer exists or is excluded by selectors
88
- - **KEEP** — existing policy that is up to date; no action needed
89
- 4. Present the TODO list to the user and wait for confirmation before proceeding to Phase 3.
100
+ - **KEEP** — existing policy that is up to date; no action needed (default for policies whose source content is unchanged per the step 3 diff)
101
+ 5. Present the TODO list to the user and wait for confirmation before proceeding to Phase 4.
90
102
 
91
- ### Phase 3: Migrate Policies (one at a time)
103
+ ### Phase 4: Migrate Policies (one at a time)
92
104
 
93
105
  For each TODO item marked CREATE or UPDATE, in sequence:
94
106
 
@@ -99,21 +111,21 @@ For each TODO item marked CREATE or UPDATE, in sequence:
99
111
 
100
112
  2. **Add a `**compilation-note:**` marker** inside a rule body only when the mapping from source to rule is not obvious — for example: indirect references, conclusions drawn by connecting multiple source sections, interpretations that required judgment, or cases where the same source passage could be read differently. Do NOT add a marker when the rule is a direct, literal transcription of a single source statement. Place the marker as a new sentence at the end of the rule body. Example:
101
113
  ```
102
- All API keys MUST be rotated every 90 days. **compilation-note:** derived from [source-1/security/api-guidelines.md section 4.2] — "Keys should be invalidated and replaced quarterly"; frequency inferred as 90 days from the term "quarterly".
114
+ All API keys MUST be rotated every 90 days. **compilation-note:** derived from [api-guidelines/security/api-guidelines.md section 4.2] — "Keys should be invalidated and replaced quarterly"; frequency inferred as 90 days from the term "quarterly".
103
115
  ```
104
116
 
105
- 3. **Add a `## Source` section** as the LAST section of the policy (after `## References` if present, otherwise after `## Decision Outcome`). List the relative file path(s) of the source document(s) as plain text — NOT as markdown links. Paths are relative to the temporary source directory (e.g., `source-1/docs/chapter-3.md`).
117
+ 3. **Add a `## Source` section** as the LAST section of the policy (after `## References` if present, otherwise after `## Decision Outcome`). List the relative file path(s) of the source document(s) as plain text — NOT as markdown links. Paths are relative to `.assets/sources/` and use the source's `name` slug as their first segment (e.g., `api-guidelines/standards/section-4/api-security.md`), regardless of whether storage is currently persistent or temporary.
106
118
  ```
107
119
  ## Source
108
120
 
109
- - source-1/standards/section-4/api-security.md
121
+ - api-guidelines/standards/section-4/api-security.md
110
122
  ```
111
123
 
112
124
  4. For REMOVE items: delete the policy file and remove its entry from the type's `index.md`.
113
125
 
114
- 5. Note any content in the source that is inconsistent, contradictory, or cannot be faithfully expressed as a valid XDRS policy. Record these for Phase 7.
126
+ 5. Note any content in the source that is inconsistent, contradictory, or cannot be faithfully expressed as a valid XDRS policy. Record these for Phase 8.
115
127
 
116
- ### Phase 4: Verify Each Policy
128
+ ### Phase 5: Verify Each Policy
117
129
 
118
130
  After writing each policy:
119
131
 
@@ -121,25 +133,25 @@ After writing each policy:
121
133
  2. Confirm the policy uses structured numbered rule blocks (`_core-adr-policy-008`).
122
134
  3. Confirm `**compilation-note:**` markers are present for every rule whose mapping from source is non-obvious (indirect references, multi-source conclusions, interpretations requiring judgment). Direct literal transcriptions do not require a marker.
123
135
  4. Confirm no normative statements are present that cannot be traced to the listed source file.
124
- 5. Add any new inconsistencies found to the list for Phase 7.
136
+ 5. Add any new inconsistencies found to the list for Phase 8.
125
137
 
126
- ### Phase 5: Lint
138
+ ### Phase 6: Lint
127
139
 
128
140
  1. Run `npx xdrs-core lint` from the workspace root (or `make lint` if a `Makefile` is present with a `lint` target).
129
141
  2. Review all reported errors and warnings.
130
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.
131
143
  4. Re-run lint after fixes until it passes or only unfixable findings remain.
132
144
 
133
- ### Phase 6: Review
145
+ ### Phase 7: Review
134
146
 
135
147
  1. Read and follow `.xdrs/_core/adrs/principles/skills/review/SKILL.md` fully, scoped to the compiled scope.
136
148
  2. Apply any review notes from the meta-policy's `## Review Notes` section as additional mandatory conventions during this review.
137
149
  3. Fix only mechanical and structural findings from the review. Do NOT invent content to resolve content-level findings.
138
- 4. Any review findings that require inventing or guessing content MUST be recorded as inconsistencies in Phase 7.
150
+ 4. Any review findings that require inventing or guessing content MUST be recorded as inconsistencies in Phase 8.
139
151
 
140
- ### Phase 7: Document Inconsistencies
152
+ ### Phase 8: Document Inconsistencies
141
153
 
142
- 1. Collect all inconsistencies noted during Phases 3, 4, and 6.
154
+ 1. Collect all inconsistencies noted during Phases 4, 5, and 7.
143
155
  2. If any inconsistencies exist:
144
156
  - Determine the next available policy number in the `001–100` principles block for the relevant type folder.
145
157
  - Create or update `[type]/principles/NNN-inconsistencies-from-source.md` in the scope.
@@ -148,25 +160,46 @@ After writing each policy:
148
160
  - Do NOT propose or include fixes.
149
161
  3. If no inconsistencies exist, report that the compilation is clean.
150
162
 
151
- ### Phase 8: Cleanup
163
+ ### Phase 9: Persist Sources and Update Tracking
152
164
 
153
- 1. Remove the temporary directory `.tmp/compilation-[ts]/` created in Phase 1.
165
+ 1. For each meta-policy and source processed in this run, check its `Source storage` setting.
166
+ 2. If storage is persistent (the default): replace the contents of `.assets/sources/[name]/` (excluding `source.md`) with the staged content actually selected and used in Phase 3–4 — remove any previously persisted file that is no longer selected or used, per `_core-adr-policy-019` rule 13.
167
+ 3. If storage is `temporary`: do not copy any bulk staged content into `.assets/sources/[name]/`; only `source.md` lives there.
168
+ 4. For every source processed in this run, update `.assets/sources/[name]/source.md`'s `last-compilation-timestamp` to the current time — this step only runs after Phase 6 (Lint) and Phase 7 (Review) both pass for the policies depending on that source.
169
+ 5. If a source's `Source storage` setting changed since its last run, reconcile per `_core-adr-policy-019` rule 12: switching to `temporary` removes previously persisted bulk content (keeping `source.md`); switching away from `temporary` begins persisting bulk content from this run onward.
170
+
171
+ ### Phase 10: Cleanup
172
+
173
+ 1. Remove the temporary staging directory `.tmp/compilation-[ts]/` created in Phase 2. Never remove or modify persisted content under `.assets/sources/` — that is governed solely by Phase 9.
154
174
  2. Report a compilation summary:
155
175
  - Policies created, updated, removed.
156
176
  - Inconsistencies documented (with file path).
157
177
  - Any sources that could not be fetched.
158
178
  - Lint and review pass/fail status.
179
+ - Sources persisted, kept temporary, or skipped as already in sync.
159
180
 
160
181
  ## Anti-Patterns
161
182
 
162
183
  - **Mistake:** Inventing content to fill gaps when source material is incomplete or ambiguous.
163
184
  **Why it happens:** The agent wants to produce a "complete" policy and fills gaps with plausible-sounding text.
164
- **Instead:** Transcribe faithfully, preserve ambiguities as-is, and record inconsistencies in Phase 7 instead of guessing.
185
+ **Instead:** Transcribe faithfully, preserve ambiguities as-is, and record inconsistencies in Phase 8 instead of guessing.
165
186
 
166
- - **Mistake:** Skipping the Phase 2 confirmation and migrating policies before the user approves the CREATE/UPDATE/REMOVE plan.
187
+ - **Mistake:** Skipping the Phase 3 confirmation and migrating policies before the user approves the CREATE/UPDATE/REMOVE plan.
167
188
  **Why it happens:** Momentum from prior compilations makes the plan feel obviously correct.
168
- **Instead:** Always present the TODO list and wait for explicit confirmation before Phase 3.
189
+ **Instead:** Always present the TODO list and wait for explicit confirmation before Phase 4.
169
190
 
170
191
  - **Mistake:** Leaving the temporary `.tmp/compilation-[ts]/` directory behind after a run.
171
- **Why it happens:** Phase 8 cleanup is easy to forget once the summary is reported.
172
- **Instead:** Always remove the temporary directory as the first Phase 8 step, even on partial failures.
192
+ **Why it happens:** Phase 10 cleanup is easy to forget once the summary is reported.
193
+ **Instead:** Always remove the temporary directory as the first Phase 10 step, even on partial failures.
194
+
195
+ - **Mistake:** Persisting every fetched file under `.assets/sources/[name]/`, including content excluded by selectors or no longer used by any compiled policy.
196
+ **Why it happens:** It feels safer to keep everything "just in case" rather than compute what is actually still used.
197
+ **Instead:** Persist only Selector-matched content actually referenced by a compiled policy's `## Source` section, per `_core-adr-policy-019` rule 13; remove the rest during Phase 9.
198
+
199
+ - **Mistake:** Skipping Phase 1 and always re-fetching and recompiling, even when every source is well within its re-sync period.
200
+ **Why it happens:** Re-running the full cycle unconditionally feels more thorough or "safe".
201
+ **Instead:** Always check `.assets/sources/[name]/source.md` timestamps against `Source re-sync period days` first, and short-circuit with a no-op report when nothing is due.
202
+
203
+ - **Mistake:** Reading or citing content under `.assets/sources/` as if it were an authoritative policy, or adding a markdown link to it from another document.
204
+ **Why it happens:** The raw source content is readily available and often more detailed than the compiled policy.
205
+ **Instead:** Treat only compiled policies as authoritative per `_core-adr-policy-019` rule 14; source content exists solely to support fetch/diff/recompilation, and linking to it is also forbidden mechanically by lint.
package/README.md CHANGED
@@ -90,13 +90,18 @@ Every scope declares a `scope-type` in its `index.md` frontmatter. The six built
90
90
  ```yaml
91
91
  # .xdrs/owasp-top10/adrs/principles/001-core.md (compilation meta-policy)
92
92
  ## Sources
93
- - [web] https://owasp.org/Top10/
94
- - [git] https://github.com/OWASP/Top10.git
93
+ - [web] owasp-top10-web: https://owasp.org/Top10/
94
+ - [git] owasp-top10-git: https://github.com/OWASP/Top10.git
95
95
 
96
96
  ## Selectors
97
97
  Include A01–A10 risk entries only.
98
+
99
+ ## Sync Settings
100
+ - Source re-sync period days: 30
98
101
  ```
99
102
 
103
+ By default, fetched and selected source content persists under `.assets/sources/[name]/` so later compilations can re-sync incrementally instead of always refetching; set `Source storage: temporary` in `## Sync Settings` to discard the bulk content after each run instead (only the per-source tracking file persists in that mode). Either way, `.assets/sources/` content is never authoritative on its own — only the compiled policies are.
104
+
100
105
  **`standard` scopes** are the default. Use them for team, product, or business-area scopes that do not fit the other types.
101
106
 
102
107
  **`_local` scopes** hold workspace-specific content such as local tooling decisions, transition overrides, or temporary decisions. This content is never packaged or distributed.
package/lib/lint.js CHANGED
@@ -25,6 +25,8 @@ const SKILL_DIR_RE = /^[a-z0-9-]+$/;
25
25
  const REQUIRED_ROOT_INDEX_TEXT = 'XDRS scopes listed last override the ones listed first';
26
26
  const SUBJECT_ARTIFACT_DIRS = new Set(['skills', 'articles', 'researches', 'initiatives']);
27
27
  const RESOURCE_DIR_NAME = '.assets';
28
+ const SOURCE_SNAPSHOT_DIR_NAME = 'sources';
29
+ const SOURCE_TRACKING_FILE_NAME = 'source.md';
28
30
  const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_DIR_NAME, 'dist']);
29
31
 
30
32
  const POLICY_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'freeze-reference']);
@@ -770,19 +772,58 @@ function lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope = fa
770
772
  errors.push(`Local meta-policy companion "${path.basename(companion.filePath)}" requires a primary "NNN-core.md" in the same type folder "${typeName}": ${toDisplayPath(companion.filePath)} [_core-adr-policy-010.10b-local-primary-required]`);
771
773
  }
772
774
  }
773
- // For compiled scopes: primary meta-policy must include ## Sources and ## Selectors
775
+ // For compiled scopes: primary meta-policy must include ## Sources, ## Selectors, and ## Sync Settings
774
776
  if (isCompiledScope && primaryInFolder) {
775
777
  let metaContent;
776
778
  try { metaContent = fs.readFileSync(primaryInFolder.filePath, 'utf8'); } catch { /* will be caught elsewhere */ }
777
779
  if (metaContent !== undefined) {
778
780
  const metaLines = metaContent.split(/\r?\n/);
779
781
  const metaIgnored = findIgnoredMarkdownLines(metaLines);
780
- if (findHeadingLine(metaLines, metaIgnored, '## Sources') === -1) {
782
+ const metaHeadingLines = findHeadingLines(metaLines, metaIgnored);
783
+
784
+ const sourcesHeadingIdx = findHeadingLine(metaLines, metaIgnored, '## Sources');
785
+ if (sourcesHeadingIdx === -1) {
781
786
  errors.push(`Compiled scope meta-policy must include a "## Sources" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
787
+ } else {
788
+ const [sourcesStart, sourcesEnd] = sectionBodyRange(metaLines, metaHeadingLines, sourcesHeadingIdx);
789
+ for (let i = sourcesStart; i < sourcesEnd; i += 1) {
790
+ if (metaIgnored[i]) continue;
791
+ const trimmed = metaLines[i].trim();
792
+ if (!trimmed.startsWith('-')) continue;
793
+ if (!/^-\s+\[(web|git|local)\]\s+[a-z0-9-]+:\s+.+$/.test(trimmed)) {
794
+ errors.push(`Compiled scope "## Sources" bullet must include a NAME slug in the format "- [web|git|local] name: value": ${toDisplayPath(primaryInFolder.filePath)}:${i + 1} [_core-adr-policy-019.07-compilation-meta-policy]`);
795
+ }
796
+ }
782
797
  }
798
+
783
799
  if (findHeadingLine(metaLines, metaIgnored, '## Selectors') === -1) {
784
800
  errors.push(`Compiled scope meta-policy must include a "## Selectors" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
785
801
  }
802
+
803
+ const syncHeadingIdx = findHeadingLine(metaLines, metaIgnored, '## Sync Settings');
804
+ if (syncHeadingIdx === -1) {
805
+ errors.push(`Compiled scope meta-policy must include a "## Sync Settings" section: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
806
+ } else {
807
+ const [syncStart, syncEnd] = sectionBodyRange(metaLines, metaHeadingLines, syncHeadingIdx);
808
+ let hasValidResyncPeriod = false;
809
+ for (let i = syncStart; i < syncEnd; i += 1) {
810
+ if (metaIgnored[i]) continue;
811
+ const trimmed = metaLines[i].trim();
812
+ if (!trimmed.startsWith('-')) continue;
813
+ const periodMatch = trimmed.match(/^-\s+Source re-sync period days:\s*(\d+)\s*$/);
814
+ if (periodMatch) {
815
+ hasValidResyncPeriod = Number(periodMatch[1]) > 0;
816
+ continue;
817
+ }
818
+ const storageMatch = trimmed.match(/^-\s+Source storage:\s*(.+)$/);
819
+ if (storageMatch && storageMatch[1].trim() !== 'temporary') {
820
+ errors.push(`Compiled scope "## Sync Settings" "Source storage" value must be "temporary" if present: ${toDisplayPath(primaryInFolder.filePath)}:${i + 1} [_core-adr-policy-019.07-compilation-meta-policy]`);
821
+ }
822
+ }
823
+ if (!hasValidResyncPeriod) {
824
+ errors.push(`Compiled scope "## Sync Settings" section must include a "Source re-sync period days: N" bullet with a positive integer: ${toDisplayPath(primaryInFolder.filePath)} [_core-adr-policy-019.07-compilation-meta-policy]`);
825
+ }
826
+ }
786
827
  }
787
828
  }
788
829
  // Check qualifier uniqueness across all type folders
@@ -884,7 +925,7 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
884
925
 
885
926
  const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
886
927
  const xdrsDocsInSubject = artifacts.filter((p) => path.dirname(p) === subjectPath);
887
- lintOrphanAssets(subjectAssetsDir, xdrsDocsInSubject, xdrsRoot, errors);
928
+ lintOrphanAssets(subjectAssetsDir, xdrsDocsInSubject, xdrsRoot, errors, isCompiledScope);
888
929
 
889
930
  return artifacts;
890
931
  }
@@ -1833,11 +1874,16 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors, externalScopes =
1833
1874
  }
1834
1875
  }
1835
1876
 
1836
- function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
1877
+ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors, isCompiledScope = false) {
1837
1878
  if (!existsDirectory(assetsDir)) {
1838
1879
  return;
1839
1880
  }
1840
1881
 
1882
+ const sourcesDir = path.join(assetsDir, SOURCE_SNAPSHOT_DIR_NAME);
1883
+ if (isCompiledScope) {
1884
+ lintSourceSnapshotTracking(sourcesDir, errors);
1885
+ }
1886
+
1841
1887
  const assetTree = collectAssetTree(assetsDir, errors);
1842
1888
  if (assetTree.files.length === 0 && assetTree.directories.length === 0) {
1843
1889
  return;
@@ -1859,6 +1905,9 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
1859
1905
  }
1860
1906
 
1861
1907
  for (const assetPath of assetTree.files) {
1908
+ if (isCompiledScope && isPathInside(sourcesDir, assetPath)) {
1909
+ continue;
1910
+ }
1862
1911
  if (!referencedAssets.has(assetPath)) {
1863
1912
  errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)} [_core-adr-policy-001]`);
1864
1913
  }
@@ -1866,6 +1915,9 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
1866
1915
 
1867
1916
  // Lint slide/presentation files inside .assets
1868
1917
  for (const assetPath of assetTree.files) {
1918
+ if (isCompiledScope && isPathInside(sourcesDir, assetPath)) {
1919
+ continue;
1920
+ }
1869
1921
  const fileName = path.basename(assetPath);
1870
1922
  if (SLIDE_FILE_RE.test(fileName)) {
1871
1923
  lintSlideFile(assetPath, documentPaths, xdrsRoot, errors);
@@ -1873,6 +1925,47 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
1873
1925
  }
1874
1926
  }
1875
1927
 
1928
+ function lintSourceSnapshotTracking(sourcesDir, errors) {
1929
+ if (!existsDirectory(sourcesDir)) {
1930
+ return;
1931
+ }
1932
+
1933
+ const entries = safeReadDir(sourcesDir, errors, `read source snapshot directory ${toDisplayPath(sourcesDir)}`);
1934
+ for (const entry of entries) {
1935
+ if (!entry.isDirectory()) {
1936
+ continue;
1937
+ }
1938
+ const trackingFilePath = path.join(sourcesDir, entry.name, SOURCE_TRACKING_FILE_NAME);
1939
+ if (!existsFile(trackingFilePath)) {
1940
+ errors.push(`Compiled source snapshot folder must contain a "${SOURCE_TRACKING_FILE_NAME}" tracking file: ${toDisplayPath(trackingFilePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
1941
+ continue;
1942
+ }
1943
+ validateSourceTrackingFile(trackingFilePath, errors);
1944
+ }
1945
+ }
1946
+
1947
+ function validateSourceTrackingFile(filePath, errors) {
1948
+ const content = fs.readFileSync(filePath, 'utf8');
1949
+ const lines = content.split(/\r?\n/);
1950
+
1951
+ if (firstNonEmptyLine(content) !== '# Source') {
1952
+ errors.push(`Source tracking file must start with a "# Source" heading: ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
1953
+ }
1954
+
1955
+ const timestampRe = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
1956
+ for (const field of ['last-fetch-timestamp', 'last-compilation-timestamp']) {
1957
+ const fieldLine = lines.find((line) => line.trim().startsWith(`${field}:`));
1958
+ if (!fieldLine) {
1959
+ errors.push(`Source tracking file must include a "${field}:" line: ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
1960
+ continue;
1961
+ }
1962
+ const value = fieldLine.trim().slice(`${field}:`.length).trim();
1963
+ if (!timestampRe.test(value)) {
1964
+ errors.push(`Source tracking file "${field}" must match "YYYY-MM-DD HH:MM:SS": ${toDisplayPath(filePath)} [_core-adr-policy-019.12-source-storage-tracking]`);
1965
+ }
1966
+ }
1967
+ }
1968
+
1876
1969
  function lintSlideFile(filePath, documentPaths, xdrsRoot, errors) {
1877
1970
  const fileName = path.basename(filePath);
1878
1971
 
@@ -1983,6 +2076,10 @@ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalSc
1983
2076
  if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)) {
1984
2077
  errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget} [_core-adr-policy-001]`);
1985
2078
  }
2079
+
2080
+ if (isSourceSnapshotPath(link.resolvedPath)) {
2081
+ errors.push(`Document must not link directly to compiled-source snapshot content, which is not authoritative (see _core-adr-policy-019 rule 14) in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-019.14-source-not-authoritative]`);
2082
+ }
1986
2083
  }
1987
2084
  }
1988
2085
  }
@@ -2069,6 +2166,11 @@ function shouldValidateResourceLink(rawTarget) {
2069
2166
  || IMAGE_EXTENSIONS.has(extension);
2070
2167
  }
2071
2168
 
2169
+ function isSourceSnapshotPath(resolvedPath) {
2170
+ const normalized = normalizePath(resolvedPath).split(path.sep).join('/');
2171
+ return normalized.includes(`/${RESOURCE_DIR_NAME}/${SOURCE_SNAPSHOT_DIR_NAME}/`);
2172
+ }
2173
+
2072
2174
  function extractDescriptionText(block) {
2073
2175
  const lines = block.split(/\r?\n/);
2074
2176
  for (let i = 0; i < lines.length; i++) {
package/lib/lint.test.js CHANGED
@@ -3122,11 +3122,15 @@ function validCompiledMetaPolicy() {
3122
3122
  '',
3123
3123
  '## Sources',
3124
3124
  '',
3125
- '- [web] https://example.com/standards',
3125
+ '- [web] example-standards: https://example.com/standards',
3126
3126
  '',
3127
3127
  '## Selectors',
3128
3128
  '',
3129
3129
  '- Include all requirement sections.',
3130
+ '',
3131
+ '## Sync Settings',
3132
+ '',
3133
+ '- Source re-sync period days: 30',
3130
3134
  ''
3131
3135
  ].join('\n');
3132
3136
  }
@@ -3416,6 +3420,331 @@ test('reports compiled scope meta-policy missing ## Selectors section', () => {
3416
3420
  expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3417
3421
  });
3418
3422
 
3423
+ test('reports compiled scope meta-policy missing ## Sync Settings section', () => {
3424
+ const workspaceRoot = createWorkspace('compiled-meta-missing-sync-settings', {
3425
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3426
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3427
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3428
+ '- [001-core](principles/001-core.md) - meta-policy',
3429
+ '- [002-a01](principles/002-a01.md) - A01'
3430
+ ]),
3431
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3432
+ '---',
3433
+ 'name: my-compiled-adr-policy-001-core',
3434
+ 'description: Compilation meta-policy.',
3435
+ 'apply-to: Compilation process',
3436
+ 'valid-from: 2026-01-01',
3437
+ '---',
3438
+ '',
3439
+ '# my-compiled-adr-policy-001: core',
3440
+ '',
3441
+ '## Context and Problem Statement',
3442
+ '',
3443
+ 'Defines compilation config.',
3444
+ '',
3445
+ '## Decision Outcome',
3446
+ '',
3447
+ 'Use configured sources.',
3448
+ '',
3449
+ '### Details',
3450
+ '',
3451
+ '#### 01-rule',
3452
+ '',
3453
+ 'MUST use configured sources.',
3454
+ '',
3455
+ '## Sources',
3456
+ '',
3457
+ '- [web] example-standards: https://example.com/standards',
3458
+ '',
3459
+ '## Selectors',
3460
+ '',
3461
+ '- Include requirement sections.',
3462
+ ''
3463
+ ].join('\n'),
3464
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3465
+ '---',
3466
+ 'name: my-compiled-adr-policy-002-a01',
3467
+ 'description: Compiled test policy.',
3468
+ 'apply-to: All teams',
3469
+ 'valid-from: 2026-01-01',
3470
+ '---',
3471
+ '',
3472
+ '# my-compiled-adr-policy-002: A01',
3473
+ '',
3474
+ '## Context and Problem Statement',
3475
+ '',
3476
+ 'External source.',
3477
+ '',
3478
+ '## Decision Outcome',
3479
+ '',
3480
+ 'Follow rule.',
3481
+ '',
3482
+ '### Details',
3483
+ '',
3484
+ '#### 01-rule',
3485
+ '',
3486
+ 'MUST comply.',
3487
+ '',
3488
+ '## Source',
3489
+ '',
3490
+ '- source-1/a01.md',
3491
+ ''
3492
+ ].join('\n'),
3493
+ });
3494
+
3495
+ const result = lintWorkspace(workspaceRoot);
3496
+
3497
+ expect(result.errors.join('\n')).toContain('"## Sync Settings" section');
3498
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3499
+ });
3500
+
3501
+ test('reports missing Source re-sync period days in ## Sync Settings', () => {
3502
+ const workspaceRoot = createWorkspace('compiled-meta-missing-resync-period', {
3503
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3504
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3505
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3506
+ '- [001-core](principles/001-core.md) - meta-policy',
3507
+ '- [002-a01](principles/002-a01.md) - A01'
3508
+ ]),
3509
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3510
+ '---',
3511
+ 'name: my-compiled-adr-policy-001-core',
3512
+ 'description: Compilation meta-policy.',
3513
+ 'apply-to: Compilation process',
3514
+ 'valid-from: 2026-01-01',
3515
+ '---',
3516
+ '',
3517
+ '# my-compiled-adr-policy-001: core',
3518
+ '',
3519
+ '## Context and Problem Statement',
3520
+ '',
3521
+ 'Defines compilation config.',
3522
+ '',
3523
+ '## Decision Outcome',
3524
+ '',
3525
+ 'Use configured sources.',
3526
+ '',
3527
+ '### Details',
3528
+ '',
3529
+ '#### 01-rule',
3530
+ '',
3531
+ 'MUST use configured sources.',
3532
+ '',
3533
+ '## Sources',
3534
+ '',
3535
+ '- [web] example-standards: https://example.com/standards',
3536
+ '',
3537
+ '## Selectors',
3538
+ '',
3539
+ '- Include requirement sections.',
3540
+ '',
3541
+ '## Sync Settings',
3542
+ '',
3543
+ '- Source storage: temporary',
3544
+ ''
3545
+ ].join('\n'),
3546
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3547
+ '---',
3548
+ 'name: my-compiled-adr-policy-002-a01',
3549
+ 'description: Compiled test policy.',
3550
+ 'apply-to: All teams',
3551
+ 'valid-from: 2026-01-01',
3552
+ '---',
3553
+ '',
3554
+ '# my-compiled-adr-policy-002: A01',
3555
+ '',
3556
+ '## Context and Problem Statement',
3557
+ '',
3558
+ 'External source.',
3559
+ '',
3560
+ '## Decision Outcome',
3561
+ '',
3562
+ 'Follow rule.',
3563
+ '',
3564
+ '### Details',
3565
+ '',
3566
+ '#### 01-rule',
3567
+ '',
3568
+ 'MUST comply.',
3569
+ '',
3570
+ '## Source',
3571
+ '',
3572
+ '- source-1/a01.md',
3573
+ ''
3574
+ ].join('\n'),
3575
+ });
3576
+
3577
+ const result = lintWorkspace(workspaceRoot);
3578
+
3579
+ expect(result.errors.join('\n')).toContain('"Source re-sync period days: N" bullet with a positive integer');
3580
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3581
+ });
3582
+
3583
+ test('reports invalid Source storage value in ## Sync Settings', () => {
3584
+ const workspaceRoot = createWorkspace('compiled-meta-invalid-storage-value', {
3585
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3586
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3587
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3588
+ '- [001-core](principles/001-core.md) - meta-policy',
3589
+ '- [002-a01](principles/002-a01.md) - A01'
3590
+ ]),
3591
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3592
+ '---',
3593
+ 'name: my-compiled-adr-policy-001-core',
3594
+ 'description: Compilation meta-policy.',
3595
+ 'apply-to: Compilation process',
3596
+ 'valid-from: 2026-01-01',
3597
+ '---',
3598
+ '',
3599
+ '# my-compiled-adr-policy-001: core',
3600
+ '',
3601
+ '## Context and Problem Statement',
3602
+ '',
3603
+ 'Defines compilation config.',
3604
+ '',
3605
+ '## Decision Outcome',
3606
+ '',
3607
+ 'Use configured sources.',
3608
+ '',
3609
+ '### Details',
3610
+ '',
3611
+ '#### 01-rule',
3612
+ '',
3613
+ 'MUST use configured sources.',
3614
+ '',
3615
+ '## Sources',
3616
+ '',
3617
+ '- [web] example-standards: https://example.com/standards',
3618
+ '',
3619
+ '## Selectors',
3620
+ '',
3621
+ '- Include requirement sections.',
3622
+ '',
3623
+ '## Sync Settings',
3624
+ '',
3625
+ '- Source re-sync period days: 30',
3626
+ '- Source storage: permanent',
3627
+ ''
3628
+ ].join('\n'),
3629
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3630
+ '---',
3631
+ 'name: my-compiled-adr-policy-002-a01',
3632
+ 'description: Compiled test policy.',
3633
+ 'apply-to: All teams',
3634
+ 'valid-from: 2026-01-01',
3635
+ '---',
3636
+ '',
3637
+ '# my-compiled-adr-policy-002: A01',
3638
+ '',
3639
+ '## Context and Problem Statement',
3640
+ '',
3641
+ 'External source.',
3642
+ '',
3643
+ '## Decision Outcome',
3644
+ '',
3645
+ 'Follow rule.',
3646
+ '',
3647
+ '### Details',
3648
+ '',
3649
+ '#### 01-rule',
3650
+ '',
3651
+ 'MUST comply.',
3652
+ '',
3653
+ '## Source',
3654
+ '',
3655
+ '- source-1/a01.md',
3656
+ ''
3657
+ ].join('\n'),
3658
+ });
3659
+
3660
+ const result = lintWorkspace(workspaceRoot);
3661
+
3662
+ expect(result.errors.join('\n')).toContain('"Source storage" value must be "temporary" if present');
3663
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3664
+ });
3665
+
3666
+ test('reports compiled scope ## Sources bullet missing NAME slug', () => {
3667
+ const workspaceRoot = createWorkspace('compiled-meta-sources-missing-name', {
3668
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3669
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3670
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3671
+ '- [001-core](principles/001-core.md) - meta-policy',
3672
+ '- [002-a01](principles/002-a01.md) - A01'
3673
+ ]),
3674
+ '.xdrs/my-compiled/adrs/principles/001-core.md': [
3675
+ '---',
3676
+ 'name: my-compiled-adr-policy-001-core',
3677
+ 'description: Compilation meta-policy.',
3678
+ 'apply-to: Compilation process',
3679
+ 'valid-from: 2026-01-01',
3680
+ '---',
3681
+ '',
3682
+ '# my-compiled-adr-policy-001: core',
3683
+ '',
3684
+ '## Context and Problem Statement',
3685
+ '',
3686
+ 'Defines compilation config.',
3687
+ '',
3688
+ '## Decision Outcome',
3689
+ '',
3690
+ 'Use configured sources.',
3691
+ '',
3692
+ '### Details',
3693
+ '',
3694
+ '#### 01-rule',
3695
+ '',
3696
+ 'MUST use configured sources.',
3697
+ '',
3698
+ '## Sources',
3699
+ '',
3700
+ '- [web] https://example.com/standards',
3701
+ '',
3702
+ '## Selectors',
3703
+ '',
3704
+ '- Include requirement sections.',
3705
+ '',
3706
+ '## Sync Settings',
3707
+ '',
3708
+ '- Source re-sync period days: 30',
3709
+ ''
3710
+ ].join('\n'),
3711
+ '.xdrs/my-compiled/adrs/principles/002-a01.md': [
3712
+ '---',
3713
+ 'name: my-compiled-adr-policy-002-a01',
3714
+ 'description: Compiled test policy.',
3715
+ 'apply-to: All teams',
3716
+ 'valid-from: 2026-01-01',
3717
+ '---',
3718
+ '',
3719
+ '# my-compiled-adr-policy-002: A01',
3720
+ '',
3721
+ '## Context and Problem Statement',
3722
+ '',
3723
+ 'External source.',
3724
+ '',
3725
+ '## Decision Outcome',
3726
+ '',
3727
+ 'Follow rule.',
3728
+ '',
3729
+ '### Details',
3730
+ '',
3731
+ '#### 01-rule',
3732
+ '',
3733
+ 'MUST comply.',
3734
+ '',
3735
+ '## Source',
3736
+ '',
3737
+ '- source-1/a01.md',
3738
+ ''
3739
+ ].join('\n'),
3740
+ });
3741
+
3742
+ const result = lintWorkspace(workspaceRoot);
3743
+
3744
+ expect(result.errors.join('\n')).toContain('must include a NAME slug');
3745
+ expect(result.errors.join('\n')).toContain('019.07-compilation-meta-policy');
3746
+ });
3747
+
3419
3748
  test('accepts valid compiled scope meta-policy with required sections', () => {
3420
3749
  const workspaceRoot = createWorkspace('compiled-meta-valid', {
3421
3750
  '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
@@ -3476,6 +3805,88 @@ test('skips ## Source check for local meta-policy files in compiled scope', () =
3476
3805
  expect(result.errors.join('\n')).not.toContain('Compiled policy must include a "## Source" section');
3477
3806
  });
3478
3807
 
3808
+ test('does not report orphan asset errors for files under .assets/sources in a compiled scope', () => {
3809
+ const workspaceRoot = createWorkspace('compiled-sources-orphan-exempt', {
3810
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3811
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3812
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3813
+ '- [001-core](principles/001-core.md) - meta-policy'
3814
+ ]),
3815
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3816
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/source.md': [
3817
+ '# Source',
3818
+ '',
3819
+ 'last-fetch-timestamp: 2026-01-01 10:00:00',
3820
+ 'last-compilation-timestamp: 2026-01-01 10:05:00',
3821
+ ''
3822
+ ].join('\n'),
3823
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3824
+ '.xdrs/my-compiled/adrs/principles/.assets/unused.png': Buffer.alloc(0),
3825
+ });
3826
+
3827
+ const result = lintWorkspace(workspaceRoot);
3828
+
3829
+ expect(result.errors.join('\n')).not.toContain('raw-standard.md');
3830
+ expect(result.errors.join('\n')).not.toContain('019.12-source-storage-tracking');
3831
+ expect(result.errors.join('\n')).toContain('Orphan asset file');
3832
+ expect(result.errors.join('\n')).toContain('unused.png');
3833
+ });
3834
+
3835
+ test('reports missing source.md tracking file in a compiled source snapshot folder', () => {
3836
+ const workspaceRoot = createWorkspace('compiled-sources-missing-tracking-file', {
3837
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3838
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3839
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3840
+ '- [001-core](principles/001-core.md) - meta-policy'
3841
+ ]),
3842
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3843
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3844
+ });
3845
+
3846
+ const result = lintWorkspace(workspaceRoot);
3847
+
3848
+ expect(result.errors.join('\n')).toContain('must contain a "source.md" tracking file');
3849
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3850
+ });
3851
+
3852
+ test('reports malformed source.md tracking file content', () => {
3853
+ const workspaceRoot = createWorkspace('compiled-sources-malformed-tracking-file', {
3854
+ '.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
3855
+ '.xdrs/my-compiled/index.md': compiledScopeIndex(),
3856
+ '.xdrs/my-compiled/adrs/index.md': compiledAdrIndex([
3857
+ '- [001-core](principles/001-core.md) - meta-policy'
3858
+ ]),
3859
+ '.xdrs/my-compiled/adrs/principles/001-core.md': validCompiledMetaPolicy(),
3860
+ '.xdrs/my-compiled/adrs/principles/.assets/sources/example-standards/source.md': [
3861
+ 'last-fetch-timestamp: not-a-date',
3862
+ ''
3863
+ ].join('\n'),
3864
+ });
3865
+
3866
+ const result = lintWorkspace(workspaceRoot);
3867
+
3868
+ expect(result.errors.join('\n')).toContain('must start with a "# Source" heading');
3869
+ expect(result.errors.join('\n')).toContain('must include a "last-compilation-timestamp:" line');
3870
+ expect(result.errors.join('\n')).toContain('must match "YYYY-MM-DD HH:MM:SS"');
3871
+ expect(result.errors.join('\n')).toContain('019.12-source-storage-tracking');
3872
+ });
3873
+
3874
+ test('reports document linking directly into a compiled-source snapshot path', () => {
3875
+ const workspaceRoot = createWorkspace('source-snapshot-link-forbidden', {
3876
+ '.xdrs/index.md': rootIndex(),
3877
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
3878
+ '- [001-main](principles/001-main.md) - Main decision'
3879
+ ]),
3880
+ '.xdrs/_local/adrs/principles/001-main.md': xdrDocument('See [raw source](.assets/sources/example-standards/raw-standard.md).'),
3881
+ '.xdrs/_local/adrs/principles/.assets/sources/example-standards/raw-standard.md': '# Raw fetched content\n',
3882
+ });
3883
+
3884
+ const result = lintWorkspace(workspaceRoot);
3885
+
3886
+ expect(result.errors.join('\n')).toContain('must not link directly to compiled-source snapshot content');
3887
+ expect(result.errors.join('\n')).toContain('019.14-source-not-authoritative');
3888
+ });
3889
+
3479
3890
  test('enforces compiled scope checks when scope-type is comma-separated with compiled', () => {
3480
3891
  const workspaceRoot = createWorkspace('compiled-multi-type-checks', {
3481
3892
  '.xdrs/index.md': rootIndex(['[my-compiled-scope](my-compiled-scope/index.md)']),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
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",