xdrs-core 0.56.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.
- package/.xdrs/_core/adrs/index.md +1 -0
- package/.xdrs/_core/adrs/principles/001-xdrs-standards.md +1 -0
- package/.xdrs/_core/adrs/principles/002-policy-standards.md +2 -0
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +2 -0
- package/.xdrs/_core/adrs/principles/004-article-standards.md +5 -0
- package/.xdrs/_core/adrs/principles/006-research-standards.md +5 -0
- package/.xdrs/_core/adrs/principles/007-initiative-standards.md +5 -0
- package/.xdrs/_core/adrs/principles/010-scope-governance.md +2 -2
- package/.xdrs/_core/adrs/principles/011-core-scope-type.md +1 -1
- package/.xdrs/_core/adrs/principles/019-compiled-scope-type.md +5 -0
- package/.xdrs/_core/adrs/principles/022-scope-activation.md +3 -1
- package/.xdrs/_core/adrs/principles/024-sync-date-frontmatter.md +76 -0
- package/.xdrs/_core/adrs/principles/skills/compile-scope/SKILL.md +5 -3
- package/.xdrs/_core/adrs/principles/skills/write-article/SKILL.md +3 -3
- package/.xdrs/_core/adrs/principles/skills/write-initiative/SKILL.md +3 -3
- package/.xdrs/_core/adrs/principles/skills/write-policy/SKILL.md +3 -3
- package/.xdrs/_core/adrs/principles/skills/write-research/SKILL.md +3 -3
- package/.xdrs/_core/adrs/principles/skills/write-skill/SKILL.md +3 -3
- package/README.md +2 -1
- package/lib/lint.js +61 -16
- package/lib/lint.test.js +293 -4
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ Foundational standards, principles, and guidelines.
|
|
|
29
29
|
- [_core-adr-policy-021](principles/021-skill-bundling.md) - **Skill bundling** — Optional per-skill Makefile mechanism to stage a self-contained dist/ output for standalone distribution
|
|
30
30
|
- [_core-adr-policy-022](principles/022-scope-activation.md) - **Scope activation** — How installed scopes are listed in the root index, activated, narrowed, or disabled, and when they become read-only
|
|
31
31
|
- [_core-adr-policy-023](principles/023-skill-runtime-standards.md) - **Skill runtime standards** — How skills place work files under .tmp, halt, ask for clarification, and cap generated content
|
|
32
|
+
- [_core-adr-policy-024](principles/024-sync-date-frontmatter.md) - **Sync date frontmatter** — Optional `sync-date` field recording when an element was last verified or reconciled with other entities
|
|
32
33
|
|
|
33
34
|
## Skills
|
|
34
35
|
|
|
@@ -91,6 +91,7 @@ ADR and EDR share the same six technical subject names (see [`_core-adr-policy-0
|
|
|
91
91
|
- `apply-to` (required): Declares in which contexts — teams, systems, codebases, or environments — the decisions in this scope are relevant. Max 30 words.
|
|
92
92
|
- `valid-from` (required): ISO date (YYYY-MM-DD) from which this scope became active.
|
|
93
93
|
- `metadata` (optional): Arbitrary key-value map for additional scope metadata.
|
|
94
|
+
- `sync-date` (optional, opt-in): see `_core-adr-policy-024`.
|
|
94
95
|
- `follows` (optional): Core scope names whose Policies apply as mandatory governance conventions to this scope, beyond `_core`. Last-listed takes precedence on conflicts (e.g., `follows: myarea-core, shared-standards`). A missing scope MUST be fixed in local scopes and makes external scopes READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`).
|
|
95
96
|
- `extends` (optional): Scope names whose **policy documents** (decision records only — not skills, articles, research, or initiatives) are inherited by this scope as if they were authored here. Any scope type except `_local` and `_core` may be referenced. Last-listed extended scope takes precedence on conflicts; the extending scope's own policies always take highest precedence. Depth-first transitive resolution applies: if A extends [B, C] and B extends [D], the effective precedence order is D < B < C < A. `extends:` and `follows:` MUST reference disjoint scope sets. A missing scope MUST be fixed in local scopes and makes external scopes READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`). When an extending scope's policy overrides an inherited policy on the same topic, a `## Conflicts` section MUST document the override. Extended scopes are still linked in the root index; tag them `extends-only` to apply them only via `extends:` (see `_core-adr-policy-022.01-root-index-completeness`). `extends:` takes precedence over root index ordering for conflict resolution between the scopes involved; root index ordering is the fallback for all other scope pairs. See `_core-adr-policy-010` Section D (rules 27–35) and Section E (rule 36) for full normative rules.
|
|
96
97
|
- `extends-only` (optional): See `_core-adr-policy-022.07-extends-only-field`.
|
|
@@ -31,6 +31,7 @@ Policy documents are the authoritative source of truth for their scope, type, an
|
|
|
31
31
|
| `valid-from` | Yes | 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. Defaults to the date the Policy was created. When updating an existing Policy whose `valid-from` date has already passed, preserve the original date—it MUST NOT be updated to the current date. The historical date shows when the policy was originally enabled and is important for understanding policy evolution. |
|
|
32
32
|
| `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. |
|
|
33
33
|
| `metadata` | No | Arbitrary key-value map for additional properties not defined by this spec. |
|
|
34
|
+
| `sync-date` | No | ISO date (`YYYY-MM-DD`) of the last verification or reconciliation of the Policy with other entities. Opt-in, never added by default, and MUST NOT appear in Policies of `compiled` scopes. See [`_core-adr-policy-024`](024-sync-date-frontmatter.md). |
|
|
34
35
|
| `freeze-reference` | No | Boolean (`true` or `false`, defaults to `false`). When `true`, the policy MUST NOT be renamed, renumbered, or moved because external parties reference it by its current identity (path, number, name, heading). Reference-identity issues are intentionally exempt. See rule [`01-freeze-reference-exemption`](#01-freeze-reference-exemption). |
|
|
35
36
|
|
|
36
37
|
- Minimal example:
|
|
@@ -116,6 +117,7 @@ description: [What this decision is about and when to use it]
|
|
|
116
117
|
apply-to: [Required. Contexts this decision applies to, <40 words. Use "All scopes" when broadly applicable.]
|
|
117
118
|
valid-from: [Required. ISO date YYYY-MM-DD from when enforcement begins. Defaults to creation date.]
|
|
118
119
|
license: [Optional. SPDX license expression]
|
|
120
|
+
sync-date: [Optional, opt-in. ISO date of the last verification with other entities; see _core-adr-policy-024]
|
|
119
121
|
metadata:
|
|
120
122
|
[optional-key]: [optional-value]
|
|
121
123
|
---
|
|
@@ -104,6 +104,7 @@ metadata: # required
|
|
|
104
104
|
author: <team-or-person> # required
|
|
105
105
|
version: "1.0.0" # required: full semantic versioning
|
|
106
106
|
updated: YYYY-MM-DD # required: date of the last material content change
|
|
107
|
+
sync-date: YYYY-MM-DD # optional, opt-in: date of the last verification with other entities (see _core-adr-policy-024)
|
|
107
108
|
---
|
|
108
109
|
|
|
109
110
|
## Overview
|
|
@@ -163,6 +164,7 @@ Rules:
|
|
|
163
164
|
- The `name` field MUST match the folder name exactly (e.g., `code-review`). This keeps skill identifiers simple and aligned with the filesystem hierarchy.
|
|
164
165
|
- The directory name and the `name:` field MUST contain only lowercase alphanumeric characters and hyphens (e.g., `code-review`, `2fa-setup`).
|
|
165
166
|
- `metadata` is REQUIRED. `metadata.version` MUST use full semantic versioning (`MAJOR.MINOR.PATCH`, e.g. `1.0.0`), consistent with [`_core-adr-policy-005`](005-semantic-versioning-for-xdrs-packages.md); `metadata.author` MUST be non-empty; `metadata.updated` MUST be an ISO `YYYY-MM-DD` date, refreshed whenever the skill's content materially changes.
|
|
167
|
+
- `metadata.sync-date` is OPTIONAL and follows [`_core-adr-policy-024`](024-sync-date-frontmatter.md); it is distinct from `metadata.updated`, which records content changes only.
|
|
166
168
|
- `description` SHOULD stay near 250 characters when the activation trigger can be stated that tightly, even though the hard cap remains 1024 characters — it is loaded into context on every activation scan.
|
|
167
169
|
- `## Overview` SHOULD state the task objective, expected outcome, and relevant prerequisites or tools when they matter, and MUST contain nested subsections, in order, for `### Inputs`, `### Outputs`, `### Halt Conditions`, then the optional `### User Interaction` and `### Runtime Requirements`.
|
|
168
170
|
- `### Inputs` MUST contain `#### Required` (the bare minimum needed to invoke the skill) then `#### Optional` (helpful extra context), in that order.
|
|
@@ -42,6 +42,7 @@ Articles are Markdown documents placed inside a subject folder alongside policie
|
|
|
42
42
|
- For diagram format preferences and non-Markdown asset rules, see [`_core-adr-policy-020`](020-media-and-asset-standards.md). Articles SHOULD use diagrams to illustrate concepts, overviews, and relationships; draw.io SHOULD be used for conceptual and relationship diagrams (e.g., component maps, context views, architecture overviews) when Mermaid cannot adequately express them.
|
|
43
43
|
- File names MUST be lowercase.
|
|
44
44
|
- MUST NOT use emojis in article content.
|
|
45
|
+
- An article MAY start with a frontmatter block containing only `sync-date`, per [`_core-adr-policy-024`](024-sync-date-frontmatter.md); the title line follows the block.
|
|
45
46
|
|
|
46
47
|
**Folder layout**
|
|
47
48
|
|
|
@@ -71,6 +72,10 @@ Examples:
|
|
|
71
72
|
All articles MUST follow this template:
|
|
72
73
|
|
|
73
74
|
```markdown
|
|
75
|
+
---
|
|
76
|
+
sync-date: [Optional, opt-in. ISO date of the last verification with other entities; see _core-adr-policy-024. When absent, omit the whole frontmatter block.]
|
|
77
|
+
---
|
|
78
|
+
|
|
74
79
|
# [scope]-[type]-article-[number]: [Short Title]
|
|
75
80
|
|
|
76
81
|
<!-- If this article is part of a series, add the line below (omit otherwise): -->
|
|
@@ -46,6 +46,7 @@ Research documents are Markdown files placed inside a subject folder alongside p
|
|
|
46
46
|
- `## Conclusion` MUST summarize the main findings, practical takeaway, applicability boundaries, and important open questions. It MUST stay under 400 words.
|
|
47
47
|
- `## References` MUST list all cited literature, websites, tutorials, documentation, discussions, or related artifacts.
|
|
48
48
|
- In general, research SHOULD roughly follow the proportion `Introduction : Methods : Results : Discussion ≈ 3 : 5 : 6 : 4`.
|
|
49
|
+
- A research document MAY start with a frontmatter block containing only `sync-date`, per [`_core-adr-policy-024`](024-sync-date-frontmatter.md); the title line follows the block.
|
|
49
50
|
- Be strict about the goal of each section. Avoid duplicating the same material across sections and prefer clarity over rhetorical flourishes.
|
|
50
51
|
- Research documents SHOULD stay under 5000 words total. They MAY exceed that only when the `## Introduction` explicitly states that the study will be lengthy because a very detailed analysis is required.
|
|
51
52
|
- Research documents SHOULD link in `## References` to the Policies, skills, articles, discussions, and external references relevant to the subject or that later cite the work.
|
|
@@ -83,6 +84,10 @@ Examples:
|
|
|
83
84
|
All research documents MUST follow this template:
|
|
84
85
|
|
|
85
86
|
```markdown
|
|
87
|
+
---
|
|
88
|
+
sync-date: [Optional, opt-in. ISO date of the last verification with other entities; see _core-adr-policy-024. When absent, omit the whole frontmatter block.]
|
|
89
|
+
---
|
|
90
|
+
|
|
86
91
|
# [scope]-[type]-research-[number]: [Short Title]
|
|
87
92
|
|
|
88
93
|
## Abstract
|
|
@@ -34,6 +34,7 @@ Initiatives are Markdown documents placed inside a subject folder alongside poli
|
|
|
34
34
|
- Initiatives MUST include an `Expected end date:` field in ISO format (YYYY-MM-DD) inside the `## Proposed Solution` section.
|
|
35
35
|
- File names MUST be lowercase.
|
|
36
36
|
- MUST NOT use emojis in initiative content.
|
|
37
|
+
- An initiative MAY start with a frontmatter block containing only `sync-date`, per [`_core-adr-policy-024`](024-sync-date-frontmatter.md); the title line follows the block.
|
|
37
38
|
- For diagram format preferences and non-Markdown asset rules, see [`_core-adr-policy-020`](020-media-and-asset-standards.md).
|
|
38
39
|
|
|
39
40
|
**Folder layout**
|
|
@@ -63,6 +64,10 @@ Examples:
|
|
|
63
64
|
All initiatives MUST follow this template:
|
|
64
65
|
|
|
65
66
|
```markdown
|
|
67
|
+
---
|
|
68
|
+
sync-date: [Optional, opt-in. ISO date of the last verification with other entities; see _core-adr-policy-024. When absent, omit the whole frontmatter block.]
|
|
69
|
+
---
|
|
70
|
+
|
|
66
71
|
# [scope]-[type]-initiative-[number]: [Short Title]
|
|
67
72
|
|
|
68
73
|
## Executive Summary
|
|
@@ -83,7 +83,7 @@ A scope-type definition policy MAY declare a parent scope type by including a ru
|
|
|
83
83
|
|
|
84
84
|
A `scope-type` value in a scope `index.md` is valid if and only if a policy file whose name ends with `{scope-type}-scope-type` exists in the `principles` subject of any `core`-type scope in the workspace. When `scope-type` is an array, this applies to every element.
|
|
85
85
|
|
|
86
|
-
When a scope declares a `scope-type` but any corresponding `{scope-type}-scope-type.md` policy is absent from the workspace or defined only in a `disabled` scope, the scope MUST be treated as READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`). Missing or disabled-only types MUST be fixed in local scopes.
|
|
86
|
+
When a scope declares a `scope-type` but any corresponding `{scope-type}-scope-type.md` policy is absent from the workspace or defined only in a `disabled` scope, the scope MUST be treated as READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`). Missing or disabled-only types MUST be fixed in local scopes, unless the scope is itself disabled.
|
|
87
87
|
|
|
88
88
|
#### 11-def-scope-type-must-be-structured
|
|
89
89
|
|
|
@@ -213,7 +213,7 @@ A scope `extends:` declaration MUST NOT reference the declaring scope itself. To
|
|
|
213
213
|
|
|
214
214
|
#### 32-extends-mandatory-presence
|
|
215
215
|
|
|
216
|
-
Every scope listed in a scope's `extends:` field MUST exist in the workspace with an accessible `index.md`. If any referenced scope is absent or its `index.md` cannot be read, the extending scope MUST be fixed when local and is READ-ONLY when external (`_core-adr-policy-022.06-read-only-dependencies`).
|
|
216
|
+
Every scope listed in a scope's `extends:` field MUST exist in the workspace with an accessible `index.md`. If any referenced scope is absent or its `index.md` cannot be read, the extending scope MUST be fixed when local and is READ-ONLY when external, unless it is itself disabled (`_core-adr-policy-022.06-read-only-dependencies`).
|
|
217
217
|
|
|
218
218
|
#### 33-extends-content-precedence
|
|
219
219
|
|
|
@@ -65,7 +65,7 @@ A `core`-type scope MAY be distributed to consumers alongside the companion cons
|
|
|
65
65
|
|
|
66
66
|
#### 08-unavailable-core-read-only
|
|
67
67
|
|
|
68
|
-
A `-core` scope referenced via `follows:` in a companion scope's `index.md` may not be present in the workspace of a consumer of that companion scope. When the referenced `-core` scope is absent or tagged `disabled` in the root index, the companion scope MUST be treated as READ-ONLY (see `_core-adr-policy-022.06-read-only-dependencies`): content in it MUST NOT be added, changed, or removed. The scope lacks the meta governance that defines how its content MUST be authored, and modifications without those standards would produce non-compliant content. Tools and agents MUST NOT propose or apply changes to a READ-ONLY scope and MUST surface the read-only status to the user when the scope is accessed.
|
|
68
|
+
A `-core` scope referenced via `follows:` in a companion scope's `index.md` may not be present in the workspace of a consumer of that companion scope. When the referenced `-core` scope is absent or tagged `disabled` in the root index, the companion scope MUST be treated as READ-ONLY, unless the companion scope is itself tagged `disabled` (see `_core-adr-policy-022.06-read-only-dependencies`): content in it MUST NOT be added, changed, or removed. The scope lacks the meta governance that defines how its content MUST be authored, and modifications without those standards would produce non-compliant content. Tools and agents MUST NOT propose or apply changes to a READ-ONLY scope and MUST surface the read-only status to the user when the scope is accessed.
|
|
69
69
|
|
|
70
70
|
#### 09-distribution-advisable
|
|
71
71
|
|
|
@@ -141,9 +141,14 @@ An exception MUST be documented in a dedicated `[type]/principles/NNN-direct-sou
|
|
|
141
141
|
|
|
142
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
143
|
|
|
144
|
+
#### 15-scope-sync-date
|
|
145
|
+
|
|
146
|
+
Compilation tools and agents MUST maintain `sync-date` in the scope `index.md` as defined in [`_core-adr-policy-024.07-compiled-scopes`](024-sync-date-frontmatter.md), setting it only after lint and review pass for the cycle.
|
|
147
|
+
|
|
144
148
|
## References
|
|
145
149
|
|
|
146
150
|
- [_core-adr-policy-008 - Policy structured standards](008-policy-structured-standards.md) — numbered rule format required for all policies in compiled scopes
|
|
147
151
|
- [_core-adr-policy-010 - Scope governance](010-scope-governance.md) — scope-type definition convention and local meta-policy mechanism
|
|
148
152
|
- [_core-adr-policy-002 - Policy standards](002-policy-standards.md) — policy document format and frontmatter requirements
|
|
149
153
|
- [_core-adr-policy-001 - XDRS standards](001-xdrs-standards.md) — scope structure and `scope-type` field definition
|
|
154
|
+
- [_core-adr-policy-024 - Sync date frontmatter](024-sync-date-frontmatter.md) — `sync-date` field set on the scope index after each completed compilation
|
|
@@ -39,12 +39,14 @@ Policies of a scope tagged `extends-only` MUST be applied only through the `exte
|
|
|
39
39
|
|
|
40
40
|
#### 05-disabled
|
|
41
41
|
|
|
42
|
-
All content of a scope tagged `disabled` MUST be ignored by agents: its policies, skills, articles, research, and initiatives are not applied, and nothing is inherited from it through `extends:` or `follows:`. The scope is still validated and MAY be edited; a disabled
|
|
42
|
+
All content of a scope tagged `disabled` MUST be ignored by agents: its policies, skills, articles, research, and initiatives are not applied, and nothing is inherited from it through `extends:` or `follows:`. The scope is still validated and MAY be edited; a disabled scope is exempt from the dependency causes of rule 06. Skills of a disabled scope that are symlinked into `.agents/skills` (see `_core-adr-policy-018`) stay loaded by tools and MUST be removed by the consumer.
|
|
43
43
|
|
|
44
44
|
#### 06-read-only-dependencies
|
|
45
45
|
|
|
46
46
|
A scope MUST be treated as READ-ONLY when a scope it follows or extends, or the scope that defines one of its scope types (including parent types), is missing, disabled, or invalid, or when it is an external scope and its own `index.md` is invalid. Content in a READ-ONLY scope MUST NOT be added, changed, or removed.
|
|
47
47
|
|
|
48
|
+
A scope tagged `disabled` is treated as not present, so it is exempt from every dependency cause above, whether the scope is local or external: it MAY follow or extend disabled, missing, or invalid scopes, or use a scope type defined only in disabled scopes. An enabled scope keeps all causes: it cannot extend or follow a disabled scope because it needs access to that scope. The exemption does not propagate along `extends:` chains.
|
|
49
|
+
|
|
48
50
|
- **Local scope** (not tracked in `.filedist.lock`, including `_local` and disabled scopes): a read-only local scope is frozen and cannot be maintained, so each cause is invalid and MUST be fixed.
|
|
49
51
|
- **External scope** (tracked in `.filedist.lock`): the scope is READ-ONLY; agents MUST tell the user when they skip a change because of it, naming the cause and fix.
|
|
50
52
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: _core-adr-policy-024-sync-date-frontmatter
|
|
3
|
+
description: Defines the optional `sync-date` frontmatter field that records when an XDRS element was last verified, checked, reconciled or aligned with other entities, internal or external. Use when a document depends on information that degrades over time, when adding or updating `sync-date`, or when a mechanism needs a refresh hint.
|
|
4
|
+
apply-to: All XDRS elements (Policies, skills, articles, research, initiatives, scope indexes) and every tool, skill or agent that syncs them
|
|
5
|
+
valid-from: 2026-11-04
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# _core-adr-policy-024: Sync date frontmatter
|
|
9
|
+
|
|
10
|
+
## Context and Problem Statement
|
|
11
|
+
|
|
12
|
+
Some XDRS elements hold content whose quality degrades over time unless it is periodically checked against other entities: other scopes, external sources, APIs, URLs, scripts or people. Version control records when a file changed, but not when its content was last verified, nor that a verification ended with a decision to change nothing.
|
|
13
|
+
|
|
14
|
+
How should an element declare when it was last checked or reconciled, so that humans, tools and agents can decide when a refresh is worth doing?
|
|
15
|
+
|
|
16
|
+
## Decision Outcome
|
|
17
|
+
|
|
18
|
+
**An optional, opt-in `sync-date` frontmatter field holding the date of the last sync**
|
|
19
|
+
|
|
20
|
+
The field is a refresh hint. It is not a status, not a change date, and carries no enforcement semantics.
|
|
21
|
+
|
|
22
|
+
### Details
|
|
23
|
+
|
|
24
|
+
#### 01-sync-date-meaning
|
|
25
|
+
|
|
26
|
+
`sync-date` MUST contain the ISO date (`YYYY-MM-DD`) on which the element content was last verified, checked, reconciled or aligned with one or more other entities. The other entity MAY be internal (another scope, element or review) or external (an API, URL, repository, script, skill or person). A sync that ended with no change, or with a deliberate decision to change nothing, still counts and MUST update the date.
|
|
27
|
+
|
|
28
|
+
#### 02-not-a-change-date
|
|
29
|
+
|
|
30
|
+
`sync-date` MUST NOT be used as a last-modified date or to record content changes, which are already available from version control. It MUST NOT be updated when the content was edited without any verification against another entity.
|
|
31
|
+
|
|
32
|
+
#### 03-opt-in
|
|
33
|
+
|
|
34
|
+
`sync-date` is OPTIONAL and MUST NOT be added by default. It MUST be added only when a compiled scope requires it (rule 07), or when the user or a skill explicitly requests it for a specific sync purpose.
|
|
35
|
+
|
|
36
|
+
#### 04-purpose-check
|
|
37
|
+
|
|
38
|
+
Before adding `sync-date`, the author or agent MUST identify the concrete sync process that will use it (for example a periodic review, a source re-sync, or a cross-scope reconciliation). When no such process exists, the field MUST NOT be added. An existing `sync-date` that no process uses SHOULD be removed.
|
|
39
|
+
|
|
40
|
+
#### 05-placement-and-format
|
|
41
|
+
|
|
42
|
+
The value MUST be an unquoted ISO date. Placement per element:
|
|
43
|
+
|
|
44
|
+
| Element | Placement |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Policy, scope `index.md` | Top-level frontmatter field `sync-date` |
|
|
47
|
+
| Article, research, initiative | Top-level `sync-date` in an optional frontmatter block placed before the title line; the block MUST contain no other field |
|
|
48
|
+
| Skill (`SKILL.md`) | Nested field `metadata.sync-date`, keeping the top-level frontmatter compatible with the agentskills spec |
|
|
49
|
+
|
|
50
|
+
Slide presentations MUST NOT carry `sync-date`.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
---
|
|
56
|
+
sync-date: 2026-11-04
|
|
57
|
+
---
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### 06-who-updates
|
|
61
|
+
|
|
62
|
+
The date MUST be updated by whoever performs the sync (human, tool or agent), only after the sync completed. A failed or partial sync MUST NOT update it.
|
|
63
|
+
|
|
64
|
+
#### 07-compiled-scopes
|
|
65
|
+
|
|
66
|
+
In a scope declaring `scope-type: compiled`, `sync-date` MUST be present only in the scope `index.md`, never in individual Policies. Compilation tools and agents MUST set it to the current date at the end of every completed compilation or re-sync cycle, including cycles where no source content changed and no Policy was modified. It MUST NOT be updated when the cycle was skipped because every source was still within its re-sync period. Placing it only in the scope index avoids touching every Policy when nothing changed.
|
|
67
|
+
|
|
68
|
+
#### 08-consumers
|
|
69
|
+
|
|
70
|
+
Mechanisms that refresh or check content (compilation, reviews, scheduled syncs, scripts) SHOULD read `sync-date` as a hint to prioritize or skip refreshes, and MUST NOT treat a missing `sync-date` as an error. How old a date must be before a refresh is worthwhile is decided by each mechanism.
|
|
71
|
+
|
|
72
|
+
## References
|
|
73
|
+
|
|
74
|
+
- [_core-adr-policy-002 - Policy standards](002-policy-standards.md) - Policy frontmatter fields
|
|
75
|
+
- [_core-adr-policy-003 - Skill standards](003-skill-standards.md) - SKILL.md frontmatter and metadata
|
|
76
|
+
- [_core-adr-policy-019 - Compiled scope type](019-compiled-scope-type.md) - Compilation and re-sync process
|
|
@@ -4,8 +4,8 @@ description: >
|
|
|
4
4
|
Compiles or updates any scope declaring `scope-type: compiled` from its configured external sources. Reads compilation meta-policies to discover sources, fetches content (git clone, local copy, or web scrape), plans policy changes, migrates policies one at a time with structured format, `## Source` sections, and `**compilation-note:**` markers, then runs lint and review. Documents source inconsistencies without inventing fixes. Activate when the user asks to compile, update, sync, refresh, or recompile a compiled scope.
|
|
5
5
|
metadata:
|
|
6
6
|
author: flaviostutz
|
|
7
|
-
version: "1.
|
|
8
|
-
updated: 2026-
|
|
7
|
+
version: "1.4.0"
|
|
8
|
+
updated: 2026-11-04
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
@@ -31,6 +31,7 @@ Performs a full compilation cycle for any scope declaring `scope-type: compiled`
|
|
|
31
31
|
#### Changes
|
|
32
32
|
|
|
33
33
|
- Policy files created, updated, or removed.
|
|
34
|
+
- Scope `index.md` `sync-date` refreshed.
|
|
34
35
|
|
|
35
36
|
### Halt Conditions
|
|
36
37
|
|
|
@@ -107,7 +108,7 @@ Keep every question and intermediate message <100 words and the final summary <1
|
|
|
107
108
|
For each TODO item marked CREATE or UPDATE, in sequence:
|
|
108
109
|
|
|
109
110
|
1. **Write the policy file** (<2600 words, per `_core-adr-policy-002`) following `_core-adr-policy-019` and `_core-adr-policy-008`:
|
|
110
|
-
- Use standard policy frontmatter: `name`, `description`, `apply-to` (<40 words), `valid-from`. Generate these from the source text; they are exempt from the no-invented-content rule per policy-019 rule 05.
|
|
111
|
+
- Use standard policy frontmatter: `name`, `description`, `apply-to` (<40 words), `valid-from`. Generate these from the source text; they are exempt from the no-invented-content rule per policy-019 rule 05. Do NOT add `sync-date` to compiled policies; it lives only in the scope `index.md` (`_core-adr-policy-024.07-compiled-scopes`).
|
|
111
112
|
- Use structured numbered rule blocks for all claims (per `_core-adr-policy-008`).
|
|
112
113
|
- Transcribe source content faithfully. Do NOT invent, infer beyond what the source states, or fill gaps. Preserve ambiguities as-is.
|
|
113
114
|
|
|
@@ -169,6 +170,7 @@ After writing each policy:
|
|
|
169
170
|
3. If storage is `temporary`: do not copy any bulk staged content into `.assets/sources/[name]/`; only `source.md` lives there.
|
|
170
171
|
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.
|
|
171
172
|
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.
|
|
173
|
+
6. After lint and review pass for the cycle, set `sync-date` in the scope `index.md` frontmatter to today's date (`YYYY-MM-DD`), adding the field when absent, even when no source changed and no policy was modified (`_core-adr-policy-024.07-compiled-scopes`). Skip this step when the run halted or ended at the Phase 1 no-op.
|
|
172
174
|
|
|
173
175
|
### Phase 10: Report
|
|
174
176
|
|
|
@@ -4,8 +4,8 @@ description: >
|
|
|
4
4
|
Creates a new article document following XDRS article standards: selects scope, type, subject, and number; then writes a focused synthetic text that combines and links multiple Policies, Research documents, and Skills around a topic. Activate this skill when the user asks to create, add, or write a new article, guide, or overview document within an XDRS project.
|
|
5
5
|
metadata:
|
|
6
6
|
author: flaviostutz
|
|
7
|
-
version: "1.1.
|
|
8
|
-
updated: 2026-
|
|
7
|
+
version: "1.1.1"
|
|
8
|
+
updated: 2026-11-04
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
@@ -103,7 +103,7 @@ If the article spans more than one subject, place it in `principles`.
|
|
|
103
103
|
|
|
104
104
|
### Phase 5: Write the Article
|
|
105
105
|
|
|
106
|
-
Use the mandatory template from `004-article-standards
|
|
106
|
+
Use the mandatory template from `004-article-standards`. Omit the optional `sync-date` frontmatter unless the user explicitly asks for it for a specific sync purpose (`_core-adr-policy-024`):
|
|
107
107
|
|
|
108
108
|
```
|
|
109
109
|
# [scope]-[type]-article-[number]: [Short Title]
|
|
@@ -6,8 +6,8 @@ description: >
|
|
|
6
6
|
Activate this skill when the user asks to create, add, or write an initiative, plan, project plan, roadmap, or execution plan within an XDRS project.
|
|
7
7
|
metadata:
|
|
8
8
|
author: flaviostutz
|
|
9
|
-
version: "1.1.
|
|
10
|
-
updated: 2026-
|
|
9
|
+
version: "1.1.1"
|
|
10
|
+
updated: 2026-11-04
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
@@ -93,7 +93,7 @@ Consult `001-xdrs-standards` while making each choice in this phase. The summari
|
|
|
93
93
|
|
|
94
94
|
### Phase 5: Write the Initiative
|
|
95
95
|
|
|
96
|
-
Use the mandatory template from `007-initiative-standards
|
|
96
|
+
Use the mandatory template from `007-initiative-standards`. Omit the optional `sync-date` frontmatter unless the user explicitly asks for it for a specific sync purpose (`_core-adr-policy-024`):
|
|
97
97
|
|
|
98
98
|
```markdown
|
|
99
99
|
# [scope]-[type]-initiative-[number]: [Short Title]
|
|
@@ -5,8 +5,8 @@ description: >
|
|
|
5
5
|
Activate this skill when the user asks to create, add, or write a new Policy (ADR, BDR, or EDR).
|
|
6
6
|
metadata:
|
|
7
7
|
author: flaviostutz
|
|
8
|
-
version: "1.2.
|
|
9
|
-
updated: 2026-
|
|
8
|
+
version: "1.2.1"
|
|
9
|
+
updated: 2026-11-04
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
@@ -107,7 +107,7 @@ Choose a title that clearly states the question this Policy answers, not the ans
|
|
|
107
107
|
|
|
108
108
|
### Phase 6: Write the First Draft
|
|
109
109
|
|
|
110
|
-
Use the mandatory template from `002-policy-standards
|
|
110
|
+
Use the mandatory template from `002-policy-standards`. Omit the optional `sync-date` field unless the user explicitly asks for it for a specific sync purpose (`_core-adr-policy-024`); never add it to Policies of `compiled` scopes:
|
|
111
111
|
|
|
112
112
|
**Check if the decision requires a structured set of rules:**
|
|
113
113
|
If the decision defines strong rules or policies that must be stated explicitly, or if other documents, skills, or agents have a clear need to reference individual rules, you MUST apply the structured rule format from `_core-adr-policy-008-policy-structured-standards`. This means:
|
|
@@ -6,8 +6,8 @@ description: >
|
|
|
6
6
|
Activate this skill when the user asks to create, add, or write a research document that backs a decision.
|
|
7
7
|
metadata:
|
|
8
8
|
author: flaviostutz
|
|
9
|
-
version: "1.5.
|
|
10
|
-
updated: 2026-
|
|
9
|
+
version: "1.5.1"
|
|
10
|
+
updated: 2026-11-04
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
@@ -124,7 +124,7 @@ If the results contain competing interpretations or the intended audience for th
|
|
|
124
124
|
|
|
125
125
|
### Phase 7: Write the Research Document
|
|
126
126
|
|
|
127
|
-
Use the mandatory template from `006-research-standards
|
|
127
|
+
Use the mandatory template from `006-research-standards`. Omit the optional `sync-date` frontmatter unless the user explicitly asks for it for a specific sync purpose (`_core-adr-policy-024`):
|
|
128
128
|
|
|
129
129
|
```markdown
|
|
130
130
|
# [scope]-[type]-research-[number]: [Short Title]
|
|
@@ -6,8 +6,8 @@ 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.3.
|
|
10
|
-
updated: 2026-
|
|
9
|
+
version: "1.3.1"
|
|
10
|
+
updated: 2026-11-04
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
@@ -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
|
---
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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,
|
package/lib/lint.test.js
CHANGED
|
@@ -5905,16 +5905,147 @@ test('reports a local scope whose type is defined only in a disabled scope as fr
|
|
|
5905
5905
|
expect(activationErrors(result)).toContain('Local scope "shop" is frozen (read-only) because scope type "widget" is defined only in disabled scope(s) "widgets-core"');
|
|
5906
5906
|
});
|
|
5907
5907
|
|
|
5908
|
-
test('
|
|
5908
|
+
test('does not report missing dependencies of a disabled scope [022.06]', () => {
|
|
5909
5909
|
const workspaceRoot = createWorkspace('activation-disabled-local-missing-deps', {
|
|
5910
5910
|
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5911
5911
|
...scopeFiles('myteam', 'follows: ghost-core\nextends: ghost-lib\n', 'phantom'),
|
|
5912
5912
|
...coreWithScopeTypes(['standard']),
|
|
5913
5913
|
});
|
|
5914
5914
|
const errors = lintWorkspace(workspaceRoot, { ignoreExternal: false }).errors.join('\n');
|
|
5915
|
-
expect(errors).toContain('
|
|
5916
|
-
expect(errors).toContain('ghost-lib');
|
|
5917
|
-
expect(errors).toContain('phantom');
|
|
5915
|
+
expect(errors).not.toContain('ghost-core');
|
|
5916
|
+
expect(errors).not.toContain('ghost-lib');
|
|
5917
|
+
expect(errors).not.toContain('phantom');
|
|
5918
|
+
});
|
|
5919
|
+
|
|
5920
|
+
test('does not report a disabled scope extending a disabled scope [022.06]', () => {
|
|
5921
|
+
const workspaceRoot = createWorkspace('activation-disabled-extends-disabled', {
|
|
5922
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope mid](mid/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5923
|
+
...scopeFiles('lib'),
|
|
5924
|
+
...scopeFiles('mid', 'extends: lib\n'),
|
|
5925
|
+
});
|
|
5926
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
5927
|
+
expect(activationErrors(result)).toBe('');
|
|
5928
|
+
expect(result.errors.join('\n')).not.toContain('frozen');
|
|
5929
|
+
});
|
|
5930
|
+
|
|
5931
|
+
test('does not report a disabled scope following a disabled core scope [022.06]', () => {
|
|
5932
|
+
const workspaceRoot = createWorkspace('activation-disabled-follows-disabled', {
|
|
5933
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope area-core](area-core/index.md) `disabled`', '[View scope myteam](myteam/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5934
|
+
...scopeFiles('area-core', '', 'core'),
|
|
5935
|
+
...scopeFiles('myteam', 'follows: area-core\n'),
|
|
5936
|
+
});
|
|
5937
|
+
expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).toBe('');
|
|
5938
|
+
});
|
|
5939
|
+
|
|
5940
|
+
test('does not report a disabled scope whose type is defined only in a disabled scope [022.06]', () => {
|
|
5941
|
+
const workspaceRoot = createWorkspace('activation-disabled-type-in-disabled', {
|
|
5942
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope widgets-core](widgets-core/index.md) `disabled`', '[View scope shop](shop/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5943
|
+
'.xdrs/widgets-core/index.md': scopeIndexWith('widgets-core', '', 'core'),
|
|
5944
|
+
'.xdrs/widgets-core/adrs/index.md': teamAdrIndex(['- [001-widget-scope-type](principles/001-widget-scope-type.md) - Widget type']),
|
|
5945
|
+
'.xdrs/widgets-core/adrs/principles/001-widget-scope-type.md': structuredScopeTypePolicy('widgets-core', '001', 'widget'),
|
|
5946
|
+
...scopeFiles('shop', '', 'widget'),
|
|
5947
|
+
...coreWithScopeTypes(['standard']),
|
|
5948
|
+
});
|
|
5949
|
+
expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).toBe('');
|
|
5950
|
+
});
|
|
5951
|
+
|
|
5952
|
+
test('does not report a disabled scope extending a scope with an invalid index [022.06]', () => {
|
|
5953
|
+
const workspaceRoot = createWorkspace('activation-disabled-extends-invalid', {
|
|
5954
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md)', '[View scope mid](mid/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5955
|
+
...scopeFiles('lib'),
|
|
5956
|
+
'.xdrs/lib/index.md': '# lib without frontmatter\n',
|
|
5957
|
+
...scopeFiles('mid', 'extends: lib\n'),
|
|
5958
|
+
});
|
|
5959
|
+
expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).not.toContain('Local scope "mid" is frozen');
|
|
5960
|
+
});
|
|
5961
|
+
|
|
5962
|
+
test('does not report a disabled external scope extending a disabled or missing scope [022.06]', () => {
|
|
5963
|
+
const workspaceRoot = createWorkspace('activation-disabled-external-extends', {
|
|
5964
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope vendor](vendor/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5965
|
+
...scopeFiles('lib'),
|
|
5966
|
+
...scopeFiles('vendor', 'extends: lib, ghost-lib\n'),
|
|
5967
|
+
'.filedist.lock': '.xdrs/vendor/index.md|some-package|1.0.0\n',
|
|
5968
|
+
});
|
|
5969
|
+
expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).toBe('');
|
|
5970
|
+
});
|
|
5971
|
+
|
|
5972
|
+
test('does not report a disabled scope with extends-only: true extending a disabled scope [022.06]', () => {
|
|
5973
|
+
const workspaceRoot = createWorkspace('activation-disabled-extends-only-field', {
|
|
5974
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope mid](mid/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
5975
|
+
...scopeFiles('lib'),
|
|
5976
|
+
...scopeFiles('mid', 'extends: lib\nextends-only: true\n'),
|
|
5977
|
+
});
|
|
5978
|
+
expect(activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }))).toBe('');
|
|
5979
|
+
});
|
|
5980
|
+
|
|
5981
|
+
test('reports only the enabled extender in the chain enabled -> disabled -> disabled [022.06]', () => {
|
|
5982
|
+
const workspaceRoot = createWorkspace('activation-chain-enabled-disabled-disabled', {
|
|
5983
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope mid](mid/index.md) `disabled`', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
|
|
5984
|
+
...scopeFiles('lib'),
|
|
5985
|
+
...scopeFiles('mid', 'extends: lib\n'),
|
|
5986
|
+
...scopeFiles('myteam', 'extends: mid\n'),
|
|
5987
|
+
});
|
|
5988
|
+
const errors = activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }));
|
|
5989
|
+
expect(errors).toContain('Local scope "myteam" is frozen (read-only) because extends: scope "mid" is tagged `disabled`');
|
|
5990
|
+
expect(errors).not.toContain('Local scope "mid"');
|
|
5991
|
+
});
|
|
5992
|
+
|
|
5993
|
+
test('reports only the direct extender in the chain enabled -> enabled -> disabled [022.06]', () => {
|
|
5994
|
+
const workspaceRoot = createWorkspace('activation-chain-enabled-enabled-disabled', {
|
|
5995
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md) `disabled`', '[View scope mid](mid/index.md)', '[View scope myteam](myteam/index.md)', ACTIVATION_LEGEND]),
|
|
5996
|
+
...scopeFiles('lib'),
|
|
5997
|
+
...scopeFiles('mid', 'extends: lib\n'),
|
|
5998
|
+
...scopeFiles('myteam', 'extends: mid\n'),
|
|
5999
|
+
});
|
|
6000
|
+
const errors = activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }));
|
|
6001
|
+
expect(errors).toContain('Local scope "mid" is frozen (read-only) because extends: scope "lib" is tagged `disabled`');
|
|
6002
|
+
expect(errors).not.toContain('Local scope "myteam"');
|
|
6003
|
+
});
|
|
6004
|
+
|
|
6005
|
+
test('still reports an enabled scope extending a missing scope [010.32]', () => {
|
|
6006
|
+
const workspaceRoot = createWorkspace('activation-enabled-extends-missing', {
|
|
6007
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope myteam](myteam/index.md)']),
|
|
6008
|
+
...scopeFiles('myteam', 'extends: ghost-lib\n'),
|
|
6009
|
+
});
|
|
6010
|
+
const errors = lintWorkspace(workspaceRoot, { ignoreExternal: false }).errors.join('\n');
|
|
6011
|
+
expect(errors).toContain('extends references scope "ghost-lib" which does not exist in the workspace');
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
test('still reports extends cycles and self-extends in disabled scopes [010.30, 010.31]', () => {
|
|
6015
|
+
const workspaceRoot = createWorkspace('activation-disabled-structural', {
|
|
6016
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope a](a/index.md) `disabled`', '[View scope b](b/index.md) `disabled`', '[View scope c](c/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
6017
|
+
...scopeFiles('a', 'extends: b\n'),
|
|
6018
|
+
...scopeFiles('b', 'extends: a\n'),
|
|
6019
|
+
...scopeFiles('c', 'extends: c\n'),
|
|
6020
|
+
});
|
|
6021
|
+
const errors = lintWorkspace(workspaceRoot, { ignoreExternal: false }).errors.join('\n');
|
|
6022
|
+
expect(errors).toContain('Circular extends chain detected');
|
|
6023
|
+
expect(errors).toContain('must not reference the scope itself');
|
|
6024
|
+
});
|
|
6025
|
+
|
|
6026
|
+
test('still reports a disabled scope following a non-core scope [001]', () => {
|
|
6027
|
+
const workspaceRoot = createWorkspace('activation-disabled-follows-non-core', {
|
|
6028
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope lib](lib/index.md)', '[View scope mid](mid/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
6029
|
+
...scopeFiles('lib'),
|
|
6030
|
+
...scopeFiles('mid', 'follows: lib\n'),
|
|
6031
|
+
});
|
|
6032
|
+
const errors = lintWorkspace(workspaceRoot, { ignoreExternal: false }).errors.join('\n');
|
|
6033
|
+
expect(errors).toContain('follows references scope "lib" which is not a core-type scope');
|
|
6034
|
+
});
|
|
6035
|
+
|
|
6036
|
+
test('reports an enabled scope type defined only in a disabled scope as frozen while a disabled user is exempt [022.06]', () => {
|
|
6037
|
+
const workspaceRoot = createWorkspace('activation-type-enabled-vs-disabled-user', {
|
|
6038
|
+
'.xdrs/index.md': rootIndex(['[_core](_core/index.md)', '[View scope widgets-core](widgets-core/index.md) `disabled`', '[View scope shop](shop/index.md)', '[View scope kiosk](kiosk/index.md) `disabled`', ACTIVATION_LEGEND]),
|
|
6039
|
+
'.xdrs/widgets-core/index.md': scopeIndexWith('widgets-core', '', 'core'),
|
|
6040
|
+
'.xdrs/widgets-core/adrs/index.md': teamAdrIndex(['- [001-widget-scope-type](principles/001-widget-scope-type.md) - Widget type']),
|
|
6041
|
+
'.xdrs/widgets-core/adrs/principles/001-widget-scope-type.md': structuredScopeTypePolicy('widgets-core', '001', 'widget'),
|
|
6042
|
+
...scopeFiles('shop', '', 'widget'),
|
|
6043
|
+
...scopeFiles('kiosk', '', 'widget'),
|
|
6044
|
+
...coreWithScopeTypes(['standard']),
|
|
6045
|
+
});
|
|
6046
|
+
const errors = activationErrors(lintWorkspace(workspaceRoot, { ignoreExternal: false }));
|
|
6047
|
+
expect(errors).toContain('Local scope "shop" is frozen (read-only)');
|
|
6048
|
+
expect(errors).not.toContain('Local scope "kiosk"');
|
|
5918
6049
|
});
|
|
5919
6050
|
|
|
5920
6051
|
test('prints frozen local scopes as errors and never prints warning or READ-ONLY lines [022.06]', () => {
|
|
@@ -6048,3 +6179,161 @@ test('accepts compiled combined with a non-core type [011.10]', () => {
|
|
|
6048
6179
|
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
6049
6180
|
expect(result.errors.join('\n')).not.toContain('011.10');
|
|
6050
6181
|
});
|
|
6182
|
+
|
|
6183
|
+
// ---------------------------------------------------------------------------
|
|
6184
|
+
// sync-date frontmatter (_core-adr-policy-024)
|
|
6185
|
+
// ---------------------------------------------------------------------------
|
|
6186
|
+
|
|
6187
|
+
const SYNC_DATE_ERRORS = (result) => result.errors.filter((e) => e.includes('sync-date')).join('\n');
|
|
6188
|
+
|
|
6189
|
+
function syncDatePolicy(extraFrontmatter) {
|
|
6190
|
+
return [
|
|
6191
|
+
'---',
|
|
6192
|
+
'name: _local-adr-policy-001-main',
|
|
6193
|
+
'description: Test XDRS element',
|
|
6194
|
+
'apply-to: All scopes',
|
|
6195
|
+
'valid-from: 2026-01-01',
|
|
6196
|
+
...extraFrontmatter,
|
|
6197
|
+
'---',
|
|
6198
|
+
'',
|
|
6199
|
+
'# _local-adr-policy-001: Main decision',
|
|
6200
|
+
'',
|
|
6201
|
+
'## Context and Problem Statement',
|
|
6202
|
+
'',
|
|
6203
|
+
'Test body.',
|
|
6204
|
+
'',
|
|
6205
|
+
'## Decision Outcome',
|
|
6206
|
+
'',
|
|
6207
|
+
'Test decision outcome.',
|
|
6208
|
+
''
|
|
6209
|
+
].join('\n');
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
test('accepts a valid sync-date in Policy frontmatter', () => {
|
|
6213
|
+
const workspaceRoot = createWorkspace('syncd-policy-valid', {
|
|
6214
|
+
'.xdrs/index.md': rootIndex(),
|
|
6215
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex(['- [001-main](principles/001-main.md) - Main decision']),
|
|
6216
|
+
'.xdrs/_local/adrs/principles/001-main.md': syncDatePolicy(['sync-date: 2026-11-04']),
|
|
6217
|
+
});
|
|
6218
|
+
|
|
6219
|
+
const result = lintWorkspace(workspaceRoot);
|
|
6220
|
+
|
|
6221
|
+
expect(SYNC_DATE_ERRORS(result)).toBe('');
|
|
6222
|
+
expect(result.errors.join('\n')).not.toContain('unknown field');
|
|
6223
|
+
});
|
|
6224
|
+
|
|
6225
|
+
test('reports invalid and empty sync-date in Policy frontmatter', () => {
|
|
6226
|
+
for (const [name, value] of [['syncd-policy-invalid', 'sync-date: yesterday'], ['syncd-policy-empty', 'sync-date:'], ['syncd-policy-quoted', 'sync-date: "2026-11-04"']]) {
|
|
6227
|
+
const workspaceRoot = createWorkspace(name, {
|
|
6228
|
+
'.xdrs/index.md': rootIndex(),
|
|
6229
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex(['- [001-main](principles/001-main.md) - Main decision']),
|
|
6230
|
+
'.xdrs/_local/adrs/principles/001-main.md': syncDatePolicy([value]),
|
|
6231
|
+
});
|
|
6232
|
+
|
|
6233
|
+
const result = lintWorkspace(workspaceRoot);
|
|
6234
|
+
|
|
6235
|
+
expect(SYNC_DATE_ERRORS(result)).toContain('Policy frontmatter sync-date must be a valid ISO date YYYY-MM-DD');
|
|
6236
|
+
}
|
|
6237
|
+
});
|
|
6238
|
+
|
|
6239
|
+
test('reports sync-date in a Policy of a compiled scope', () => {
|
|
6240
|
+
const policy = validCompiledPolicy().replace('valid-from: 2026-01-01\n', 'valid-from: 2026-01-01\nsync-date: 2026-11-04\n');
|
|
6241
|
+
const workspaceRoot = createWorkspace('syncd-compiled-policy', {
|
|
6242
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
6243
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex(),
|
|
6244
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex(['- [001-a01](principles/001-a01.md) - A01']),
|
|
6245
|
+
'.xdrs/my-compiled/adrs/principles/001-a01.md': policy,
|
|
6246
|
+
});
|
|
6247
|
+
|
|
6248
|
+
const result = lintWorkspace(workspaceRoot);
|
|
6249
|
+
|
|
6250
|
+
expect(SYNC_DATE_ERRORS(result)).toContain('024.07-compiled-scopes');
|
|
6251
|
+
});
|
|
6252
|
+
|
|
6253
|
+
test('accepts sync-date in the scope index of a compiled scope', () => {
|
|
6254
|
+
const workspaceRoot = createWorkspace('syncd-compiled-scope-index', {
|
|
6255
|
+
'.xdrs/index.md': rootIndex(['[my-compiled](my-compiled/index.md)']),
|
|
6256
|
+
'.xdrs/my-compiled/index.md': compiledScopeIndex().replace('valid-from: 2026-01-01\n', 'valid-from: 2026-01-01\nsync-date: 2026-11-04\n'),
|
|
6257
|
+
'.xdrs/my-compiled/adrs/index.md': compiledAdrIndex(['- [001-a01](principles/001-a01.md) - A01']),
|
|
6258
|
+
'.xdrs/my-compiled/adrs/principles/001-a01.md': validCompiledPolicy(),
|
|
6259
|
+
});
|
|
6260
|
+
|
|
6261
|
+
const result = lintWorkspace(workspaceRoot);
|
|
6262
|
+
|
|
6263
|
+
expect(SYNC_DATE_ERRORS(result)).toBe('');
|
|
6264
|
+
expect(result.errors.join('\n')).not.toContain('unknown field');
|
|
6265
|
+
});
|
|
6266
|
+
|
|
6267
|
+
test('reports invalid sync-date in scope index frontmatter', () => {
|
|
6268
|
+
const workspaceRoot = createWorkspace('syncd-scope-index-invalid', {
|
|
6269
|
+
'.xdrs/index.md': rootIndex(['[myteam](myteam/index.md)']),
|
|
6270
|
+
'.xdrs/myteam/index.md': '---\nscope-type: standard\nname: myteam\ndescription: Team.\napply-to: Test team\nvalid-from: 2026-01-01\nsync-date: 2026-13-45\n---\n\n# myteam Scope Overview\n\n[ADRs](adrs/index.md)\n',
|
|
6271
|
+
'.xdrs/myteam/adrs/index.md': teamAdrIndex(['- [001-team](principles/001-team.md) - Team decision']),
|
|
6272
|
+
'.xdrs/myteam/adrs/principles/001-team.md': teamXdrDocument('Team decision.'),
|
|
6273
|
+
});
|
|
6274
|
+
|
|
6275
|
+
const result = lintWorkspace(workspaceRoot, { ignoreExternal: false });
|
|
6276
|
+
|
|
6277
|
+
expect(SYNC_DATE_ERRORS(result)).toContain('Scope index frontmatter sync-date must be a valid ISO date YYYY-MM-DD');
|
|
6278
|
+
});
|
|
6279
|
+
|
|
6280
|
+
test('validates metadata.sync-date in SKILL.md frontmatter', () => {
|
|
6281
|
+
const skillWith = (syncDate) => fullSkillDocument().replace(
|
|
6282
|
+
'description: Test skill document\n---',
|
|
6283
|
+
`description: Test skill document\nmetadata:\n version: "1.0.0"\n author: test\n updated: 2026-11-04\n sync-date: ${syncDate}\n---`
|
|
6284
|
+
);
|
|
6285
|
+
const build = (name, syncDate) => createWorkspace(name, {
|
|
6286
|
+
'.xdrs/index.md': rootIndex(),
|
|
6287
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex(['- [check-links](principles/skills/check-links/SKILL.md) - Check links']),
|
|
6288
|
+
'.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillWith(syncDate),
|
|
6289
|
+
});
|
|
6290
|
+
|
|
6291
|
+
const valid = lintWorkspace(build('syncd-skill-valid', '2026-11-04'));
|
|
6292
|
+
const invalid = lintWorkspace(build('syncd-skill-invalid', 'soon'));
|
|
6293
|
+
|
|
6294
|
+
expect(SYNC_DATE_ERRORS(valid)).toBe('');
|
|
6295
|
+
expect(SYNC_DATE_ERRORS(invalid)).toContain('SKILL.md frontmatter metadata.sync-date must be a valid ISO date YYYY-MM-DD');
|
|
6296
|
+
});
|
|
6297
|
+
|
|
6298
|
+
test('accepts sync-date-only frontmatter in article, research and initiative documents', () => {
|
|
6299
|
+
const fm = '---\nsync-date: 2026-11-04\n---\n\n';
|
|
6300
|
+
const workspaceRoot = createWorkspace('syncd-docs-valid', {
|
|
6301
|
+
'.xdrs/index.md': rootIndex(),
|
|
6302
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
6303
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide',
|
|
6304
|
+
'- [001-study](principles/researches/001-study.md) - Study',
|
|
6305
|
+
'- [001-myinitiative](principles/initiatives/001-myinitiative.md) - My initiative',
|
|
6306
|
+
]),
|
|
6307
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': fm + articleDocument('Body.'),
|
|
6308
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': fm + researchDocument('Context.', 'Question: Is this right?'),
|
|
6309
|
+
'.xdrs/_local/adrs/principles/initiatives/001-myinitiative.md': fm + initiativeDocument('Body.'),
|
|
6310
|
+
});
|
|
6311
|
+
|
|
6312
|
+
const result = lintWorkspace(workspaceRoot);
|
|
6313
|
+
|
|
6314
|
+
expect(SYNC_DATE_ERRORS(result)).toBe('');
|
|
6315
|
+
expect(result.errors.join('\n')).not.toContain('title must start with');
|
|
6316
|
+
expect(result.errors.join('\n')).not.toContain('frontmatter');
|
|
6317
|
+
});
|
|
6318
|
+
|
|
6319
|
+
test('reports unknown, missing and invalid frontmatter fields in article, research and initiative documents', () => {
|
|
6320
|
+
const workspaceRoot = createWorkspace('syncd-docs-invalid', {
|
|
6321
|
+
'.xdrs/index.md': rootIndex(),
|
|
6322
|
+
'.xdrs/_local/adrs/index.md': localAdrIndex([
|
|
6323
|
+
'- [001-guide](principles/articles/001-guide.md) - Guide',
|
|
6324
|
+
'- [001-study](principles/researches/001-study.md) - Study',
|
|
6325
|
+
'- [001-myinitiative](principles/initiatives/001-myinitiative.md) - My initiative',
|
|
6326
|
+
]),
|
|
6327
|
+
'.xdrs/_local/adrs/principles/articles/001-guide.md': '---\nsync-date: 2026-11-04\nauthor: me\n---\n\n' + articleDocument('Body.'),
|
|
6328
|
+
'.xdrs/_local/adrs/principles/researches/001-study.md': '---\nsync-date: never\n---\n\n' + researchDocument('Context.', 'Question: Is this right?'),
|
|
6329
|
+
'.xdrs/_local/adrs/principles/initiatives/001-myinitiative.md': '---\nauthor: me\n---\n\n' + initiativeDocument('Body.'),
|
|
6330
|
+
});
|
|
6331
|
+
|
|
6332
|
+
const result = lintWorkspace(workspaceRoot);
|
|
6333
|
+
const errors = result.errors.join('\n');
|
|
6334
|
+
|
|
6335
|
+
expect(errors).toContain('Article frontmatter has unknown field "author" (only sync-date is allowed)');
|
|
6336
|
+
expect(errors).toContain('Research frontmatter sync-date must be a valid ISO date YYYY-MM-DD');
|
|
6337
|
+
expect(errors).toContain('Initiative frontmatter has unknown field "author" (only sync-date is allowed)');
|
|
6338
|
+
expect(errors).toContain('Initiative frontmatter must include a sync-date field when present');
|
|
6339
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xdrs-core",
|
|
3
|
-
"version": "0.56.
|
|
3
|
+
"version": "0.56.1",
|
|
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",
|