xdrs-core 0.55.0 → 0.56.1

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.
Files changed (26) hide show
  1. package/.xdrs/_core/adrs/index.md +2 -0
  2. package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +1 -0
  3. package/.xdrs/_core/adrs/principles/002-policy-standards.md +2 -0
  4. package/.xdrs/_core/adrs/principles/003-skill-standards.md +6 -33
  5. package/.xdrs/_core/adrs/principles/004-article-standards.md +5 -0
  6. package/.xdrs/_core/adrs/principles/006-research-standards.md +5 -0
  7. package/.xdrs/_core/adrs/principles/007-initiative-standards.md +5 -0
  8. package/.xdrs/_core/adrs/principles/010-scope-governance.md +2 -2
  9. package/.xdrs/_core/adrs/principles/011-core-scope-type.md +1 -1
  10. package/.xdrs/_core/adrs/principles/019-compiled-scope-type.md +6 -1
  11. package/.xdrs/_core/adrs/principles/022-scope-activation.md +3 -1
  12. package/.xdrs/_core/adrs/principles/023-skill-runtime-standards.md +63 -0
  13. package/.xdrs/_core/adrs/principles/024-sync-date-frontmatter.md +76 -0
  14. package/.xdrs/_core/adrs/principles/skills/compile-scope/SKILL.md +10 -8
  15. package/.xdrs/_core/adrs/principles/skills/review/SKILL.md +5 -6
  16. package/.xdrs/_core/adrs/principles/skills/write-article/SKILL.md +3 -3
  17. package/.xdrs/_core/adrs/principles/skills/write-initiative/SKILL.md +3 -3
  18. package/.xdrs/_core/adrs/principles/skills/write-policy/SKILL.md +3 -3
  19. package/.xdrs/_core/adrs/principles/skills/write-research/SKILL.md +3 -3
  20. package/.xdrs/_core/adrs/principles/skills/write-skill/SKILL.md +9 -8
  21. package/.xdrs/_core/adrs/principles/skills/write-xdrs-doc/SKILL.md +4 -4
  22. package/.xdrs/_core/index.md +1 -1
  23. package/README.md +2 -1
  24. package/lib/lint.js +61 -16
  25. package/lib/lint.test.js +293 -4
  26. package/package.json +1 -1
@@ -6,13 +6,13 @@ description: >
6
6
  Activate this skill when the user asks to create, add, or write a new skill, agent skill, or SKILL.md file.
7
7
  metadata:
8
8
  author: flaviostutz
9
- version: "1.2.0"
10
- updated: 2026-09-30
9
+ version: "1.3.1"
10
+ updated: 2026-11-04
11
11
  ---
12
12
 
13
13
  ## Overview
14
14
 
15
- Guides the creation of a well-structured skill package by following `_core-adr-policy-003` skill standards, consulting `xdrs-core` for every core element definition, checking existing skills to avoid duplication, and producing a complete SKILL.md ready to activate in VS Code.
15
+ Guides the creation of a well-structured skill package by following `_core-adr-policy-003` skill standards and `_core-adr-policy-023` runtime standards, consulting `xdrs-core` for every core element definition, checking existing skills to avoid duplication, and producing a complete SKILL.md ready to activate in VS Code.
16
16
 
17
17
  ### Inputs
18
18
 
@@ -57,7 +57,7 @@ Keep every question and intermediate message <100 words and the final summary <1
57
57
 
58
58
  ### Phase 1: Understand the Skill Goal
59
59
 
60
- 1. Read `.xdrs/_core/adrs/principles/003-skill-standards.md` in full to internalize the SKILL.md format and folder layout.
60
+ 1. Read `.xdrs/_core/adrs/principles/003-skill-standards.md` and `.xdrs/_core/adrs/principles/023-skill-runtime-standards.md` in full to internalize the SKILL.md format, folder layout and runtime rules.
61
61
  2. Read `.xdrs/_core/adrs/principles/001-xdrs-standards.md` in full before defining any core element for the skill package. Treat it as the canonical source for type, scope, subject, naming constraints, and folder placement rules.
62
62
  3. Identify what the skill must do, the concrete outcome it should produce, and the exact conditions under which an agent should activate it. Do NOT proceed without a clear goal, outcome, and activation trigger.
63
63
 
@@ -90,7 +90,7 @@ Quick test:
90
90
 
91
91
  ### Phase 4: Write the SKILL.md
92
92
 
93
- Use the mandatory agentskills format:
93
+ Use the mandatory agentskills format. Omit the optional `metadata.sync-date` unless the user explicitly asks for it for a specific sync purpose (`_core-adr-policy-024`):
94
94
 
95
95
  ```
96
96
  ---
@@ -170,9 +170,9 @@ Rules:
170
170
  - The `description` field must state both *what* the skill does and *when* to activate it.
171
171
  - Keep the skill task-oriented. It should have a clear starting trigger and a concrete ending result.
172
172
  - Mention tools or prerequisites when they are required to complete the task reliably.
173
- - Declare a "<N words" cap, or "(uncapped: <reason>)", exactly once for every natural-language content the skill generates, per **Generated content caps** in `_core-adr-policy-003`.
173
+ - Declare a "<N words" cap, or "(uncapped: <reason>)", exactly once for every natural-language content the skill generates, per **Generated content caps** in `_core-adr-policy-023`.
174
174
  - Do not duplicate content from referenced Policies — link instead.
175
- - If the skill creates files at runtime (outputs, intermediate files, ad-hoc scripts), restate in `## Instructions`, next to the steps writing them, the parts of **Work files** in `_core-adr-policy-003` it uses: `.tmp/[skill-name]-[YYYYMMDDHHMMSS]/`, `.work/`, `.work/scripts/`, and the final `results-path:` line. This restatement is the one exception to the no-duplication rule, so the skill runs standalone.
175
+ - If the skill creates files at runtime (outputs, intermediate files, ad-hoc scripts), restate in `## Instructions`, next to the steps writing them, the parts of **Work files** in `_core-adr-policy-023` it uses: `.tmp/[skill-name]/[run-name]/` (never files directly in `.tmp/[skill-name]/`), `.work/`, the OS temp dir for throwaway files, and the final `results-path:` line when a run folder is created. This restatement is the one exception to the no-duplication rule, so the skill runs standalone.
176
176
  - Do not present the skill itself as policy; mandatory behavior must come from referenced Policies or other policy artifacts.
177
177
  - When the skill depends on Policies, make the activation logic and instructions consistent with the Policy metadata so the skill does not operationalize inactive or out-of-scope decisions.
178
178
  - For diagrams and non-Markdown assets, follow `_core-adr-policy-020`: prefer plain Markdown tables/lists first, then ASCII art for very simple cases, then Mermaid.js (sequence, state, activity, entity diagrams) for complex ones, then draw.io when Mermaid is insufficient — save as Editable Vector (File → Save As → Editable Vector) and store as `.svg` in the sibling `.assets/` folder.
@@ -193,7 +193,7 @@ Before writing files, verify:
193
193
  7. **Required sections**: Does `## Overview` contain nested `### Inputs` (Required/Optional bullets), `### Outputs` (Contents/Changes bullets), and `### Halt Conditions` listing this skill's specific stop triggers — each bullet under 20 words or a single "None"?
194
194
  8. **Meta-policy compliance**: Run the shared module at `.xdrs/_core/adrs/principles/skills/.assets/meta-policy-compliance.md`. Substitute `[DOCUMENT]` with `skill`.
195
195
  9. **Output caps**: Does every natural-language content the skill generates (files, messages, questions, summaries, external posts) have exactly one "<N words" cap or "(uncapped: <reason>)" marker, with code and structured data unmarked?
196
- 10. **Work files**: If the skill creates files at runtime, do its Instructions restate the default execution dir, `.work/`, `.work/scripts/`, and `results-path:` line from `_core-adr-policy-003`, or an explicit alternative location?
196
+ 10. **Work files**: If the skill creates files at runtime, do its Instructions restate the run folder, `.work/`, OS temp dir and `results-path:` rules from `_core-adr-policy-023`, or an explicit alternative location?
197
197
 
198
198
  If any check fails, revise before continuing.
199
199
 
@@ -261,6 +261,7 @@ Follow the lint verification steps in `.xdrs/_core/adrs/principles/skills/.asset
261
261
  ## References
262
262
 
263
263
  - [_core-adr-policy-003 - Skill standards](../../003-skill-standards.md)
264
+ - [_core-adr-policy-023 - Skill runtime standards](../../023-skill-runtime-standards.md)
264
265
  - [_core-adr-policy-001 - XDRS standards](../../001-xdrs-standards.md)
265
266
  - [_core-adr-policy-021 - Skill bundling](../../021-skill-bundling.md)
266
267
  - [write-policy skill](../write-policy/SKILL.md)
@@ -5,8 +5,8 @@ description: >
5
5
  Activate this skill when the user asks to create or write any XDRS element.
6
6
  metadata:
7
7
  author: flaviostutz
8
- version: "1.2.0"
9
- updated: 2026-09-30
8
+ version: "1.3.0"
9
+ updated: 2026-10-08
10
10
  ---
11
11
 
12
12
  ## Overview
@@ -81,12 +81,12 @@ Read the full content of the skill file for the inferred type, then follow all i
81
81
  ### Phase 3: Validate Mermaid Diagrams
82
82
 
83
83
  1. After the delegated skill completes, scan all files written in this session for fenced ` ```mermaid ` code blocks.
84
- 2. For each diagram found, extract the content to a `.mmd` file in `.tmp/write-xdrs-doc-[YYYYMMDDHHMMSS]/.work/` at the workspace root (local start time; create it only when the first diagram is found, append `-2`, `-3`... if it exists, and use the same layout under the OS temp dir if the workspace is read-only) and run:
84
+ 2. For each diagram found, extract the content to a `.mmd` file in a temporary folder under the OS temp dir (create it only when the first diagram is found) and run:
85
85
  ```bash
86
86
  npx -y @mermaid-js/mermaid-cli -i <file>.mmd --quiet 2>&1
87
87
  ```
88
88
  3. For each diagram that fails validation, report the error to the user and ask them to correct the diagram before saving.
89
- 4. Keep the extracted files after the run; if this step created the dir, end the final summary with `results-path: .tmp/write-xdrs-doc-[ts]/` (the actual dir), including when halting.
89
+ 4. Delete the temporary folder and its `.mmd` files when the run ends, including when halting.
90
90
 
91
91
  ### Constraints
92
92
 
@@ -32,7 +32,7 @@ Each artifact type has its own writing standard:
32
32
 
33
33
  - **Policy documents** follow [_core-adr-policy-002](adrs/principles/002-policy-standards.md), which defines the mandatory template, frontmatter metadata, applicability rules, conflict handling, and word limits that keep decisions concise and authoritative.
34
34
  - **Structured Policies** with individually referenceable rules follow the extension [_core-adr-policy-008](adrs/principles/008-policy-structured-standards.md), adding numbered rule blocks and a dot-notation citation syntax.
35
- - **Skills** follow [_core-adr-policy-003](adrs/principles/003-skill-standards.md), using the agentskills format so they work for both humans and AI agents on an automation gradient from fully manual to fully automated.
35
+ - **Skills** follow [_core-adr-policy-003](adrs/principles/003-skill-standards.md) and [_core-adr-policy-023](adrs/principles/023-skill-runtime-standards.md), using the agentskills format so they work for both humans and AI agents on an automation gradient from fully manual to fully automated.
36
36
  - **Articles** follow [_core-adr-policy-004](adrs/principles/004-article-standards.md), providing synthetic views that combine and link multiple Policies, research, and skills without replacing them as the source of truth.
37
37
  - **Research** follows [_core-adr-policy-006](adrs/principles/006-research-standards.md), using an IMRAD-based structure for studies that back decisions with reproducible evidence.
38
38
  - **Initiatives** follow [_core-adr-policy-007](adrs/principles/007-initiative-standards.md), capturing ephemeral execution initiatives with problem context, proposed solutions, milestones, and deliverables that are deleted after implementation.
package/README.md CHANGED
@@ -190,7 +190,7 @@ Scopes tagged `disabled` MUST be ignored; `extends-only` scopes apply only via e
190
190
  ```
191
191
 
192
192
  - `extends-only`: the scope's policies apply only through scopes that list it in `extends:`.
193
- - `disabled`: the scope is ignored; scopes that follow, extend or take their type from it become READ-ONLY.
193
+ - `disabled`: the scope is ignored; enabled scopes that follow, extend or take their type from it become READ-ONLY, while a disabled scope may depend on other disabled scopes freely.
194
194
  - Scope authors declare `extends-only: true` in the scope `index.md` frontmatter when the scope is meant only to be extended; lint requires the root index tag to match.
195
195
  - The root index belongs to the consumer: packages ship it unmanaged so tags survive reinstalls.
196
196
 
@@ -217,6 +217,7 @@ The folder layout, file naming, and document format are designed so that AI agen
217
217
  - The canonical index per scope and type lists all XDRS elements with short descriptions, enabling agents to identify relevant records without reading every file.
218
218
  - The root index at `.xdrs/index.md` provides a single entry point for discovery.
219
219
  - Policy 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.
220
+ - An optional, opt-in `sync-date` frontmatter field records when an element was last verified or reconciled against external entities (even if nothing changed). It is never added by default; compiled scopes set it in their scope index on every compile. See [_core-adr-policy-024](.xdrs/_core/adrs/principles/024-sync-date-frontmatter.md).
220
221
  - Decisions cross-reference each other by Policy ID rather than duplicating content, keeping individual files concise.
221
222
  - Subject folders reduce the search space when a query maps to a known domain.
222
223
 
package/lib/lint.js CHANGED
@@ -32,9 +32,10 @@ const SOURCE_SNAPSHOT_DIR_NAME = 'sources';
32
32
  const SOURCE_TRACKING_FILE_NAME = 'source.md';
33
33
  const SKILL_PACKAGE_OPTIONAL_DIRS = new Set(['scripts', 'references', RESOURCE_DIR_NAME, 'dist']);
34
34
 
35
- const POLICY_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'freeze-reference']);
35
+ const POLICY_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'freeze-reference', 'sync-date']);
36
36
  const SKILL_ALLOWED_FRONTMATTER_KEYS = new Set(['name', 'description', 'license', 'metadata']);
37
- const SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS = new Set(['scope-type', 'name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'follows', 'extends', 'extends-only']);
37
+ const SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS = new Set(['scope-type', 'name', 'description', 'apply-to', 'valid-from', 'license', 'metadata', 'follows', 'extends', 'extends-only', 'sync-date']);
38
+ const SYNC_DATE_ONLY_FRONTMATTER_KEYS = new Set(['sync-date']);
38
39
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
39
40
  const SLIDE_FILE_RE = /^.+-slides(?:-[a-z0-9-]+)?\.md$/;
40
41
  const SLIDE_MAX_NAME_LENGTH = 64;
@@ -238,8 +239,11 @@ function lintWorkspace(targetPath, options = {}) {
238
239
  // Populated during lintScopeIndexFrontmatter: scopeName -> [extendedScopeNames...]
239
240
  const extendsRelationships = new Map();
240
241
 
242
+ // A disabled scope is treated as not present, so it is exempt from dependency checks (_core-adr-policy-022.06)
243
+ const disabledScopes = new Set([...activation].filter(([, entry]) => entry.tag === 'disabled').map(([name]) => name));
244
+
241
245
  for (const scopeEntry of scopeEntries) {
242
- lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, extendsRelationships, externalScopes);
246
+ lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, extendsRelationships, externalScopes, disabledScopes);
243
247
  }
244
248
 
245
249
  // Detect cycles in extends: chains (DFS)
@@ -434,7 +438,7 @@ function readScopeMeta(scopeIndexPath) {
434
438
  function lintLocalReadOnlyDependencies(xdrsRoot, scopeMeta, activation, externalScopes, knownScopeTypes, scopeTypeDefiningScopes, errors) {
435
439
  const isDisabled = (scopeName) => (activation.get(scopeName) || {}).tag === 'disabled';
436
440
  for (const [scopeName, meta] of scopeMeta) {
437
- if (!meta || externalScopes.has(scopeName)) continue;
441
+ if (!meta || externalScopes.has(scopeName) || isDisabled(scopeName)) continue;
438
442
  const causes = [];
439
443
  for (const [field, targets] of [['follows', meta.follows], ['extends', meta.extends]]) {
440
444
  for (const target of targets) {
@@ -576,8 +580,9 @@ function deriveScopeNameListEntries(block, lineMatch, inlineValue) {
576
580
  return parseYamlListItems(block, lineMatch).filter(Boolean);
577
581
  }
578
582
 
579
- function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), extendsRelationships = new Map(), externalScopes = new Set()) {
583
+ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), extendsRelationships = new Map(), externalScopes = new Set(), disabledScopes = new Set()) {
580
584
  const isExternal = externalScopes.has(scopeName);
585
+ const skipMissingDependencies = isExternal || disabledScopes.has(scopeName);
581
586
  const content = fs.readFileSync(scopeIndexPath, 'utf8');
582
587
  const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
583
588
  if (!fmMatch) {
@@ -638,7 +643,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
638
643
  for (const st of scopeTypes) {
639
644
  if (st === '_local') continue;
640
645
  if (!knownScopeTypes.has(st)) {
641
- if (!isExternal) {
646
+ if (!skipMissingDependencies) {
642
647
  errors.push(`Scope index scope-type "${st}" has no corresponding ${st}-scope-type policy in the principles of any scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.10-def-valid-iff-policy-exists]`);
643
648
  }
644
649
  continue;
@@ -728,6 +733,9 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
728
733
  } else if (!isIsoDate(fm.validFrom)) {
729
734
  errors.push(`Scope index frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
730
735
  }
736
+ if (fm.syncDate !== undefined && !isIsoDate(fm.syncDate)) {
737
+ errors.push(`Scope index frontmatter sync-date must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-024.05-placement-and-format]`);
738
+ }
731
739
 
732
740
  let followsEntries = [];
733
741
  const followsLineMatch = block.match(/^follows:[ \t]*(.*)/m);
@@ -759,8 +767,8 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
759
767
  }
760
768
  const followedIndexPath = path.join(xdrsRoot, entry, 'index.md');
761
769
  if (!existsFile(followedIndexPath)) {
762
- // A missing dependency of an external scope makes it READ-ONLY (_core-adr-policy-022.06); lint does not report it.
763
- if (!isExternal) {
770
+ // A missing dependency of an external scope makes it READ-ONLY and a disabled scope is exempt (_core-adr-policy-022.06); lint does not report it.
771
+ if (!skipMissingDependencies) {
764
772
  errors.push(`Scope index frontmatter follows references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
765
773
  }
766
774
  } else {
@@ -816,7 +824,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
816
824
  continue;
817
825
  }
818
826
  if (!existsFile(path.join(xdrsRoot, entry, 'index.md'))) {
819
- if (!isExternal) {
827
+ if (!skipMissingDependencies) {
820
828
  errors.push(`Scope index frontmatter extends references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.32-extends-mandatory-presence]`);
821
829
  }
822
830
  }
@@ -829,7 +837,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
829
837
  }
830
838
 
831
839
 
832
- function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), extendsRelationships = new Map(), trackedExternalScopes = new Set()) {
840
+ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), extendsRelationships = new Map(), trackedExternalScopes = new Set(), disabledScopes = new Set()) {
833
841
  const scopePath = path.join(xdrsRoot, scopeName);
834
842
 
835
843
  if (ignoreExternal && externalScopes.has(scopeName)) {
@@ -888,7 +896,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
888
896
  errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
889
897
  } else {
890
898
  lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors, externalScopes);
891
- lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, extendsRelationships, trackedExternalScopes);
899
+ lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, extendsRelationships, trackedExternalScopes, disabledScopes);
892
900
  }
893
901
  lintLocalMetaPolicies(xdrsRoot, scopeName, errors, isCompiledScope);
894
902
  }
@@ -1122,7 +1130,7 @@ function lintXdrsElementFile(xdrsRoot, scopeName, typeName, subjectName, filePat
1122
1130
 
1123
1131
  const expectedName = extractExpectedXdrsNameFromHeading(firstLine)
1124
1132
  || `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
1125
- lintXdrsElementFrontmatter(content, expectedName, filePath, errors, freezeReference);
1133
+ lintXdrsElementFrontmatter(content, expectedName, filePath, errors, freezeReference, isCompiledScope);
1126
1134
  if (enforceStructured) {
1127
1135
  validateStructuredFormatContent(content, filePath, errors, '_core-adr-policy-010.12-def-core-scope-policies-must-be-structured');
1128
1136
  } else if (isCompiledScope) {
@@ -1166,7 +1174,7 @@ function slugifyTitle(title) {
1166
1174
  .replace(/--+/g, '-');
1167
1175
  }
1168
1176
 
1169
- function lintXdrsElementFrontmatter(content, expectedName, filePath, errors, freezeReference = false) {
1177
+ function lintXdrsElementFrontmatter(content, expectedName, filePath, errors, freezeReference = false, isCompiledScope = false) {
1170
1178
  const fm = extractFrontmatter(content);
1171
1179
  if (!fm.present) {
1172
1180
  errors.push(`Policy must start with a YAML frontmatter block: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
@@ -1210,6 +1218,34 @@ function lintXdrsElementFrontmatter(content, expectedName, filePath, errors, fre
1210
1218
  errors.push(`Policy frontmatter has unknown field "${key}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
1211
1219
  }
1212
1220
  }
1221
+ if (fm.syncDate !== undefined) {
1222
+ if (isCompiledScope) {
1223
+ errors.push(`Policy frontmatter must not include sync-date in a compiled scope (it belongs in the scope index): ${toDisplayPath(filePath)} [_core-adr-policy-024.07-compiled-scopes]`);
1224
+ } else if (!isIsoDate(fm.syncDate)) {
1225
+ errors.push(`Policy frontmatter sync-date must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)} [_core-adr-policy-024.05-placement-and-format]`);
1226
+ }
1227
+ }
1228
+ }
1229
+
1230
+ /**
1231
+ * Validates the optional frontmatter block of articles, research and initiatives: it may only
1232
+ * contain a valid `sync-date` (_core-adr-policy-024.05-placement-and-format).
1233
+ */
1234
+ function lintSyncDateOnlyFrontmatter(content, filePath, docType, policyId, errors) {
1235
+ const fm = extractFrontmatter(content);
1236
+ if (!fm.present) {
1237
+ return;
1238
+ }
1239
+ for (const key of fm.topLevelKeys) {
1240
+ if (!SYNC_DATE_ONLY_FRONTMATTER_KEYS.has(key)) {
1241
+ errors.push(`${docType} frontmatter has unknown field "${key}" (only sync-date is allowed): ${toDisplayPath(filePath)} [${policyId}]`);
1242
+ }
1243
+ }
1244
+ if (fm.syncDate === undefined) {
1245
+ errors.push(`${docType} frontmatter must include a sync-date field when present: ${toDisplayPath(filePath)} [${policyId}]`);
1246
+ } else if (!isIsoDate(fm.syncDate)) {
1247
+ errors.push(`${docType} frontmatter sync-date must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)} [_core-adr-policy-024.05-placement-and-format]`);
1248
+ }
1213
1249
  }
1214
1250
 
1215
1251
  function lintSkillsDirectory(xdrsRoot, scopeName, skillsPath, errors, externalScopes = new Set()) {
@@ -1346,6 +1382,10 @@ function lintSkillMetadata(metadata, skillFilePath, errors) {
1346
1382
  } else if (!isIsoDate(fields.updated)) {
1347
1383
  errors.push(`SKILL.md frontmatter metadata.updated must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
1348
1384
  }
1385
+
1386
+ if (fields['sync-date'] !== undefined && !isIsoDate(fields['sync-date'])) {
1387
+ errors.push(`SKILL.md frontmatter metadata.sync-date must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(skillFilePath)} [_core-adr-policy-024.05-placement-and-format]`);
1388
+ }
1349
1389
  }
1350
1390
 
1351
1391
  /**
@@ -1715,10 +1755,11 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
1715
1755
  const content = fs.readFileSync(entryPath, 'utf8');
1716
1756
  const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
1717
1757
  const expectedHeader = `# ${scopeName}-${typeId}-article-${number}:`;
1718
- const firstLine = firstNonEmptyLine(content);
1758
+ const firstLine = firstNonEmptyLine(stripFrontmatter(content));
1719
1759
  if (!firstLine.startsWith(expectedHeader)) {
1720
1760
  errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)} [_core-adr-policy-004]`);
1721
1761
  }
1762
+ lintSyncDateOnlyFrontmatter(content, entryPath, 'Article', '_core-adr-policy-004', errors);
1722
1763
 
1723
1764
  lintRequiredSections(content, entryPath, ARTICLE_REQUIRED_SECTIONS, 'Article', errors);
1724
1765
  lintNoEmojis(content, entryPath, 'Article', errors);
@@ -1770,10 +1811,11 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
1770
1811
  const content = fs.readFileSync(entryPath, 'utf8');
1771
1812
  const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
1772
1813
  const expectedHeader = `# ${scopeName}-${typeId}-research-${number}:`;
1773
- const firstLine = firstNonEmptyLine(content);
1814
+ const firstLine = firstNonEmptyLine(stripFrontmatter(content));
1774
1815
  if (!firstLine.startsWith(expectedHeader)) {
1775
1816
  errors.push(`Research title must start with "${expectedHeader}": ${toDisplayPath(entryPath)} [_core-adr-policy-006]`);
1776
1817
  }
1818
+ lintSyncDateOnlyFrontmatter(content, entryPath, 'Research', '_core-adr-policy-006', errors);
1777
1819
 
1778
1820
  lintRequiredSections(content, entryPath, RESEARCH_REQUIRED_SECTIONS, 'Research', errors);
1779
1821
  lintResearchIntroductionQuestion(content, entryPath, errors);
@@ -1827,10 +1869,11 @@ function lintInitiativesDirectory(xdrsRoot, scopeName, typeName, subjectName, in
1827
1869
  const content = fs.readFileSync(entryPath, 'utf8');
1828
1870
  const typeId = TYPE_TO_ID[typeName].split('-')[0]; // 'adr', 'bdr', or 'edr'
1829
1871
  const expectedHeader = `# ${scopeName}-${typeId}-initiative-${number}:`;
1830
- const firstLine = firstNonEmptyLine(content);
1872
+ const firstLine = firstNonEmptyLine(stripFrontmatter(content));
1831
1873
  if (!firstLine.startsWith(expectedHeader)) {
1832
1874
  errors.push(`Initiative title must start with "${expectedHeader}": ${toDisplayPath(entryPath)} [_core-adr-policy-007]`);
1833
1875
  }
1876
+ lintSyncDateOnlyFrontmatter(content, entryPath, 'Initiative', '_core-adr-policy-007', errors);
1834
1877
 
1835
1878
  lintRequiredSections(content, entryPath, INITIATIVE_REQUIRED_SECTIONS, 'Initiative', errors);
1836
1879
  lintInitiativeExpectedEndDate(content, entryPath, errors);
@@ -2381,6 +2424,7 @@ function extractFrontmatter(content) {
2381
2424
  const nameMatch = block.match(/^name:\s*(.+)$/m);
2382
2425
  const validFromMatch = block.match(/^valid-from:\s*(.+)$/m);
2383
2426
  const appliedToMatch = block.match(/^apply-to:\s*(.+)$/m);
2427
+ const syncDateMatch = block.match(/^sync-date:[ \t]*(.*)$/m);
2384
2428
  const topLevelKeys = [];
2385
2429
  for (const line of block.split('\n')) {
2386
2430
  const keyMatch = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):\s*/);
@@ -2396,6 +2440,7 @@ function extractFrontmatter(content) {
2396
2440
  descriptionText,
2397
2441
  validFrom: validFromMatch ? validFromMatch[1].trim() : undefined,
2398
2442
  appliedTo: appliedToMatch ? appliedToMatch[1].trim() : undefined,
2443
+ syncDate: syncDateMatch ? syncDateMatch[1].trim() : undefined,
2399
2444
  freezeReference: freezeReferenceMatch ? freezeReferenceMatch[1].trim() === 'true' : false,
2400
2445
  metadata,
2401
2446
  topLevelKeys,