waymark-docs 0.2.0 → 0.2.2
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/CHANGELOG.md +14 -0
- package/README.md +64 -42
- package/dist/commands/find.js +61 -24
- package/dist/commands/ls.js +4 -7
- package/dist/commands/status.js +4 -9
- package/dist/configuration/load.d.ts +16 -4
- package/dist/configuration/load.js +96 -131
- package/dist/documents/classify.d.ts +10 -4
- package/dist/documents/classify.js +116 -137
- package/dist/documents/filter.d.ts +10 -4
- package/dist/documents/filter.js +10 -46
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Waymark are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.2](https://github.com/ysfaran/waymark/compare/v0.2.1...v0.2.2) (2026-09-18)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **cli:** align help text with documentation ([#10](https://github.com/ysfaran/waymark/issues/10)) ([dbabc14](https://github.com/ysfaran/waymark/commit/dbabc14244d775cc8a38166d11a386674e282610))
|
|
11
|
+
|
|
12
|
+
## [0.2.1](https://github.com/ysfaran/waymark/compare/v0.2.0...v0.2.1) (2026-08-13)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* **readme:** explain where Waymark fits ([ffe25dc](https://github.com/ysfaran/waymark/commit/ffe25dc15f1651519ad34e9e67f7fee90c69ef62))
|
|
18
|
+
|
|
5
19
|
## [0.2.0](https://github.com/ysfaran/waymark/compare/v0.1.1...v0.2.0) (2026-08-03)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# 🪧 Waymark
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/waymark-docs)
|
|
4
4
|
|
|
5
|
-
Waymark is a small, offline-first CLI
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Waymark is a small, offline-first CLI that helps coding agents find the right
|
|
6
|
+
repository docs for each task. Add structured frontmatter to existing Markdown
|
|
7
|
+
or MDX files, and agents can discover only relevant paths before opening a
|
|
8
|
+
document without reading a large index or following linked navigation files
|
|
9
|
+
that load unrelated context. It is as easy to set up as file-based navigation,
|
|
10
|
+
but remains deterministic and token-efficient; unlike RAG or MCP-backed
|
|
11
|
+
retrieval, it needs no ranking, maintained index or retrieval infrastructure.
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
searching unrelated files and more time working with the context they need.
|
|
13
|
+

|
|
12
14
|
|
|
13
15
|
## Table of contents
|
|
14
16
|
|
|
@@ -35,7 +37,7 @@ npm install --save-dev waymark-docs
|
|
|
35
37
|
|
|
36
38
|
## Quick start
|
|
37
39
|
|
|
38
|
-
1. **Create a Waymark configuration
|
|
40
|
+
1. **Create a Waymark configuration**
|
|
39
41
|
|
|
40
42
|
Run `init` in the repository root:
|
|
41
43
|
|
|
@@ -43,7 +45,7 @@ npm install --save-dev waymark-docs
|
|
|
43
45
|
npx waymark init
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
2. **Define searchable metadata
|
|
48
|
+
2. **Define searchable metadata**
|
|
47
49
|
|
|
48
50
|
Add the document kinds and tags that agents can search:
|
|
49
51
|
|
|
@@ -53,11 +55,11 @@ npm install --save-dev waymark-docs
|
|
|
53
55
|
convention: Read before changing code to follow required repository practices
|
|
54
56
|
|
|
55
57
|
tags:
|
|
56
|
-
architecture: System boundaries, component relationships
|
|
58
|
+
architecture: System boundaries, component relationships and dependencies
|
|
57
59
|
typescript: TypeScript-related documentation
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
3. **Register a document
|
|
62
|
+
3. **Register a document**
|
|
61
63
|
|
|
62
64
|
Add Waymark metadata to a Markdown or MDX file. For example, save this as
|
|
63
65
|
`docs/conventions/typescript.md`:
|
|
@@ -71,7 +73,7 @@ npm install --save-dev waymark-docs
|
|
|
71
73
|
# TypeScript conventions
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
4. **Validate the repository
|
|
76
|
+
4. **Validate the repository**
|
|
75
77
|
|
|
76
78
|
Check the configuration and discovered documents:
|
|
77
79
|
|
|
@@ -88,16 +90,29 @@ npm install --save-dev waymark-docs
|
|
|
88
90
|
Tags: 2
|
|
89
91
|
```
|
|
90
92
|
|
|
91
|
-
5. **Discover
|
|
93
|
+
5. **Discover documents**
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
For a quick check, run `npx waymark find` with relevant kind and tag filters:
|
|
94
96
|
|
|
95
97
|
```sh
|
|
96
98
|
npx waymark find --kinds convention --tags typescript --show description
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
```text
|
|
100
|
-
docs/conventions/typescript.md
|
|
102
|
+
docs/conventions/typescript.md: TypeScript conventions for this repository
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
To have agents use Waymark continuously, add this to `AGENTS.md`,
|
|
106
|
+
`CLAUDE.md` or an equivalent file:
|
|
107
|
+
|
|
108
|
+
```md
|
|
109
|
+
## Context Discovery
|
|
110
|
+
|
|
111
|
+
Before working on a non-trivial task, run `npx waymark status --show kind,tags`,
|
|
112
|
+
then use `npx waymark find` with relevant comma-separated `--kinds` and
|
|
113
|
+
`--tags` values, using `--show kind,tags,description` to inspect results.
|
|
114
|
+
Use `--query` for literal text searches and `--filter` for boolean expressions
|
|
115
|
+
over metadata when you need more detailed results.
|
|
101
116
|
```
|
|
102
117
|
|
|
103
118
|
Waymark uses `waymark.yml` by default and also recognizes `waymark.yaml`. It
|
|
@@ -122,8 +137,10 @@ Create a starter configuration in the current directory.
|
|
|
122
137
|
```text
|
|
123
138
|
Usage: waymark init [options]
|
|
124
139
|
|
|
140
|
+
Create a starter Waymark configuration
|
|
141
|
+
|
|
125
142
|
Options:
|
|
126
|
-
-h, --help
|
|
143
|
+
-h, --help display help for command
|
|
127
144
|
```
|
|
128
145
|
|
|
129
146
|
```sh
|
|
@@ -152,7 +169,7 @@ ignore:
|
|
|
152
169
|
- vendor/**
|
|
153
170
|
```
|
|
154
171
|
|
|
155
|
-
Ignore patterns are relative to the repository root and support `*`,
|
|
172
|
+
Ignore patterns are relative to the repository root and support `*`, `?` and
|
|
156
173
|
`**` wildcards. Waymark also honors `.gitignore` automatically.
|
|
157
174
|
|
|
158
175
|
`init` never overwrites an existing configuration and does not allow a nested
|
|
@@ -162,15 +179,17 @@ configuration beneath another Waymark root.
|
|
|
162
179
|
|
|
163
180
|
Validate the Waymark configuration and all discovered Waymark Documents, then
|
|
164
181
|
print the repository root and counts for registered documents, unregistered
|
|
165
|
-
documents, kinds
|
|
182
|
+
documents, kinds and tags. Invalid repositories produce diagnostics and a
|
|
166
183
|
non-zero exit code, which makes this command suitable for CI.
|
|
167
184
|
|
|
168
185
|
```text
|
|
169
186
|
Usage: waymark status [options]
|
|
170
187
|
|
|
188
|
+
Validate and summarize the Waymark repository
|
|
189
|
+
|
|
171
190
|
Options:
|
|
172
|
-
-s, --show <fields>
|
|
173
|
-
-h, --help
|
|
191
|
+
-s, --show <fields> show declared kind and tag details (kind,tags)
|
|
192
|
+
-h, --help display help for command
|
|
174
193
|
```
|
|
175
194
|
|
|
176
195
|
Validate the repository:
|
|
@@ -199,16 +218,21 @@ Find registered Waymark Documents across the repository. With no filters,
|
|
|
199
218
|
```text
|
|
200
219
|
Usage: waymark find [options]
|
|
201
220
|
|
|
221
|
+
Discover Waymark Documents
|
|
222
|
+
|
|
202
223
|
Options:
|
|
203
|
-
-k, --kinds <identifiers>
|
|
204
|
-
-t, --tags <identifiers>
|
|
205
|
-
-T, --require-tags <identifiers>
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
--
|
|
211
|
-
|
|
224
|
+
-k, --kinds <identifiers> match any kind (comma-separated, repeatable)
|
|
225
|
+
-t, --tags <identifiers> match any tag (comma-separated, repeatable)
|
|
226
|
+
-T, --require-tags <identifiers> require every tag (comma-separated,
|
|
227
|
+
repeatable)
|
|
228
|
+
-f, --filter <expression> match a boolean filter expression
|
|
229
|
+
-q, --query <text> match literal text content
|
|
230
|
+
(case-insensitive)
|
|
231
|
+
-s, --show <fields> show kind, tags and description
|
|
232
|
+
(comma-separated)
|
|
233
|
+
--json return a flat JSON array
|
|
234
|
+
--tree output documents as directory tree
|
|
235
|
+
-h, --help display help for command
|
|
212
236
|
```
|
|
213
237
|
|
|
214
238
|
Simple filter values use OR within an option. Different options combine with
|
|
@@ -230,20 +254,20 @@ npx waymark find --kinds adr --kinds convention
|
|
|
230
254
|
```
|
|
231
255
|
|
|
232
256
|
Use `--query` for a case-insensitive literal search of document bodies. It can
|
|
233
|
-
be combined with either simple or
|
|
257
|
+
be combined with either simple or boolean metadata filters:
|
|
234
258
|
|
|
235
259
|
```sh
|
|
236
260
|
npx waymark find --kinds convention --query "dependency injection"
|
|
237
261
|
```
|
|
238
262
|
|
|
239
|
-
Use `--filter` for advanced
|
|
240
|
-
`AND`, `OR
|
|
263
|
+
Use `--filter` for advanced boolean expressions over metadata with `kind:`,
|
|
264
|
+
`tag:`, `NOT`, `AND`, `OR` and parentheses:
|
|
241
265
|
|
|
242
266
|
```sh
|
|
243
267
|
npx waymark find --filter '(kind:adr OR kind:convention) AND tag:typescript AND NOT tag:architecture'
|
|
244
268
|
```
|
|
245
269
|
|
|
246
|
-
`--filter` cannot be combined with `--kinds`, `--tags
|
|
270
|
+
`--filter` cannot be combined with `--kinds`, `--tags` or `--require-tags`.
|
|
247
271
|
|
|
248
272
|
Add metadata fields to the default line-oriented output with `--show`:
|
|
249
273
|
|
|
@@ -275,13 +299,15 @@ Paths are returned relative to the repository root in deterministic order.
|
|
|
275
299
|
```text
|
|
276
300
|
Usage: waymark ls [options] [directory]
|
|
277
301
|
|
|
302
|
+
Inventory document registration in a directory
|
|
303
|
+
|
|
278
304
|
Arguments:
|
|
279
|
-
directory
|
|
305
|
+
directory directory to inspect (defaults to the current directory)
|
|
280
306
|
|
|
281
307
|
Options:
|
|
282
|
-
-R, --recursive
|
|
283
|
-
-u, --unregistered
|
|
284
|
-
-h, --help
|
|
308
|
+
-R, --recursive inspect directories recursively
|
|
309
|
+
-u, --unregistered list only unregistered documents
|
|
310
|
+
-h, --help display help for command
|
|
285
311
|
```
|
|
286
312
|
|
|
287
313
|
List registered documents directly inside `docs`:
|
|
@@ -302,17 +328,13 @@ Find Markdown and MDX files that are missing Waymark metadata:
|
|
|
302
328
|
npx waymark ls -R --unregistered docs
|
|
303
329
|
```
|
|
304
330
|
|
|
305
|
-
`ls` respects `.gitignore`, Waymark ignore patterns
|
|
331
|
+
`ls` respects `.gitignore`, Waymark ignore patterns and Git directory boundaries.
|
|
306
332
|
The selected directory must be inside the repository root.
|
|
307
333
|
|
|
308
334
|
### `waymark help`
|
|
309
335
|
|
|
310
336
|
Show the command list or detailed help for one command:
|
|
311
337
|
|
|
312
|
-
```text
|
|
313
|
-
Usage: waymark help [command]
|
|
314
|
-
```
|
|
315
|
-
|
|
316
338
|
```sh
|
|
317
339
|
npx waymark --help
|
|
318
340
|
npx waymark help find
|
package/dist/commands/find.js
CHANGED
|
@@ -5,19 +5,20 @@ import { filterDocuments, scanDocuments, } from "../documents/index.js";
|
|
|
5
5
|
export function createFindCommand() {
|
|
6
6
|
return new Command("find")
|
|
7
7
|
.description("Discover Waymark Documents")
|
|
8
|
-
.option("-k, --kinds <identifiers>", "
|
|
9
|
-
.option("-t, --tags <identifiers>", "
|
|
10
|
-
.option("-T, --require-tags <identifiers>", "
|
|
11
|
-
.option("-f, --filter <expression>", "
|
|
12
|
-
.option("-q, --query <text>", "
|
|
13
|
-
.option("-s, --show <fields>", "
|
|
14
|
-
.option("--json", "
|
|
15
|
-
.option("--tree", "
|
|
8
|
+
.option("-k, --kinds <identifiers>", "match any kind (comma-separated, repeatable)", collectOptionValue)
|
|
9
|
+
.option("-t, --tags <identifiers>", "match any tag (comma-separated, repeatable)", collectOptionValue)
|
|
10
|
+
.option("-T, --require-tags <identifiers>", "require every tag (comma-separated, repeatable)", collectOptionValue)
|
|
11
|
+
.option("-f, --filter <expression>", "match a boolean filter expression")
|
|
12
|
+
.option("-q, --query <text>", "match literal text content (case-insensitive)")
|
|
13
|
+
.option("-s, --show <fields>", "show kind, tags and description (comma-separated)")
|
|
14
|
+
.option("--json", "return a flat JSON array")
|
|
15
|
+
.option("--tree", "output documents as directory tree")
|
|
16
16
|
.action(async (options) => {
|
|
17
|
+
const kinds = options.kinds ?? [];
|
|
18
|
+
const tags = options.tags ?? [];
|
|
19
|
+
const requiredTags = options.requireTags ?? [];
|
|
17
20
|
if (options.filter !== undefined &&
|
|
18
|
-
(
|
|
19
|
-
options.tags.length > 0 ||
|
|
20
|
-
options.requireTags.length > 0)) {
|
|
21
|
+
(kinds.length > 0 || tags.length > 0 || requiredTags.length > 0)) {
|
|
21
22
|
throw new Error("--filter cannot be combined with --kinds, --tags, or --require-tags.");
|
|
22
23
|
}
|
|
23
24
|
if (options.json && options.tree) {
|
|
@@ -25,27 +26,45 @@ export function createFindCommand() {
|
|
|
25
26
|
}
|
|
26
27
|
const shownFields = parseShownFields(options.show);
|
|
27
28
|
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
28
|
-
if (loadedConfiguration.kind === "
|
|
29
|
+
if (loadedConfiguration.kind === "invalid") {
|
|
29
30
|
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
30
31
|
}
|
|
31
32
|
const { configuration, rootPath } = loadedConfiguration;
|
|
33
|
+
const metadataCriteria = options.filter === undefined
|
|
34
|
+
? {
|
|
35
|
+
method: "filter-groups",
|
|
36
|
+
kinds: parseIdentifierOptions({
|
|
37
|
+
optionName: "--kinds",
|
|
38
|
+
values: kinds,
|
|
39
|
+
declarations: configuration.kinds,
|
|
40
|
+
declarationName: "kind",
|
|
41
|
+
}),
|
|
42
|
+
tags: parseIdentifierOptions({
|
|
43
|
+
optionName: "--tags",
|
|
44
|
+
values: tags,
|
|
45
|
+
declarations: configuration.tags,
|
|
46
|
+
declarationName: "tag",
|
|
47
|
+
}),
|
|
48
|
+
requiredTags: parseIdentifierOptions({
|
|
49
|
+
optionName: "--require-tags",
|
|
50
|
+
values: requiredTags,
|
|
51
|
+
declarations: configuration.tags,
|
|
52
|
+
declarationName: "tag",
|
|
53
|
+
}),
|
|
54
|
+
}
|
|
55
|
+
: {
|
|
56
|
+
method: "filter-expression",
|
|
57
|
+
expression: options.filter,
|
|
58
|
+
};
|
|
32
59
|
const documentScan = await scanDocuments({ rootPath, configuration });
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
...(documentScan.kind === "invalid" ? documentScan.diagnostics : []),
|
|
36
|
-
].sort(compareDiagnostics);
|
|
37
|
-
if (loadedConfiguration.diagnostics.length > 0 ||
|
|
38
|
-
documentScan.kind === "invalid") {
|
|
39
|
-
throwDiagnostics(diagnostics);
|
|
60
|
+
if (documentScan.kind === "invalid") {
|
|
61
|
+
throwDiagnostics(documentScan.diagnostics.sort(compareDiagnostics));
|
|
40
62
|
}
|
|
41
63
|
const matchingDocuments = filterDocuments({
|
|
42
64
|
documents: documentScan.documents,
|
|
43
65
|
configuration,
|
|
44
66
|
criteria: {
|
|
45
|
-
|
|
46
|
-
tags: options.tags,
|
|
47
|
-
requiredTags: options.requireTags,
|
|
48
|
-
filter: options.filter,
|
|
67
|
+
metadata: metadataCriteria,
|
|
49
68
|
query: options.query,
|
|
50
69
|
},
|
|
51
70
|
});
|
|
@@ -169,7 +188,25 @@ function createTreeDirectory() {
|
|
|
169
188
|
};
|
|
170
189
|
}
|
|
171
190
|
function collectOptionValue(value, previous) {
|
|
172
|
-
return [...previous, value];
|
|
191
|
+
return [...(previous ?? []), value];
|
|
192
|
+
}
|
|
193
|
+
function parseIdentifierOptions({ optionName, values, declarations, declarationName, }) {
|
|
194
|
+
const identifiers = new Set();
|
|
195
|
+
for (const value of values) {
|
|
196
|
+
for (const identifier of value.split(",")) {
|
|
197
|
+
if (identifier === "") {
|
|
198
|
+
throw new Error(`${optionName} contains an empty identifier.`);
|
|
199
|
+
}
|
|
200
|
+
if (identifiers.has(identifier)) {
|
|
201
|
+
throw new Error(`${optionName} contains duplicate identifier "${identifier}".`);
|
|
202
|
+
}
|
|
203
|
+
if (!declarations.has(identifier)) {
|
|
204
|
+
throw new Error(`${optionName} contains undeclared ${declarationName} "${identifier}".`);
|
|
205
|
+
}
|
|
206
|
+
identifiers.add(identifier);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return identifiers;
|
|
173
210
|
}
|
|
174
211
|
function compareText(left, right) {
|
|
175
212
|
return left < right ? -1 : left > right ? 1 : 0;
|
package/dist/commands/ls.js
CHANGED
|
@@ -6,15 +6,12 @@ import { scanDocuments } from "../documents/index.js";
|
|
|
6
6
|
export function createLsCommand() {
|
|
7
7
|
return new Command("ls")
|
|
8
8
|
.description("Inventory document registration in a directory")
|
|
9
|
-
.argument("[directory]", "
|
|
10
|
-
.option("-R, --recursive", "
|
|
11
|
-
.option("-u, --unregistered", "
|
|
9
|
+
.argument("[directory]", "directory to inspect (defaults to the current directory)")
|
|
10
|
+
.option("-R, --recursive", "inspect directories recursively")
|
|
11
|
+
.option("-u, --unregistered", "list only unregistered documents")
|
|
12
12
|
.action(async (directory, options) => {
|
|
13
13
|
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
14
|
-
if (loadedConfiguration.kind === "
|
|
15
|
-
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
16
|
-
}
|
|
17
|
-
if (loadedConfiguration.diagnostics.length > 0) {
|
|
14
|
+
if (loadedConfiguration.kind === "invalid") {
|
|
18
15
|
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
19
16
|
}
|
|
20
17
|
const rootPath = loadedConfiguration.rootPath;
|
package/dist/commands/status.js
CHANGED
|
@@ -5,24 +5,19 @@ import { scanDocuments } from "../documents/index.js";
|
|
|
5
5
|
export function createStatusCommand() {
|
|
6
6
|
return new Command("status")
|
|
7
7
|
.description("Validate and summarize the Waymark repository")
|
|
8
|
-
.option("-s, --show <fields>", "
|
|
8
|
+
.option("-s, --show <fields>", "show declared kind and tag details (kind,tags)")
|
|
9
9
|
.action(async (options) => {
|
|
10
10
|
const shownFields = parseShownFields(options.show);
|
|
11
11
|
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
12
|
-
if (loadedConfiguration.kind === "
|
|
12
|
+
if (loadedConfiguration.kind === "invalid") {
|
|
13
13
|
process.stdout.write(`Root: ${loadedConfiguration.rootPath}\n` + "Status: invalid\n");
|
|
14
14
|
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
15
15
|
}
|
|
16
16
|
const { configuration, rootPath } = loadedConfiguration;
|
|
17
17
|
const documentScan = await scanDocuments({ rootPath, configuration });
|
|
18
|
-
|
|
19
|
-
...loadedConfiguration.diagnostics,
|
|
20
|
-
...(documentScan.kind === "invalid" ? documentScan.diagnostics : []),
|
|
21
|
-
].sort(compareDiagnostics);
|
|
22
|
-
if (loadedConfiguration.diagnostics.length > 0 ||
|
|
23
|
-
documentScan.kind === "invalid") {
|
|
18
|
+
if (documentScan.kind === "invalid") {
|
|
24
19
|
process.stdout.write(`Root: ${rootPath}\n` + "Status: invalid\n");
|
|
25
|
-
throwDiagnostics(diagnostics);
|
|
20
|
+
throwDiagnostics(documentScan.diagnostics.sort(compareDiagnostics));
|
|
26
21
|
}
|
|
27
22
|
let output = `Root: ${rootPath}\n` +
|
|
28
23
|
"Status: valid\n" +
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export declare const defaultConfigFileName = "waymark.yml";
|
|
2
3
|
export declare const allowedConfigFileNames: readonly ["waymark.yml", "waymark.yaml"];
|
|
3
4
|
export type ConfigurationDeclaration = {
|
|
@@ -8,22 +9,33 @@ export type ConfigurationDiagnostic = {
|
|
|
8
9
|
field: string;
|
|
9
10
|
message: string;
|
|
10
11
|
};
|
|
11
|
-
|
|
12
|
+
declare const configurationSchema: z.ZodPipe<z.ZodObject<{
|
|
13
|
+
"require-namespace": z.ZodDefault<z.ZodBoolean>;
|
|
14
|
+
kinds: z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<Map<string, ConfigurationDeclaration>, Record<string, unknown>>>;
|
|
15
|
+
tags: z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<Map<string, ConfigurationDeclaration>, Record<string, unknown>>>;
|
|
16
|
+
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
17
|
+
}, z.core.$strict>, z.ZodTransform<{
|
|
12
18
|
requireNamespace: boolean;
|
|
13
19
|
kinds: Map<string, ConfigurationDeclaration>;
|
|
14
20
|
tags: Map<string, ConfigurationDeclaration>;
|
|
15
21
|
ignorePatterns: string[];
|
|
16
|
-
}
|
|
22
|
+
}, {
|
|
23
|
+
"require-namespace": boolean;
|
|
24
|
+
kinds: Map<string, ConfigurationDeclaration>;
|
|
25
|
+
tags: Map<string, ConfigurationDeclaration>;
|
|
26
|
+
ignore: string[];
|
|
27
|
+
}>>;
|
|
28
|
+
export type Configuration = z.infer<typeof configurationSchema>;
|
|
17
29
|
export type ConfigurationLoadResult = {
|
|
18
30
|
kind: "loaded";
|
|
19
31
|
rootPath: string;
|
|
20
32
|
configuration: Configuration;
|
|
21
|
-
diagnostics: ConfigurationDiagnostic[];
|
|
22
33
|
} | {
|
|
23
|
-
kind: "
|
|
34
|
+
kind: "invalid";
|
|
24
35
|
rootPath: string;
|
|
25
36
|
diagnostics: ConfigurationDiagnostic[];
|
|
26
37
|
};
|
|
27
38
|
export declare function loadConfiguration(startingDirectoryPath: string): Promise<ConfigurationLoadResult>;
|
|
28
39
|
export declare function findConfigurationPath(startingDirectoryPath: string): Promise<string | undefined>;
|
|
29
40
|
export declare function findConfigurationPathInDirectory(directoryPath: string): Promise<string | undefined>;
|
|
41
|
+
export {};
|
|
@@ -1,12 +1,80 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { compareDiagnostics } from "../diagnostics.js";
|
|
4
6
|
import { pathExists } from "../filesystem.js";
|
|
5
7
|
export const defaultConfigFileName = "waymark.yml";
|
|
6
8
|
export const allowedConfigFileNames = [
|
|
7
9
|
defaultConfigFileName,
|
|
8
10
|
"waymark.yaml",
|
|
9
11
|
];
|
|
12
|
+
const ignorePatternSchema = z
|
|
13
|
+
.string({ error: "Ignore glob must be a non-empty string." })
|
|
14
|
+
.superRefine((pattern, context) => {
|
|
15
|
+
if (pattern.trim() === "") {
|
|
16
|
+
context.addIssue({
|
|
17
|
+
code: "custom",
|
|
18
|
+
message: "Ignore glob must be a non-empty string.",
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (pattern.startsWith("!")) {
|
|
23
|
+
context.addIssue({
|
|
24
|
+
code: "custom",
|
|
25
|
+
message: "Negation is not supported in ignore globs.",
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (/[![\]{}()]/.test(pattern)) {
|
|
30
|
+
context.addIssue({
|
|
31
|
+
code: "custom",
|
|
32
|
+
message: "Only *, ?, and ** wildcard syntax is supported.",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const configurationDeclarationsSchema = z
|
|
37
|
+
.record(z.string(), z.unknown(), { error: "Expected a mapping." })
|
|
38
|
+
.superRefine((declarations, context) => {
|
|
39
|
+
for (const [identifier, description] of Object.entries(declarations)) {
|
|
40
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identifier)) {
|
|
41
|
+
context.addIssue({
|
|
42
|
+
code: "custom",
|
|
43
|
+
path: [identifier],
|
|
44
|
+
message: "Identifier must use lowercase kebab-case.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (typeof description !== "string" || description.trim() === "") {
|
|
48
|
+
context.addIssue({
|
|
49
|
+
code: "custom",
|
|
50
|
+
path: [identifier],
|
|
51
|
+
message: "Description must be a non-empty string.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.transform((declarations) => new Map(Object.entries(declarations)
|
|
57
|
+
.filter((entry) => typeof entry[1] === "string")
|
|
58
|
+
.map(([identifier, description]) => [identifier, { description }])));
|
|
59
|
+
const configurationSchema = z
|
|
60
|
+
.strictObject({
|
|
61
|
+
"require-namespace": z
|
|
62
|
+
.boolean({ error: "Expected a boolean." })
|
|
63
|
+
.default(false),
|
|
64
|
+
kinds: configurationDeclarationsSchema,
|
|
65
|
+
tags: configurationDeclarationsSchema,
|
|
66
|
+
ignore: z
|
|
67
|
+
.array(ignorePatternSchema, {
|
|
68
|
+
error: "Expected a sequence of ignore globs.",
|
|
69
|
+
})
|
|
70
|
+
.default([]),
|
|
71
|
+
}, { error: "Expected a mapping." })
|
|
72
|
+
.transform((configuration) => ({
|
|
73
|
+
requireNamespace: configuration["require-namespace"],
|
|
74
|
+
kinds: configuration.kinds,
|
|
75
|
+
tags: configuration.tags,
|
|
76
|
+
ignorePatterns: configuration.ignore,
|
|
77
|
+
}));
|
|
10
78
|
export async function loadConfiguration(startingDirectoryPath) {
|
|
11
79
|
const configurationPath = await findConfigurationPath(startingDirectoryPath);
|
|
12
80
|
if (!configurationPath) {
|
|
@@ -20,7 +88,7 @@ export async function loadConfiguration(startingDirectoryPath) {
|
|
|
20
88
|
}
|
|
21
89
|
catch {
|
|
22
90
|
return {
|
|
23
|
-
kind: "
|
|
91
|
+
kind: "invalid",
|
|
24
92
|
rootPath,
|
|
25
93
|
diagnostics: [
|
|
26
94
|
{
|
|
@@ -31,16 +99,21 @@ export async function loadConfiguration(startingDirectoryPath) {
|
|
|
31
99
|
],
|
|
32
100
|
};
|
|
33
101
|
}
|
|
34
|
-
const
|
|
102
|
+
const configurationResult = configurationSchema.safeParse(parsedConfiguration);
|
|
103
|
+
if (!configurationResult.success) {
|
|
104
|
+
return {
|
|
105
|
+
kind: "invalid",
|
|
106
|
+
rootPath,
|
|
107
|
+
diagnostics: createConfigurationDiagnostics({
|
|
108
|
+
error: configurationResult.error,
|
|
109
|
+
configurationFileName: loadedConfigurationFileName,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
35
113
|
return {
|
|
36
114
|
kind: "loaded",
|
|
37
115
|
rootPath,
|
|
38
|
-
configuration:
|
|
39
|
-
value: parsedConfiguration,
|
|
40
|
-
configurationFileName: loadedConfigurationFileName,
|
|
41
|
-
diagnostics,
|
|
42
|
-
}),
|
|
43
|
-
diagnostics,
|
|
116
|
+
configuration: configurationResult.data,
|
|
44
117
|
};
|
|
45
118
|
}
|
|
46
119
|
export async function findConfigurationPath(startingDirectoryPath) {
|
|
@@ -70,133 +143,25 @@ export async function findConfigurationPathInDirectory(directoryPath) {
|
|
|
70
143
|
}
|
|
71
144
|
return configurationPaths[0];
|
|
72
145
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"ignore",
|
|
87
|
-
]);
|
|
88
|
-
for (const field of Object.keys(configuration)) {
|
|
89
|
-
if (!allowedFields.has(field)) {
|
|
90
|
-
diagnostics.push({
|
|
91
|
-
path: configurationFileName,
|
|
92
|
-
field,
|
|
93
|
-
message: "Unknown field.",
|
|
94
|
-
});
|
|
146
|
+
function createConfigurationDiagnostics({ error, configurationFileName, }) {
|
|
147
|
+
const diagnostics = [];
|
|
148
|
+
for (const issue of error.issues) {
|
|
149
|
+
const field = issue.path.join(".") || "configuration";
|
|
150
|
+
if (issue.code === "unrecognized_keys") {
|
|
151
|
+
for (const key of issue.keys) {
|
|
152
|
+
diagnostics.push({
|
|
153
|
+
path: configurationFileName,
|
|
154
|
+
field: [...issue.path, key].join("."),
|
|
155
|
+
message: "Unknown field.",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
95
159
|
}
|
|
96
|
-
}
|
|
97
|
-
if (configuration["require-namespace"] !== undefined &&
|
|
98
|
-
typeof configuration["require-namespace"] !== "boolean") {
|
|
99
|
-
diagnostics.push({
|
|
100
|
-
path: configurationFileName,
|
|
101
|
-
field: "require-namespace",
|
|
102
|
-
message: "Expected a boolean.",
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
requireNamespace: configuration["require-namespace"] === true,
|
|
107
|
-
kinds: validateDeclarations({
|
|
108
|
-
value: configuration.kinds,
|
|
109
|
-
field: "kinds",
|
|
110
|
-
configurationFileName,
|
|
111
|
-
diagnostics,
|
|
112
|
-
}),
|
|
113
|
-
tags: validateDeclarations({
|
|
114
|
-
value: configuration.tags,
|
|
115
|
-
field: "tags",
|
|
116
|
-
configurationFileName,
|
|
117
|
-
diagnostics,
|
|
118
|
-
}),
|
|
119
|
-
ignorePatterns: validateIgnorePatterns({
|
|
120
|
-
value: configuration.ignore,
|
|
121
|
-
configurationFileName,
|
|
122
|
-
diagnostics,
|
|
123
|
-
}),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function validateDeclarations({ value, field, configurationFileName, diagnostics, }) {
|
|
127
|
-
if (!isRecord(value)) {
|
|
128
160
|
diagnostics.push({
|
|
129
161
|
path: configurationFileName,
|
|
130
162
|
field,
|
|
131
|
-
message:
|
|
132
|
-
});
|
|
133
|
-
return new Map();
|
|
134
|
-
}
|
|
135
|
-
const declarations = new Map();
|
|
136
|
-
for (const [identifier, description] of Object.entries(value)) {
|
|
137
|
-
const diagnosticField = `${field}.${identifier}`;
|
|
138
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identifier)) {
|
|
139
|
-
diagnostics.push({
|
|
140
|
-
path: configurationFileName,
|
|
141
|
-
field: diagnosticField,
|
|
142
|
-
message: "Identifier must use lowercase kebab-case.",
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
if (typeof description !== "string" || description.trim() === "") {
|
|
146
|
-
diagnostics.push({
|
|
147
|
-
path: configurationFileName,
|
|
148
|
-
field: diagnosticField,
|
|
149
|
-
message: "Description must be a non-empty string.",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
declarations.set(identifier, {
|
|
153
|
-
description: typeof description === "string" ? description : "",
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
return declarations;
|
|
157
|
-
}
|
|
158
|
-
function validateIgnorePatterns({ value, configurationFileName, diagnostics, }) {
|
|
159
|
-
if (value === undefined)
|
|
160
|
-
return [];
|
|
161
|
-
if (!Array.isArray(value)) {
|
|
162
|
-
diagnostics.push({
|
|
163
|
-
path: configurationFileName,
|
|
164
|
-
field: "ignore",
|
|
165
|
-
message: "Expected a sequence of ignore globs.",
|
|
163
|
+
message: issue.message,
|
|
166
164
|
});
|
|
167
|
-
return [];
|
|
168
165
|
}
|
|
169
|
-
|
|
170
|
-
for (const [index, pattern] of value.entries()) {
|
|
171
|
-
const field = `ignore.${index}`;
|
|
172
|
-
if (typeof pattern !== "string" || pattern.trim() === "") {
|
|
173
|
-
diagnostics.push({
|
|
174
|
-
path: configurationFileName,
|
|
175
|
-
field,
|
|
176
|
-
message: "Ignore glob must be a non-empty string.",
|
|
177
|
-
});
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (pattern.startsWith("!")) {
|
|
181
|
-
diagnostics.push({
|
|
182
|
-
path: configurationFileName,
|
|
183
|
-
field,
|
|
184
|
-
message: "Negation is not supported in ignore globs.",
|
|
185
|
-
});
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (/[![\]{}()]/.test(pattern)) {
|
|
189
|
-
diagnostics.push({
|
|
190
|
-
path: configurationFileName,
|
|
191
|
-
field,
|
|
192
|
-
message: "Only *, ?, and ** wildcard syntax is supported.",
|
|
193
|
-
});
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
ignorePatterns.push(pattern);
|
|
197
|
-
}
|
|
198
|
-
return ignorePatterns;
|
|
199
|
-
}
|
|
200
|
-
function isRecord(value) {
|
|
201
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
166
|
+
return diagnostics.sort(compareDiagnostics);
|
|
202
167
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import type { ConfigurationDeclaration } from "../configuration/index.js";
|
|
2
|
-
type ClassifiedDocument = {
|
|
3
|
-
kind: string;
|
|
4
|
-
description: string;
|
|
5
|
-
tags: string[];
|
|
3
|
+
type ClassifiedDocument = z.infer<ReturnType<typeof createDocumentSchema>> & {
|
|
6
4
|
body: string;
|
|
7
5
|
};
|
|
8
6
|
type DocumentDiagnostic = {
|
|
@@ -26,4 +24,12 @@ export declare function classifyDocument({ source, requireNamespace, declaredKin
|
|
|
26
24
|
declaredTags: Map<string, ConfigurationDeclaration>;
|
|
27
25
|
path: string;
|
|
28
26
|
}): DocumentClassification;
|
|
27
|
+
declare function createDocumentSchema({ declaredKinds, declaredTags, }: {
|
|
28
|
+
declaredKinds: Map<string, ConfigurationDeclaration>;
|
|
29
|
+
declaredTags: Map<string, ConfigurationDeclaration>;
|
|
30
|
+
}): z.ZodObject<{
|
|
31
|
+
kind: z.ZodString;
|
|
32
|
+
description: z.ZodString;
|
|
33
|
+
tags: z.ZodPipe<z.ZodDefault<z.ZodArray<z.ZodUnknown>>, z.ZodTransform<string[], unknown[]>>;
|
|
34
|
+
}, z.core.$strict>;
|
|
29
35
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { parse } from "yaml";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const rawFrontmatterSchema = z.record(z.string(), z.unknown());
|
|
2
4
|
export function classifyDocument({ source, requireNamespace, declaredKinds, declaredTags, path, }) {
|
|
3
5
|
const frontmatter = readFrontmatter(source);
|
|
4
6
|
if (frontmatter.kind === "malformed") {
|
|
@@ -13,11 +15,14 @@ export function classifyDocument({ source, requireNamespace, declaredKinds, decl
|
|
|
13
15
|
],
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
|
-
if (frontmatter.kind === "none"
|
|
18
|
+
if (frontmatter.kind === "none") {
|
|
17
19
|
return { kind: "unregistered" };
|
|
18
20
|
}
|
|
21
|
+
const frontmatterResult = rawFrontmatterSchema.safeParse(frontmatter.value);
|
|
22
|
+
if (!frontmatterResult.success)
|
|
23
|
+
return { kind: "unregistered" };
|
|
19
24
|
return validateRegistration({
|
|
20
|
-
frontmatter:
|
|
25
|
+
frontmatter: frontmatterResult.data,
|
|
21
26
|
body: frontmatter.body,
|
|
22
27
|
requireNamespace,
|
|
23
28
|
declaredKinds,
|
|
@@ -52,152 +57,126 @@ function validateRegistration({ frontmatter, body, requireNamespace, declaredKin
|
|
|
52
57
|
if (!requireNamespace && !hasNamespace && !hasRecognizedFlatMetadata) {
|
|
53
58
|
return { kind: "unregistered" };
|
|
54
59
|
}
|
|
55
|
-
const diagnostics = [];
|
|
56
60
|
if (hasNamespace && hasRecognizedFlatMetadata) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
return {
|
|
62
|
+
kind: "invalid",
|
|
63
|
+
diagnostics: [
|
|
64
|
+
{
|
|
65
|
+
path,
|
|
66
|
+
field: "waymark",
|
|
67
|
+
message: "Flat and namespaced metadata cannot both be declared.",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
63
71
|
}
|
|
64
72
|
const namespaced = hasNamespace;
|
|
65
|
-
const value = namespaced
|
|
73
|
+
const value = namespaced
|
|
74
|
+
? frontmatter.waymark
|
|
75
|
+
: {
|
|
76
|
+
kind: frontmatter.kind,
|
|
77
|
+
description: frontmatter.description,
|
|
78
|
+
tags: frontmatter.tags,
|
|
79
|
+
};
|
|
66
80
|
const fieldPrefix = namespaced ? "waymark." : "";
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
field: `${fieldPrefix}${field}`,
|
|
82
|
-
message: "Unknown field.",
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const context = { path, declaredKinds, declaredTags, diagnostics };
|
|
88
|
-
const kind = validateDocumentKind({
|
|
89
|
-
value: value.kind,
|
|
90
|
-
field: `${fieldPrefix}kind`,
|
|
91
|
-
context,
|
|
92
|
-
});
|
|
93
|
-
const description = validateDocumentDescription({
|
|
94
|
-
value: value.description,
|
|
95
|
-
field: `${fieldPrefix}description`,
|
|
96
|
-
context,
|
|
97
|
-
});
|
|
98
|
-
const tags = validateDocumentTags({
|
|
99
|
-
value: value.tags,
|
|
100
|
-
field: `${fieldPrefix}tags`,
|
|
101
|
-
context,
|
|
102
|
-
});
|
|
103
|
-
if (diagnostics.length > 0 ||
|
|
104
|
-
kind === undefined ||
|
|
105
|
-
description === undefined ||
|
|
106
|
-
tags === undefined) {
|
|
107
|
-
return { kind: "invalid", diagnostics };
|
|
81
|
+
const metadataResult = createDocumentSchema({
|
|
82
|
+
declaredKinds,
|
|
83
|
+
declaredTags,
|
|
84
|
+
}).safeParse(value);
|
|
85
|
+
if (!metadataResult.success) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "invalid",
|
|
88
|
+
diagnostics: createDocumentDiagnostics({
|
|
89
|
+
error: metadataResult.error,
|
|
90
|
+
path,
|
|
91
|
+
fieldPrefix,
|
|
92
|
+
rootField: namespaced ? "waymark" : "frontmatter",
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
108
95
|
}
|
|
109
96
|
return {
|
|
110
97
|
kind: "registered",
|
|
111
|
-
document: {
|
|
98
|
+
document: { ...metadataResult.data, body },
|
|
112
99
|
};
|
|
113
100
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
101
|
+
function createDocumentSchema({ declaredKinds, declaredTags, }) {
|
|
102
|
+
return z.strictObject({
|
|
103
|
+
kind: z
|
|
104
|
+
.string({
|
|
105
|
+
error: (issue) => issue.input === undefined
|
|
106
|
+
? "Document kind is required."
|
|
107
|
+
: "Document kind must be a non-empty string.",
|
|
108
|
+
})
|
|
109
|
+
.refine((kind) => kind.trim() !== "", {
|
|
110
|
+
error: "Document kind must be a non-empty string.",
|
|
111
|
+
})
|
|
112
|
+
.superRefine((kind, context) => {
|
|
113
|
+
if (kind.trim() !== "" && !declaredKinds.has(kind)) {
|
|
114
|
+
context.addIssue({
|
|
115
|
+
code: "custom",
|
|
116
|
+
message: `Undeclared kind "${kind}".`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
description: z
|
|
121
|
+
.string({
|
|
122
|
+
error: (issue) => issue.input === undefined
|
|
123
|
+
? "Document Description is required."
|
|
124
|
+
: "Document Description must be a non-empty string.",
|
|
125
|
+
})
|
|
126
|
+
.refine((description) => description.trim() !== "", {
|
|
127
|
+
error: "Document Description must be a non-empty string.",
|
|
128
|
+
}),
|
|
129
|
+
tags: z
|
|
130
|
+
.array(z.unknown(), { error: "Expected a YAML sequence of tags." })
|
|
131
|
+
.default([])
|
|
132
|
+
.superRefine((tags, context) => {
|
|
133
|
+
const seenTags = new Set();
|
|
134
|
+
for (const tag of tags) {
|
|
135
|
+
if (typeof tag !== "string" || tag.trim() === "") {
|
|
136
|
+
context.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
message: "Every tag must be a non-empty string.",
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (seenTags.has(tag)) {
|
|
143
|
+
context.addIssue({
|
|
144
|
+
code: "custom",
|
|
145
|
+
message: `Duplicate tag "${tag}".`,
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
seenTags.add(tag);
|
|
150
|
+
if (!declaredTags.has(tag)) {
|
|
151
|
+
context.addIssue({
|
|
152
|
+
code: "custom",
|
|
153
|
+
message: `Undeclared tag "${tag}".`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
.transform((tags) => tags.filter((tag) => typeof tag === "string")),
|
|
159
|
+
}, { error: "Expected a mapping." });
|
|
158
160
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const tags = [];
|
|
171
|
-
const seenTags = new Set();
|
|
172
|
-
for (const tag of value) {
|
|
173
|
-
if (typeof tag !== "string" || tag.trim() === "") {
|
|
174
|
-
context.diagnostics.push({
|
|
175
|
-
path: context.path,
|
|
176
|
-
field,
|
|
177
|
-
message: "Every tag must be a non-empty string.",
|
|
178
|
-
});
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (seenTags.has(tag)) {
|
|
182
|
-
context.diagnostics.push({
|
|
183
|
-
path: context.path,
|
|
184
|
-
field,
|
|
185
|
-
message: `Duplicate tag "${tag}".`,
|
|
186
|
-
});
|
|
161
|
+
function createDocumentDiagnostics({ error, path, fieldPrefix, rootField, }) {
|
|
162
|
+
const diagnostics = [];
|
|
163
|
+
for (const issue of error.issues) {
|
|
164
|
+
if (issue.code === "unrecognized_keys") {
|
|
165
|
+
for (const key of issue.keys) {
|
|
166
|
+
diagnostics.push({
|
|
167
|
+
path,
|
|
168
|
+
field: `${fieldPrefix}${key}`,
|
|
169
|
+
message: "Unknown field.",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
187
172
|
continue;
|
|
188
173
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
message: `Undeclared tag "${tag}".`,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
174
|
+
const field = issue.path.find((segment) => typeof segment === "string");
|
|
175
|
+
diagnostics.push({
|
|
176
|
+
path,
|
|
177
|
+
field: field === undefined ? rootField : `${fieldPrefix}${field}`,
|
|
178
|
+
message: issue.message,
|
|
179
|
+
});
|
|
198
180
|
}
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
function isRecord(value) {
|
|
202
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
181
|
+
return diagnostics;
|
|
203
182
|
}
|
|
@@ -4,11 +4,17 @@ type FilterableDocument = {
|
|
|
4
4
|
kind: string;
|
|
5
5
|
tags: string[];
|
|
6
6
|
};
|
|
7
|
+
type MetadataCriteria = {
|
|
8
|
+
method: "filter-groups";
|
|
9
|
+
kinds: Set<string>;
|
|
10
|
+
tags: Set<string>;
|
|
11
|
+
requiredTags: Set<string>;
|
|
12
|
+
} | {
|
|
13
|
+
method: "filter-expression";
|
|
14
|
+
expression: string;
|
|
15
|
+
};
|
|
7
16
|
type DocumentCriteria = {
|
|
8
|
-
|
|
9
|
-
tags: string[];
|
|
10
|
-
requiredTags: string[];
|
|
11
|
-
filter?: string;
|
|
17
|
+
metadata: MetadataCriteria;
|
|
12
18
|
query?: string;
|
|
13
19
|
};
|
|
14
20
|
export declare function filterDocuments({ documents, configuration, criteria, }: {
|
package/dist/documents/filter.js
CHANGED
|
@@ -1,36 +1,18 @@
|
|
|
1
1
|
export function filterDocuments({ documents, configuration, criteria, }) {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
declarationName: "kind",
|
|
7
|
-
});
|
|
8
|
-
const tags = parseIdentifierOptions({
|
|
9
|
-
optionName: "--tags",
|
|
10
|
-
values: criteria.tags,
|
|
11
|
-
declarations: configuration.tags,
|
|
12
|
-
declarationName: "tag",
|
|
13
|
-
});
|
|
14
|
-
const requiredTags = parseIdentifierOptions({
|
|
15
|
-
optionName: "--require-tags",
|
|
16
|
-
values: criteria.requiredTags,
|
|
17
|
-
declarations: configuration.tags,
|
|
18
|
-
declarationName: "tag",
|
|
19
|
-
});
|
|
20
|
-
const matchesMetadataFilter = criteria.filter === undefined
|
|
21
|
-
? undefined
|
|
22
|
-
: parseMetadataFilter({
|
|
23
|
-
expression: criteria.filter,
|
|
2
|
+
const metadata = criteria.metadata;
|
|
3
|
+
const matchesMetadata = metadata.method === "filter-expression"
|
|
4
|
+
? parseMetadataFilter({
|
|
5
|
+
expression: metadata.expression,
|
|
24
6
|
declaredKinds: new Set(configuration.kinds.keys()),
|
|
25
7
|
declaredTags: new Set(configuration.tags.keys()),
|
|
26
|
-
})
|
|
8
|
+
})
|
|
9
|
+
: (document) => (metadata.kinds.size === 0 || metadata.kinds.has(document.kind)) &&
|
|
10
|
+
(metadata.tags.size === 0 ||
|
|
11
|
+
document.tags.some((tag) => metadata.tags.has(tag))) &&
|
|
12
|
+
[...metadata.requiredTags].every((tag) => document.tags.includes(tag));
|
|
27
13
|
const normalizedQuery = criteria.query?.toLowerCase();
|
|
28
14
|
return documents
|
|
29
|
-
.filter((document) => (
|
|
30
|
-
(tags.size === 0 || document.tags.some((tag) => tags.has(tag))) &&
|
|
31
|
-
[...requiredTags].every((tag) => document.tags.includes(tag)) &&
|
|
32
|
-
(matchesMetadataFilter === undefined ||
|
|
33
|
-
matchesMetadataFilter(document)) &&
|
|
15
|
+
.filter((document) => matchesMetadata(document) &&
|
|
34
16
|
(normalizedQuery === undefined ||
|
|
35
17
|
document.body.toLowerCase().includes(normalizedQuery)))
|
|
36
18
|
.sort((left, right) => compareText(left.path, right.path));
|
|
@@ -189,24 +171,6 @@ function describeToken(token) {
|
|
|
189
171
|
return '"("';
|
|
190
172
|
return `"${token.type.toUpperCase()}"`;
|
|
191
173
|
}
|
|
192
|
-
function parseIdentifierOptions({ optionName, values, declarations, declarationName, }) {
|
|
193
|
-
const identifiers = new Set();
|
|
194
|
-
for (const value of values) {
|
|
195
|
-
for (const identifier of value.split(",")) {
|
|
196
|
-
if (identifier === "") {
|
|
197
|
-
throw new Error(`${optionName} contains an empty identifier.`);
|
|
198
|
-
}
|
|
199
|
-
if (identifiers.has(identifier)) {
|
|
200
|
-
throw new Error(`${optionName} contains duplicate identifier "${identifier}".`);
|
|
201
|
-
}
|
|
202
|
-
if (!declarations.has(identifier)) {
|
|
203
|
-
throw new Error(`${optionName} contains undeclared ${declarationName} "${identifier}".`);
|
|
204
|
-
}
|
|
205
|
-
identifiers.add(identifier);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return identifiers;
|
|
209
|
-
}
|
|
210
174
|
function compareText(left, right) {
|
|
211
175
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
212
176
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waymark-docs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Agent-focused CLI that surfaces repository documentation to improve coding-agent performance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"commander": "14.0.3",
|
|
47
47
|
"globby": "16.2.2",
|
|
48
|
-
"yaml": "2.9.0"
|
|
48
|
+
"yaml": "2.9.0",
|
|
49
|
+
"zod": "4.4.3"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/node": "24.10.10",
|