waymark-docs 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +82 -104
- package/dist/cli.js +2 -0
- package/dist/commands/find.js +25 -6
- 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 +17 -13
- 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.1](https://github.com/ysfaran/waymark/compare/v0.3.0...v0.3.1) (2026-09-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **cli:** support root document inventory ([#14](https://github.com/ysfaran/waymark/issues/14)) ([6c61727](https://github.com/ysfaran/waymark/commit/6c617278dc20bf938f3ef07c0975893483de9825))
|
|
11
|
+
|
|
12
|
+
## [0.3.0](https://github.com/ysfaran/waymark/compare/v0.2.2...v0.3.0) (2026-09-25)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **cli:** add document scope option ([#12](https://github.com/ysfaran/waymark/issues/12)) ([2a6b67f](https://github.com/ysfaran/waymark/commit/2a6b67f00244de004a0c1eb47548ace90a720e7d))
|
|
18
|
+
|
|
5
19
|
## [0.2.2](https://github.com/ysfaran/waymark/compare/v0.2.1...v0.2.2) (2026-09-18)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -4,12 +4,18 @@
|
|
|
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
|
|
7
|
+
or MDX files and agents can discover only relevant paths before opening a
|
|
8
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
|
|
10
|
+
but remains deterministic and token-efficient. Unlike RAG or MCP-backed
|
|
11
11
|
retrieval, it needs no ranking, maintained index or retrieval infrastructure.
|
|
12
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`)
|
|
18
|
+
|
|
13
19
|

|
|
14
20
|
|
|
15
21
|
## Table of contents
|
|
@@ -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,88 +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:
|
|
56
|
+
1. Install the skills in the repository you want to set up:
|
|
79
57
|
|
|
80
58
|
```sh
|
|
81
|
-
npx waymark
|
|
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
|
|
59
|
+
npx skills add ysfaran/waymark --skill waymark --skill waymark-setup
|
|
91
60
|
```
|
|
92
61
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```text
|
|
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.
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Waymark uses `waymark.yml` by default and also recognizes `waymark.yaml`. It
|
|
119
|
-
looks for the configuration in the current directory and its ancestors, so
|
|
120
|
-
commands can also run from a nested repository directory. Use only one filename
|
|
121
|
-
per directory.
|
|
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.
|
|
122
69
|
|
|
123
70
|
## Commands
|
|
124
71
|
|
|
@@ -126,6 +73,7 @@ per directory.
|
|
|
126
73
|
| ---------------- | --------------------------------------------------------- |
|
|
127
74
|
| `waymark init` | Create a starter configuration |
|
|
128
75
|
| `waymark status` | Validate and summarize the repository |
|
|
76
|
+
| `waymark show` | List declared scopes, kinds and tags |
|
|
129
77
|
| `waymark find` | Find registered documents across the repository |
|
|
130
78
|
| `waymark ls` | Audit registered or unregistered documents in a directory |
|
|
131
79
|
| `waymark help` | Show CLI or command-specific help |
|
|
@@ -148,17 +96,26 @@ npx waymark init
|
|
|
148
96
|
```
|
|
149
97
|
|
|
150
98
|
The generated file explains metadata namespacing and includes declarations to
|
|
151
|
-
replace with your own kind and tag:
|
|
99
|
+
replace with your own scope, kind and tag:
|
|
152
100
|
|
|
153
101
|
```yaml
|
|
154
102
|
# When true, document metadata must be nested under a `waymark` frontmatter key.
|
|
155
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
|
|
156
108
|
kinds:
|
|
157
109
|
example-kind: Explain when agents should read this kind of document
|
|
158
110
|
tags:
|
|
159
111
|
example-tag: Explain the topic represented by this tag
|
|
160
112
|
```
|
|
161
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
|
+
|
|
162
119
|
To skip generated or vendored documentation during discovery, add `ignore`
|
|
163
120
|
patterns:
|
|
164
121
|
|
|
@@ -179,8 +136,8 @@ configuration beneath another Waymark root.
|
|
|
179
136
|
|
|
180
137
|
Validate the Waymark configuration and all discovered Waymark Documents, then
|
|
181
138
|
print the repository root and counts for registered documents, unregistered
|
|
182
|
-
documents, kinds and tags. Invalid repositories produce diagnostics
|
|
183
|
-
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.
|
|
184
141
|
|
|
185
142
|
```text
|
|
186
143
|
Usage: waymark status [options]
|
|
@@ -188,8 +145,7 @@ Usage: waymark status [options]
|
|
|
188
145
|
Validate and summarize the Waymark repository
|
|
189
146
|
|
|
190
147
|
Options:
|
|
191
|
-
-
|
|
192
|
-
-h, --help display help for command
|
|
148
|
+
-h, --help display help for command
|
|
193
149
|
```
|
|
194
150
|
|
|
195
151
|
Validate the repository:
|
|
@@ -198,16 +154,33 @@ Validate the repository:
|
|
|
198
154
|
npx waymark status
|
|
199
155
|
```
|
|
200
156
|
|
|
201
|
-
|
|
157
|
+
### `waymark show`
|
|
202
158
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
206
163
|
|
|
207
|
-
|
|
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
|
+
```
|
|
208
178
|
|
|
209
179
|
```sh
|
|
210
|
-
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
|
|
211
184
|
```
|
|
212
185
|
|
|
213
186
|
### `waymark find`
|
|
@@ -221,6 +194,8 @@ Usage: waymark find [options]
|
|
|
221
194
|
Discover Waymark Documents
|
|
222
195
|
|
|
223
196
|
Options:
|
|
197
|
+
--scopes <identifiers> match any scope (comma-separated,
|
|
198
|
+
repeatable)
|
|
224
199
|
-k, --kinds <identifiers> match any kind (comma-separated, repeatable)
|
|
225
200
|
-t, --tags <identifiers> match any tag (comma-separated, repeatable)
|
|
226
201
|
-T, --require-tags <identifiers> require every tag (comma-separated,
|
|
@@ -228,25 +203,26 @@ Options:
|
|
|
228
203
|
-f, --filter <expression> match a boolean filter expression
|
|
229
204
|
-q, --query <text> match literal text content
|
|
230
205
|
(case-insensitive)
|
|
231
|
-
-s, --show <fields> show
|
|
232
|
-
(comma-separated)
|
|
206
|
+
-s, --show <fields> show only selected metadata fields
|
|
207
|
+
(comma-separated; defaults to all)
|
|
233
208
|
--json return a flat JSON array
|
|
234
209
|
--tree output documents as directory tree
|
|
235
210
|
-h, --help display help for command
|
|
236
211
|
```
|
|
237
212
|
|
|
238
213
|
Simple filter values use OR within an option. Different options combine with
|
|
239
|
-
AND:
|
|
214
|
+
AND. A scope filter excludes documents that declare no scope:
|
|
240
215
|
|
|
241
216
|
```sh
|
|
242
|
-
#
|
|
243
|
-
|
|
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
|
|
244
220
|
|
|
245
221
|
# Kind is adr, and both architecture AND typescript tags are required
|
|
246
222
|
npx waymark find --kinds adr --require-tags architecture,typescript
|
|
247
223
|
```
|
|
248
224
|
|
|
249
|
-
The
|
|
225
|
+
The four simple metadata filters are repeatable. Repeating an option is
|
|
250
226
|
equivalent to passing a comma-separated list:
|
|
251
227
|
|
|
252
228
|
```sh
|
|
@@ -260,19 +236,21 @@ be combined with either simple or boolean metadata filters:
|
|
|
260
236
|
npx waymark find --kinds convention --query "dependency injection"
|
|
261
237
|
```
|
|
262
238
|
|
|
263
|
-
Use `--filter` for advanced
|
|
264
|
-
`tag:`, `NOT`, `AND`, `OR` and parentheses:
|
|
239
|
+
Use `--filter` for advanced Boolean expressions over metadata with `scope:`,
|
|
240
|
+
`kind:`, `tag:`, `NOT`, `AND`, `OR` and parentheses:
|
|
265
241
|
|
|
266
242
|
```sh
|
|
267
|
-
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'
|
|
268
244
|
```
|
|
269
245
|
|
|
270
|
-
`--filter` cannot be combined with `--kinds`, `--tags` or
|
|
246
|
+
`--filter` cannot be combined with `--scopes`, `--kinds`, `--tags` or
|
|
247
|
+
`--require-tags`.
|
|
271
248
|
|
|
272
|
-
|
|
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:
|
|
273
251
|
|
|
274
252
|
```sh
|
|
275
|
-
npx waymark find --kinds convention --show kind,
|
|
253
|
+
npx waymark find --kinds convention --show kind,description
|
|
276
254
|
```
|
|
277
255
|
|
|
278
256
|
Return structured output for scripts and agents:
|
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,24 +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")
|
|
9
|
+
.option("--scopes <identifiers>", "match any scope (comma-separated, repeatable)", collectOptionValue)
|
|
8
10
|
.option("-k, --kinds <identifiers>", "match any kind (comma-separated, repeatable)", collectOptionValue)
|
|
9
11
|
.option("-t, --tags <identifiers>", "match any tag (comma-separated, repeatable)", collectOptionValue)
|
|
10
12
|
.option("-T, --require-tags <identifiers>", "require every tag (comma-separated, repeatable)", collectOptionValue)
|
|
11
13
|
.option("-f, --filter <expression>", "match a boolean filter expression")
|
|
12
14
|
.option("-q, --query <text>", "match literal text content (case-insensitive)")
|
|
13
|
-
.option("-s, --show <fields>", "show
|
|
15
|
+
.option("-s, --show <fields>", "show only selected metadata fields (comma-separated; defaults to all)")
|
|
14
16
|
.option("--json", "return a flat JSON array")
|
|
15
17
|
.option("--tree", "output documents as directory tree")
|
|
16
18
|
.action(async (options) => {
|
|
19
|
+
const scopes = options.scopes ?? [];
|
|
17
20
|
const kinds = options.kinds ?? [];
|
|
18
21
|
const tags = options.tags ?? [];
|
|
19
22
|
const requiredTags = options.requireTags ?? [];
|
|
20
23
|
if (options.filter !== undefined &&
|
|
21
|
-
(
|
|
22
|
-
|
|
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.");
|
|
23
29
|
}
|
|
24
30
|
if (options.json && options.tree) {
|
|
25
31
|
throw new Error("--json cannot be combined with --tree.");
|
|
@@ -33,6 +39,12 @@ export function createFindCommand() {
|
|
|
33
39
|
const metadataCriteria = options.filter === undefined
|
|
34
40
|
? {
|
|
35
41
|
method: "filter-groups",
|
|
42
|
+
scopes: parseIdentifierOptions({
|
|
43
|
+
optionName: "--scopes",
|
|
44
|
+
values: scopes,
|
|
45
|
+
declarations: configuration.scopes,
|
|
46
|
+
declarationName: "scope",
|
|
47
|
+
}),
|
|
36
48
|
kinds: parseIdentifierOptions({
|
|
37
49
|
optionName: "--kinds",
|
|
38
50
|
values: kinds,
|
|
@@ -83,6 +95,8 @@ export function createFindCommand() {
|
|
|
83
95
|
}
|
|
84
96
|
function projectDocument(document, shownFields) {
|
|
85
97
|
const projection = { path: document.path };
|
|
98
|
+
if (shownFields.has("scopes"))
|
|
99
|
+
projection.scopes = document.scopes;
|
|
86
100
|
if (shownFields.has("kind"))
|
|
87
101
|
projection.kind = document.kind;
|
|
88
102
|
if (shownFields.has("tags"))
|
|
@@ -94,11 +108,11 @@ function projectDocument(document, shownFields) {
|
|
|
94
108
|
}
|
|
95
109
|
function parseShownFields(value) {
|
|
96
110
|
if (value === undefined)
|
|
97
|
-
return new Set();
|
|
111
|
+
return new Set(SHOWN_FIELDS);
|
|
98
112
|
const shownFields = new Set();
|
|
99
113
|
for (const field of value.split(",")) {
|
|
100
|
-
if (field
|
|
101
|
-
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.`);
|
|
102
116
|
}
|
|
103
117
|
if (shownFields.has(field)) {
|
|
104
118
|
throw new Error(`Duplicate find field "${field}".`);
|
|
@@ -107,8 +121,13 @@ function parseShownFields(value) {
|
|
|
107
121
|
}
|
|
108
122
|
return shownFields;
|
|
109
123
|
}
|
|
124
|
+
function isShownField(field) {
|
|
125
|
+
return SHOWN_FIELDS.some((shownField) => shownField === field);
|
|
126
|
+
}
|
|
110
127
|
function renderDocumentLine(document, shownFields, displayedPath = document.path) {
|
|
111
128
|
let line = displayedPath;
|
|
129
|
+
if (shownFields.has("scopes"))
|
|
130
|
+
line += ` [${document.scopes.join(",")}]`;
|
|
112
131
|
if (shownFields.has("kind"))
|
|
113
132
|
line += ` [${document.kind}]`;
|
|
114
133
|
if (shownFields.has("tags"))
|
|
@@ -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
|
};
|
|
@@ -108,7 +119,9 @@ function createCandidatePatterns(scope) {
|
|
|
108
119
|
...allowedConfigFileNames.map((fileName) => `**/${fileName}`),
|
|
109
120
|
];
|
|
110
121
|
}
|
|
111
|
-
const directoryPattern =
|
|
122
|
+
const directoryPattern = scope.relativeDirectoryPath === ""
|
|
123
|
+
? ""
|
|
124
|
+
: convertPathToPattern(scope.relativeDirectoryPath);
|
|
112
125
|
const directoryPrefix = directoryPattern === "" ? "" : `${directoryPattern}/`;
|
|
113
126
|
const candidatePrefix = scope.recursive
|
|
114
127
|
? `${directoryPrefix}**/`
|
|
@@ -119,15 +132,6 @@ function createCandidatePatterns(scope) {
|
|
|
119
132
|
...allowedConfigFileNames.map((fileName) => `${candidatePrefix}${fileName}`),
|
|
120
133
|
];
|
|
121
134
|
}
|
|
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
135
|
function compareDiagnostics(left, right) {
|
|
132
136
|
return (compareText(left.path, right.path) ||
|
|
133
137
|
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
|
+
}
|