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 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, and agents can discover only relevant paths before opening a
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; unlike RAG or MCP-backed
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
  ![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
 
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-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,88 +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:
56
+ 1. Install the skills in the repository you want to set up:
79
57
 
80
58
  ```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
59
+ npx skills add ysfaran/waymark --skill waymark --skill waymark-setup
91
60
  ```
92
61
 
93
- 5. **Discover documents**
94
-
95
- For a quick check, run `npx waymark find` with relevant kind and tag filters:
96
-
97
- ```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
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 and a
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
- -s, --show <fields> show declared kind and tag details (kind,tags)
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
- List every declared kind and tag with its description and usage count:
157
+ ### `waymark show`
202
158
 
203
- ```sh
204
- npx waymark status --show kind,tags
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
- 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
+ ```
208
178
 
209
179
  ```sh
210
- 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
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 kind, tags and description
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
- # Kind is adr OR convention, and at least one tag is typescript OR architecture
243
- 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
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 three simple metadata filters are repeatable. Repeating an option is
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 boolean expressions over metadata with `kind:`,
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 `--require-tags`.
246
+ `--filter` cannot be combined with `--scopes`, `--kinds`, `--tags` or
247
+ `--require-tags`.
271
248
 
272
- 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:
273
251
 
274
252
  ```sh
275
- npx waymark find --kinds convention --show kind,tags,description
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 {
@@ -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 kind, tags and description (comma-separated)")
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
- (kinds.length > 0 || tags.length > 0 || requiredTags.length > 0)) {
22
- 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.");
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 !== "kind" && field !== "tags" && field !== "description") {
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,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
  };
@@ -108,7 +119,9 @@ function createCandidatePatterns(scope) {
108
119
  ...allowedConfigFileNames.map((fileName) => `**/${fileName}`),
109
120
  ];
110
121
  }
111
- const directoryPattern = convertPathToPattern(scope.relativeDirectoryPath);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waymark-docs",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Agent-focused CLI that surfaces repository documentation to improve coding-agent performance.",
5
5
  "keywords": [
6
6
  "ai-agents",