xdrs-core 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.xdrs/_core/adrs/index.md +10 -0
- package/.xdrs/_core/adrs/principles/004-article-standards.md +1 -0
- package/.xdrs/_core/adrs/principles/005-semantic-versioning-for-xdr-packages.md +45 -0
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +111 -0
- package/.xdrs/_local/adrs/index.md +11 -0
- package/.xdrs/_local/adrs/principles/articles/001-create-your-own-xdrs-extension-package.md +123 -0
- package/.xdrs/index.md +1 -5
- package/README.md +31 -0
- package/bin/filedist.js +10 -1
- package/lib/lint.js +462 -0
- package/package.json +4 -3
|
@@ -11,6 +11,16 @@ Foundational standards, principles, and guidelines.
|
|
|
11
11
|
- [_core-adr-001](principles/001-xdr-standards.md) - **XDR standards** (includes XDR template)
|
|
12
12
|
- [_core-adr-003](principles/003-skill-standards.md) - **Skill standards**
|
|
13
13
|
- [_core-adr-004](principles/004-article-standards.md) - **Article standards**
|
|
14
|
+
- [_core-adr-005](principles/005-semantic-versioning-for-xdr-packages.md) - **Semantic versioning for XDR packages**
|
|
15
|
+
|
|
16
|
+
## Skills
|
|
17
|
+
|
|
18
|
+
Step-by-step procedural guides for humans and AI agents.
|
|
19
|
+
|
|
20
|
+
- [001-lint](principles/skills/001-lint/SKILL.md) - **Lint** — review code and files against XDRs
|
|
21
|
+
- [002-write-xdr](principles/skills/002-write-xdr/SKILL.md) - **Write XDR** — create a new Decision Record
|
|
22
|
+
- [003-write-skill](principles/skills/003-write-skill/SKILL.md) - **Write Skill** — create a new skill package
|
|
23
|
+
- [004-write-article](principles/skills/004-write-article/SKILL.md) - **Write Article** — create a new article document
|
|
14
24
|
|
|
15
25
|
## Articles
|
|
16
26
|
|
|
@@ -76,3 +76,4 @@ when referencing an information from those documents.]
|
|
|
76
76
|
|
|
77
77
|
- [_core-adr-001 - XDR standards](001-xdr-standards.md)
|
|
78
78
|
- [_core-adr-003 - Skill standards](003-skill-standards.md)
|
|
79
|
+
- [004-write-article skill](skills/004-write-article/SKILL.md) - Step-by-step instructions for creating a new article
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# _core-adr-005: Semantic versioning for XDR packages
|
|
2
|
+
|
|
3
|
+
## Context and Problem Statement
|
|
4
|
+
|
|
5
|
+
Teams consume XDR packages as reusable guidance, constraints, skills, and articles. If package versions do not reflect the real impact of a release, upgrades become risky and teams lose trust in reuse.
|
|
6
|
+
|
|
7
|
+
Question: How should semantic versioning be used when publishing or versioning a package containing XDRs, skills, and articles?
|
|
8
|
+
|
|
9
|
+
## Decision Outcome
|
|
10
|
+
|
|
11
|
+
**semantic versioning aligned with decision impact**
|
|
12
|
+
|
|
13
|
+
XDR packages must use semantic versioning so the package version communicates the expected upgrade impact on consuming teams and projects.
|
|
14
|
+
|
|
15
|
+
### Implementation Details
|
|
16
|
+
|
|
17
|
+
- Package versions MUST follow `MAJOR.MINOR.PATCH`.
|
|
18
|
+
- The published package version MUST represent the impact of the package as a whole, not only of a single changed file.
|
|
19
|
+
- If a release contains changes of different severities, the highest-impact change MUST define the version bump.
|
|
20
|
+
- When uncertainty exists between two levels, the safer and more explicit version bump SHOULD be chosen.
|
|
21
|
+
|
|
22
|
+
**MAJOR**
|
|
23
|
+
- Use a major bump for breaking changes.
|
|
24
|
+
- Use a major bump when an existing XDR changes meaning in a way that can require consuming teams to revisit architecture, governance, operations, or implementation decisions.
|
|
25
|
+
- Use a major bump when impactful concepts are introduced or changed in a way that materially alters how the package should be adopted or interpreted.
|
|
26
|
+
- Typical cases: removed or renamed XDRs that affect references, changed mandatory rules, changed conflict/override behavior, or changed guidance that invalidates previously compliant usage.
|
|
27
|
+
|
|
28
|
+
**MINOR**
|
|
29
|
+
- Use a minor bump for backward-compatible additions and new capabilities.
|
|
30
|
+
- Use a minor bump for new XDRs that do not break existing guidance.
|
|
31
|
+
- Use a minor bump for new or updated articles and skill changes that extend the package without requiring consumers to undo previous adoption work.
|
|
32
|
+
- Typical cases: new features, new optional guidance, new articles, expanded skills, or additive non-breaking decision coverage.
|
|
33
|
+
|
|
34
|
+
**PATCH**
|
|
35
|
+
- Use a patch bump for low-risk fixes and simple improvements.
|
|
36
|
+
- Use a patch bump for corrections that preserve the existing meaning and upgrade expectations of the package.
|
|
37
|
+
- Typical cases: typo fixes, broken links, wording clarifications, examples, simple additions, formatting fixes, and small consistency improvements.
|
|
38
|
+
|
|
39
|
+
- Teams publishing XDR packages SHOULD treat the version number as an upgrade contract.
|
|
40
|
+
- Consumers SHOULD be able to assume that patch upgrades are low risk, minor upgrades are additive, and major upgrades may require review or migration work.
|
|
41
|
+
- Release notes SHOULD explain the reason for the chosen bump, especially for major releases.
|
|
42
|
+
|
|
43
|
+
## References
|
|
44
|
+
|
|
45
|
+
- https://semver.org/
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 004-write-article
|
|
3
|
+
description: >
|
|
4
|
+
Creates a new article document following XDR article standards: selects scope, type, subject, and number;
|
|
5
|
+
then writes a focused synthetic text that combines and links multiple XDRs and Skills around a topic.
|
|
6
|
+
Activate this skill when the user asks to create, add, or write a new article, guide, or overview document
|
|
7
|
+
within an XDR project.
|
|
8
|
+
metadata:
|
|
9
|
+
author: flaviostutz
|
|
10
|
+
version: "1.0"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
Guides the creation of a well-structured article by following `_core-adr-004`, researching the XDRs and
|
|
16
|
+
Skills to synthesize, and producing a concise document that serves as a navigable view without duplicating
|
|
17
|
+
decision content.
|
|
18
|
+
|
|
19
|
+
## Instructions
|
|
20
|
+
|
|
21
|
+
### Phase 1: Understand the Article Goal
|
|
22
|
+
|
|
23
|
+
1. Read `.xdrs/_core/adrs/principles/004-article-standards.md` in full to internalize the template,
|
|
24
|
+
placement rules, numbering rules, and the constraint that articles are views, not decisions.
|
|
25
|
+
2. Identify the topic and intended audience from user input or context. Do NOT proceed without a clear
|
|
26
|
+
topic.
|
|
27
|
+
|
|
28
|
+
### Phase 2: Select Scope, Type, and Subject
|
|
29
|
+
|
|
30
|
+
**Scope** — use `_local` unless the user explicitly names another scope.
|
|
31
|
+
|
|
32
|
+
**Type** — match the type of the XDRs the article primarily synthesizes (`adrs`, `bdrs`, or `edrs`).
|
|
33
|
+
If the topic spans multiple types, use `adrs`.
|
|
34
|
+
|
|
35
|
+
**Subject** — pick the subject that best matches the article's topic (see `004-article-standards`).
|
|
36
|
+
If the article spans more than one subject, place it in `principles`.
|
|
37
|
+
|
|
38
|
+
### Phase 3: Assign a Number and Name
|
|
39
|
+
|
|
40
|
+
1. List `.xdrs/[scope]/[type]/[subject]/articles/` (create the folder if it does not exist).
|
|
41
|
+
2. Find the highest existing article number in that namespace and increment by 1. Never reuse numbers.
|
|
42
|
+
3. Choose a short lowercase kebab-case title that describes the topic clearly.
|
|
43
|
+
- Good: `onboarding-guide`, `checkout-flow-overview`, `api-design-principles`
|
|
44
|
+
- Avoid: `summary`, `notes`, `misc`
|
|
45
|
+
|
|
46
|
+
### Phase 4: Research XDRs and Skills to Synthesize
|
|
47
|
+
|
|
48
|
+
1. Read all XDRs and Skills relevant to the article topic across all scopes listed in `.xdrs/index.md`.
|
|
49
|
+
2. Identify the key points a reader needs to understand the topic end-to-end.
|
|
50
|
+
3. Collect XDR IDs and file paths for cross-references. Never copy decision text verbatim; link to it.
|
|
51
|
+
|
|
52
|
+
### Phase 5: Write the Article
|
|
53
|
+
|
|
54
|
+
Use the mandatory template from `004-article-standards`:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
# [scope]-article-[number]: [Short Title]
|
|
58
|
+
|
|
59
|
+
## Overview
|
|
60
|
+
|
|
61
|
+
[Brief description of what this article covers and its intended audience. Under 3 lines.]
|
|
62
|
+
|
|
63
|
+
## Content
|
|
64
|
+
|
|
65
|
+
[Synthetic text combining and explaining the topic. Use links to Decision Records and Skills
|
|
66
|
+
when referencing information from those documents. Keep under 150 lines total.]
|
|
67
|
+
|
|
68
|
+
## References
|
|
69
|
+
|
|
70
|
+
- [XDR id or Skill name](relative/path/to/file.md) - Brief description of relevance
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Rules to apply while drafting:
|
|
74
|
+
|
|
75
|
+
- Write for the stated audience; avoid jargon unexplained elsewhere.
|
|
76
|
+
- Every factual claim must link back to the authoritative XDR or Skill.
|
|
77
|
+
- Never reproduce decision text verbatim; summarize and link.
|
|
78
|
+
- Keep the article under 150 lines; move detailed content to XDRs or Skills.
|
|
79
|
+
- Use lowercase file names. Never use emojis.
|
|
80
|
+
- If a conflict exists between the article and a Decision Record, note it and defer to the XDR.
|
|
81
|
+
|
|
82
|
+
### Phase 6: Place and Register
|
|
83
|
+
|
|
84
|
+
1. Save the file at `.xdrs/[scope]/[type]/[subject]/articles/[number]-[short-title].md`.
|
|
85
|
+
2. Add a link to the article in the canonical index for that scope+type (`.xdrs/[scope]/[type]/index.md`).
|
|
86
|
+
3. Add back-references in the XDRs and Skills that the article synthesizes, under their `## References`
|
|
87
|
+
section.
|
|
88
|
+
|
|
89
|
+
## Examples
|
|
90
|
+
|
|
91
|
+
**Input:** "Write an article about how skills work in this project."
|
|
92
|
+
|
|
93
|
+
**Expected actions:**
|
|
94
|
+
1. Read `004-article-standards.md`.
|
|
95
|
+
2. Topic: how skills work. Audience: developers new to the project.
|
|
96
|
+
3. Scope: `_local`, type: `adrs`, subject: `principles`.
|
|
97
|
+
4. Scan `.xdrs/_local/adrs/principles/articles/` — no articles exist → number is `001`.
|
|
98
|
+
5. Research `_core-adr-003` and all existing skill SKILL.md files.
|
|
99
|
+
6. Write `.xdrs/_local/adrs/principles/articles/001-skills-overview.md` following the template, linking
|
|
100
|
+
to `_core-adr-003` and the individual skill files.
|
|
101
|
+
7. Update `.xdrs/_local/adrs/index.md` with a link to the new article.
|
|
102
|
+
8. Add a reference to the article in `_core-adr-003` under `## References`.
|
|
103
|
+
|
|
104
|
+
## Edge Cases
|
|
105
|
+
|
|
106
|
+
- **Article vs. XDR confusion** — if the user asks for a document that makes a decision, write an XDR
|
|
107
|
+
(use the `002-write-xdr` skill), not an article.
|
|
108
|
+
- **Cross-subject topic** — place the article in `principles`, not in any single subject folder.
|
|
109
|
+
- **No existing articles folder** — create it; it is optional in the folder layout.
|
|
110
|
+
- **Conflicting information found** — note the conflict in the article and always defer to the XDR.
|
|
111
|
+
- **Article would exceed 150 lines** — move detailed content to a new Skills or XDR and link back.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# local ADRs Index
|
|
2
|
+
|
|
3
|
+
This index covers project-local architectural documentation for this repository. These documents
|
|
4
|
+
are not meant to be distributed as reusable scope packages unless they are promoted into a named
|
|
5
|
+
shared scope.
|
|
6
|
+
|
|
7
|
+
## Articles
|
|
8
|
+
|
|
9
|
+
Synthetic views combining XDRs, repository examples, and packaging guidance.
|
|
10
|
+
|
|
11
|
+
- [_local-article-001](principles/articles/001-create-your-own-xdrs-extension-package.md) - **Create your own xdrs-core extension package** (package layout, filedist sets, publishing, versioning, and consumer overrides)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# _local-article-001: Create your own xdrs-core extension package
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This article explains how to turn your own XDR scope into a distributable npm package. It is for
|
|
6
|
+
teams that want to publish their own DRs, skills, and articles while staying compatible with the
|
|
7
|
+
xdrs-core structure and extraction flow.
|
|
8
|
+
|
|
9
|
+
## Content
|
|
10
|
+
|
|
11
|
+
### Start with a real shared scope, not `_local`
|
|
12
|
+
|
|
13
|
+
Use `_local` only for project-only records that must stay inside one repository. Shared packages
|
|
14
|
+
should publish a named scope such as `acme-platform` or `team-ml`, because scopes are the unit of
|
|
15
|
+
ownership, distribution, and override ordering in XDRs. The root structure and precedence rules are
|
|
16
|
+
defined in [_core-adr-001](../../../../_core/adrs/principles/001-xdr-standards.md).
|
|
17
|
+
|
|
18
|
+
### Package the whole scope as a normal npm package
|
|
19
|
+
|
|
20
|
+
The package shape used by this repository is the simplest reference implementation: the published
|
|
21
|
+
artifact is a regular npm package with a `files` whitelist, a thin CLI entrypoint, and `filedist`
|
|
22
|
+
configuration embedded in [package.json](../../../../../package.json). The important parts are:
|
|
23
|
+
|
|
24
|
+
- include the shipped XDR tree, agent instruction files, and CLI files in `files`
|
|
25
|
+
- expose `bin/filedist.js` as the package `bin` so consumers run the package through the same
|
|
26
|
+
`extract` and `check` interface
|
|
27
|
+
- define one `filedist.sets` entry for managed files and another for editable local overrides
|
|
28
|
+
|
|
29
|
+
For a minimal consumer flow, see [example/package.json](../../../../../example/package.json) and
|
|
30
|
+
[example/Makefile](../../../../../example/Makefile).
|
|
31
|
+
|
|
32
|
+
### Separate managed files from local overrides
|
|
33
|
+
|
|
34
|
+
This repository's `filedist` config in [package.json](../../../../../package.json) shows the key
|
|
35
|
+
pattern:
|
|
36
|
+
|
|
37
|
+
- managed set: ships `AGENTS.md` and `.xdrs/**`, while excluding `.xdrs/index.md`
|
|
38
|
+
- keep-existing set: ships `.xdrs/index.md` and `AGENTS.local.md` with `managed=false` and
|
|
39
|
+
`keepExisting=true`
|
|
40
|
+
|
|
41
|
+
That split matters because consumers need some files to stay under package control and others to
|
|
42
|
+
remain editable in their own repository. The current example verifies exactly that behavior by
|
|
43
|
+
re-extracting into [example/output](../../../../../example/output) and asserting that local edits to
|
|
44
|
+
`.xdrs/index.md` survive `extract --keep-existing` while managed files are still checked for drift in
|
|
45
|
+
[example/Makefile](../../../../../example/Makefile).
|
|
46
|
+
|
|
47
|
+
### Keep DRs, skills, and articles together
|
|
48
|
+
|
|
49
|
+
Your reusable package should place DRs, skills, and articles under the same scope folder so they
|
|
50
|
+
ship together:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
.xdrs/
|
|
54
|
+
my-scope/
|
|
55
|
+
adrs/
|
|
56
|
+
index.md
|
|
57
|
+
principles/
|
|
58
|
+
001-my-decision.md
|
|
59
|
+
skills/
|
|
60
|
+
001-my-skill/SKILL.md
|
|
61
|
+
articles/
|
|
62
|
+
001-my-overview.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The co-location rule for skills comes from [_core-adr-003](../../../../_core/adrs/principles/003-skill-standards.md),
|
|
66
|
+
and article placement rules come from [_core-adr-004](../../../../_core/adrs/principles/004-article-standards.md).
|
|
67
|
+
When you publish the scope folder, those documents travel together and stay version-aligned.
|
|
68
|
+
|
|
69
|
+
### Expose skills to Copilot-compatible tooling
|
|
70
|
+
|
|
71
|
+
This repository's managed `filedist` set creates symlinks from `.xdrs/**/skills/*` into
|
|
72
|
+
`.github/skills`, configured in [package.json](../../../../../package.json). That means your skills
|
|
73
|
+
remain authored next to the XDRs they implement, but consumers also get the discovery path expected
|
|
74
|
+
by GitHub Copilot and similar tooling.
|
|
75
|
+
|
|
76
|
+
If your package targets non-Copilot agents too, keep the source of truth in `.xdrs/[scope]/.../skills/`
|
|
77
|
+
and treat `.github/skills` as an exposure mechanism rather than the canonical location.
|
|
78
|
+
|
|
79
|
+
### Verify with a consumer example before publishing
|
|
80
|
+
|
|
81
|
+
The [example](../../../../../example) folder is the best reference in this repository for the
|
|
82
|
+
consumer side of the workflow:
|
|
83
|
+
|
|
84
|
+
1. depend on the locally packed tarball from `dist/`
|
|
85
|
+
2. run `pnpm exec xdrs-core extract --output ./output`
|
|
86
|
+
3. verify the expected files exist
|
|
87
|
+
4. re-run extraction to confirm keep-existing behavior
|
|
88
|
+
5. run `check` and `lint` against the extracted tree
|
|
89
|
+
|
|
90
|
+
That same pattern should exist in your extension package repository. A runnable example catches bad
|
|
91
|
+
selectors, missing files, broken indexes, and skill exposure problems before you publish.
|
|
92
|
+
|
|
93
|
+
### Publish and version the package as an upgrade contract
|
|
94
|
+
|
|
95
|
+
The release flow in [Makefile](../../../../../Makefile) packs the project and publishes it with npm,
|
|
96
|
+
including prerelease tag handling. Your extension package can follow the same shape: `pnpm pack` for
|
|
97
|
+
local verification, then `npm publish` to your public or internal registry.
|
|
98
|
+
|
|
99
|
+
Version the package with semantic versioning according to the impact on consumers, not only on the
|
|
100
|
+
changed file. [_core-adr-005](../../../../_core/adrs/principles/005-semantic-versioning-for-xdr-packages.md)
|
|
101
|
+
defines the practical rule: breaking guidance or changed mandatory behavior is `MAJOR`, additive
|
|
102
|
+
guidance such as new DRs, skills, or articles is usually `MINOR`, and low-risk corrections are
|
|
103
|
+
`PATCH`.
|
|
104
|
+
|
|
105
|
+
### Use agentme as the fuller packaged example
|
|
106
|
+
|
|
107
|
+
This repository shows the baseline xdrs-core packaging model. For a fuller distribution package that
|
|
108
|
+
combines reusable XDR scopes with additional agent workflow files and presets, use
|
|
109
|
+
[flaviostutz/agentme](https://github.com/flaviostutz/agentme) as the reference. Its README shows the
|
|
110
|
+
same extraction model applied to a richer package: install the dependency, run `extract`, review the
|
|
111
|
+
generated output, and re-run `check` when upgrading.
|
|
112
|
+
|
|
113
|
+
## References
|
|
114
|
+
|
|
115
|
+
- [_core-adr-001](../../../../_core/adrs/principles/001-xdr-standards.md) - Scope structure, precedence, and distribution model
|
|
116
|
+
- [_core-adr-003](../../../../_core/adrs/principles/003-skill-standards.md) - Skill co-location and discovery rules
|
|
117
|
+
- [_core-adr-004](../../../../_core/adrs/principles/004-article-standards.md) - Article placement and template rules
|
|
118
|
+
- [_core-adr-005](../../../../_core/adrs/principles/005-semantic-versioning-for-xdr-packages.md) - Versioning policy for published XDR packages
|
|
119
|
+
- [package.json](../../../../../package.json) - Reference `files`, `bin`, symlink, and `filedist` set layout
|
|
120
|
+
- [Makefile](../../../../../Makefile) - Reference pack and publish flow
|
|
121
|
+
- [example/package.json](../../../../../example/package.json) - Minimal consumer dependency setup
|
|
122
|
+
- [example/Makefile](../../../../../example/Makefile) - Extraction, keep-existing, check, and lint verification flow
|
|
123
|
+
- [agentme](https://github.com/flaviostutz/agentme) - Full distribution package example built on top of xdrs-core
|
package/.xdrs/index.md
CHANGED
|
@@ -21,8 +21,4 @@ Decisions about how XDRs work
|
|
|
21
21
|
|
|
22
22
|
Project-local XDRs that must not be shared with other contexts. Always keep this scope last so its decisions override or extend all scopes listed above. Add specific `_local` ADR/BDR/EDR index links here when present.
|
|
23
23
|
|
|
24
|
-
[View
|
|
25
|
-
|
|
26
|
-
[View _local ADRs Index](_local/adrs/index.md)
|
|
27
|
-
|
|
28
|
-
[View _local EDRs Index](_local/edrs/index.md)
|
|
24
|
+
[View local ADRs Index](_local/adrs/index.md)
|
package/README.md
CHANGED
|
@@ -32,6 +32,36 @@ Every XDR package contains three types of documents:
|
|
|
32
32
|
|
|
33
33
|
> Create an ADR about our decision on using Python for AI related projects. For high volume projects (expected >1000 t/s), an exception can be made on using Golang.
|
|
34
34
|
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
- [examples/basic-usage](examples/basic-usage) shows the minimal consumer flow for installing the packaged `xdrs-core` tarball, extracting files, checking drift, and linting the resulting tree.
|
|
38
|
+
- [examples/mydevkit](examples/mydevkit) shows a reusable extension package that uses `.filedistrc` as its package config source, composes `xdrs-core`, and ships its own named scope.
|
|
39
|
+
- For a fuller real-world package built on the same distribution model, see [flaviostutz/agentme](https://github.com/flaviostutz/agentme).
|
|
40
|
+
|
|
41
|
+
## CLI
|
|
42
|
+
|
|
43
|
+
The published package exposes the `xdrs-core` CLI.
|
|
44
|
+
|
|
45
|
+
- Bootstrap or extract managed XDR files with the existing `filedist`-backed commands such as `npx -y xdrs-core extract` and `npx -y xdrs-core check`.
|
|
46
|
+
- Lint an XDR tree with `npx -y xdrs-core lint .`.
|
|
47
|
+
|
|
48
|
+
The `lint` command reads `./.xdrs/**` from the given workspace path and checks common consistency rules, including:
|
|
49
|
+
|
|
50
|
+
- allowed scope, type, and subject folder structure
|
|
51
|
+
- XDR numbering uniqueness per `scope/type`
|
|
52
|
+
- skill numbering uniqueness per `scope/type/subject/skills`
|
|
53
|
+
- article numbering uniqueness per `scope/type/subject/articles`
|
|
54
|
+
- canonical index presence and link consistency
|
|
55
|
+
- root index coverage for all discovered canonical indexes
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx -y xdrs-core lint .
|
|
61
|
+
npx -y xdrs-core lint ./some-project
|
|
62
|
+
pnpm exec xdrs-core lint .
|
|
63
|
+
```
|
|
64
|
+
|
|
35
65
|
## Requirements
|
|
36
66
|
|
|
37
67
|
### Multi-scope support
|
|
@@ -94,6 +124,7 @@ Document types:
|
|
|
94
124
|
See [.xdrs/index.md](.xdrs/index.md) for the full list of active decision records.
|
|
95
125
|
|
|
96
126
|
For a deeper overview of XDRs — objective, structure, guidelines, extension, and usage — see the [XDRs Overview article](.xdrs/_core/adrs/principles/articles/001-xdrs-overview.md).
|
|
127
|
+
For packaging guidance on publishing your own reusable scope with DRs, skills, and articles, see the [Create your own xdrs-core extension package article](.xdrs/_local/adrs/principles/articles/001-create-your-own-xdrs-extension-package.md), then compare [examples/basic-usage](examples/basic-usage) and [examples/mydevkit](examples/mydevkit).
|
|
97
128
|
|
|
98
129
|
## Flow: Decision -> Distribution -> Usage
|
|
99
130
|
|
package/bin/filedist.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const { runLintCli } = require('../lib/lint');
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
if (args[0] === 'lint') {
|
|
9
|
+
process.exitCode = runLintCli(args.slice(1));
|
|
10
|
+
} else {
|
|
11
|
+
require('filedist').binpkg(__dirname, args);
|
|
12
|
+
}
|
package/lib/lint.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const TYPE_TO_ID = {
|
|
8
|
+
adrs: 'adr',
|
|
9
|
+
bdrs: 'bdr',
|
|
10
|
+
edrs: 'edr'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ALLOWED_SUBJECTS = {
|
|
14
|
+
adrs: new Set(['principles', 'application', 'data', 'integration', 'platform', 'controls', 'operations']),
|
|
15
|
+
bdrs: new Set(['principles', 'marketing', 'product', 'controls', 'operations', 'organization', 'finance', 'sustainability']),
|
|
16
|
+
edrs: new Set(['principles', 'application', 'infra', 'ai', 'observability', 'devops', 'governance'])
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const TYPE_NAMES = new Set(Object.keys(TYPE_TO_ID));
|
|
20
|
+
const RESERVED_SCOPES = new Set(['_core', '_local']);
|
|
21
|
+
const NUMBERED_FILE_RE = /^(\d{3,})-([a-z0-9-]+)\.md$/;
|
|
22
|
+
const NUMBERED_DIR_RE = /^(\d{3,})-([a-z0-9-]+)$/;
|
|
23
|
+
const REQUIRED_ROOT_INDEX_TEXT = 'XDRs in scopes listed last override the ones listed first';
|
|
24
|
+
|
|
25
|
+
function runLintCli(args) {
|
|
26
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
27
|
+
printHelp();
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const targetPath = args[0] || '.';
|
|
32
|
+
const result = lintWorkspace(targetPath);
|
|
33
|
+
|
|
34
|
+
if (result.errors.length === 0) {
|
|
35
|
+
console.log(`Lint passed for ${toDisplayPath(result.xdrsRoot)}`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.error(`Lint failed for ${toDisplayPath(result.xdrsRoot)}`);
|
|
40
|
+
for (const error of result.errors) {
|
|
41
|
+
console.error(`- ${error}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printHelp() {
|
|
48
|
+
console.log('Usage: xdrs-core lint [path]\n');
|
|
49
|
+
console.log('Lint the XDR tree rooted at [path]/.xdrs or at [path] when [path] already points to .xdrs.');
|
|
50
|
+
console.log('All other commands continue to be delegated to the bundled filedist CLI.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function lintWorkspace(targetPath) {
|
|
54
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
55
|
+
const xdrsRoot = path.basename(resolvedTarget) === '.xdrs'
|
|
56
|
+
? resolvedTarget
|
|
57
|
+
: path.join(resolvedTarget, '.xdrs');
|
|
58
|
+
const errors = [];
|
|
59
|
+
|
|
60
|
+
if (!existsDirectory(xdrsRoot)) {
|
|
61
|
+
errors.push(`Missing XDR root directory: ${toDisplayPath(xdrsRoot)}`);
|
|
62
|
+
return { xdrsRoot, errors };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const actualTypeIndexes = [];
|
|
66
|
+
const rootEntries = safeReadDir(xdrsRoot, errors, 'read XDR root directory');
|
|
67
|
+
const scopeEntries = rootEntries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
68
|
+
|
|
69
|
+
for (const entry of rootEntries) {
|
|
70
|
+
if (entry.isFile() && entry.name !== 'index.md') {
|
|
71
|
+
errors.push(`Unexpected file at .xdrs root: ${entry.name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const scopeEntry of scopeEntries) {
|
|
76
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rootIndexPath = path.join(xdrsRoot, 'index.md');
|
|
80
|
+
if (!existsFile(rootIndexPath)) {
|
|
81
|
+
errors.push('Missing required root index: .xdrs/index.md');
|
|
82
|
+
} else {
|
|
83
|
+
lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { xdrsRoot, errors };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors) {
|
|
90
|
+
const content = fs.readFileSync(rootIndexPath, 'utf8');
|
|
91
|
+
|
|
92
|
+
if (!content.includes(REQUIRED_ROOT_INDEX_TEXT)) {
|
|
93
|
+
errors.push(`Root index is missing required override text: ${toDisplayPath(rootIndexPath)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const localLinks = parseLocalLinks(content, path.dirname(rootIndexPath));
|
|
97
|
+
for (const linkPath of localLinks) {
|
|
98
|
+
if (!fs.existsSync(linkPath)) {
|
|
99
|
+
errors.push(`Broken link in root index: ${displayPath(rootIndexPath, linkPath)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const linkedTypeIndexes = localLinks.filter((linkPath) => isCanonicalTypeIndex(linkPath, xdrsRoot));
|
|
104
|
+
const linkedSet = new Set(linkedTypeIndexes.map(normalizePath));
|
|
105
|
+
|
|
106
|
+
for (const indexPath of actualTypeIndexes) {
|
|
107
|
+
if (!linkedSet.has(normalizePath(indexPath))) {
|
|
108
|
+
errors.push(`Root index is missing canonical index link: ${toDisplayPath(indexPath)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let seenLocal = false;
|
|
113
|
+
for (const indexPath of linkedTypeIndexes) {
|
|
114
|
+
const scopeName = path.basename(path.dirname(path.dirname(indexPath)));
|
|
115
|
+
if (scopeName === '_local') {
|
|
116
|
+
seenLocal = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (seenLocal) {
|
|
120
|
+
errors.push('Root index must keep all _local scope links after every non-_local scope link');
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes) {
|
|
127
|
+
const scopePath = path.join(xdrsRoot, scopeName);
|
|
128
|
+
|
|
129
|
+
if (!isValidScopeName(scopeName)) {
|
|
130
|
+
errors.push(`Invalid scope name: ${toDisplayPath(scopePath)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const entries = safeReadDir(scopePath, errors, `read scope directory ${scopeName}`);
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const entryPath = path.join(scopePath, entry.name);
|
|
136
|
+
if (entry.isDirectory()) {
|
|
137
|
+
if (!TYPE_NAMES.has(entry.name)) {
|
|
138
|
+
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
errors.push(`Unexpected file under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes) {
|
|
150
|
+
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
151
|
+
const indexPath = path.join(typePath, 'index.md');
|
|
152
|
+
const xdrNumbers = new Map();
|
|
153
|
+
const artifacts = [];
|
|
154
|
+
|
|
155
|
+
if (!existsFile(indexPath)) {
|
|
156
|
+
errors.push(`Missing canonical index: ${toDisplayPath(indexPath)}`);
|
|
157
|
+
} else {
|
|
158
|
+
actualTypeIndexes.push(indexPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const entries = safeReadDir(typePath, errors, `read type directory ${scopeName}/${typeName}`);
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const entryPath = path.join(typePath, entry.name);
|
|
164
|
+
if (entry.isFile()) {
|
|
165
|
+
if (entry.name !== 'index.md') {
|
|
166
|
+
errors.push(`Unexpected file under ${scopeName}/${typeName}: ${toDisplayPath(entryPath)}`);
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!ALLOWED_SUBJECTS[typeName].has(entry.name)) {
|
|
172
|
+
errors.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrNumbers, errors));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (existsFile(indexPath)) {
|
|
180
|
+
lintTypeIndex(indexPath, xdrsRoot, artifacts, errors);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNumbers, errors) {
|
|
185
|
+
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
186
|
+
const artifacts = [];
|
|
187
|
+
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
188
|
+
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const entryPath = path.join(subjectPath, entry.name);
|
|
191
|
+
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
if (entry.name === 'skills') {
|
|
194
|
+
artifacts.push(...lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (entry.name === 'articles') {
|
|
198
|
+
artifacts.push(...lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
errors.push(`Unexpected directory under ${scopeName}/${typeName}/${subjectName}: ${toDisplayPath(entryPath)}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!NUMBERED_FILE_RE.test(entry.name)) {
|
|
207
|
+
errors.push(`Invalid XDR file name: ${toDisplayPath(entryPath)}`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
artifacts.push(entryPath);
|
|
212
|
+
lintXdrFile(xdrsRoot, scopeName, typeName, entryPath, xdrNumbers, errors);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return artifacts;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function lintXdrFile(xdrsRoot, scopeName, typeName, filePath, xdrNumbers, errors) {
|
|
219
|
+
const baseName = path.basename(filePath);
|
|
220
|
+
const match = baseName.match(NUMBERED_FILE_RE);
|
|
221
|
+
if (!match) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const number = match[1];
|
|
226
|
+
const previous = xdrNumbers.get(number);
|
|
227
|
+
if (previous) {
|
|
228
|
+
errors.push(`Duplicate XDR number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)}`);
|
|
229
|
+
} else {
|
|
230
|
+
xdrNumbers.set(number, filePath);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (baseName !== baseName.toLowerCase()) {
|
|
234
|
+
errors.push(`XDR file name must be lowercase: ${toDisplayPath(filePath)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
238
|
+
const expectedHeader = `# ${scopeName}-${TYPE_TO_ID[typeName]}-${number}:`;
|
|
239
|
+
const firstLine = firstNonEmptyLine(content);
|
|
240
|
+
if (!firstLine.startsWith(expectedHeader)) {
|
|
241
|
+
errors.push(`XDR title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsPath, errors) {
|
|
246
|
+
const artifacts = [];
|
|
247
|
+
const skillNumbers = new Map();
|
|
248
|
+
const entries = safeReadDir(skillsPath, errors, `read skills directory ${scopeName}/${typeName}/${subjectName}/skills`);
|
|
249
|
+
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const entryPath = path.join(skillsPath, entry.name);
|
|
252
|
+
if (!entry.isDirectory()) {
|
|
253
|
+
errors.push(`Unexpected file in skills directory: ${toDisplayPath(entryPath)}`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const match = entry.name.match(NUMBERED_DIR_RE);
|
|
258
|
+
if (!match) {
|
|
259
|
+
errors.push(`Invalid skill package name: ${toDisplayPath(entryPath)}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const number = match[1];
|
|
264
|
+
const previous = skillNumbers.get(number);
|
|
265
|
+
if (previous) {
|
|
266
|
+
errors.push(`Duplicate skill number ${number} in ${scopeName}/${typeName}/${subjectName}/skills: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
|
|
267
|
+
} else {
|
|
268
|
+
skillNumbers.set(number, entryPath);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (entry.name !== entry.name.toLowerCase()) {
|
|
272
|
+
errors.push(`Skill package name must be lowercase: ${toDisplayPath(entryPath)}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const skillFilePath = path.join(entryPath, 'SKILL.md');
|
|
276
|
+
artifacts.push(skillFilePath);
|
|
277
|
+
|
|
278
|
+
if (!existsFile(skillFilePath)) {
|
|
279
|
+
errors.push(`Missing SKILL.md in skill package: ${toDisplayPath(entryPath)}`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const skillContent = fs.readFileSync(skillFilePath, 'utf8');
|
|
284
|
+
const frontmatterName = extractFrontmatterName(skillContent);
|
|
285
|
+
if (!frontmatterName) {
|
|
286
|
+
errors.push(`SKILL.md is missing a frontmatter name field: ${toDisplayPath(skillFilePath)}`);
|
|
287
|
+
} else if (frontmatterName !== entry.name) {
|
|
288
|
+
errors.push(`Skill frontmatter name must match package directory "${entry.name}": ${toDisplayPath(skillFilePath)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return artifacts;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, articlesPath, errors) {
|
|
296
|
+
const artifacts = [];
|
|
297
|
+
const articleNumbers = new Map();
|
|
298
|
+
const entries = safeReadDir(articlesPath, errors, `read articles directory ${scopeName}/${typeName}/${subjectName}/articles`);
|
|
299
|
+
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
const entryPath = path.join(articlesPath, entry.name);
|
|
302
|
+
if (!entry.isFile()) {
|
|
303
|
+
errors.push(`Unexpected directory in articles folder: ${toDisplayPath(entryPath)}`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const match = entry.name.match(NUMBERED_FILE_RE);
|
|
308
|
+
if (!match) {
|
|
309
|
+
errors.push(`Invalid article file name: ${toDisplayPath(entryPath)}`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
artifacts.push(entryPath);
|
|
314
|
+
|
|
315
|
+
const number = match[1];
|
|
316
|
+
const previous = articleNumbers.get(number);
|
|
317
|
+
if (previous) {
|
|
318
|
+
errors.push(`Duplicate article number ${number} in ${scopeName}/${typeName}/${subjectName}/articles: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
|
|
319
|
+
} else {
|
|
320
|
+
articleNumbers.set(number, entryPath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (entry.name !== entry.name.toLowerCase()) {
|
|
324
|
+
errors.push(`Article file name must be lowercase: ${toDisplayPath(entryPath)}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const content = fs.readFileSync(entryPath, 'utf8');
|
|
328
|
+
const expectedHeader = `# ${scopeName}-article-${number}:`;
|
|
329
|
+
const firstLine = firstNonEmptyLine(content);
|
|
330
|
+
if (!firstLine.startsWith(expectedHeader)) {
|
|
331
|
+
errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return artifacts;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
|
|
339
|
+
const content = fs.readFileSync(indexPath, 'utf8');
|
|
340
|
+
const localLinks = parseLocalLinks(content, path.dirname(indexPath));
|
|
341
|
+
const linkedSet = new Set();
|
|
342
|
+
|
|
343
|
+
for (const linkPath of localLinks) {
|
|
344
|
+
if (!fs.existsSync(linkPath)) {
|
|
345
|
+
errors.push(`Broken link in canonical index ${toDisplayPath(indexPath)}: ${displayPath(indexPath, linkPath)}`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
linkedSet.add(normalizePath(linkPath));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const artifactPath of artifacts) {
|
|
353
|
+
if (!linkedSet.has(normalizePath(artifactPath))) {
|
|
354
|
+
errors.push(`Canonical index ${toDisplayPath(indexPath)} is missing an entry for ${toDisplayPath(artifactPath)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseLocalLinks(markdown, baseDir) {
|
|
360
|
+
const links = [];
|
|
361
|
+
const linkRe = /\[[^\]]+\]\(([^)]+)\)/g;
|
|
362
|
+
let match = linkRe.exec(markdown);
|
|
363
|
+
while (match) {
|
|
364
|
+
const rawTarget = match[1].trim();
|
|
365
|
+
if (isLocalLink(rawTarget)) {
|
|
366
|
+
const targetWithoutAnchor = rawTarget.split('#')[0];
|
|
367
|
+
links.push(path.resolve(baseDir, targetWithoutAnchor));
|
|
368
|
+
}
|
|
369
|
+
match = linkRe.exec(markdown);
|
|
370
|
+
}
|
|
371
|
+
return links;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isLocalLink(target) {
|
|
375
|
+
return target !== ''
|
|
376
|
+
&& !target.startsWith('#')
|
|
377
|
+
&& !target.startsWith('http://')
|
|
378
|
+
&& !target.startsWith('https://')
|
|
379
|
+
&& !target.startsWith('mailto:');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isCanonicalTypeIndex(filePath, xdrsRoot) {
|
|
383
|
+
const relative = relativeFrom(xdrsRoot, filePath).split(path.sep);
|
|
384
|
+
return relative.length === 3 && TYPE_NAMES.has(relative[1]) && relative[2] === 'index.md';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function extractFrontmatterName(content) {
|
|
388
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
389
|
+
if (!frontmatterMatch) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const nameMatch = frontmatterMatch[1].match(/^name:\s*(.+)$/m);
|
|
394
|
+
return nameMatch ? nameMatch[1].trim() : null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function firstNonEmptyLine(content) {
|
|
398
|
+
const lines = content.split(/\r?\n/);
|
|
399
|
+
for (const line of lines) {
|
|
400
|
+
if (line.trim() !== '') {
|
|
401
|
+
return line.trim();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return '';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function safeReadDir(dirPath, errors, operation) {
|
|
408
|
+
try {
|
|
409
|
+
return fs.readdirSync(dirPath, { withFileTypes: true });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
errors.push(`Failed to ${operation}: ${toDisplayPath(dirPath)} (${error.message})`);
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isValidScopeName(scopeName) {
|
|
417
|
+
if (RESERVED_SCOPES.has(scopeName)) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return /^[a-z0-9][a-z0-9-]*$/.test(scopeName);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function existsDirectory(dirPath) {
|
|
424
|
+
try {
|
|
425
|
+
return fs.statSync(dirPath).isDirectory();
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function existsFile(filePath) {
|
|
432
|
+
try {
|
|
433
|
+
return fs.statSync(filePath).isFile();
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function displayPath(indexPath, targetPath) {
|
|
440
|
+
return `${toDisplayPath(indexPath)} -> ${toDisplayPath(targetPath)}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function toDisplayPath(targetPath) {
|
|
444
|
+
return relativeFrom(process.cwd(), targetPath);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function relativeFrom(basePath, targetPath) {
|
|
448
|
+
return path.relative(basePath, targetPath) || '.';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function normalizePath(filePath) {
|
|
452
|
+
return path.normalize(filePath);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = {
|
|
456
|
+
runLintCli,
|
|
457
|
+
lintWorkspace
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (require.main === module) {
|
|
461
|
+
process.exitCode = runLintCli(process.argv.slice(2));
|
|
462
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xdrs-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A standard way to organize Decision Records (XDRs) across scopes, subjects, and teams so that AI agents can reliably query and follow them.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
"package.json",
|
|
19
19
|
"AGENTS.md",
|
|
20
20
|
"AGENTS.local.md",
|
|
21
|
-
"bin/filedist.js"
|
|
21
|
+
"bin/filedist.js",
|
|
22
|
+
"lib/**/*.js"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"filedist": "^0.
|
|
25
|
+
"filedist": "^0.26.0"
|
|
25
26
|
},
|
|
26
27
|
"filedist": {
|
|
27
28
|
"sets": [
|