waymark-docs 0.2.1 → 0.3.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/CHANGELOG.md +14 -0
- package/README.md +110 -112
- package/dist/cli.js +2 -0
- package/dist/commands/find.js +39 -19
- package/dist/commands/ls.js +3 -3
- package/dist/commands/show.d.ts +2 -0
- package/dist/commands/show.js +97 -0
- package/dist/commands/status.js +4 -41
- package/dist/configuration/initialize.js +4 -0
- package/dist/configuration/load.d.ts +6 -0
- package/dist/configuration/load.js +8 -0
- package/dist/documents/classify.d.ts +13 -5
- package/dist/documents/classify.js +44 -6
- package/dist/documents/filter.d.ts +4 -1
- package/dist/documents/filter.js +26 -12
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/documents/scan.d.ts +2 -0
- package/dist/documents/scan.js +14 -12
- package/dist/documents/usage-counts.d.ts +18 -0
- package/dist/documents/usage-counts.js +24 -0
- package/package.json +1 -1
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.3.0](https://github.com/ysfaran/waymark/compare/v0.2.2...v0.3.0) (2026-09-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **cli:** add document scope option ([#12](https://github.com/ysfaran/waymark/issues/12)) ([2a6b67f](https://github.com/ysfaran/waymark/commit/2a6b67f00244de004a0c1eb47548ace90a720e7d))
|
|
11
|
+
|
|
12
|
+
## [0.2.2](https://github.com/ysfaran/waymark/compare/v0.2.1...v0.2.2) (2026-09-18)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **cli:** align help text with documentation ([#10](https://github.com/ysfaran/waymark/issues/10)) ([dbabc14](https://github.com/ysfaran/waymark/commit/dbabc14244d775cc8a38166d11a386674e282610))
|
|
18
|
+
|
|
5
19
|
## [0.2.1](https://github.com/ysfaran/waymark/compare/v0.2.0...v0.2.1) (2026-08-13)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
Waymark is a small, offline-first CLI that helps coding agents find the right
|
|
6
6
|
repository docs for each task. Add structured frontmatter to existing Markdown
|
|
7
|
-
or MDX files
|
|
8
|
-
document
|
|
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
9
|
that load unrelated context. It is as easy to set up as file-based navigation,
|
|
10
|
-
but remains deterministic and token-efficient
|
|
11
|
-
retrieval, it needs no ranking, maintained index
|
|
10
|
+
but remains deterministic and token-efficient. Unlike RAG or MCP-backed
|
|
11
|
+
retrieval, it needs no ranking, maintained index or retrieval infrastructure.
|
|
12
|
+
|
|
13
|
+
Each document has three searchable metadata dimensions:
|
|
14
|
+
|
|
15
|
+
1. **Scope:** Where does this document apply? (`backend`, `search-service`)
|
|
16
|
+
2. **Kind:** What role does this document serve? (`convention`, `agent-guide`, `adr`)
|
|
17
|
+
3. **Tags:** What topics does it cover? (`testing`, `typescript`)
|
|
12
18
|
|
|
13
19
|

|
|
14
20
|
|
|
@@ -19,6 +25,7 @@ retrieval, it needs no ranking, maintained index, or retrieval infrastructure.
|
|
|
19
25
|
- [Commands](#commands)
|
|
20
26
|
- [`waymark init`](#waymark-init)
|
|
21
27
|
- [`waymark status`](#waymark-status)
|
|
28
|
+
- [`waymark show`](#waymark-show)
|
|
22
29
|
- [`waymark find`](#waymark-find)
|
|
23
30
|
- [`waymark ls`](#waymark-ls)
|
|
24
31
|
- [`waymark help`](#waymark-help)
|
|
@@ -26,7 +33,16 @@ retrieval, it needs no ranking, maintained index, or retrieval infrastructure.
|
|
|
26
33
|
|
|
27
34
|
## Installation
|
|
28
35
|
|
|
29
|
-
Install the `waymark-
|
|
36
|
+
Install the `waymark` and `waymark-setup` skills for your coding agents:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx skills add ysfaran/waymark --skill waymark --skill waymark-setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`waymark-setup` automatically detects the active package manager and installs
|
|
43
|
+
`waymark-docs` locally as a development dependency.
|
|
44
|
+
|
|
45
|
+
To install the CLI manually instead, run the matching command:
|
|
30
46
|
|
|
31
47
|
```sh
|
|
32
48
|
pnpm add -D waymark-docs
|
|
@@ -37,75 +53,19 @@ npm install --save-dev waymark-docs
|
|
|
37
53
|
|
|
38
54
|
## Quick start
|
|
39
55
|
|
|
40
|
-
1.
|
|
41
|
-
|
|
42
|
-
Run `init` in the repository root:
|
|
43
|
-
|
|
44
|
-
```sh
|
|
45
|
-
npx waymark init
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
2. **Define searchable metadata.**
|
|
49
|
-
|
|
50
|
-
Add the document kinds and tags that agents can search:
|
|
51
|
-
|
|
52
|
-
```yaml
|
|
53
|
-
kinds:
|
|
54
|
-
adr: Read to understand past architectural decisions and their constraints
|
|
55
|
-
convention: Read before changing code to follow required repository practices
|
|
56
|
-
|
|
57
|
-
tags:
|
|
58
|
-
architecture: System boundaries, component relationships, and dependencies
|
|
59
|
-
typescript: TypeScript-related documentation
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
3. **Register a document.**
|
|
63
|
-
|
|
64
|
-
Add Waymark metadata to a Markdown or MDX file. For example, save this as
|
|
65
|
-
`docs/conventions/typescript.md`:
|
|
66
|
-
|
|
67
|
-
```yaml
|
|
68
|
-
---
|
|
69
|
-
kind: convention
|
|
70
|
-
description: TypeScript conventions for this repository
|
|
71
|
-
tags: [typescript]
|
|
72
|
-
---
|
|
73
|
-
# TypeScript conventions
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
4. **Validate the repository.**
|
|
77
|
-
|
|
78
|
-
Check the configuration and discovered documents:
|
|
79
|
-
|
|
80
|
-
```sh
|
|
81
|
-
npx waymark status
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```text
|
|
85
|
-
Root: /path/to/repository
|
|
86
|
-
Status: valid
|
|
87
|
-
Waymark Documents: 1
|
|
88
|
-
Unregistered Documents: 1
|
|
89
|
-
Kinds: 2
|
|
90
|
-
Tags: 2
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
5. **Discover the document.**
|
|
94
|
-
|
|
95
|
-
Find the registered convention by kind and tag:
|
|
56
|
+
1. Install the skills in the repository you want to set up:
|
|
96
57
|
|
|
97
58
|
```sh
|
|
98
|
-
npx
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
```text
|
|
102
|
-
docs/conventions/typescript.md — TypeScript conventions for this repository
|
|
59
|
+
npx skills add ysfaran/waymark --skill waymark --skill waymark-setup
|
|
103
60
|
```
|
|
104
61
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
62
|
+
2. Ask your coding agent to use `waymark-setup`. This one-time interactive
|
|
63
|
+
setup installs the CLI, configures the repository, registers useful
|
|
64
|
+
documents and adds an instruction to `AGENTS.md` or an equivalent file to
|
|
65
|
+
use the `waymark` skill for non-trivial tasks.
|
|
66
|
+
3. Review the generated `waymark.yml` configuration file, then give a fresh
|
|
67
|
+
agent a non-trivial task and confirm it uses Waymark to discover relevant
|
|
68
|
+
context before working.
|
|
109
69
|
|
|
110
70
|
## Commands
|
|
111
71
|
|
|
@@ -113,6 +73,7 @@ per directory.
|
|
|
113
73
|
| ---------------- | --------------------------------------------------------- |
|
|
114
74
|
| `waymark init` | Create a starter configuration |
|
|
115
75
|
| `waymark status` | Validate and summarize the repository |
|
|
76
|
+
| `waymark show` | List declared scopes, kinds and tags |
|
|
116
77
|
| `waymark find` | Find registered documents across the repository |
|
|
117
78
|
| `waymark ls` | Audit registered or unregistered documents in a directory |
|
|
118
79
|
| `waymark help` | Show CLI or command-specific help |
|
|
@@ -124,8 +85,10 @@ Create a starter configuration in the current directory.
|
|
|
124
85
|
```text
|
|
125
86
|
Usage: waymark init [options]
|
|
126
87
|
|
|
88
|
+
Create a starter Waymark configuration
|
|
89
|
+
|
|
127
90
|
Options:
|
|
128
|
-
-h, --help
|
|
91
|
+
-h, --help display help for command
|
|
129
92
|
```
|
|
130
93
|
|
|
131
94
|
```sh
|
|
@@ -133,17 +96,26 @@ npx waymark init
|
|
|
133
96
|
```
|
|
134
97
|
|
|
135
98
|
The generated file explains metadata namespacing and includes declarations to
|
|
136
|
-
replace with your own kind and tag:
|
|
99
|
+
replace with your own scope, kind and tag:
|
|
137
100
|
|
|
138
101
|
```yaml
|
|
139
102
|
# When true, document metadata must be nested under a `waymark` frontmatter key.
|
|
140
103
|
require-namespace: false
|
|
104
|
+
# When true, every waymark document must declare at least one scope.
|
|
105
|
+
require-scopes: false
|
|
106
|
+
scopes:
|
|
107
|
+
example-scope: Explain the repository area represented by this scope
|
|
141
108
|
kinds:
|
|
142
109
|
example-kind: Explain when agents should read this kind of document
|
|
143
110
|
tags:
|
|
144
111
|
example-tag: Explain the topic represented by this tag
|
|
145
112
|
```
|
|
146
113
|
|
|
114
|
+
The `scopes` map is optional, so existing configurations remain valid. Document
|
|
115
|
+
scopes are flat declared identifiers: they express applicability independently
|
|
116
|
+
of file paths and do not inherit from one another. Documents may omit `scopes`
|
|
117
|
+
unless `require-scopes: true` is configured.
|
|
118
|
+
|
|
147
119
|
To skip generated or vendored documentation during discovery, add `ignore`
|
|
148
120
|
patterns:
|
|
149
121
|
|
|
@@ -154,7 +126,7 @@ ignore:
|
|
|
154
126
|
- vendor/**
|
|
155
127
|
```
|
|
156
128
|
|
|
157
|
-
Ignore patterns are relative to the repository root and support `*`,
|
|
129
|
+
Ignore patterns are relative to the repository root and support `*`, `?` and
|
|
158
130
|
`**` wildcards. Waymark also honors `.gitignore` automatically.
|
|
159
131
|
|
|
160
132
|
`init` never overwrites an existing configuration and does not allow a nested
|
|
@@ -164,15 +136,16 @@ configuration beneath another Waymark root.
|
|
|
164
136
|
|
|
165
137
|
Validate the Waymark configuration and all discovered Waymark Documents, then
|
|
166
138
|
print the repository root and counts for registered documents, unregistered
|
|
167
|
-
documents,
|
|
168
|
-
non-zero exit code, which makes this command suitable for CI.
|
|
139
|
+
documents, scopes, kinds and tags. Invalid repositories produce diagnostics
|
|
140
|
+
and a non-zero exit code, which makes this command suitable for CI.
|
|
169
141
|
|
|
170
142
|
```text
|
|
171
143
|
Usage: waymark status [options]
|
|
172
144
|
|
|
145
|
+
Validate and summarize the Waymark repository
|
|
146
|
+
|
|
173
147
|
Options:
|
|
174
|
-
-
|
|
175
|
-
-h, --help Display help for the command
|
|
148
|
+
-h, --help display help for command
|
|
176
149
|
```
|
|
177
150
|
|
|
178
151
|
Validate the repository:
|
|
@@ -181,16 +154,33 @@ Validate the repository:
|
|
|
181
154
|
npx waymark status
|
|
182
155
|
```
|
|
183
156
|
|
|
184
|
-
|
|
157
|
+
### `waymark show`
|
|
185
158
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
159
|
+
List the declared scope, kind and tag vocabulary with descriptions and document
|
|
160
|
+
usage counts. Pass a category to show only that vocabulary. `--scopes` narrows
|
|
161
|
+
kind and tag counts to documents matching any selected scope and omits values
|
|
162
|
+
unused by that selection.
|
|
189
163
|
|
|
190
|
-
|
|
164
|
+
```text
|
|
165
|
+
Usage: waymark show [options] [category]
|
|
166
|
+
|
|
167
|
+
List declared scopes, kinds, and tags
|
|
168
|
+
|
|
169
|
+
Arguments:
|
|
170
|
+
category list only scopes, kinds, or tags
|
|
171
|
+
(choices: "scopes", "kinds", "tags")
|
|
172
|
+
|
|
173
|
+
Options:
|
|
174
|
+
--scopes <identifiers> select documents matching any scope (comma-separated,
|
|
175
|
+
repeatable)
|
|
176
|
+
-h, --help display help for command
|
|
177
|
+
```
|
|
191
178
|
|
|
192
179
|
```sh
|
|
193
|
-
npx waymark
|
|
180
|
+
npx waymark show
|
|
181
|
+
npx waymark show scopes
|
|
182
|
+
npx waymark show kinds --scopes backend,search-service
|
|
183
|
+
npx waymark show tags --scopes backend
|
|
194
184
|
```
|
|
195
185
|
|
|
196
186
|
### `waymark find`
|
|
@@ -201,30 +191,38 @@ Find registered Waymark Documents across the repository. With no filters,
|
|
|
201
191
|
```text
|
|
202
192
|
Usage: waymark find [options]
|
|
203
193
|
|
|
194
|
+
Discover Waymark Documents
|
|
195
|
+
|
|
204
196
|
Options:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
--
|
|
212
|
-
--
|
|
213
|
-
|
|
197
|
+
--scopes <identifiers> match any scope (comma-separated,
|
|
198
|
+
repeatable)
|
|
199
|
+
-k, --kinds <identifiers> match any kind (comma-separated, repeatable)
|
|
200
|
+
-t, --tags <identifiers> match any tag (comma-separated, repeatable)
|
|
201
|
+
-T, --require-tags <identifiers> require every tag (comma-separated,
|
|
202
|
+
repeatable)
|
|
203
|
+
-f, --filter <expression> match a boolean filter expression
|
|
204
|
+
-q, --query <text> match literal text content
|
|
205
|
+
(case-insensitive)
|
|
206
|
+
-s, --show <fields> show only selected metadata fields
|
|
207
|
+
(comma-separated; defaults to all)
|
|
208
|
+
--json return a flat JSON array
|
|
209
|
+
--tree output documents as directory tree
|
|
210
|
+
-h, --help display help for command
|
|
214
211
|
```
|
|
215
212
|
|
|
216
213
|
Simple filter values use OR within an option. Different options combine with
|
|
217
|
-
AND:
|
|
214
|
+
AND. A scope filter excludes documents that declare no scope:
|
|
218
215
|
|
|
219
216
|
```sh
|
|
220
|
-
#
|
|
221
|
-
|
|
217
|
+
# Scope is backend OR search-service, kind is adr OR convention, and at least
|
|
218
|
+
# one tag is typescript OR architecture
|
|
219
|
+
npx waymark find --scopes backend,search-service --kinds adr,convention --tags typescript,architecture
|
|
222
220
|
|
|
223
221
|
# Kind is adr, and both architecture AND typescript tags are required
|
|
224
222
|
npx waymark find --kinds adr --require-tags architecture,typescript
|
|
225
223
|
```
|
|
226
224
|
|
|
227
|
-
The
|
|
225
|
+
The four simple metadata filters are repeatable. Repeating an option is
|
|
228
226
|
equivalent to passing a comma-separated list:
|
|
229
227
|
|
|
230
228
|
```sh
|
|
@@ -232,25 +230,27 @@ npx waymark find --kinds adr --kinds convention
|
|
|
232
230
|
```
|
|
233
231
|
|
|
234
232
|
Use `--query` for a case-insensitive literal search of document bodies. It can
|
|
235
|
-
be combined with either simple or
|
|
233
|
+
be combined with either simple or boolean metadata filters:
|
|
236
234
|
|
|
237
235
|
```sh
|
|
238
236
|
npx waymark find --kinds convention --query "dependency injection"
|
|
239
237
|
```
|
|
240
238
|
|
|
241
|
-
Use `--filter` for advanced
|
|
242
|
-
`AND`, `OR
|
|
239
|
+
Use `--filter` for advanced Boolean expressions over metadata with `scope:`,
|
|
240
|
+
`kind:`, `tag:`, `NOT`, `AND`, `OR` and parentheses:
|
|
243
241
|
|
|
244
242
|
```sh
|
|
245
|
-
npx waymark find --filter '(kind:adr OR kind:convention) AND tag:typescript AND NOT tag:architecture'
|
|
243
|
+
npx waymark find --filter 'scope:backend AND (kind:adr OR kind:convention) AND tag:typescript AND NOT tag:architecture'
|
|
246
244
|
```
|
|
247
245
|
|
|
248
|
-
`--filter` cannot be combined with `--kinds`, `--tags
|
|
246
|
+
`--filter` cannot be combined with `--scopes`, `--kinds`, `--tags` or
|
|
247
|
+
`--require-tags`.
|
|
249
248
|
|
|
250
|
-
|
|
249
|
+
By default, line, JSON and tree output include scopes, kind, tags and
|
|
250
|
+
description in that order. Use `--show` to select a non-empty subset:
|
|
251
251
|
|
|
252
252
|
```sh
|
|
253
|
-
npx waymark find --kinds convention --show kind,
|
|
253
|
+
npx waymark find --kinds convention --show kind,description
|
|
254
254
|
```
|
|
255
255
|
|
|
256
256
|
Return structured output for scripts and agents:
|
|
@@ -277,13 +277,15 @@ Paths are returned relative to the repository root in deterministic order.
|
|
|
277
277
|
```text
|
|
278
278
|
Usage: waymark ls [options] [directory]
|
|
279
279
|
|
|
280
|
+
Inventory document registration in a directory
|
|
281
|
+
|
|
280
282
|
Arguments:
|
|
281
|
-
directory
|
|
283
|
+
directory directory to inspect (defaults to the current directory)
|
|
282
284
|
|
|
283
285
|
Options:
|
|
284
|
-
-R, --recursive
|
|
285
|
-
-u, --unregistered
|
|
286
|
-
-h, --help
|
|
286
|
+
-R, --recursive inspect directories recursively
|
|
287
|
+
-u, --unregistered list only unregistered documents
|
|
288
|
+
-h, --help display help for command
|
|
287
289
|
```
|
|
288
290
|
|
|
289
291
|
List registered documents directly inside `docs`:
|
|
@@ -304,17 +306,13 @@ Find Markdown and MDX files that are missing Waymark metadata:
|
|
|
304
306
|
npx waymark ls -R --unregistered docs
|
|
305
307
|
```
|
|
306
308
|
|
|
307
|
-
`ls` respects `.gitignore`, Waymark ignore patterns
|
|
309
|
+
`ls` respects `.gitignore`, Waymark ignore patterns and Git directory boundaries.
|
|
308
310
|
The selected directory must be inside the repository root.
|
|
309
311
|
|
|
310
312
|
### `waymark help`
|
|
311
313
|
|
|
312
314
|
Show the command list or detailed help for one command:
|
|
313
315
|
|
|
314
|
-
```text
|
|
315
|
-
Usage: waymark help [command]
|
|
316
|
-
```
|
|
317
|
-
|
|
318
316
|
```sh
|
|
319
317
|
npx waymark --help
|
|
320
318
|
npx waymark help find
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
4
4
|
import { createFindCommand } from "./commands/find.js";
|
|
5
5
|
import { createInitCommand } from "./commands/init.js";
|
|
6
6
|
import { createLsCommand } from "./commands/ls.js";
|
|
7
|
+
import { createShowCommand } from "./commands/show.js";
|
|
7
8
|
import { createStatusCommand } from "./commands/status.js";
|
|
8
9
|
const program = new Command()
|
|
9
10
|
.name("waymark")
|
|
@@ -12,6 +13,7 @@ const program = new Command()
|
|
|
12
13
|
.exitOverride();
|
|
13
14
|
program.addCommand(createInitCommand());
|
|
14
15
|
program.addCommand(createStatusCommand());
|
|
16
|
+
program.addCommand(createShowCommand());
|
|
15
17
|
program.addCommand(createFindCommand());
|
|
16
18
|
program.addCommand(createLsCommand());
|
|
17
19
|
try {
|
package/dist/commands/find.js
CHANGED
|
@@ -2,23 +2,30 @@ import { Command } from "commander";
|
|
|
2
2
|
import { loadConfiguration } from "../configuration/index.js";
|
|
3
3
|
import { compareDiagnostics, throwDiagnostics } from "../diagnostics.js";
|
|
4
4
|
import { filterDocuments, scanDocuments, } from "../documents/index.js";
|
|
5
|
+
const SHOWN_FIELDS = ["scopes", "kind", "tags", "description"];
|
|
5
6
|
export function createFindCommand() {
|
|
6
7
|
return new Command("find")
|
|
7
8
|
.description("Discover Waymark Documents")
|
|
8
|
-
.option("
|
|
9
|
-
.option("-
|
|
10
|
-
.option("-
|
|
11
|
-
.option("-
|
|
12
|
-
.option("-
|
|
13
|
-
.option("-
|
|
14
|
-
.option("--
|
|
15
|
-
.option("--
|
|
9
|
+
.option("--scopes <identifiers>", "match any scope (comma-separated, repeatable)", collectOptionValue)
|
|
10
|
+
.option("-k, --kinds <identifiers>", "match any kind (comma-separated, repeatable)", collectOptionValue)
|
|
11
|
+
.option("-t, --tags <identifiers>", "match any tag (comma-separated, repeatable)", collectOptionValue)
|
|
12
|
+
.option("-T, --require-tags <identifiers>", "require every tag (comma-separated, repeatable)", collectOptionValue)
|
|
13
|
+
.option("-f, --filter <expression>", "match a boolean filter expression")
|
|
14
|
+
.option("-q, --query <text>", "match literal text content (case-insensitive)")
|
|
15
|
+
.option("-s, --show <fields>", "show only selected metadata fields (comma-separated; defaults to all)")
|
|
16
|
+
.option("--json", "return a flat JSON array")
|
|
17
|
+
.option("--tree", "output documents as directory tree")
|
|
16
18
|
.action(async (options) => {
|
|
19
|
+
const scopes = options.scopes ?? [];
|
|
20
|
+
const kinds = options.kinds ?? [];
|
|
21
|
+
const tags = options.tags ?? [];
|
|
22
|
+
const requiredTags = options.requireTags ?? [];
|
|
17
23
|
if (options.filter !== undefined &&
|
|
18
|
-
(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
(scopes.length > 0 ||
|
|
25
|
+
kinds.length > 0 ||
|
|
26
|
+
tags.length > 0 ||
|
|
27
|
+
requiredTags.length > 0)) {
|
|
28
|
+
throw new Error("--filter cannot be combined with --scopes, --kinds, --tags, or --require-tags.");
|
|
22
29
|
}
|
|
23
30
|
if (options.json && options.tree) {
|
|
24
31
|
throw new Error("--json cannot be combined with --tree.");
|
|
@@ -32,21 +39,27 @@ export function createFindCommand() {
|
|
|
32
39
|
const metadataCriteria = options.filter === undefined
|
|
33
40
|
? {
|
|
34
41
|
method: "filter-groups",
|
|
42
|
+
scopes: parseIdentifierOptions({
|
|
43
|
+
optionName: "--scopes",
|
|
44
|
+
values: scopes,
|
|
45
|
+
declarations: configuration.scopes,
|
|
46
|
+
declarationName: "scope",
|
|
47
|
+
}),
|
|
35
48
|
kinds: parseIdentifierOptions({
|
|
36
49
|
optionName: "--kinds",
|
|
37
|
-
values:
|
|
50
|
+
values: kinds,
|
|
38
51
|
declarations: configuration.kinds,
|
|
39
52
|
declarationName: "kind",
|
|
40
53
|
}),
|
|
41
54
|
tags: parseIdentifierOptions({
|
|
42
55
|
optionName: "--tags",
|
|
43
|
-
values:
|
|
56
|
+
values: tags,
|
|
44
57
|
declarations: configuration.tags,
|
|
45
58
|
declarationName: "tag",
|
|
46
59
|
}),
|
|
47
60
|
requiredTags: parseIdentifierOptions({
|
|
48
61
|
optionName: "--require-tags",
|
|
49
|
-
values:
|
|
62
|
+
values: requiredTags,
|
|
50
63
|
declarations: configuration.tags,
|
|
51
64
|
declarationName: "tag",
|
|
52
65
|
}),
|
|
@@ -82,6 +95,8 @@ export function createFindCommand() {
|
|
|
82
95
|
}
|
|
83
96
|
function projectDocument(document, shownFields) {
|
|
84
97
|
const projection = { path: document.path };
|
|
98
|
+
if (shownFields.has("scopes"))
|
|
99
|
+
projection.scopes = document.scopes;
|
|
85
100
|
if (shownFields.has("kind"))
|
|
86
101
|
projection.kind = document.kind;
|
|
87
102
|
if (shownFields.has("tags"))
|
|
@@ -93,11 +108,11 @@ function projectDocument(document, shownFields) {
|
|
|
93
108
|
}
|
|
94
109
|
function parseShownFields(value) {
|
|
95
110
|
if (value === undefined)
|
|
96
|
-
return new Set();
|
|
111
|
+
return new Set(SHOWN_FIELDS);
|
|
97
112
|
const shownFields = new Set();
|
|
98
113
|
for (const field of value.split(",")) {
|
|
99
|
-
if (field
|
|
100
|
-
throw new Error(`Unknown find field "${field}". Expected kind, tags, or description.`);
|
|
114
|
+
if (!isShownField(field)) {
|
|
115
|
+
throw new Error(`Unknown find field "${field}". Expected scopes, kind, tags, or description.`);
|
|
101
116
|
}
|
|
102
117
|
if (shownFields.has(field)) {
|
|
103
118
|
throw new Error(`Duplicate find field "${field}".`);
|
|
@@ -106,8 +121,13 @@ function parseShownFields(value) {
|
|
|
106
121
|
}
|
|
107
122
|
return shownFields;
|
|
108
123
|
}
|
|
124
|
+
function isShownField(field) {
|
|
125
|
+
return SHOWN_FIELDS.some((shownField) => shownField === field);
|
|
126
|
+
}
|
|
109
127
|
function renderDocumentLine(document, shownFields, displayedPath = document.path) {
|
|
110
128
|
let line = displayedPath;
|
|
129
|
+
if (shownFields.has("scopes"))
|
|
130
|
+
line += ` [${document.scopes.join(",")}]`;
|
|
111
131
|
if (shownFields.has("kind"))
|
|
112
132
|
line += ` [${document.kind}]`;
|
|
113
133
|
if (shownFields.has("tags"))
|
|
@@ -187,7 +207,7 @@ function createTreeDirectory() {
|
|
|
187
207
|
};
|
|
188
208
|
}
|
|
189
209
|
function collectOptionValue(value, previous) {
|
|
190
|
-
return [...previous, value];
|
|
210
|
+
return [...(previous ?? []), value];
|
|
191
211
|
}
|
|
192
212
|
function parseIdentifierOptions({ optionName, values, declarations, declarationName, }) {
|
|
193
213
|
const identifiers = new Set();
|
package/dist/commands/ls.js
CHANGED
|
@@ -6,9 +6,9 @@ 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
14
|
if (loadedConfiguration.kind === "invalid") {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Argument, Command } from "commander";
|
|
2
|
+
import { loadConfiguration, } from "../configuration/index.js";
|
|
3
|
+
import { throwDiagnostics } from "../diagnostics.js";
|
|
4
|
+
import { countDocumentMetadataUsage, scanDocuments, } from "../documents/index.js";
|
|
5
|
+
const showCategories = ["scopes", "kinds", "tags"];
|
|
6
|
+
export function createShowCommand() {
|
|
7
|
+
return new Command("show")
|
|
8
|
+
.description("List declared scopes, kinds, and tags")
|
|
9
|
+
.addArgument(new Argument("[category]", "list only scopes, kinds, or tags").choices(showCategories))
|
|
10
|
+
.option("--scopes <identifiers>", "select documents matching any scope (comma-separated, repeatable)", collectScopeOptionValue)
|
|
11
|
+
.action(async (category, options) => {
|
|
12
|
+
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
13
|
+
if (loadedConfiguration.kind === "invalid") {
|
|
14
|
+
throwDiagnostics(loadedConfiguration.diagnostics);
|
|
15
|
+
}
|
|
16
|
+
const { configuration, rootPath } = loadedConfiguration;
|
|
17
|
+
const selectedScopes = parseScopeOptions({
|
|
18
|
+
values: options.scopes ?? [],
|
|
19
|
+
declarations: configuration.scopes,
|
|
20
|
+
});
|
|
21
|
+
const documentScan = await scanDocuments({ rootPath, configuration });
|
|
22
|
+
if (documentScan.kind === "invalid") {
|
|
23
|
+
throwDiagnostics(documentScan.diagnostics);
|
|
24
|
+
}
|
|
25
|
+
const categories = category ? [category] : showCategories;
|
|
26
|
+
const { kindUsageCounts, tagUsageCounts } = countDocumentMetadataUsage({
|
|
27
|
+
documents: documentScan.documents,
|
|
28
|
+
selectedScopes,
|
|
29
|
+
declaredKinds: configuration.kinds,
|
|
30
|
+
declaredTags: configuration.tags,
|
|
31
|
+
});
|
|
32
|
+
const usedOnly = selectedScopes.size > 0;
|
|
33
|
+
let output = "";
|
|
34
|
+
for (const shownCategory of categories) {
|
|
35
|
+
if (shownCategory === "scopes") {
|
|
36
|
+
output += renderDeclaredValues({
|
|
37
|
+
heading: "Scopes",
|
|
38
|
+
values: configuration.scopes,
|
|
39
|
+
usageCounts: documentScan.scopeUsageCounts,
|
|
40
|
+
usedOnly: false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (shownCategory === "kinds") {
|
|
44
|
+
output += renderDeclaredValues({
|
|
45
|
+
heading: "Kinds",
|
|
46
|
+
values: configuration.kinds,
|
|
47
|
+
usageCounts: kindUsageCounts,
|
|
48
|
+
usedOnly,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
output += renderDeclaredValues({
|
|
53
|
+
heading: "Tags",
|
|
54
|
+
values: configuration.tags,
|
|
55
|
+
usageCounts: tagUsageCounts,
|
|
56
|
+
usedOnly,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
process.stdout.write(output);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function collectScopeOptionValue(value, previous) {
|
|
64
|
+
return [...(previous ?? []), value];
|
|
65
|
+
}
|
|
66
|
+
function parseScopeOptions({ values, declarations, }) {
|
|
67
|
+
const scopes = new Set();
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
for (const scope of value.split(",")) {
|
|
70
|
+
if (scope === "") {
|
|
71
|
+
throw new Error("--scopes contains an empty identifier.");
|
|
72
|
+
}
|
|
73
|
+
if (scopes.has(scope)) {
|
|
74
|
+
throw new Error(`--scopes contains duplicate identifier "${scope}".`);
|
|
75
|
+
}
|
|
76
|
+
if (!declarations.has(scope)) {
|
|
77
|
+
throw new Error(`--scopes contains undeclared scope "${scope}".`);
|
|
78
|
+
}
|
|
79
|
+
scopes.add(scope);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return scopes;
|
|
83
|
+
}
|
|
84
|
+
function renderDeclaredValues({ heading, values, usageCounts, usedOnly, }) {
|
|
85
|
+
let output = `${heading}:\n`;
|
|
86
|
+
const sortedValues = [...values.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
87
|
+
for (const [identifier, value] of sortedValues) {
|
|
88
|
+
const documentCount = usageCounts.get(identifier) ?? 0;
|
|
89
|
+
if (usedOnly && documentCount === 0)
|
|
90
|
+
continue;
|
|
91
|
+
const noun = documentCount === 1 ? "document" : "documents";
|
|
92
|
+
const description = value.description.replaceAll(/\s+/g, " ").trim();
|
|
93
|
+
output +=
|
|
94
|
+
` ${identifier} — ${description} ` + `(${documentCount} ${noun})\n`;
|
|
95
|
+
}
|
|
96
|
+
return output;
|
|
97
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { loadConfiguration
|
|
2
|
+
import { loadConfiguration } from "../configuration/index.js";
|
|
3
3
|
import { compareDiagnostics, throwDiagnostics } from "../diagnostics.js";
|
|
4
4
|
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
|
-
.
|
|
9
|
-
.action(async (options) => {
|
|
10
|
-
const shownFields = parseShownFields(options.show);
|
|
8
|
+
.action(async () => {
|
|
11
9
|
const loadedConfiguration = await loadConfiguration(process.cwd());
|
|
12
10
|
if (loadedConfiguration.kind === "invalid") {
|
|
13
11
|
process.stdout.write(`Root: ${loadedConfiguration.rootPath}\n` + "Status: invalid\n");
|
|
@@ -19,48 +17,13 @@ export function createStatusCommand() {
|
|
|
19
17
|
process.stdout.write(`Root: ${rootPath}\n` + "Status: invalid\n");
|
|
20
18
|
throwDiagnostics(documentScan.diagnostics.sort(compareDiagnostics));
|
|
21
19
|
}
|
|
22
|
-
|
|
20
|
+
const output = `Root: ${rootPath}\n` +
|
|
23
21
|
"Status: valid\n" +
|
|
24
22
|
`Waymark Documents: ${documentScan.documents.length}\n` +
|
|
25
23
|
`Unregistered Documents: ${documentScan.unregisteredDocuments.length}\n` +
|
|
24
|
+
`Scopes: ${configuration.scopes.size}\n` +
|
|
26
25
|
`Kinds: ${configuration.kinds.size}\n` +
|
|
27
26
|
`Tags: ${configuration.tags.size}\n`;
|
|
28
|
-
if (shownFields.size > 0)
|
|
29
|
-
output += "\n";
|
|
30
|
-
if (shownFields.has("kind")) {
|
|
31
|
-
output += renderDeclaredValues("Kinds", configuration.kinds, documentScan.kindUsageCounts);
|
|
32
|
-
}
|
|
33
|
-
if (shownFields.has("tags")) {
|
|
34
|
-
output += renderDeclaredValues("Tags", configuration.tags, documentScan.tagUsageCounts);
|
|
35
|
-
}
|
|
36
27
|
process.stdout.write(output);
|
|
37
28
|
});
|
|
38
29
|
}
|
|
39
|
-
function parseShownFields(value) {
|
|
40
|
-
if (value === undefined)
|
|
41
|
-
return new Set();
|
|
42
|
-
const fields = value.split(",");
|
|
43
|
-
const shownFields = new Set();
|
|
44
|
-
for (const field of fields) {
|
|
45
|
-
if (field !== "kind" && field !== "tags") {
|
|
46
|
-
throw new Error(`Unknown status field "${field}". Expected kind or tags.`);
|
|
47
|
-
}
|
|
48
|
-
if (shownFields.has(field)) {
|
|
49
|
-
throw new Error(`Duplicate status field "${field}".`);
|
|
50
|
-
}
|
|
51
|
-
shownFields.add(field);
|
|
52
|
-
}
|
|
53
|
-
return shownFields;
|
|
54
|
-
}
|
|
55
|
-
function renderDeclaredValues(heading, values, usageCounts) {
|
|
56
|
-
let output = `${heading}:\n`;
|
|
57
|
-
const sortedValues = [...values.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
58
|
-
for (const [identifier, value] of sortedValues) {
|
|
59
|
-
const documentCount = usageCounts.get(identifier) ?? 0;
|
|
60
|
-
const noun = documentCount === 1 ? "document" : "documents";
|
|
61
|
-
const description = value.description.replaceAll(/\s+/g, " ").trim();
|
|
62
|
-
output +=
|
|
63
|
-
` ${identifier} — ${description} ` + `(${documentCount} ${noun})\n`;
|
|
64
|
-
}
|
|
65
|
-
return output;
|
|
66
|
-
}
|
|
@@ -4,6 +4,10 @@ import { isErrorWithCode } from "../filesystem.js";
|
|
|
4
4
|
import { defaultConfigFileName, findConfigurationPath, findConfigurationPathInDirectory, } from "./load.js";
|
|
5
5
|
const starterConfiguration = "# When true, document metadata must be nested under a `waymark` frontmatter key.\n" +
|
|
6
6
|
"require-namespace: false\n" +
|
|
7
|
+
"# When true, every waymark document must declare at least one scope.\n" +
|
|
8
|
+
"require-scopes: false\n" +
|
|
9
|
+
"scopes:\n" +
|
|
10
|
+
" example-scope: Explain the repository area represented by this scope\n" +
|
|
7
11
|
"kinds:\n" +
|
|
8
12
|
" example-kind: Explain when agents should read this kind of document\n" +
|
|
9
13
|
"tags:\n" +
|
|
@@ -11,16 +11,22 @@ export type ConfigurationDiagnostic = {
|
|
|
11
11
|
};
|
|
12
12
|
declare const configurationSchema: z.ZodPipe<z.ZodObject<{
|
|
13
13
|
"require-namespace": z.ZodDefault<z.ZodBoolean>;
|
|
14
|
+
"require-scopes": z.ZodDefault<z.ZodBoolean>;
|
|
15
|
+
scopes: z.ZodPipe<z.ZodOptional<z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<Map<string, ConfigurationDeclaration>, Record<string, unknown>>>>, z.ZodTransform<Map<string, ConfigurationDeclaration>, Map<string, ConfigurationDeclaration> | undefined>>;
|
|
14
16
|
kinds: z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<Map<string, ConfigurationDeclaration>, Record<string, unknown>>>;
|
|
15
17
|
tags: z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<Map<string, ConfigurationDeclaration>, Record<string, unknown>>>;
|
|
16
18
|
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
17
19
|
}, z.core.$strict>, z.ZodTransform<{
|
|
18
20
|
requireNamespace: boolean;
|
|
21
|
+
requireScopes: boolean;
|
|
22
|
+
scopes: Map<string, ConfigurationDeclaration>;
|
|
19
23
|
kinds: Map<string, ConfigurationDeclaration>;
|
|
20
24
|
tags: Map<string, ConfigurationDeclaration>;
|
|
21
25
|
ignorePatterns: string[];
|
|
22
26
|
}, {
|
|
23
27
|
"require-namespace": boolean;
|
|
28
|
+
"require-scopes": boolean;
|
|
29
|
+
scopes: Map<string, ConfigurationDeclaration>;
|
|
24
30
|
kinds: Map<string, ConfigurationDeclaration>;
|
|
25
31
|
tags: Map<string, ConfigurationDeclaration>;
|
|
26
32
|
ignore: string[];
|
|
@@ -61,6 +61,12 @@ const configurationSchema = z
|
|
|
61
61
|
"require-namespace": z
|
|
62
62
|
.boolean({ error: "Expected a boolean." })
|
|
63
63
|
.default(false),
|
|
64
|
+
"require-scopes": z
|
|
65
|
+
.boolean({ error: "Expected a boolean." })
|
|
66
|
+
.default(false),
|
|
67
|
+
scopes: configurationDeclarationsSchema
|
|
68
|
+
.optional()
|
|
69
|
+
.transform((declarations) => declarations ?? new Map()),
|
|
64
70
|
kinds: configurationDeclarationsSchema,
|
|
65
71
|
tags: configurationDeclarationsSchema,
|
|
66
72
|
ignore: z
|
|
@@ -71,6 +77,8 @@ const configurationSchema = z
|
|
|
71
77
|
}, { error: "Expected a mapping." })
|
|
72
78
|
.transform((configuration) => ({
|
|
73
79
|
requireNamespace: configuration["require-namespace"],
|
|
80
|
+
requireScopes: configuration["require-scopes"],
|
|
81
|
+
scopes: configuration.scopes,
|
|
74
82
|
kinds: configuration.kinds,
|
|
75
83
|
tags: configuration.tags,
|
|
76
84
|
ignorePatterns: configuration.ignore,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ConfigurationDeclaration } from "../configuration/index.js";
|
|
3
|
+
type DocumentRegistrationPolicy = {
|
|
4
|
+
requireNamespace: boolean;
|
|
5
|
+
requireScopes: boolean;
|
|
6
|
+
declaredScopes: Map<string, ConfigurationDeclaration>;
|
|
7
|
+
declaredKinds: Map<string, ConfigurationDeclaration>;
|
|
8
|
+
declaredTags: Map<string, ConfigurationDeclaration>;
|
|
9
|
+
};
|
|
3
10
|
type ClassifiedDocument = z.infer<ReturnType<typeof createDocumentSchema>> & {
|
|
4
11
|
body: string;
|
|
5
12
|
};
|
|
@@ -17,19 +24,20 @@ type DocumentClassification = {
|
|
|
17
24
|
kind: "registered";
|
|
18
25
|
document: ClassifiedDocument;
|
|
19
26
|
};
|
|
20
|
-
export declare function classifyDocument({ source,
|
|
27
|
+
export declare function classifyDocument({ source, registrationPolicy, path, }: {
|
|
21
28
|
source: string;
|
|
22
|
-
|
|
23
|
-
declaredKinds: Map<string, ConfigurationDeclaration>;
|
|
24
|
-
declaredTags: Map<string, ConfigurationDeclaration>;
|
|
29
|
+
registrationPolicy: DocumentRegistrationPolicy;
|
|
25
30
|
path: string;
|
|
26
31
|
}): DocumentClassification;
|
|
27
|
-
declare function createDocumentSchema({ declaredKinds, declaredTags, }: {
|
|
32
|
+
declare function createDocumentSchema({ requireScopes, declaredScopes, declaredKinds, declaredTags, }: {
|
|
33
|
+
requireScopes: boolean;
|
|
34
|
+
declaredScopes: Map<string, ConfigurationDeclaration>;
|
|
28
35
|
declaredKinds: Map<string, ConfigurationDeclaration>;
|
|
29
36
|
declaredTags: Map<string, ConfigurationDeclaration>;
|
|
30
37
|
}): z.ZodObject<{
|
|
31
38
|
kind: z.ZodString;
|
|
32
39
|
description: z.ZodString;
|
|
40
|
+
scopes: z.ZodPipe<z.ZodDefault<z.ZodArray<z.ZodUnknown>>, z.ZodTransform<string[], unknown[]>>;
|
|
33
41
|
tags: z.ZodPipe<z.ZodDefault<z.ZodArray<z.ZodUnknown>>, z.ZodTransform<string[], unknown[]>>;
|
|
34
42
|
}, z.core.$strict>;
|
|
35
43
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parse } from "yaml";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
const rawFrontmatterSchema = z.record(z.string(), z.unknown());
|
|
4
|
-
export function classifyDocument({ source,
|
|
4
|
+
export function classifyDocument({ source, registrationPolicy, path, }) {
|
|
5
5
|
const frontmatter = readFrontmatter(source);
|
|
6
6
|
if (frontmatter.kind === "malformed") {
|
|
7
7
|
return {
|
|
@@ -24,9 +24,7 @@ export function classifyDocument({ source, requireNamespace, declaredKinds, decl
|
|
|
24
24
|
return validateRegistration({
|
|
25
25
|
frontmatter: frontmatterResult.data,
|
|
26
26
|
body: frontmatter.body,
|
|
27
|
-
|
|
28
|
-
declaredKinds,
|
|
29
|
-
declaredTags,
|
|
27
|
+
registrationPolicy,
|
|
30
28
|
path,
|
|
31
29
|
});
|
|
32
30
|
}
|
|
@@ -48,7 +46,8 @@ function readFrontmatter(source) {
|
|
|
48
46
|
return { kind: "malformed" };
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
|
-
function validateRegistration({ frontmatter, body,
|
|
49
|
+
function validateRegistration({ frontmatter, body, registrationPolicy, path, }) {
|
|
50
|
+
const { requireNamespace, requireScopes, declaredScopes, declaredKinds, declaredTags, } = registrationPolicy;
|
|
52
51
|
const hasNamespace = Object.hasOwn(frontmatter, "waymark");
|
|
53
52
|
const hasRecognizedFlatMetadata = Object.hasOwn(frontmatter, "kind") &&
|
|
54
53
|
Object.hasOwn(frontmatter, "description");
|
|
@@ -75,10 +74,13 @@ function validateRegistration({ frontmatter, body, requireNamespace, declaredKin
|
|
|
75
74
|
: {
|
|
76
75
|
kind: frontmatter.kind,
|
|
77
76
|
description: frontmatter.description,
|
|
77
|
+
scopes: frontmatter.scopes,
|
|
78
78
|
tags: frontmatter.tags,
|
|
79
79
|
};
|
|
80
80
|
const fieldPrefix = namespaced ? "waymark." : "";
|
|
81
81
|
const metadataResult = createDocumentSchema({
|
|
82
|
+
requireScopes,
|
|
83
|
+
declaredScopes,
|
|
82
84
|
declaredKinds,
|
|
83
85
|
declaredTags,
|
|
84
86
|
}).safeParse(value);
|
|
@@ -98,7 +100,7 @@ function validateRegistration({ frontmatter, body, requireNamespace, declaredKin
|
|
|
98
100
|
document: { ...metadataResult.data, body },
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
|
-
function createDocumentSchema({ declaredKinds, declaredTags, }) {
|
|
103
|
+
function createDocumentSchema({ requireScopes, declaredScopes, declaredKinds, declaredTags, }) {
|
|
102
104
|
return z.strictObject({
|
|
103
105
|
kind: z
|
|
104
106
|
.string({
|
|
@@ -126,6 +128,42 @@ function createDocumentSchema({ declaredKinds, declaredTags, }) {
|
|
|
126
128
|
.refine((description) => description.trim() !== "", {
|
|
127
129
|
error: "Document Description must be a non-empty string.",
|
|
128
130
|
}),
|
|
131
|
+
scopes: z
|
|
132
|
+
.array(z.unknown(), { error: "Expected a YAML sequence of scopes." })
|
|
133
|
+
.default([])
|
|
134
|
+
.superRefine((scopes, context) => {
|
|
135
|
+
if (requireScopes && scopes.length === 0) {
|
|
136
|
+
context.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
message: "At least one scope is required.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const seenScopes = new Set();
|
|
142
|
+
for (const scope of scopes) {
|
|
143
|
+
if (typeof scope !== "string" || scope.trim() === "") {
|
|
144
|
+
context.addIssue({
|
|
145
|
+
code: "custom",
|
|
146
|
+
message: "Every scope must be a non-empty string.",
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (seenScopes.has(scope)) {
|
|
151
|
+
context.addIssue({
|
|
152
|
+
code: "custom",
|
|
153
|
+
message: `Duplicate scope "${scope}".`,
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
seenScopes.add(scope);
|
|
158
|
+
if (!declaredScopes.has(scope)) {
|
|
159
|
+
context.addIssue({
|
|
160
|
+
code: "custom",
|
|
161
|
+
message: `Undeclared scope "${scope}".`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
.transform((scopes) => scopes.filter((scope) => typeof scope === "string")),
|
|
129
167
|
tags: z
|
|
130
168
|
.array(z.unknown(), { error: "Expected a YAML sequence of tags." })
|
|
131
169
|
.default([])
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Configuration } from "../configuration/index.js";
|
|
2
2
|
import type { WaymarkDocument } from "./scan.js";
|
|
3
3
|
type FilterableDocument = {
|
|
4
|
+
scopes: string[];
|
|
4
5
|
kind: string;
|
|
5
6
|
tags: string[];
|
|
6
7
|
};
|
|
7
8
|
type MetadataCriteria = {
|
|
8
9
|
method: "filter-groups";
|
|
10
|
+
scopes: Set<string>;
|
|
9
11
|
kinds: Set<string>;
|
|
10
12
|
tags: Set<string>;
|
|
11
13
|
requiredTags: Set<string>;
|
|
@@ -22,8 +24,9 @@ export declare function filterDocuments({ documents, configuration, criteria, }:
|
|
|
22
24
|
configuration: Configuration;
|
|
23
25
|
criteria: DocumentCriteria;
|
|
24
26
|
}): WaymarkDocument[];
|
|
25
|
-
export declare function parseMetadataFilter({ expression, declaredKinds, declaredTags, }: {
|
|
27
|
+
export declare function parseMetadataFilter({ expression, declaredScopes, declaredKinds, declaredTags, }: {
|
|
26
28
|
expression: string;
|
|
29
|
+
declaredScopes: Set<string>;
|
|
27
30
|
declaredKinds: Set<string>;
|
|
28
31
|
declaredTags: Set<string>;
|
|
29
32
|
}): (document: FilterableDocument) => boolean;
|
package/dist/documents/filter.js
CHANGED
|
@@ -3,10 +3,13 @@ export function filterDocuments({ documents, configuration, criteria, }) {
|
|
|
3
3
|
const matchesMetadata = metadata.method === "filter-expression"
|
|
4
4
|
? parseMetadataFilter({
|
|
5
5
|
expression: metadata.expression,
|
|
6
|
+
declaredScopes: new Set(configuration.scopes.keys()),
|
|
6
7
|
declaredKinds: new Set(configuration.kinds.keys()),
|
|
7
8
|
declaredTags: new Set(configuration.tags.keys()),
|
|
8
9
|
})
|
|
9
|
-
: (document) => (metadata.
|
|
10
|
+
: (document) => (metadata.scopes.size === 0 ||
|
|
11
|
+
document.scopes.some((scope) => metadata.scopes.has(scope))) &&
|
|
12
|
+
(metadata.kinds.size === 0 || metadata.kinds.has(document.kind)) &&
|
|
10
13
|
(metadata.tags.size === 0 ||
|
|
11
14
|
document.tags.some((tag) => metadata.tags.has(tag))) &&
|
|
12
15
|
[...metadata.requiredTags].every((tag) => document.tags.includes(tag));
|
|
@@ -17,8 +20,8 @@ export function filterDocuments({ documents, configuration, criteria, }) {
|
|
|
17
20
|
document.body.toLowerCase().includes(normalizedQuery)))
|
|
18
21
|
.sort((left, right) => compareText(left.path, right.path));
|
|
19
22
|
}
|
|
20
|
-
export function parseMetadataFilter({ expression, declaredKinds, declaredTags, }) {
|
|
21
|
-
const tokens = tokenize(expression, declaredKinds, declaredTags);
|
|
23
|
+
export function parseMetadataFilter({ expression, declaredScopes, declaredKinds, declaredTags, }) {
|
|
24
|
+
const tokens = tokenize(expression, declaredScopes, declaredKinds, declaredTags);
|
|
22
25
|
let nextTokenIndex = 0;
|
|
23
26
|
function peek() {
|
|
24
27
|
return tokens[nextTokenIndex];
|
|
@@ -26,14 +29,16 @@ export function parseMetadataFilter({ expression, declaredKinds, declaredTags, }
|
|
|
26
29
|
function consume() {
|
|
27
30
|
const token = tokens[nextTokenIndex];
|
|
28
31
|
if (!token) {
|
|
29
|
-
throw syntaxError(expression.length, 'Expected a kind
|
|
32
|
+
throw syntaxError(expression.length, 'Expected a scope:, kind:, or tag: predicate, or "(".');
|
|
30
33
|
}
|
|
31
34
|
nextTokenIndex += 1;
|
|
32
35
|
return token;
|
|
33
36
|
}
|
|
34
37
|
function parsePrimary() {
|
|
35
38
|
const token = consume();
|
|
36
|
-
if (token.type === "
|
|
39
|
+
if (token.type === "scope" ||
|
|
40
|
+
token.type === "kind" ||
|
|
41
|
+
token.type === "tag") {
|
|
37
42
|
return {
|
|
38
43
|
type: token.type,
|
|
39
44
|
identifier: token.identifier,
|
|
@@ -48,7 +53,7 @@ export function parseMetadataFilter({ expression, declaredKinds, declaredTags, }
|
|
|
48
53
|
consume();
|
|
49
54
|
return expressionNode;
|
|
50
55
|
}
|
|
51
|
-
throw syntaxError(token.position, 'Expected a kind
|
|
56
|
+
throw syntaxError(token.position, 'Expected a scope:, kind:, or tag: predicate, or "(".');
|
|
52
57
|
}
|
|
53
58
|
function parseNot() {
|
|
54
59
|
if (peek()?.type !== "not")
|
|
@@ -78,7 +83,8 @@ export function parseMetadataFilter({ expression, declaredKinds, declaredTags, }
|
|
|
78
83
|
const root = parseOr();
|
|
79
84
|
const remainingToken = peek();
|
|
80
85
|
if (remainingToken) {
|
|
81
|
-
const message = remainingToken.type === "
|
|
86
|
+
const message = remainingToken.type === "scope" ||
|
|
87
|
+
remainingToken.type === "kind" ||
|
|
82
88
|
remainingToken.type === "tag" ||
|
|
83
89
|
remainingToken.type === "not" ||
|
|
84
90
|
remainingToken.type === "left-parenthesis"
|
|
@@ -88,7 +94,7 @@ export function parseMetadataFilter({ expression, declaredKinds, declaredTags, }
|
|
|
88
94
|
}
|
|
89
95
|
return (document) => evaluate(root, document);
|
|
90
96
|
}
|
|
91
|
-
function tokenize(expression, declaredKinds, declaredTags) {
|
|
97
|
+
function tokenize(expression, declaredScopes, declaredKinds, declaredTags) {
|
|
92
98
|
const tokens = [];
|
|
93
99
|
let position = 0;
|
|
94
100
|
while (position < expression.length) {
|
|
@@ -125,17 +131,23 @@ function tokenize(expression, declaredKinds, declaredTags) {
|
|
|
125
131
|
tokens.push({ type: "or", position: tokenPosition });
|
|
126
132
|
continue;
|
|
127
133
|
}
|
|
128
|
-
const predicate = /^(kind|tag):([a-z0-9]+(?:-[a-z0-9]+)*)$/.exec(value);
|
|
134
|
+
const predicate = /^(scope|kind|tag):([a-z0-9]+(?:-[a-z0-9]+)*)$/.exec(value);
|
|
129
135
|
if (!predicate) {
|
|
130
|
-
throw syntaxError(tokenPosition, `Unsupported token "${value}". Expected kind:<identifier>, tag:<identifier>, NOT, AND, OR, or parentheses.`);
|
|
136
|
+
throw syntaxError(tokenPosition, `Unsupported token "${value}". Expected scope:<identifier>, kind:<identifier>, tag:<identifier>, NOT, AND, OR, or parentheses.`);
|
|
131
137
|
}
|
|
132
138
|
const [, predicateType, identifier] = predicate;
|
|
133
|
-
if (predicateType !== "
|
|
139
|
+
if (predicateType !== "scope" &&
|
|
140
|
+
predicateType !== "kind" &&
|
|
141
|
+
predicateType !== "tag") {
|
|
134
142
|
throw new Error("Metadata Filter parser invariant failed.");
|
|
135
143
|
}
|
|
136
144
|
if (!identifier)
|
|
137
145
|
throw new Error("Metadata Filter parser invariant failed.");
|
|
138
|
-
const declarations = predicateType === "
|
|
146
|
+
const declarations = predicateType === "scope"
|
|
147
|
+
? declaredScopes
|
|
148
|
+
: predicateType === "kind"
|
|
149
|
+
? declaredKinds
|
|
150
|
+
: declaredTags;
|
|
139
151
|
if (!declarations.has(identifier)) {
|
|
140
152
|
throw syntaxError(tokenPosition, `Undeclared ${predicateType} "${identifier}".`);
|
|
141
153
|
}
|
|
@@ -149,6 +161,8 @@ function tokenize(expression, declaredKinds, declaredTags) {
|
|
|
149
161
|
}
|
|
150
162
|
function evaluate(node, document) {
|
|
151
163
|
switch (node.type) {
|
|
164
|
+
case "scope":
|
|
165
|
+
return document.scopes.includes(node.identifier);
|
|
152
166
|
case "kind":
|
|
153
167
|
return document.kind === node.identifier;
|
|
154
168
|
case "tag":
|
package/dist/documents/index.js
CHANGED
package/dist/documents/scan.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type WaymarkDocument = {
|
|
|
3
3
|
path: string;
|
|
4
4
|
kind: string;
|
|
5
5
|
description: string;
|
|
6
|
+
scopes: string[];
|
|
6
7
|
tags: string[];
|
|
7
8
|
body: string;
|
|
8
9
|
};
|
|
@@ -19,6 +20,7 @@ export type DocumentScanResult = {
|
|
|
19
20
|
kind: "valid";
|
|
20
21
|
documents: WaymarkDocument[];
|
|
21
22
|
unregisteredDocuments: string[];
|
|
23
|
+
scopeUsageCounts: Map<string, number>;
|
|
22
24
|
kindUsageCounts: Map<string, number>;
|
|
23
25
|
tagUsageCounts: Map<string, number>;
|
|
24
26
|
} | {
|
package/dist/documents/scan.js
CHANGED
|
@@ -4,6 +4,7 @@ import { convertPathToPattern, globby } from "globby";
|
|
|
4
4
|
import { allowedConfigFileNames, } from "../configuration/index.js";
|
|
5
5
|
import { isErrorWithCode } from "../filesystem.js";
|
|
6
6
|
import { classifyDocument } from "./classify.js";
|
|
7
|
+
import { createUsageCounts, incrementUsageCount } from "./usage-counts.js";
|
|
7
8
|
export async function scanDocuments({ rootPath, configuration, scope, }) {
|
|
8
9
|
const { rootPath: scanRootPath, scope: resolvedScope } = await resolveDocumentScanScope({ rootPath, scope });
|
|
9
10
|
const discoveredPaths = await globby(createCandidatePatterns(resolvedScope), {
|
|
@@ -16,10 +17,18 @@ export async function scanDocuments({ rootPath, configuration, scope, }) {
|
|
|
16
17
|
extglob: false,
|
|
17
18
|
});
|
|
18
19
|
const documents = [];
|
|
20
|
+
const scopeUsageCounts = createUsageCounts(configuration.scopes);
|
|
19
21
|
const kindUsageCounts = createUsageCounts(configuration.kinds);
|
|
20
22
|
const tagUsageCounts = createUsageCounts(configuration.tags);
|
|
21
23
|
const diagnostics = [];
|
|
22
24
|
const unregisteredDocuments = [];
|
|
25
|
+
const registrationPolicy = {
|
|
26
|
+
requireNamespace: configuration.requireNamespace,
|
|
27
|
+
requireScopes: configuration.requireScopes,
|
|
28
|
+
declaredScopes: configuration.scopes,
|
|
29
|
+
declaredKinds: configuration.kinds,
|
|
30
|
+
declaredTags: configuration.tags,
|
|
31
|
+
};
|
|
23
32
|
for (const path of discoveredPaths.sort(compareText)) {
|
|
24
33
|
if (allowedConfigFileNames.some((fileName) => fileName === path))
|
|
25
34
|
continue;
|
|
@@ -34,9 +43,7 @@ export async function scanDocuments({ rootPath, configuration, scope, }) {
|
|
|
34
43
|
const source = await readFile(join(scanRootPath, path), "utf8");
|
|
35
44
|
const classification = classifyDocument({
|
|
36
45
|
source,
|
|
37
|
-
|
|
38
|
-
declaredKinds: configuration.kinds,
|
|
39
|
-
declaredTags: configuration.tags,
|
|
46
|
+
registrationPolicy,
|
|
40
47
|
path,
|
|
41
48
|
});
|
|
42
49
|
if (classification.kind === "unregistered") {
|
|
@@ -49,6 +56,9 @@ export async function scanDocuments({ rootPath, configuration, scope, }) {
|
|
|
49
56
|
}
|
|
50
57
|
const document = { path, ...classification.document };
|
|
51
58
|
documents.push(document);
|
|
59
|
+
for (const scope of document.scopes) {
|
|
60
|
+
incrementUsageCount(scopeUsageCounts, scope);
|
|
61
|
+
}
|
|
52
62
|
incrementUsageCount(kindUsageCounts, document.kind);
|
|
53
63
|
for (const tag of document.tags)
|
|
54
64
|
incrementUsageCount(tagUsageCounts, tag);
|
|
@@ -63,6 +73,7 @@ export async function scanDocuments({ rootPath, configuration, scope, }) {
|
|
|
63
73
|
kind: "valid",
|
|
64
74
|
documents,
|
|
65
75
|
unregisteredDocuments,
|
|
76
|
+
scopeUsageCounts,
|
|
66
77
|
kindUsageCounts,
|
|
67
78
|
tagUsageCounts,
|
|
68
79
|
};
|
|
@@ -119,15 +130,6 @@ function createCandidatePatterns(scope) {
|
|
|
119
130
|
...allowedConfigFileNames.map((fileName) => `${candidatePrefix}${fileName}`),
|
|
120
131
|
];
|
|
121
132
|
}
|
|
122
|
-
function createUsageCounts(declarations) {
|
|
123
|
-
return new Map([...declarations.keys()].map((identifier) => [identifier, 0]));
|
|
124
|
-
}
|
|
125
|
-
function incrementUsageCount(usageCounts, identifier) {
|
|
126
|
-
const currentCount = usageCounts.get(identifier);
|
|
127
|
-
if (currentCount !== undefined) {
|
|
128
|
-
usageCounts.set(identifier, currentCount + 1);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
133
|
function compareDiagnostics(left, right) {
|
|
132
134
|
return (compareText(left.path, right.path) ||
|
|
133
135
|
compareText(left.field, right.field) ||
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type DocumentMetadata = {
|
|
2
|
+
scopes: string[];
|
|
3
|
+
kind: string;
|
|
4
|
+
tags: string[];
|
|
5
|
+
};
|
|
6
|
+
type DocumentMetadataUsageCounts = {
|
|
7
|
+
kindUsageCounts: Map<string, number>;
|
|
8
|
+
tagUsageCounts: Map<string, number>;
|
|
9
|
+
};
|
|
10
|
+
export declare function countDocumentMetadataUsage({ documents, selectedScopes, declaredKinds, declaredTags, }: {
|
|
11
|
+
documents: DocumentMetadata[];
|
|
12
|
+
selectedScopes: Set<string>;
|
|
13
|
+
declaredKinds: Map<string, unknown>;
|
|
14
|
+
declaredTags: Map<string, unknown>;
|
|
15
|
+
}): DocumentMetadataUsageCounts;
|
|
16
|
+
export declare function createUsageCounts(declarations: Map<string, unknown>): Map<string, number>;
|
|
17
|
+
export declare function incrementUsageCount(usageCounts: Map<string, number>, identifier: string): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function countDocumentMetadataUsage({ documents, selectedScopes, declaredKinds, declaredTags, }) {
|
|
2
|
+
const kindUsageCounts = createUsageCounts(declaredKinds);
|
|
3
|
+
const tagUsageCounts = createUsageCounts(declaredTags);
|
|
4
|
+
for (const document of documents) {
|
|
5
|
+
if (selectedScopes.size > 0 &&
|
|
6
|
+
!document.scopes.some((scope) => selectedScopes.has(scope))) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
incrementUsageCount(kindUsageCounts, document.kind);
|
|
10
|
+
for (const tag of document.tags) {
|
|
11
|
+
incrementUsageCount(tagUsageCounts, tag);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { kindUsageCounts, tagUsageCounts };
|
|
15
|
+
}
|
|
16
|
+
export function createUsageCounts(declarations) {
|
|
17
|
+
return new Map([...declarations.keys()].map((identifier) => [identifier, 0]));
|
|
18
|
+
}
|
|
19
|
+
export function incrementUsageCount(usageCounts, identifier) {
|
|
20
|
+
const currentCount = usageCounts.get(identifier);
|
|
21
|
+
if (currentCount !== undefined) {
|
|
22
|
+
usageCounts.set(identifier, currentCount + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|