docsgov 0.1.0 → 0.1.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/README.md +46 -24
- package/dist/apispec/apispec.js +1 -1
- package/dist/apispec/apispec.test.js +2 -2
- package/dist/check/run.js +1 -1
- package/dist/check/run.test.js +6 -6
- package/dist/check/suggest.js +1 -1
- package/dist/check/tokens.js +1 -1
- package/dist/cmd/main.js +17 -17
- package/dist/cmd/main.test.js +13 -13
- package/dist/codeq/errors.js +2 -2
- package/dist/codeq/index.js +1 -1
- package/dist/codeq/resolver.test.js +1 -1
- package/dist/config/config.js +2 -2
- package/dist/config/config.test.js +5 -5
- package/dist/config/fs.js +1 -1
- package/dist/config/glob.js +1 -1
- package/dist/config/glob.test.js +1 -1
- package/dist/dedup/configload.js +3 -3
- package/dist/dedup/configload.test.js +5 -5
- package/dist/dedup/dedup.index.test.js +9 -9
- package/dist/dedup/dedup.js +5 -5
- package/dist/dedup/dedup.test.js +4 -4
- package/dist/dedup/dedupcfg/config.js +2 -2
- package/dist/dedup/embedder/cache.js +4 -4
- package/dist/dedup/embedder/cache.test.js +13 -13
- package/dist/dedup/embedder/embedder.js +2 -2
- package/dist/dedup/embedder/embedder.mock.test.js +1 -1
- package/dist/dedup/embedder/embedder.test.js +4 -4
- package/dist/dedup/embedder/session.test.js +1 -1
- package/dist/dedup/gitignore.js +5 -5
- package/dist/dedup/gitignore.test.js +2 -2
- package/dist/dedup/indexdb/indexdb.js +2 -2
- package/dist/repo/exists.test.js +7 -7
- package/dist/repo/index.js +1 -1
- package/dist/repo/overlay.test.js +6 -6
- package/dist/repo/repo.js +10 -10
- package/dist/repo/repo.test.js +35 -35
- package/dist/repo/testutil.js +1 -1
- package/dist/repo/write.test.js +12 -12
- package/dist/violation/types.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# docsgov
|
|
2
2
|
|
|
3
|
-
Config-driven **documentation governance**. `
|
|
3
|
+
Config-driven **documentation governance**. `docsgov` checks that the
|
|
4
4
|
references inside your Markdown docs still point at things that actually exist —
|
|
5
5
|
so docs rot loudly (a failing check) instead of silently.
|
|
6
6
|
|
|
@@ -22,19 +22,19 @@ The package source lives at the repository root (a TypeScript/Node project).
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
npm install -g
|
|
26
|
-
|
|
25
|
+
npm install -g docsgov # the npm package is "docsgov"
|
|
26
|
+
docsgov --version # the command it installs is "docsgov"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Or run it without installing:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx
|
|
32
|
+
npx docsgov check
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
**Requirements**
|
|
36
36
|
|
|
37
|
-
- **Node.js >= 22.**
|
|
37
|
+
- **Node.js >= 22.** docsgov uses the built-in `node:sqlite` module (for the
|
|
38
38
|
`dedup` index), which requires Node 22 or newer.
|
|
39
39
|
- **No system libraries needed.** The tree-sitter grammars ship as bundled
|
|
40
40
|
WebAssembly, and the `dedup` embedder runs through `@huggingface/transformers`
|
|
@@ -44,10 +44,32 @@ npx docgov check
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
+
### Skills
|
|
48
|
+
|
|
49
|
+
Add the marketplace:
|
|
50
|
+
```bash
|
|
51
|
+
claude plugin marketplace add
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Marketplace name is `docsgov-skills` (from marketplace.json). Use `@docsgov-skills` when installing plugins.
|
|
55
|
+
In Claude Code, use `/plugin ...` slash commands. In your terminal, use `claude plugin ...`.
|
|
56
|
+
|
|
57
|
+
**Init Skill** (recommended to setup for new repo):
|
|
58
|
+
```bash
|
|
59
|
+
claude plugin install docsgov-init@docsgov-skills
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Operations Suite** (the guide for agent to handle docsgov error codes):
|
|
63
|
+
```bash
|
|
64
|
+
claude plugin install docsgov@docsgov-skills
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
47
69
|
## Quick start
|
|
48
70
|
|
|
49
71
|
1. Mark the repo root and declare what to govern by creating
|
|
50
|
-
**`.
|
|
72
|
+
**`.docsgov/docsgov.yaml`**:
|
|
51
73
|
|
|
52
74
|
```yaml
|
|
53
75
|
code:
|
|
@@ -60,7 +82,7 @@ npx docgov check
|
|
|
60
82
|
2. Run the check from anywhere inside the repo:
|
|
61
83
|
|
|
62
84
|
```bash
|
|
63
|
-
|
|
85
|
+
docsgov check
|
|
64
86
|
```
|
|
65
87
|
|
|
66
88
|
Exit `0` = clean, `1` = violations found, `2` = config/usage error.
|
|
@@ -69,25 +91,25 @@ npx docgov check
|
|
|
69
91
|
|
|
70
92
|
## Commands
|
|
71
93
|
|
|
72
|
-
### `
|
|
94
|
+
### `docsgov check [--format text|json]`
|
|
73
95
|
|
|
74
96
|
Runs the code, doc, and api guards over their configured scopes and reports
|
|
75
97
|
every violation. `--format json` emits machine-readable output for CI;
|
|
76
98
|
`--format text` (the default) is styled for the terminal.
|
|
77
99
|
|
|
78
|
-
### `
|
|
100
|
+
### `docsgov dedup`
|
|
79
101
|
|
|
80
102
|
Refreshes the embedding index for `docs/` and reports near-duplicate concepts.
|
|
81
103
|
Exits `1` if a high-confidence duplicate group is found.
|
|
82
104
|
|
|
83
|
-
### `
|
|
105
|
+
### `docsgov update [--version vX.Y.Z] [--check]`
|
|
84
106
|
|
|
85
|
-
|
|
107
|
+
docsgov is distributed via npm, so `update` does not self-replace the binary.
|
|
86
108
|
|
|
87
|
-
- `
|
|
109
|
+
- `docsgov update --check` reports the current version and the latest version
|
|
88
110
|
from the npm registry (best-effort; degrades gracefully when offline).
|
|
89
|
-
- `
|
|
90
|
-
e.g. `npm install -g
|
|
111
|
+
- `docsgov update` (or with `--version vX.Y.Z`) prints the install command to run,
|
|
112
|
+
e.g. `npm install -g docsgov@latest`.
|
|
91
113
|
|
|
92
114
|
### Exit codes
|
|
93
115
|
|
|
@@ -95,7 +117,7 @@ docgov is distributed via npm, so `update` does not self-replace the binary.
|
|
|
95
117
|
| --- | --- |
|
|
96
118
|
| `0` | success — no violations |
|
|
97
119
|
| `1` | `check` found violations (or `dedup` found duplicates) |
|
|
98
|
-
| `2` | usage or configuration error (e.g. no `.
|
|
120
|
+
| `2` | usage or configuration error (e.g. no `.docsgov/` found, bad `--format`) |
|
|
99
121
|
|
|
100
122
|
These are stable, so CI can gate on them directly.
|
|
101
123
|
|
|
@@ -103,8 +125,8 @@ These are stable, so CI can gate on them directly.
|
|
|
103
125
|
|
|
104
126
|
## Configuration
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
is also the **repo-root marker** —
|
|
128
|
+
docsgov reads a **single** file: `.docsgov/docsgov.yaml`. The `.docsgov/` directory
|
|
129
|
+
is also the **repo-root marker** — docsgov walks up from the working directory
|
|
108
130
|
until it finds one. A missing file makes `check` exit `2`.
|
|
109
131
|
|
|
110
132
|
The file has three optional top-level sections. Each has a `boundary` (which
|
|
@@ -181,12 +203,12 @@ the repo root are rejected.
|
|
|
181
203
|
## Folder structure
|
|
182
204
|
|
|
183
205
|
A governed repository looks like this — the only required piece is
|
|
184
|
-
`.
|
|
206
|
+
`.docsgov/docsgov.yaml`; the `docs/` layout is yours to choose:
|
|
185
207
|
|
|
186
208
|
```
|
|
187
209
|
your-repo/
|
|
188
|
-
├── .
|
|
189
|
-
│ └──
|
|
210
|
+
├── .docsgov/
|
|
211
|
+
│ └── docsgov.yaml # the single config file + repo-root marker
|
|
190
212
|
├── docs/ # whatever structure you like
|
|
191
213
|
│ ├── README.md
|
|
192
214
|
│ ├── config.md
|
|
@@ -195,7 +217,7 @@ your-repo/
|
|
|
195
217
|
└── cmd/
|
|
196
218
|
```
|
|
197
219
|
|
|
198
|
-
This repository governs **its own** `docs/` with
|
|
220
|
+
This repository governs **its own** `docs/` with docsgov; see
|
|
199
221
|
[`docs/README.md`](docs/README.md) for that layout as a worked example.
|
|
200
222
|
|
|
201
223
|
---
|
|
@@ -208,8 +230,8 @@ This repository governs **its own** `docs/` with docgov; see
|
|
|
208
230
|
docs-governance:
|
|
209
231
|
image: node:22
|
|
210
232
|
script:
|
|
211
|
-
- npm install -g
|
|
212
|
-
-
|
|
233
|
+
- npm install -g docsgov
|
|
234
|
+
- docsgov check # exits 1 on violations → job fails
|
|
213
235
|
```
|
|
214
236
|
|
|
215
237
|
---
|
package/dist/apispec/apispec.js
CHANGED
|
@@ -19,7 +19,7 @@ function asString(v) {
|
|
|
19
19
|
return typeof v === "string" ? v : "";
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Spec is a loaded, self-contained OpenAPI v3.1 document. Only the parts
|
|
22
|
+
* Spec is a loaded, self-contained OpenAPI v3.1 document. Only the parts docsgov
|
|
23
23
|
* needs are typed; schema bodies stay as generic nodes for recursive walking.
|
|
24
24
|
*/
|
|
25
25
|
export class Spec {
|
|
@@ -400,7 +400,7 @@ describe("load/parse error semantics", () => {
|
|
|
400
400
|
// I/O and parse failures are exceptional (thrown sentinels), unlike domain
|
|
401
401
|
// results which are returned data.
|
|
402
402
|
test("missing file throws SpecNotFoundError", async () => {
|
|
403
|
-
await expect(load(join(tmpdir(), "
|
|
403
|
+
await expect(load(join(tmpdir(), "docsgov-apispec-does-not-exist.json"))).rejects.toBeInstanceOf(SpecNotFoundError);
|
|
404
404
|
});
|
|
405
405
|
test("malformed syntax throws SpecParseError", () => {
|
|
406
406
|
expect(() => parse("{not json", "x.json")).toThrow(SpecParseError);
|
|
@@ -413,7 +413,7 @@ describe("load/parse error semantics", () => {
|
|
|
413
413
|
describe("load: reads YAML and JSON from disk", () => {
|
|
414
414
|
let dir;
|
|
415
415
|
beforeAll(async () => {
|
|
416
|
-
dir = await mkdtemp(join(tmpdir(), "
|
|
416
|
+
dir = await mkdtemp(join(tmpdir(), "docsgov-apispec-"));
|
|
417
417
|
});
|
|
418
418
|
afterAll(async () => {
|
|
419
419
|
await rm(dir, { recursive: true, force: true });
|
package/dist/check/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Port of internal/check/run.go — the three-pass Run pipeline (Stage 5 of
|
|
2
|
-
// minimize-
|
|
2
|
+
// minimize-docsgov) and the {{api:…}} qualifier dispatch from apitokens' caller.
|
|
3
3
|
//
|
|
4
4
|
// run() runs three independent passes (code, doc, api) over the scopes declared
|
|
5
5
|
// in cfg. Only present (non-undefined) sections are executed. Violations are
|
package/dist/check/run.test.js
CHANGED
|
@@ -59,13 +59,13 @@ afterEach(async () => {
|
|
|
59
59
|
});
|
|
60
60
|
/**
|
|
61
61
|
* tempRepo creates a temp dir, writes the given files (map path→content), and
|
|
62
|
-
* creates a .
|
|
62
|
+
* creates a .docsgov/ sentinel dir so find() anchors there. Mirrors the Go
|
|
63
63
|
* tempRepo helper.
|
|
64
64
|
*/
|
|
65
65
|
async function tempRepo(files) {
|
|
66
|
-
const root = await nodefs.realpath(await nodefs.mkdtemp(path.join(os.tmpdir(), "
|
|
66
|
+
const root = await nodefs.realpath(await nodefs.mkdtemp(path.join(os.tmpdir(), "docsgov-check-")));
|
|
67
67
|
createdRoots.push(root);
|
|
68
|
-
await nodefs.mkdir(path.join(root, ".
|
|
68
|
+
await nodefs.mkdir(path.join(root, ".docsgov"), { recursive: true });
|
|
69
69
|
for (const [rel, content] of Object.entries(files)) {
|
|
70
70
|
const abs = path.join(root, ...rel.split("/"));
|
|
71
71
|
await nodefs.mkdir(path.dirname(abs), { recursive: true });
|
|
@@ -429,10 +429,10 @@ describe("run / api pass", () => {
|
|
|
429
429
|
// --- E2E + ORDERING ---
|
|
430
430
|
describe("run / e2e", () => {
|
|
431
431
|
// A clean tree configured with all three guards must produce zero violations
|
|
432
|
-
// when loaded from a real
|
|
432
|
+
// when loaded from a real docsgov.yaml — the end-to-end happy path.
|
|
433
433
|
it("returns no violations for a clean tree across all three guards", async () => {
|
|
434
434
|
const r = await tempRepo({
|
|
435
|
-
".
|
|
435
|
+
".docsgov/docsgov.yaml": `
|
|
436
436
|
code:
|
|
437
437
|
boundary: [docs/**]
|
|
438
438
|
source: [src/**]
|
|
@@ -455,7 +455,7 @@ api:
|
|
|
455
455
|
// passes run independently and collect together.
|
|
456
456
|
it("returns one violation per guard for a drifted tree", async () => {
|
|
457
457
|
const r = await tempRepo({
|
|
458
|
-
".
|
|
458
|
+
".docsgov/docsgov.yaml": `
|
|
459
459
|
code:
|
|
460
460
|
boundary: [docs/**]
|
|
461
461
|
source: [src/**]
|
package/dist/check/suggest.js
CHANGED
|
@@ -105,7 +105,7 @@ function rankCandidates(target, candidates) {
|
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
107
|
* levenshtein is the standard two-row edit-distance DP (insert/delete/substitute,
|
|
108
|
-
* cost 1 each). Case-sensitive, since the languages
|
|
108
|
+
* cost 1 each). Case-sensitive, since the languages docsgov resolves treat
|
|
109
109
|
* identifiers as case-sensitive.
|
|
110
110
|
*/
|
|
111
111
|
function levenshtein(a, b) {
|
package/dist/check/tokens.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// The Go original walks the goldmark AST and uses byte offsets; this port walks
|
|
14
14
|
// the mdast tree (remark-parse + remark-gfm) and uses string character offsets.
|
|
15
15
|
// Everything operates on the same decoded string, so offsets are internally
|
|
16
|
-
// consistent (and identical to bytes for the ASCII token bodies
|
|
16
|
+
// consistent (and identical to bytes for the ASCII token bodies docsgov scans).
|
|
17
17
|
import { parseMarkdown } from "../dedup/mdsection/index.js";
|
|
18
18
|
/**
|
|
19
19
|
* codeTokenRE matches a {{code:…}} token anywhere in a text span. It does not
|
package/dist/cmd/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
|
-
// Command
|
|
3
|
-
// engine. Port of cmd/
|
|
2
|
+
// Command docsgov is the CLI entrypoint for the docsgov documentation-governance
|
|
3
|
+
// engine. Port of cmd/docsgov/{main,dedup_on,dedup_off,update}.go.
|
|
4
4
|
//
|
|
5
5
|
// PORTING NOTES (deviations recorded prominently):
|
|
6
6
|
//
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// is an npm-appropriate command:
|
|
25
25
|
// - `update --check` reports the current version and the latest version
|
|
26
26
|
// from the npm registry (best-effort; handles offline gracefully);
|
|
27
|
-
// - plain `update` prints guidance to run `npm install -g
|
|
27
|
+
// - plain `update` prints guidance to run `npm install -g docsgov@latest`.
|
|
28
28
|
// The `--version <tag>` flag is kept for compatibility (it is reported back
|
|
29
29
|
// in the install guidance). The asset-name mapping and replaceExecutable
|
|
30
30
|
// logic from update.go have no npm analogue and are omitted.
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// userland process.emitWarning wrapper cannot suppress it — the builtin is
|
|
36
36
|
// initialized at module-link time, before any of our code runs. The shebang
|
|
37
37
|
// therefore launches node with `--disable-warning=ExperimentalWarning`
|
|
38
|
-
// (env -S splits the args) so the installed `
|
|
38
|
+
// (env -S splits the args) so the installed `docsgov` runs clean. Requires
|
|
39
39
|
// Node >=22 (engines) where the flag exists.
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
import { readFileSync, realpathSync } from "node:fs";
|
|
@@ -137,7 +137,7 @@ async function runCheckAction(format) {
|
|
|
137
137
|
throw new ViolationsError();
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
// runDedupAction implements "
|
|
140
|
+
// runDedupAction implements "docsgov dedup": find repo root → refresh the
|
|
141
141
|
// embedding index → analyze → render the report. The index is rebuilt
|
|
142
142
|
// automatically on every run, so there is no separate index step. Index
|
|
143
143
|
// progress goes to stderr; the report goes to stdout. Throws DuplicatesError
|
|
@@ -178,17 +178,17 @@ async function runUpdateAction(opts) {
|
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
const target = opts.version !== undefined && opts.version !== ""
|
|
181
|
-
? `
|
|
182
|
-
: "
|
|
181
|
+
? `docsgov@${opts.version}`
|
|
182
|
+
: "docsgov@latest";
|
|
183
183
|
process.stdout.write(`current: ${current}\n`);
|
|
184
|
-
process.stdout.write(`
|
|
184
|
+
process.stdout.write(`docsgov is installed via npm; update it with:\n npm install -g ${target}\n`);
|
|
185
185
|
}
|
|
186
186
|
// fetchLatestVersion queries the npm registry for the latest published version.
|
|
187
187
|
// Best-effort: returns null on any network/parse failure (offline-safe) so the
|
|
188
188
|
// update command degrades gracefully rather than erroring out.
|
|
189
189
|
async function fetchLatestVersion() {
|
|
190
190
|
try {
|
|
191
|
-
const resp = await fetch("https://registry.npmjs.org/
|
|
191
|
+
const resp = await fetch("https://registry.npmjs.org/docsgov/latest");
|
|
192
192
|
if (!resp.ok) {
|
|
193
193
|
return null;
|
|
194
194
|
}
|
|
@@ -204,7 +204,7 @@ async function fetchLatestVersion() {
|
|
|
204
204
|
export function newApp(version) {
|
|
205
205
|
const app = new Command();
|
|
206
206
|
app
|
|
207
|
-
.name("
|
|
207
|
+
.name("docsgov")
|
|
208
208
|
.usage("[command]")
|
|
209
209
|
.description("config-driven documentation governance")
|
|
210
210
|
.version(version)
|
|
@@ -212,7 +212,7 @@ export function newApp(version) {
|
|
|
212
212
|
// process.exit, so run() is the single owner of the exit-code convention.
|
|
213
213
|
.exitOverride()
|
|
214
214
|
// Suppress commander's own "(see --help)" suggestion noise on errors; run()
|
|
215
|
-
// reports usage errors via stderr "
|
|
215
|
+
// reports usage errors via stderr "docsgov: ...".
|
|
216
216
|
.showSuggestionAfterError(false);
|
|
217
217
|
app
|
|
218
218
|
.command("check")
|
|
@@ -223,7 +223,7 @@ export function newApp(version) {
|
|
|
223
223
|
});
|
|
224
224
|
app
|
|
225
225
|
.command("update")
|
|
226
|
-
.description("report the latest
|
|
226
|
+
.description("report the latest docsgov release (install via npm)")
|
|
227
227
|
.option("--version <tag>", "report guidance for a specific tag (e.g. v1.2.3)")
|
|
228
228
|
.option("--check", "report current and latest versions without installing", false)
|
|
229
229
|
.action(async (opts) => {
|
|
@@ -271,7 +271,7 @@ export async function run(argv) {
|
|
|
271
271
|
return exitUsage;
|
|
272
272
|
}
|
|
273
273
|
// Engine/config/operational error → exit 2 with the Go-style prefix.
|
|
274
|
-
process.stderr.write(`
|
|
274
|
+
process.stderr.write(`docsgov: ${errMsg(err)}\n`);
|
|
275
275
|
return exitUsage;
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -287,7 +287,7 @@ async function main() {
|
|
|
287
287
|
// Flush stdout/stderr before exiting. When stdout is a pipe or file (not a
|
|
288
288
|
// TTY), Node buffers writes asynchronously; calling process.exit() directly
|
|
289
289
|
// would terminate before the buffer drains and silently truncate the report
|
|
290
|
-
// (a piped/redirected `
|
|
290
|
+
// (a piped/redirected `docsgov check` would print nothing). Draining both
|
|
291
291
|
// streams first guarantees the output is delivered; process.exit then forces
|
|
292
292
|
// termination so lingering tree-sitter/onnx handles cannot keep the process
|
|
293
293
|
// alive (Go used os.Exit for the same immediacy).
|
|
@@ -306,10 +306,10 @@ function drainStream(stream) {
|
|
|
306
306
|
// import.meta.url !== the entry file URL, so main() is not called.
|
|
307
307
|
//
|
|
308
308
|
// The entry (process.argv[1]) must be resolved through realpathSync before
|
|
309
|
-
// comparing: an npm-installed bin is reached via symlinks (bin/
|
|
310
|
-
// lib/node_modules/
|
|
309
|
+
// comparing: an npm-installed bin is reached via symlinks (bin/docsgov →
|
|
310
|
+
// lib/node_modules/docsgov → this file), so path.resolve alone leaves it as the
|
|
311
311
|
// symlink path and the comparison fails — which would make main() silently never
|
|
312
|
-
// run when invoked as `
|
|
312
|
+
// run when invoked as `docsgov`. import.meta.url is already a realpath (Node
|
|
313
313
|
// resolves module specifiers through realpath), so we realpath the entry to match.
|
|
314
314
|
const invokedDirectly = (() => {
|
|
315
315
|
const entry = process.argv[1];
|
package/dist/cmd/main.test.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// Tests the CLI wire-up: exit-code mapping, command routing, --format flag, the
|
|
2
2
|
// check e2e loop, dedup command registration, and the npm-appropriate update
|
|
3
|
-
// command. Port of cmd/
|
|
3
|
+
// command. Port of cmd/docsgov/{main,dedup_cli,update}_test.go, adapted to the
|
|
4
4
|
// commander framework and npm distribution.
|
|
5
5
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
9
|
import { run, newApp, readVersion, ViolationsError, DuplicatesError, exitOK, exitViolation, exitUsage, } from "./main.js";
|
|
10
|
-
// makeNewSchemaRepo builds a minimal temp repo with a .
|
|
10
|
+
// makeNewSchemaRepo builds a minimal temp repo with a .docsgov/docsgov.yaml and
|
|
11
11
|
// the provided docs files (relPath→content). Returns the temp dir path. Mirrors
|
|
12
12
|
// the Go makeNewSchemaRepo helper.
|
|
13
|
-
function makeNewSchemaRepo(
|
|
14
|
-
const tmp = mkdtempSync(path.join(tmpdir(), "
|
|
15
|
-
mkdirSync(path.join(tmp, ".
|
|
16
|
-
writeFileSync(path.join(tmp, ".
|
|
13
|
+
function makeNewSchemaRepo(docsgovYAML, docs) {
|
|
14
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "docsgov-cmd-"));
|
|
15
|
+
mkdirSync(path.join(tmp, ".docsgov"), { recursive: true });
|
|
16
|
+
writeFileSync(path.join(tmp, ".docsgov", "docsgov.yaml"), docsgovYAML);
|
|
17
17
|
for (const [relPath, content] of Object.entries(docs)) {
|
|
18
18
|
const abs = path.join(tmp, ...relPath.split("/"));
|
|
19
19
|
mkdirSync(path.dirname(abs), { recursive: true });
|
|
@@ -142,11 +142,11 @@ describe("check command", () => {
|
|
|
142
142
|
}));
|
|
143
143
|
expect(code).toBe(exitViolation);
|
|
144
144
|
});
|
|
145
|
-
// Missing .
|
|
145
|
+
// Missing .docsgov/docsgov.yaml is a usage/config error → exit 2. Mirrors Go's
|
|
146
146
|
// TestCheckNewSchema_MissingConfigExitsTwo.
|
|
147
|
-
it("missing
|
|
148
|
-
const tmp = mkdtempSync(path.join(tmpdir(), "
|
|
149
|
-
mkdirSync(path.join(tmp, ".
|
|
147
|
+
it("missing docsgov.yaml exits 2", async () => {
|
|
148
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "docsgov-cmd-"));
|
|
149
|
+
mkdirSync(path.join(tmp, ".docsgov"), { recursive: true });
|
|
150
150
|
const restore = silenceStderr();
|
|
151
151
|
const code = await withCwd(tmp, () => run(["check"]));
|
|
152
152
|
restore();
|
|
@@ -305,7 +305,7 @@ describe("update command (npm)", () => {
|
|
|
305
305
|
code = await run(["update"]);
|
|
306
306
|
});
|
|
307
307
|
expect(code).toBe(exitOK);
|
|
308
|
-
expect(out).toContain("npm install -g
|
|
308
|
+
expect(out).toContain("npm install -g docsgov@latest");
|
|
309
309
|
});
|
|
310
310
|
// `update --version <tag>` is shadowed by commander's program-level --version
|
|
311
311
|
// flag (added by .version()), which prints the program version and exits 0
|
|
@@ -392,14 +392,14 @@ describe("update command (npm)", () => {
|
|
|
392
392
|
});
|
|
393
393
|
describe("dedup command", () => {
|
|
394
394
|
// The dedup action runs Index (which loads the dedup config) before anything
|
|
395
|
-
// else. A malformed .
|
|
395
|
+
// else. A malformed .docsgov/dedup/config.yml makes Load throw, which must map
|
|
396
396
|
// to a usage/config error (exit 2) — NOT exit 1 (which means duplicates were
|
|
397
397
|
// found) and NOT a crash. This exercises runDedupAction's Index call and the
|
|
398
398
|
// engine-error → exit-2 mapping without triggering a model download (Load
|
|
399
399
|
// throws first). Falsifiable: swallowing the error would change the code to 0.
|
|
400
400
|
it("malformed dedup config exits 2 (usage)", async () => {
|
|
401
401
|
const tmp = makeNewSchemaRepo("doc:\n boundary:\n - docs/**\n", {
|
|
402
|
-
".
|
|
402
|
+
".docsgov/dedup/config.yml": ": : : not valid yaml ][\n",
|
|
403
403
|
"docs/guide.md": "# Guide\n\nHello.\n",
|
|
404
404
|
});
|
|
405
405
|
const restore = silenceStderr();
|
package/dist/codeq/errors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Package codeq resolves code references against the project source tree.
|
|
2
|
-
// It is the only place tree-sitter touches
|
|
2
|
+
// It is the only place tree-sitter touches docsgov; every consumer above this
|
|
3
3
|
// layer sees the Resolver interface as a boolean oracle.
|
|
4
4
|
//
|
|
5
5
|
// Go's codeq uses sentinel `var Err… = errors.New(…)` values wrapped with
|
|
@@ -34,7 +34,7 @@ export class ParseFailedError extends Error {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* UnsupportedLanguageError is thrown when the file extension has no registered
|
|
37
|
-
* resolver. It is distinct from an operational failure — it means
|
|
37
|
+
* resolver. It is distinct from an operational failure — it means docsgov has no
|
|
38
38
|
* grammar for that language, not that the symbol is absent.
|
|
39
39
|
*
|
|
40
40
|
* Go original: `ErrUnsupportedLanguage`, wrapped as `fmt.Errorf("%w: %q", …, ext)`.
|
package/dist/codeq/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Public surface of the codeq package — the only place tree-sitter touches
|
|
2
|
-
//
|
|
2
|
+
// docsgov. Consumers above this layer see the Resolver/DefaultResolver boolean
|
|
3
3
|
// oracle and the typed errors, never the binding.
|
|
4
4
|
export { FileNotFoundError, ParseFailedError, UnsupportedLanguageError, } from "./errors.js";
|
|
5
5
|
export { DefaultResolver, createDefaultResolver } from "./resolver.js";
|
|
@@ -64,7 +64,7 @@ describe("DefaultResolver dispatch", () => {
|
|
|
64
64
|
expect(await r.resolve(fs, ref("pkg/foo.go", "Nope"))).toBe(false);
|
|
65
65
|
});
|
|
66
66
|
it("throws UnsupportedLanguageError for an unregistered extension", async () => {
|
|
67
|
-
// WHY: an unknown extension is NOT "symbol absent" — it means
|
|
67
|
+
// WHY: an unknown extension is NOT "symbol absent" — it means docsgov has no
|
|
68
68
|
// grammar for it; the caller reports it differently. Mirrors Go returning
|
|
69
69
|
// ErrUnsupportedLanguage (wrapped with the ext).
|
|
70
70
|
const r = new DefaultResolver(new Map([[".go", spyResolver(true)]]));
|
package/dist/config/config.js
CHANGED
|
@@ -32,7 +32,7 @@ function toStringArray(value) {
|
|
|
32
32
|
return value.map((item) => String(item));
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* loadConfig reads .
|
|
35
|
+
* loadConfig reads .docsgov/docsgov.yaml from fsys and returns the parsed Config.
|
|
36
36
|
*
|
|
37
37
|
* A missing file propagates a NotExistError (the CLI maps this to its exit code
|
|
38
38
|
* in a later stage). A malformed YAML file throws a descriptive error. No
|
|
@@ -45,7 +45,7 @@ function toStringArray(value) {
|
|
|
45
45
|
*/
|
|
46
46
|
export async function loadConfig(fsys) {
|
|
47
47
|
// readFile throws NotExistError for a missing file; let it propagate.
|
|
48
|
-
const data = await fsys.readFile(".
|
|
48
|
+
const data = await fsys.readFile(".docsgov/docsgov.yaml");
|
|
49
49
|
const text = new TextDecoder().decode(data);
|
|
50
50
|
// parseYaml throws on malformed YAML (descriptive error); let it propagate.
|
|
51
51
|
const parsed = parseYaml(text);
|
|
@@ -6,7 +6,7 @@ describe("loadConfig", () => {
|
|
|
6
6
|
// whole check pipeline depends on.
|
|
7
7
|
it("loads code, doc, and api scopes with their exact glob lists", async () => {
|
|
8
8
|
const fsys = new MapFS({
|
|
9
|
-
".
|
|
9
|
+
".docsgov/docsgov.yaml": `
|
|
10
10
|
code:
|
|
11
11
|
boundary: [docs/**]
|
|
12
12
|
source: [internal/**, cmd/**]
|
|
@@ -31,7 +31,7 @@ api:
|
|
|
31
31
|
// entirely (undefined vs defined is the "is this guard configured?" signal).
|
|
32
32
|
it("leaves absent sections undefined", async () => {
|
|
33
33
|
const fsys = new MapFS({
|
|
34
|
-
".
|
|
34
|
+
".docsgov/docsgov.yaml": `
|
|
35
35
|
doc:
|
|
36
36
|
boundary: [docs/**]
|
|
37
37
|
`,
|
|
@@ -47,7 +47,7 @@ doc:
|
|
|
47
47
|
// guard) and "explicitly configured but empty" (run the guard, match nothing).
|
|
48
48
|
it("yields a defined empty scope for a present-but-null section", async () => {
|
|
49
49
|
const fsys = new MapFS({
|
|
50
|
-
".
|
|
50
|
+
".docsgov/docsgov.yaml": `
|
|
51
51
|
code:
|
|
52
52
|
`,
|
|
53
53
|
});
|
|
@@ -74,7 +74,7 @@ code:
|
|
|
74
74
|
// docs go unchecked.
|
|
75
75
|
it("throws on malformed YAML", async () => {
|
|
76
76
|
const fsys = new MapFS({
|
|
77
|
-
".
|
|
77
|
+
".docsgov/docsgov.yaml": `
|
|
78
78
|
code: {boundary: [not closed
|
|
79
79
|
`,
|
|
80
80
|
});
|
|
@@ -83,7 +83,7 @@ code: {boundary: [not closed
|
|
|
83
83
|
// Unknown top-level keys are silently ignored (forward-compat per the plan).
|
|
84
84
|
it("silently ignores unknown top-level keys", async () => {
|
|
85
85
|
const fsys = new MapFS({
|
|
86
|
-
".
|
|
86
|
+
".docsgov/docsgov.yaml": `
|
|
87
87
|
future:
|
|
88
88
|
boundary: [x/**]
|
|
89
89
|
doc:
|
package/dist/config/fs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Filesystem abstraction for the config package.
|
|
2
2
|
//
|
|
3
|
-
// FS RECONCILIATION (minimize-
|
|
3
|
+
// FS RECONCILIATION (minimize-docsgov port): the config package and the repo
|
|
4
4
|
// package originally carried two different FS abstractions — config's was
|
|
5
5
|
// synchronous (readFile + walk), repo's is asynchronous (readFile + readDir +
|
|
6
6
|
// sub). The check orchestrator composes both, so they are unified here onto
|
package/dist/config/glob.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Port of internal/config/glob.go.
|
|
2
2
|
//
|
|
3
3
|
// Go uses github.com/bmatcuk/doublestar; we use picomatch with { dot: true },
|
|
4
|
-
// which matches doublestar's semantics for the patterns
|
|
4
|
+
// which matches doublestar's semantics for the patterns docsgov uses:
|
|
5
5
|
// - "**" matches zero or more path segments (so "docs/**" matches "docs"),
|
|
6
6
|
// - dotfiles are matched (doublestar matches them by default),
|
|
7
7
|
// - a malformed pattern contributes no match (doublestar returns an error
|
package/dist/config/glob.test.js
CHANGED
|
@@ -23,7 +23,7 @@ describe("inScope", () => {
|
|
|
23
23
|
});
|
|
24
24
|
// A path matching none of the patterns must be out of scope.
|
|
25
25
|
it("returns false when no pattern matches", () => {
|
|
26
|
-
expect(inScope(["docs/**", "internal/**"], "cmd/
|
|
26
|
+
expect(inScope(["docs/**", "internal/**"], "cmd/docsgov/main.go")).toBe(false);
|
|
27
27
|
});
|
|
28
28
|
// No patterns means nothing is ever in scope.
|
|
29
29
|
it("returns false for an empty pattern list", () => {
|
package/dist/dedup/configload.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Loads and validates the dedup config: Default() overlaid with any
|
|
2
|
-
// .
|
|
2
|
+
// .docsgov/dedup/config.yml present at repoRoot.
|
|
3
3
|
//
|
|
4
4
|
// Ported from internal/dedup/configload.go. Go used yaml.Unmarshal(data, &cfg),
|
|
5
5
|
// which sets only the fields present in the YAML (scalars override, list fields
|
|
@@ -42,14 +42,14 @@ function quote(s) {
|
|
|
42
42
|
return JSON.stringify(s);
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* Load returns Default() overlaid with any .
|
|
45
|
+
* Load returns Default() overlaid with any .docsgov/dedup/config.yml present at
|
|
46
46
|
* repoRoot. A missing file is silently skipped. A malformed YAML file throws a
|
|
47
47
|
* wrapped error (hard error — never falls back to defaults on parse failure).
|
|
48
48
|
* Validation runs last so user overlays are fully applied before checking.
|
|
49
49
|
*/
|
|
50
50
|
export async function Load(repoRoot) {
|
|
51
51
|
const cfg = Default();
|
|
52
|
-
const dedupDir = path.join(repoRoot, ".
|
|
52
|
+
const dedupDir = path.join(repoRoot, ".docsgov", "dedup");
|
|
53
53
|
// Step 1: overlay config.yml onto defaults.
|
|
54
54
|
const configPath = path.join(dedupDir, "config.yml");
|
|
55
55
|
let data;
|
|
@@ -23,15 +23,15 @@ afterEach(() => {
|
|
|
23
23
|
rmSync(d, { recursive: true, force: true });
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
|
-
/** newRepo returns a fresh temp repo root with no .
|
|
26
|
+
/** newRepo returns a fresh temp repo root with no .docsgov/dedup. */
|
|
27
27
|
function newRepo() {
|
|
28
28
|
const dir = mkdtempSync(join(tmpdir(), "dedup-cfg-"));
|
|
29
29
|
tmpDirs.push(dir);
|
|
30
30
|
return dir;
|
|
31
31
|
}
|
|
32
|
-
/** writeConfigYML writes config.yml under repoRoot/.
|
|
32
|
+
/** writeConfigYML writes config.yml under repoRoot/.docsgov/dedup. */
|
|
33
33
|
function writeConfigYML(repoRoot, yaml) {
|
|
34
|
-
const dedupDir = join(repoRoot, ".
|
|
34
|
+
const dedupDir = join(repoRoot, ".docsgov", "dedup");
|
|
35
35
|
mkdirSync(dedupDir, { recursive: true });
|
|
36
36
|
writeFileSync(join(dedupDir, "config.yml"), yaml);
|
|
37
37
|
}
|
|
@@ -60,7 +60,7 @@ describe("Default", () => {
|
|
|
60
60
|
const cfg = Default();
|
|
61
61
|
expect(cfg.Markdown.ignored_dirs).toEqual([
|
|
62
62
|
".git", "node_modules", "vendor", "dist", "build",
|
|
63
|
-
".next", ".cache", ".
|
|
63
|
+
".next", ".cache", ".docsgov", "dedup-poc", ".venv",
|
|
64
64
|
]);
|
|
65
65
|
expect(cfg.Analyzer.universal_stopwords).toEqual([
|
|
66
66
|
"the", "a", "an", "of", "and", "or", "to", "with",
|
|
@@ -401,7 +401,7 @@ describe("Load", () => {
|
|
|
401
401
|
// the cause attached — never silently fall back to defaults (Rule 12).
|
|
402
402
|
it("throws (not falls back) when config.yml cannot be read for a non-ENOENT reason", async () => {
|
|
403
403
|
const repo = newRepo();
|
|
404
|
-
const dedupDir = join(repo, ".
|
|
404
|
+
const dedupDir = join(repo, ".docsgov", "dedup");
|
|
405
405
|
mkdirSync(dedupDir, { recursive: true });
|
|
406
406
|
// Make config.yml a directory so reading it fails with EISDIR, not ENOENT.
|
|
407
407
|
mkdirSync(join(dedupDir, "config.yml"));
|