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 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, and agents can discover only relevant paths before opening a
8
- document—without reading a large index or following linked navigation files
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; unlike RAG or MCP-backed
11
- retrieval, it needs no ranking, maintained index, or retrieval infrastructure.
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
  ![Waymark reduces the effort required to find relevant context without retrieval infrastructure.](https://raw.githubusercontent.com/ysfaran/waymark/main/docs/assets/why-waymark.svg)
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-docs` package as a development dependency:
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. **Create a Waymark configuration.**
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 waymark find --kinds convention --tags typescript --show description
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
- Waymark uses `waymark.yml` by default and also recognizes `waymark.yaml`. It
106
- looks for the configuration in the current directory and its ancestors, so
107
- commands can also run from a nested repository directory. Use only one filename
108
- 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.
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 Display help for the command
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 `*`, `?`, and
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, kinds, and tags. Invalid repositories produce diagnostics and a
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
- -s, --show <fields> Show declared kind and tag details (kind,tags)
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
- List every declared kind and tag with its description and usage count:
157
+ ### `waymark show`
185
158
 
186
- ```sh
187
- npx waymark status --show kind,tags
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
- Show only kind details:
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 status --show kind
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
- -k, --kinds <identifiers> Match any kind (comma-separated, repeatable)
206
- -t, --tags <identifiers> Match any tag (comma-separated, repeatable)
207
- -T, --require-tags <identifiers> Require every tag (comma-separated, repeatable)
208
- -f, --filter <expression> Match a Boolean metadata filter
209
- -q, --query <text> Match a literal content query
210
- -s, --show <fields> Show kind, tags, and description
211
- --json Return a flat JSON array
212
- --tree Return a directory tree
213
- -h, --help Display help for the command
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
- # Kind is adr OR convention, and at least one tag is typescript OR architecture
221
- npx waymark find --kinds adr,convention --tags typescript,architecture
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 three simple metadata filters are repeatable. Repeating an option is
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 Boolean metadata filters:
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 metadata expressions with `kind:`, `tag:`, `NOT`,
242
- `AND`, `OR`, and parentheses:
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`, or `--require-tags`.
246
+ `--filter` cannot be combined with `--scopes`, `--kinds`, `--tags` or
247
+ `--require-tags`.
249
248
 
250
- Add metadata fields to the default line-oriented output with `--show`:
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,tags,description
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 Directory to inspect (defaults to the current directory)
283
+ directory directory to inspect (defaults to the current directory)
282
284
 
283
285
  Options:
284
- -R, --recursive Inspect directories recursively
285
- -u, --unregistered List only unregistered documents
286
- -h, --help Display help for the command
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, and Git directory boundaries.
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 {
@@ -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("-k, --kinds <identifiers>", "Match any Document Kind (comma-separated, repeatable)", collectOptionValue, [])
9
- .option("-t, --tags <identifiers>", "Match any Document Tag (comma-separated, repeatable)", collectOptionValue, [])
10
- .option("-T, --require-tags <identifiers>", "Require every Document Tag (comma-separated, repeatable)", collectOptionValue, [])
11
- .option("-f, --filter <expression>", "Match a Boolean Metadata Filter")
12
- .option("-q, --query <text>", "Match a literal Content Query")
13
- .option("-s, --show <fields>", "Show kind, tags, and description (comma-separated)")
14
- .option("--json", "Return a flat JSON array")
15
- .option("--tree", "Return a directory-tree presentation")
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
- (options.kinds.length > 0 ||
19
- options.tags.length > 0 ||
20
- options.requireTags.length > 0)) {
21
- throw new Error("--filter cannot be combined with --kinds, --tags, or --require-tags.");
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: options.kinds,
50
+ values: kinds,
38
51
  declarations: configuration.kinds,
39
52
  declarationName: "kind",
40
53
  }),
41
54
  tags: parseIdentifierOptions({
42
55
  optionName: "--tags",
43
- values: options.tags,
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: options.requireTags,
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 !== "kind" && field !== "tags" && field !== "description") {
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();
@@ -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]", "Directory to inspect")
10
- .option("-R, --recursive", "Inspect directories recursively")
11
- .option("-u, --unregistered", "List only Unregistered Documents")
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,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createShowCommand(): Command;
@@ -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
+ }
@@ -1,13 +1,11 @@
1
1
  import { Command } from "commander";
2
- import { loadConfiguration, } from "../configuration/index.js";
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
- .option("-s, --show <fields>", "Show declared kind and tag details (kind,tags)")
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
- let output = `Root: ${rootPath}\n` +
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, requireNamespace, declaredKinds, declaredTags, path, }: {
27
+ export declare function classifyDocument({ source, registrationPolicy, path, }: {
21
28
  source: string;
22
- requireNamespace: boolean;
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, requireNamespace, declaredKinds, declaredTags, path, }) {
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
- requireNamespace,
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, requireNamespace, declaredKinds, declaredTags, path, }) {
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;
@@ -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.kinds.size === 0 || metadata.kinds.has(document.kind)) &&
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: or tag: predicate, or "(".');
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 === "kind" || token.type === "tag") {
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: or tag: predicate, or "(".');
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 === "kind" ||
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 !== "kind" && predicateType !== "tag") {
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 === "kind" ? declaredKinds : declaredTags;
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":
@@ -1,3 +1,4 @@
1
1
  export { filterDocuments } from "./filter.js";
2
2
  export { scanDocuments } from "./scan.js";
3
+ export { countDocumentMetadataUsage } from "./usage-counts.js";
3
4
  export type { DocumentScanDiagnostic, DocumentScanResult, WaymarkDocument, } from "./scan.js";
@@ -1,2 +1,3 @@
1
1
  export { filterDocuments } from "./filter.js";
2
2
  export { scanDocuments } from "./scan.js";
3
+ export { countDocumentMetadataUsage } from "./usage-counts.js";
@@ -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
  } | {
@@ -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
- requireNamespace: configuration.requireNamespace,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waymark-docs",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Agent-focused CLI that surfaces repository documentation to improve coding-agent performance.",
5
5
  "keywords": [
6
6
  "ai-agents",