docguard-cli 0.22.1 → 0.24.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/README.md +4 -4
- package/cli/commands/demo.mjs +1 -1
- package/cli/commands/diff.mjs +19 -8
- package/cli/commands/explain.mjs +178 -17
- package/cli/commands/fix.mjs +17 -2
- package/cli/commands/generate.mjs +2 -2
- package/cli/commands/guard.mjs +86 -11
- package/cli/commands/hooks.mjs +12 -7
- package/cli/commands/init.mjs +18 -6
- package/cli/commands/score.mjs +147 -61
- package/cli/commands/setup.mjs +2 -2
- package/cli/commands/trace.mjs +3 -101
- package/cli/commands/upgrade.mjs +61 -13
- package/cli/config.mjs +245 -0
- package/cli/docguard.mjs +21 -217
- package/cli/ensure-skills.mjs +24 -26
- package/cli/scanners/api-doc.mjs +17 -3
- package/cli/scanners/doc-tools.mjs +32 -15
- package/cli/scanners/frontend.mjs +24 -8
- package/cli/scanners/js-ast.mjs +432 -0
- package/cli/scanners/memory-plan.mjs +1 -1
- package/cli/scanners/py-ast.mjs +213 -0
- package/cli/scanners/routes.mjs +194 -69
- package/cli/scanners/schemas.mjs +97 -51
- package/cli/scanners/speckit.mjs +14 -0
- package/cli/shared-git.mjs +0 -0
- package/cli/shared-ignore.mjs +16 -1
- package/cli/shared-source.mjs +59 -2
- package/cli/shared-trace-patterns.mjs +118 -0
- package/cli/shared.mjs +60 -1
- package/cli/validator-markers.mjs +91 -0
- package/cli/validators/api-surface.mjs +37 -3
- package/cli/validators/canonical-sync.mjs +22 -19
- package/cli/validators/doc-quality.mjs +27 -44
- package/cli/validators/docs-coverage.mjs +13 -0
- package/cli/validators/docs-diff.mjs +16 -6
- package/cli/validators/docs-sync.mjs +4 -3
- package/cli/validators/drift.mjs +3 -2
- package/cli/validators/freshness.mjs +47 -15
- package/cli/validators/metadata-sync.mjs +21 -11
- package/cli/validators/metrics-consistency.mjs +45 -17
- package/cli/validators/security.mjs +13 -5
- package/cli/validators/structure.mjs +6 -5
- package/cli/validators/surface-sync.mjs +7 -5
- package/cli/validators/test-spec.mjs +76 -51
- package/cli/validators/todo-tracking.mjs +4 -2
- package/cli/validators/traceability.mjs +12 -54
- package/cli/writers/sections.mjs +32 -19
- package/docs/commands.md +1 -1
- package/docs/configuration.md +11 -0
- package/docs/faq.md +1 -1
- package/extensions/spec-kit-docguard/README.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -1
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +3 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +2 -2
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://pypi.org/project/docguard-cli/)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[](https://nodejs.org)
|
|
12
|
-
[-green)](package.json)
|
|
13
13
|
[](https://github.com/github/spec-kit)
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -106,9 +106,9 @@ Recent highlights across the v0.16 → v0.19 line:
|
|
|
106
106
|
two commits. Pinpoints regressions without re-running the full suite by hand.
|
|
107
107
|
- **`docguard upgrade --apply --pr`** — when the config schema bumps, DocGuard migrates
|
|
108
108
|
`.docguard.json` for you and (optionally) opens a PR with the change.
|
|
109
|
-
- **Language-aware
|
|
110
|
-
Java, Ruby, and PHP layouts in addition to JS/TS
|
|
111
|
-
|
|
109
|
+
- **Language-aware traceability** — both `docguard trace` *and* the guard-time Traceability validator
|
|
110
|
+
understand Python, Rust, Go, Java, Ruby, and PHP layouts in addition to JS/TS, via a shared pattern
|
|
111
|
+
set (`cli/shared-trace-patterns.mjs`) so the two never drift apart.
|
|
112
112
|
- **Per-validator severity overrides** — escalate `freshness` to `high` for production repos,
|
|
113
113
|
demote `doc-quality` to `low` for prototypes. Configurable per-project.
|
|
114
114
|
- **JSON Schema for `.docguard.json`** — IDE autocomplete, in-line docs, and validation via
|
package/cli/commands/demo.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
25
25
|
import { c } from '../shared.mjs';
|
|
26
26
|
import { runGuardInternal, classifyResult } from './guard.mjs';
|
|
27
27
|
import { runScoreInternal } from './score.mjs';
|
|
28
|
-
import { loadConfig } from '../
|
|
28
|
+
import { loadConfig } from '../config.mjs';
|
|
29
29
|
|
|
30
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
31
|
const __dirname = dirname(__filename);
|
package/cli/commands/diff.mjs
CHANGED
|
@@ -174,7 +174,7 @@ export function diffEntities(dir, config = {}) {
|
|
|
174
174
|
// an entity). scanSchemasDeep covers JS ORMs, SQLAlchemy/Pydantic, Diesel,
|
|
175
175
|
// Go structs, JPA, Rails, and OpenAPI schemas.
|
|
176
176
|
const docTools = detectDocTools(dir);
|
|
177
|
-
const schemas = scanSchemasDeep(dir, {}, docTools);
|
|
177
|
+
const schemas = scanSchemasDeep(dir, {}, docTools, config);
|
|
178
178
|
const codeEntities = new Set();
|
|
179
179
|
for (const e of (schemas.entities || [])) {
|
|
180
180
|
const n = String(e.name || '').toLowerCase();
|
|
@@ -347,22 +347,33 @@ function diffTests(dir, config = {}) {
|
|
|
347
347
|
|
|
348
348
|
// Glob-aware matching (documented entries are often patterns or basenames).
|
|
349
349
|
const codeArr = [...codeTests];
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
|
|
351
|
+
// PERFORMANCE OPTIMIZATION: Pre-compile regular expressions to avoid O(N*M)
|
|
352
|
+
// instantiation bottlenecks inside the nested .filter and .some loops below.
|
|
353
|
+
const docMatchers = [...docTests].map(docEntry => {
|
|
352
354
|
const entry = String(docEntry).trim();
|
|
353
355
|
const hasSlash = entry.includes('/');
|
|
354
356
|
const target = hasSlash ? entry : basename(entry);
|
|
355
|
-
const subject = hasSlash ? codeRel : basename(codeRel);
|
|
356
357
|
const rx = new RegExp('^' + target.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*+/g, '.*') + '$');
|
|
357
|
-
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
original: docEntry,
|
|
361
|
+
hasSlash,
|
|
362
|
+
rx
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const matches = (matcher, codeRel) => {
|
|
367
|
+
const subject = matcher.hasSlash ? codeRel : basename(codeRel);
|
|
368
|
+
return matcher.rx.test(subject);
|
|
358
369
|
};
|
|
359
370
|
|
|
360
371
|
return {
|
|
361
372
|
title: 'Test Files',
|
|
362
373
|
icon: '🧪',
|
|
363
|
-
onlyInDocs:
|
|
364
|
-
onlyInCode: codeArr.filter(c => !
|
|
365
|
-
matched:
|
|
374
|
+
onlyInDocs: docMatchers.filter(m => !codeArr.some(c => matches(m, c))).map(m => m.original),
|
|
375
|
+
onlyInCode: codeArr.filter(c => !docMatchers.some(m => matches(m, c))),
|
|
376
|
+
matched: docMatchers.filter(m => codeArr.some(c => matches(m, c))).map(m => m.original),
|
|
366
377
|
};
|
|
367
378
|
}
|
|
368
379
|
|
package/cli/commands/explain.mjs
CHANGED
|
@@ -76,13 +76,14 @@ const EXPLAINERS = {
|
|
|
76
76
|
},
|
|
77
77
|
testSpec: {
|
|
78
78
|
title: 'Test-Spec — declared tests exist',
|
|
79
|
-
what: 'Reads TEST-SPEC.md\'s test
|
|
79
|
+
what: 'Reads TEST-SPEC.md\'s "## Source-to-Test Map" table and verifies every referenced file exists. Parsing is column-HEADER-aware: it locates the source column, the status column, and EVERY test-file column (Unit Test, Integration Test, …) by name, so both the minimal 3-column table and the 4-column table `docguard generate` emits are checked in full — a missing Integration Test is no longer skipped, and a blank cell no longer shifts the columns.',
|
|
80
80
|
why: 'A spec that claims test coverage for X but the test file is missing is a stale promise.',
|
|
81
81
|
triggers: [
|
|
82
|
-
['no service-to-test mappings', 'TEST-SPEC.md has no recognized mapping table. Add a
|
|
82
|
+
['no service-to-test mappings', 'TEST-SPEC.md has no recognized mapping table. Add a "## Source-to-Test Map" with column 1 = source, column 2 = test file, last = status. Both `| Source | Test file | Status |` and the generated `| Source File | Unit Test | Integration Test | Status |` are accepted.'],
|
|
83
83
|
['referenced test file does not exist', 'A path in TEST-SPEC.md\'s mapping doesn\'t exist. Update the path or remove the row.'],
|
|
84
|
+
['this project has no automated tests (POC, spike, library)', 'Declare it visibly instead of fighting the validator: add `<!-- docguard:validator testSpec n/a — POC, no automated tests yet -->` to TEST-SPEC.md or AGENTS.md. Test-Spec then renders ➖ [N/A] with your reason (git-tracked), not a warning.'],
|
|
84
85
|
],
|
|
85
|
-
example: '| `src/auth.ts` | `tests/auth.test.ts` | ✅ |',
|
|
86
|
+
example: '| `src/auth.ts` | `tests/auth.test.ts` | ✅ | (or the generated 4-column shape)',
|
|
86
87
|
standard: 'ISO/IEC/IEEE 29119-3 (test specification)',
|
|
87
88
|
},
|
|
88
89
|
environment: {
|
|
@@ -119,14 +120,16 @@ const EXPLAINERS = {
|
|
|
119
120
|
standard: 'CDD principle: docs and code commit together',
|
|
120
121
|
},
|
|
121
122
|
traceability: {
|
|
122
|
-
title: 'Traceability —
|
|
123
|
-
what: '
|
|
124
|
-
why: 'Untraceable requirements drift from implementation.',
|
|
123
|
+
title: 'Traceability — requirement IDs have test coverage + docs link to code',
|
|
124
|
+
what: 'Two linkages under one validator: (1) scans specs/ for FR-###/SC-### (also REQ-###, T-###) requirement IDs, each of which must appear in a test as `@req FR-###`; (2) flags a canonical doc that exists but that no source file references back to — the "unlinked doc" warning.',
|
|
125
|
+
why: 'Untraceable requirements drift from implementation, and a doc no code points to is memory nothing reads.',
|
|
125
126
|
triggers: [
|
|
126
127
|
['has no test coverage', 'Add `// @req FR-012` (or similar) as a comment in the test that verifies the requirement.'],
|
|
127
128
|
['orphaned test reference', 'A `@req` comment references an ID that doesn\'t exist in any spec. Update the ID or remove the marker.'],
|
|
129
|
+
['unlinked doc', 'A canonical doc (e.g. TEST-SPEC.md) exists but no source file references it. Link it from code/tests, or treat it as advisory if the doc is intentionally standalone. This is a doc→source check, distinct from the FR/SC→test check above — they share the Traceability bucket.'],
|
|
130
|
+
['this project has no formal requirements', 'Declare it visibly: add `<!-- docguard:validator traceability n/a — no formal requirements doc -->` to a canonical doc or AGENTS.md. Renders as ➖ [N/A] with the reason instead of warning on every loose ID.'],
|
|
128
131
|
],
|
|
129
|
-
example: 'spec.md defines `**FR-012**: ...` and test
|
|
132
|
+
example: 'spec.md defines `**FR-012**: ...` and a test has `// @req FR-012`; and TEST-SPEC.md is referenced from a test/source file',
|
|
130
133
|
standard: 'ISO/IEC/IEEE 29148 (requirements traceability)',
|
|
131
134
|
},
|
|
132
135
|
apiSurface: {
|
|
@@ -215,8 +218,155 @@ const EXPLAINERS = {
|
|
|
215
218
|
example: 'plan.md has Summary, Technical Context, Constitution Check, Project Structure',
|
|
216
219
|
standard: 'GitHub Spec Kit',
|
|
217
220
|
},
|
|
221
|
+
|
|
222
|
+
// ── Backfilled in v0.24 (field report, Issue A) ─────────────────────────
|
|
223
|
+
// These validators were registered in guard but had no explain entry, so
|
|
224
|
+
// `docguard explain <key>` returned "not found" — including the very
|
|
225
|
+
// negation-load escape hatch that v0.23.0 shipped (docQuality). The new
|
|
226
|
+
// tests/explain-coverage.test.mjs asserts this table covers every key the
|
|
227
|
+
// guard registry exposes, so the gap can't silently reopen.
|
|
228
|
+
docSections: {
|
|
229
|
+
// Reported by guard under the `structure` severity key, but it's a
|
|
230
|
+
// distinct check (required headings, not file existence) with its own
|
|
231
|
+
// warning + N/A marker — so it gets its own explainer.
|
|
232
|
+
title: 'Doc Sections — each canonical doc has its required headings',
|
|
233
|
+
what: 'For each canonical doc, verifies the required `##` sections exist as real headings: ARCHITECTURE.md (System Overview, Component Map, Tech Stack), DATA-MODEL.md (Entities), SECURITY.md (Authentication, Secrets Management), TEST-SPEC.md (Test Categories, Coverage Rules), ENVIRONMENT.md (Environment Variables, Setup Steps). The DATA-MODEL and ENVIRONMENT requirements relax automatically for CLI/library projects.',
|
|
234
|
+
why: 'A canonical doc that exists but lacks its sections is an empty shell — the section structure is what makes it a reliable memory anchor for humans and agents.',
|
|
235
|
+
triggers: [
|
|
236
|
+
['missing section', 'Add the named `## Section` heading. If the section is genuinely not applicable (e.g. a CLI with no auth), add the N/A marker — a reason is required: `<!-- docguard:section authentication n/a — CLI tool, no auth layer -->`.'],
|
|
237
|
+
],
|
|
238
|
+
example: 'SECURITY.md contains both `## Authentication` and `## Secrets Management` as real headings — or carries `<!-- docguard:section authentication n/a — CLI, no auth -->`',
|
|
239
|
+
standard: 'CDD STANDARD (canonical doc section contract)',
|
|
240
|
+
},
|
|
241
|
+
architecture: {
|
|
242
|
+
title: 'Architecture — module imports respect layer boundaries',
|
|
243
|
+
what: 'Builds an import graph across JS/TS files (ES imports, dynamic imports, CommonJS require) and flags (a) imports that cross a forbidden layer boundary declared in `config.layers` or a "Layer Boundaries" table in ARCHITECTURE.md, and (b) circular dependency cycles (length 3–6). N/A unless layers are declared in config or ARCHITECTURE.md.',
|
|
244
|
+
why: 'Layer violations and import cycles are how a clean architecture rots silently. Catching them at doc-time keeps the documented design honest.',
|
|
245
|
+
triggers: [
|
|
246
|
+
['Circular dependency', 'Break the cycle — extract the shared piece into a module both can import, or invert one of the dependencies.'],
|
|
247
|
+
['forbidden by ARCHITECTURE.md', 'An import crosses a boundary your ARCHITECTURE.md "Layer Boundaries" table forbids. Remove the import or update the declared boundary.'],
|
|
248
|
+
['layer imports from forbidden layer', 'Same violation, declared via `config.layers.<layer>.canImport`. Adjust the import or widen the allowed list.'],
|
|
249
|
+
],
|
|
250
|
+
example: 'ARCHITECTURE.md declares the routes layer may not import from routes, every import respects it, and there are no import cycles',
|
|
251
|
+
standard: 'Layered architecture / Acyclic Dependencies Principle',
|
|
252
|
+
},
|
|
253
|
+
docsDiff: {
|
|
254
|
+
title: 'Docs-Diff — declared tech stack + tests match reality',
|
|
255
|
+
what: 'Two soft-signal diffs: (1) technologies named in ARCHITECTURE.md vs. technologies implied by your dependencies (plus Dockerfile / Terraform detection); (2) test files referenced in TEST-SPEC.md vs. the `*.test.*` / `*.spec.*` files actually on disk. Warnings only — drift is a signal, not a failure. (Env-var drift is handled separately by the Environment validator.)',
|
|
256
|
+
why: 'The tech-stack section and the test list are the doc parts that rot fastest as dependencies and tests come and go.',
|
|
257
|
+
triggers: [
|
|
258
|
+
['drift:', 'ARCHITECTURE.md or TEST-SPEC.md disagrees with the code. The warning spells out which items are "in code but not documented" vs. "documented but not found in code".'],
|
|
259
|
+
['in code but not documented', 'Add the technology or test file to the relevant doc.'],
|
|
260
|
+
['documented but not found in code', 'Remove the stale reference, or restore the file if it was deleted by mistake.'],
|
|
261
|
+
],
|
|
262
|
+
example: 'ARCHITECTURE.md names exactly the stack the dependencies imply, and every glob in TEST-SPEC.md matches a real test file',
|
|
263
|
+
standard: 'CDD principle: documented surfaces match implemented surfaces',
|
|
264
|
+
},
|
|
265
|
+
metadataSync: {
|
|
266
|
+
title: 'Metadata-Sync — version strings agree with package.json',
|
|
267
|
+
what: 'Takes package.json `version` as the source of truth and flags (a) a different `version:` in extension.yml/yaml, and (b) older same-major version references in docs — but only in actionable contexts (release/download URLs, `@x.y.z` install specs, `version:` declarations). CHANGELOG.md and DRIFT-LOG.md are skipped as historical by definition.',
|
|
268
|
+
why: 'A README install line pinned to an old version, or an extension manifest left behind on release, sends users to the wrong artifact.',
|
|
269
|
+
triggers: [
|
|
270
|
+
['but package.json is', 'A tracked version string disagrees with package.json. Update it (`docguard fix --write` handles this when marked auto-fixable).'],
|
|
271
|
+
['in an actionable context', 'A docs install command / URL / declaration references an older same-major version. Bump it to the current version.'],
|
|
272
|
+
],
|
|
273
|
+
example: 'package.json is 0.23.0, extension.yml says 0.23.0, and every install command / release URL points at 0.23.0',
|
|
274
|
+
standard: 'Semantic Versioning (consistency of published version references)',
|
|
275
|
+
},
|
|
276
|
+
docsCoverage: {
|
|
277
|
+
title: 'Docs-Coverage — documentable artifacts are mentioned somewhere',
|
|
278
|
+
what: 'Collects all doc text (README, AGENTS.md, CLAUDE.md, CONTRIBUTING.md, STANDARD.md, docs-canonical/, docs/, docs-implementation/, extensions/) and checks that root config files, package.json `bin` commands, source directories, and config files the code actually reads each appear in at least one doc. Also checks the README has Installation / Usage / License sections.',
|
|
279
|
+
why: 'A feature, command, or config file that no doc mentions is invisible to new contributors and to AI agents reading the project.',
|
|
280
|
+
triggers: [
|
|
281
|
+
['exists but is not mentioned in any documentation', 'Document the config file\'s purpose in ARCHITECTURE.md or README.md.'],
|
|
282
|
+
['but it\'s not mentioned in any documentation', 'A package.json `bin` command is undocumented — mention it in the README.'],
|
|
283
|
+
['is not referenced in ARCHITECTURE.md', 'A source directory has no mention in ARCHITECTURE.md — add it to the Component Map.'],
|
|
284
|
+
['is missing a', 'README.md lacks a required section (Installation / Usage / License, per the Standard README spec).'],
|
|
285
|
+
],
|
|
286
|
+
example: 'Every root config file, package.json bin command, and source directory is named in README.md or ARCHITECTURE.md; the README has Installation, Usage, and License',
|
|
287
|
+
standard: 'Standard README (github.com/RichardLitt/standard-readme)',
|
|
288
|
+
},
|
|
289
|
+
docQuality: {
|
|
290
|
+
title: 'Doc-Quality — prose is clear and verifiable',
|
|
291
|
+
what: 'Runs 8 deterministic prose metrics on each canonical doc + README: passive voice, ambiguous pronouns, atomicity, Flesch readability, Flesch-Kincaid grade, sentence length, negation load, conditional load. Docs under 50 prose words or 3 sentences are skipped as reference material.',
|
|
292
|
+
why: 'Vague, passive, negation-heavy docs are hard for both humans and AI agents to act on. Metrics inspired by IEEE 830 / ISO 29148.',
|
|
293
|
+
triggers: [
|
|
294
|
+
['High negation load', 'Rephrase in positive terms ("must not fail" → "must succeed"). If the negation is intentional (security/operational docs legitimately use "never"/"must not"), add the per-doc override: `<!-- docguard:quality negation-load off — your reason -->`, or set a custom bar with `<!-- docguard:quality negation-load 0.35 — reason -->`. Project-wide default: `docQuality.negationLoadThreshold` in .docguard.json.'],
|
|
295
|
+
['High passive voice ratio', 'Use active voice: "the config is read by the loader" → "the loader reads the config".'],
|
|
296
|
+
['High ambiguous pronoun ratio', 'Replace "it/this/that/they" with the specific noun.'],
|
|
297
|
+
['Low atomicity', 'Split compound sentences so each states one verifiable fact (IEEE 830 §4.1).'],
|
|
298
|
+
['Reading level too high', 'Aim for grade 12–16 for technical docs — shorter sentences, simpler words.'],
|
|
299
|
+
['High conditional load', 'Split tangled conditionals into separate requirements.'],
|
|
300
|
+
],
|
|
301
|
+
example: 'SECURITY.md written in active voice with negation in ≤20% of sentences — or carrying `<!-- docguard:quality negation-load off — prohibitive language is precise here -->`',
|
|
302
|
+
standard: 'IEEE 830 / ISO/IEC/IEEE 29148 (readable, verifiable requirements)',
|
|
303
|
+
},
|
|
304
|
+
schemaSync: {
|
|
305
|
+
title: 'Schema-Sync — DB models in code are documented in DATA-MODEL.md',
|
|
306
|
+
what: 'Detects schema files for 7 ORMs/frameworks (Prisma, Drizzle, Sequelize, TypeORM, Knex, Django, Rails), extracts the model/table names, and checks each appears (case-insensitive, singular/plural aware) in docs-canonical/DATA-MODEL.md. Migration/utility tables are filtered out. No schema files → passes silently.',
|
|
307
|
+
why: 'An undocumented table is a data model only the code knows — exactly the institutional memory CDD exists to preserve.',
|
|
308
|
+
triggers: [
|
|
309
|
+
['not documented in DATA-MODEL.md', 'Add the model to DATA-MODEL.md\'s Entity Definitions section.'],
|
|
310
|
+
['but no DATA-MODEL.md exists', 'Models were found but DATA-MODEL.md is missing. Run `docguard init` to create it, then document the schema.'],
|
|
311
|
+
],
|
|
312
|
+
example: 'Every `model User` / `model Order` in schema.prisma is named in docs-canonical/DATA-MODEL.md',
|
|
313
|
+
standard: 'CDD principle: the data model is documented, not implied',
|
|
314
|
+
},
|
|
218
315
|
};
|
|
219
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Validator-key → display name, mirroring the names guard prints in its
|
|
319
|
+
* report. Users only ever see these names (e.g. "Doc-Quality", "Doc Sections"),
|
|
320
|
+
* so `docguard explain` must resolve them too — typing what you see should work.
|
|
321
|
+
*
|
|
322
|
+
* This intentionally lists every key the guard registry exposes (guard.mjs).
|
|
323
|
+
* tests/explain-coverage.test.mjs asserts it stays in lock-step with the live
|
|
324
|
+
* registry, so a new validator can't ship without an explain entry + name.
|
|
325
|
+
*/
|
|
326
|
+
const DISPLAY_NAMES = {
|
|
327
|
+
structure: 'Structure',
|
|
328
|
+
docSections: 'Doc Sections',
|
|
329
|
+
docsSync: 'Docs-Sync',
|
|
330
|
+
drift: 'Drift-Comments',
|
|
331
|
+
changelog: 'Changelog',
|
|
332
|
+
testSpec: 'Test-Spec',
|
|
333
|
+
environment: 'Environment',
|
|
334
|
+
security: 'Security',
|
|
335
|
+
architecture: 'Architecture',
|
|
336
|
+
freshness: 'Freshness',
|
|
337
|
+
traceability: 'Traceability',
|
|
338
|
+
docsDiff: 'Docs-Diff',
|
|
339
|
+
apiSurface: 'API-Surface',
|
|
340
|
+
metadataSync: 'Metadata-Sync',
|
|
341
|
+
docsCoverage: 'Docs-Coverage',
|
|
342
|
+
docQuality: 'Doc-Quality',
|
|
343
|
+
todoTracking: 'TODO-Tracking',
|
|
344
|
+
schemaSync: 'Schema-Sync',
|
|
345
|
+
specKit: 'Spec-Kit',
|
|
346
|
+
crossReference: 'Cross-Reference',
|
|
347
|
+
generatedStaleness: 'Generated-Staleness',
|
|
348
|
+
surfaceSync: 'Surface-Sync',
|
|
349
|
+
canonicalSync: 'Canonical-Sync',
|
|
350
|
+
metricsConsistency: 'Metrics-Consistency',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/** Collapse a key / display name to a comparable form: lowercase, alnum only. */
|
|
354
|
+
const normalizeKey = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* normalized(alias) → canonical key, built once from both the explainer keys
|
|
358
|
+
* and the guard display names. Lets `docQuality`, `doc-quality`, `Doc-Quality`,
|
|
359
|
+
* and `"doc quality"` all resolve to the same entry.
|
|
360
|
+
*/
|
|
361
|
+
const ALIAS_INDEX = (() => {
|
|
362
|
+
const idx = {};
|
|
363
|
+
for (const key of Object.keys(EXPLAINERS)) idx[normalizeKey(key)] = key;
|
|
364
|
+
for (const [key, name] of Object.entries(DISPLAY_NAMES)) {
|
|
365
|
+
if (EXPLAINERS[key]) idx[normalizeKey(name)] = key; // name → key (only if explainable)
|
|
366
|
+
}
|
|
367
|
+
return idx;
|
|
368
|
+
})();
|
|
369
|
+
|
|
220
370
|
/**
|
|
221
371
|
* Match a warning text fragment against the explainer table. Returns the
|
|
222
372
|
* matching entry's key + the trigger entry that best matches, or null when
|
|
@@ -225,11 +375,10 @@ const EXPLAINERS = {
|
|
|
225
375
|
function matchWarning(query) {
|
|
226
376
|
const q = query.toLowerCase();
|
|
227
377
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (EXPLAINERS[camelized]) return { key: camelized, trigger: null };
|
|
378
|
+
// Key / display-name lookup, casing- and separator-insensitive. Covers
|
|
379
|
+
// `freshness`, `cross-reference`, `Doc-Quality`, `"doc sections"`, etc.
|
|
380
|
+
const aliased = ALIAS_INDEX[normalizeKey(query)];
|
|
381
|
+
if (aliased) return { key: aliased, trigger: null };
|
|
233
382
|
|
|
234
383
|
// Search trigger phrases
|
|
235
384
|
let best = null;
|
|
@@ -253,16 +402,19 @@ export function runExplain(projectDir, _config, flags) {
|
|
|
253
402
|
const isJson = flags.format === 'json';
|
|
254
403
|
|
|
255
404
|
if (!query) {
|
|
405
|
+
// Exhaustive by construction: iterate the guard display-name map so the
|
|
406
|
+
// list always covers every registered validator (field report, Issue A.3).
|
|
407
|
+
const listed = Object.keys(DISPLAY_NAMES).filter(k => EXPLAINERS[k]);
|
|
256
408
|
if (isJson) {
|
|
257
|
-
console.log(JSON.stringify({ validators:
|
|
409
|
+
console.log(JSON.stringify({ validators: listed }, null, 2));
|
|
258
410
|
return;
|
|
259
411
|
}
|
|
260
412
|
console.log(`${c.bold}🧭 docguard explain${c.reset} ${c.dim}— usage:${c.reset}`);
|
|
261
|
-
console.log(` ${c.cyan}docguard explain <validator
|
|
413
|
+
console.log(` ${c.cyan}docguard explain <validator>${c.reset} e.g. docguard explain doc-quality ${c.dim}(key or the name shown in guard)${c.reset}`);
|
|
262
414
|
console.log(` ${c.cyan}docguard explain "<warning text>"${c.reset} e.g. docguard explain "no service-to-test mappings"`);
|
|
263
|
-
console.log(`\n${c.dim}Known validators:${c.reset}`);
|
|
264
|
-
for (const
|
|
265
|
-
console.log(` ${c.cyan}${k.padEnd(22)}${c.reset} ${c.dim}${
|
|
415
|
+
console.log(`\n${c.dim}Known validators (${listed.length}):${c.reset}`);
|
|
416
|
+
for (const k of listed) {
|
|
417
|
+
console.log(` ${c.cyan}${DISPLAY_NAMES[k].padEnd(22)}${c.reset} ${c.dim}${EXPLAINERS[k].title}${c.reset}`);
|
|
266
418
|
}
|
|
267
419
|
return;
|
|
268
420
|
}
|
|
@@ -305,4 +457,13 @@ export function runExplain(projectDir, _config, flags) {
|
|
|
305
457
|
|
|
306
458
|
console.log(`${c.bold}Passing example:${c.reset}\n ${c.dim}${e.example}${c.reset}\n`);
|
|
307
459
|
console.log(`${c.bold}Standard:${c.reset} ${c.dim}${e.standard}${c.reset}`);
|
|
460
|
+
|
|
461
|
+
// v0.24: surface how to mute/tune from config — there was no in-tool way to
|
|
462
|
+
// discover this, so users reached for severity:"off" (a no-op) instead of
|
|
463
|
+
// the real switch (field report). docSections is reported under the
|
|
464
|
+
// `structure` key in guard, so it disables via that key.
|
|
465
|
+
const cfgKey = match.key === 'docSections' ? 'structure' : match.key;
|
|
466
|
+
console.log(`\n${c.bold}Tune it${c.reset} ${c.dim}(.docguard.json):${c.reset}`);
|
|
467
|
+
console.log(` ${c.cyan}validators.${cfgKey}: false${c.reset} ${c.dim}— disable this validator entirely${c.reset}`);
|
|
468
|
+
console.log(` ${c.cyan}severity.${cfgKey}: "high" | "low"${c.reset} ${c.dim}— change exit-code weight (low = warn-only). Severity never hides the warning from output.${c.reset}`);
|
|
308
469
|
}
|
package/cli/commands/fix.mjs
CHANGED
|
@@ -699,10 +699,25 @@ function outputResults(issues, projectDir, config, flags) {
|
|
|
699
699
|
const isPrompt = flags.format === 'prompt';
|
|
700
700
|
|
|
701
701
|
if (issues.length === 0) {
|
|
702
|
+
// v0.24: `fix` only handles issues it can mechanically resolve / prompt for.
|
|
703
|
+
// It used to claim "documentation is complete!" even when `guard` still had
|
|
704
|
+
// advisory warnings (e.g. an unmapped Test-Spec table), so the two commands
|
|
705
|
+
// contradicted each other (field report). Surface guard's advisory count so
|
|
706
|
+
// they agree: 0 fixable ≠ 0 warnings.
|
|
707
|
+
let advisory = 0;
|
|
708
|
+
try {
|
|
709
|
+
const g = runGuardInternal(projectDir, config);
|
|
710
|
+
advisory = (g.validators || []).reduce((n, v) => n + ((v.warnings && v.warnings.length) || 0), 0);
|
|
711
|
+
} catch { /* if guard can't run, fall back to the plain clean message */ }
|
|
712
|
+
|
|
702
713
|
if (isJson) {
|
|
703
|
-
console.log(JSON.stringify({ status: 'clean', issues: [], fixCount: 0 }));
|
|
714
|
+
console.log(JSON.stringify({ status: 'clean', issues: [], fixCount: 0, advisoryWarnings: advisory }));
|
|
704
715
|
} else if (isPrompt) {
|
|
705
|
-
console.log(
|
|
716
|
+
console.log(advisory > 0
|
|
717
|
+
? `No mechanically-fixable issues. ${advisory} advisory warning(s) remain — run \`docguard guard\` to see them.`
|
|
718
|
+
: 'No CDD issues found. All documentation is complete.');
|
|
719
|
+
} else if (advisory > 0) {
|
|
720
|
+
console.log(` ${c.green}${c.bold}✅ No fixable issues here.${c.reset} ${c.dim}guard still reports ${advisory} advisory warning(s) that need authoring, not mechanical fixes — run ${c.reset}${c.cyan}docguard guard${c.dim} to see them.${c.reset}\n`);
|
|
706
721
|
} else {
|
|
707
722
|
console.log(` ${c.green}${c.bold}✅ No issues — documentation is complete!${c.reset}\n`);
|
|
708
723
|
}
|
|
@@ -234,13 +234,13 @@ export function runGenerate(projectDir, config, flags) {
|
|
|
234
234
|
const scan = scanProject(projectDir);
|
|
235
235
|
|
|
236
236
|
// ── 4. Deep Scan Routes ──
|
|
237
|
-
const deepRoutes = scanRoutesDeep(projectDir, stack, docTools);
|
|
237
|
+
const deepRoutes = scanRoutesDeep(projectDir, stack, docTools, { config });
|
|
238
238
|
if (deepRoutes.length > 0) {
|
|
239
239
|
console.log(` ${c.bold}Route Scanning:${c.reset} ${deepRoutes.length} endpoints found (source: ${deepRoutes[0]?.source || 'code'})`);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
// ── 5. Deep Scan Schemas ──
|
|
243
|
-
const deepSchemas = scanSchemasDeep(projectDir, stack, docTools);
|
|
243
|
+
const deepSchemas = scanSchemasDeep(projectDir, stack, docTools, config);
|
|
244
244
|
if (deepSchemas.entities.length > 0) {
|
|
245
245
|
console.log(` ${c.bold}Schema Scanning:${c.reset} ${deepSchemas.entities.length} entities, ${deepSchemas.relationships.length} relationships (source: ${deepSchemas.source})`);
|
|
246
246
|
}
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { c, resolveSeverity } from '../shared.mjs';
|
|
11
|
+
import { loadValidatorSuppressions } from '../validator-markers.mjs';
|
|
11
12
|
import { detectAgentMode, isSpecKitInitialized } from '../ensure-skills.mjs';
|
|
12
13
|
import { checkUpgradeStatus } from './upgrade.mjs';
|
|
13
14
|
import { changedFilesSince, isGitRepo } from '../shared-git.mjs';
|
|
@@ -220,10 +221,26 @@ export function runGuardInternal(projectDir, config) {
|
|
|
220
221
|
// Metrics-Consistency runs post-loop (needs guard results)
|
|
221
222
|
];
|
|
222
223
|
|
|
224
|
+
// Inline `<!-- docguard:validator <key> n/a — reason -->` markers let a
|
|
225
|
+
// project declare a whole validator non-applicable, visibly and in-repo
|
|
226
|
+
// (e.g. a POC marking testSpec/traceability N/A). Distinct from the config
|
|
227
|
+
// `validators:{k:false}` switch: a marked validator renders as ➖ [N/A] with
|
|
228
|
+
// its reason, not a silent skip. Resolve once against the full key set.
|
|
229
|
+
const allValidatorKeys = [...new Set(validatorMap.map(v => v.key)), 'canonicalSync', 'metricsConsistency'];
|
|
230
|
+
const { suppressed: naMarkers, unknown: unknownMarkers } = loadValidatorSuppressions(projectDir, allValidatorKeys);
|
|
231
|
+
const naResult = (name, key) => ({
|
|
232
|
+
name, key, status: 'na', quality: null, errors: [], warnings: [], passed: 0, total: 0, durationMs: 0,
|
|
233
|
+
note: naMarkers.get(key) ? `declared N/A: ${naMarkers.get(key)}` : 'declared N/A',
|
|
234
|
+
});
|
|
235
|
+
|
|
223
236
|
// v0.14-Q2: per-validator timing. Cheap (one `performance.now()` pair per
|
|
224
237
|
// validator) and the data is what we'd need to optimize anything later.
|
|
225
238
|
// Exposed via --profile in the public guard.
|
|
226
239
|
for (const { key, name, fn } of validatorMap) {
|
|
240
|
+
if (naMarkers.has(key)) {
|
|
241
|
+
results.push(naResult(name, key));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
227
244
|
if (validators[key] === false) {
|
|
228
245
|
results.push({ name, key, status: 'skipped', quality: null, errors: [], warnings: [], passed: 0, total: 0, durationMs: 0 });
|
|
229
246
|
continue;
|
|
@@ -244,7 +261,9 @@ export function runGuardInternal(projectDir, config) {
|
|
|
244
261
|
// Needs the live validator results to count "real" validators that ran.
|
|
245
262
|
// (Pre-canonical-sync ordering — comes before metrics-consistency so the
|
|
246
263
|
// metrics validator sees a stable surface count.)
|
|
247
|
-
if (
|
|
264
|
+
if (naMarkers.has('canonicalSync')) {
|
|
265
|
+
results.push(naResult('Canonical-Sync', 'canonicalSync'));
|
|
266
|
+
} else if (validators.canonicalSync !== false) {
|
|
248
267
|
const start = performance.now();
|
|
249
268
|
try {
|
|
250
269
|
const result = validateCanonicalSync(projectDir, config, results);
|
|
@@ -257,7 +276,9 @@ export function runGuardInternal(projectDir, config) {
|
|
|
257
276
|
}
|
|
258
277
|
|
|
259
278
|
// ── Metrics-Consistency runs AFTER all other validators (needs their results) ──
|
|
260
|
-
if (
|
|
279
|
+
if (naMarkers.has('metricsConsistency')) {
|
|
280
|
+
results.push(naResult('Metrics-Consistency', 'metricsConsistency'));
|
|
281
|
+
} else if (validators.metricsConsistency !== false) {
|
|
261
282
|
const start = performance.now();
|
|
262
283
|
try {
|
|
263
284
|
const result = validateMetricsConsistency(projectDir, config, results);
|
|
@@ -294,7 +315,13 @@ export function runGuardInternal(projectDir, config) {
|
|
|
294
315
|
else effectiveWarnings += wCount;
|
|
295
316
|
}
|
|
296
317
|
|
|
297
|
-
|
|
318
|
+
// The headline status word MUST agree with the exit code, which is
|
|
319
|
+
// severity-aware (effectiveErrors/effectiveWarnings, computed above).
|
|
320
|
+
// Deriving it from RAW counts was a bug: a validator marked severity=high
|
|
321
|
+
// with only warnings printed "WARN" yet exited 1 (FAIL), and one marked
|
|
322
|
+
// severity=low printed "WARN" yet exited 0 (PASS). Use effective counts so
|
|
323
|
+
// what the user reads is what CI does.
|
|
324
|
+
const overallStatus = effectiveErrors > 0 ? 'FAIL' : effectiveWarnings > 0 ? 'WARN' : 'PASS';
|
|
298
325
|
|
|
299
326
|
return {
|
|
300
327
|
project: config.projectName,
|
|
@@ -310,6 +337,11 @@ export function runGuardInternal(projectDir, config) {
|
|
|
310
337
|
effectiveErrors,
|
|
311
338
|
effectiveWarnings,
|
|
312
339
|
validators: results,
|
|
340
|
+
// Unknown keys in `docguard:validator … n/a` markers — typo protection so
|
|
341
|
+
// a mistyped key doesn't silently fail to suppress. Surfaced by runGuard.
|
|
342
|
+
validatorMarkerWarnings: unknownMarkers.map(
|
|
343
|
+
u => `Unknown validator key "${u.raw}" in a docguard:validator marker — ignored. Valid keys: ${allValidatorKeys.join(', ')}`
|
|
344
|
+
),
|
|
313
345
|
timestamp: new Date().toISOString(),
|
|
314
346
|
};
|
|
315
347
|
}
|
|
@@ -328,10 +360,18 @@ export function runGuardInternal(projectDir, config) {
|
|
|
328
360
|
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface', 'drift', 'todoTracking'];
|
|
329
361
|
|
|
330
362
|
/**
|
|
331
|
-
* Build a validators map that enables
|
|
363
|
+
* Build a validators map that enables the pre-commit-lite set — PLUS any
|
|
364
|
+
* validator the team explicitly escalated to `severity: high`.
|
|
332
365
|
* Used by `docguard guard --changed-only`.
|
|
366
|
+
*
|
|
367
|
+
* Why the union: `--changed-only` trades coverage for speed, but a
|
|
368
|
+
* `severity: high` override is an explicit "this must always block CI" signal.
|
|
369
|
+
* Silently dropping such a validator here meant a changed-only gate could pass
|
|
370
|
+
* (exit 0) on exactly the drift the team most wanted blocked — e.g. a committed
|
|
371
|
+
* secret when `security` is marked high. So high-severity validators are forced
|
|
372
|
+
* on regardless of the lite set, unless the user also explicitly disabled them.
|
|
333
373
|
*/
|
|
334
|
-
function liteValidatorsConfig() {
|
|
374
|
+
export function liteValidatorsConfig(config = {}) {
|
|
335
375
|
const all = [
|
|
336
376
|
'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
|
|
337
377
|
'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
|
|
@@ -339,8 +379,15 @@ function liteValidatorsConfig() {
|
|
|
339
379
|
'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
|
|
340
380
|
'canonicalSync', 'metricsConsistency',
|
|
341
381
|
];
|
|
382
|
+
const userValidators = (config && config.validators) || {};
|
|
342
383
|
const out = {};
|
|
343
|
-
for (const k of all)
|
|
384
|
+
for (const k of all) {
|
|
385
|
+
let enabled = CHANGED_ONLY_VALIDATORS.includes(k);
|
|
386
|
+
if (!enabled && resolveSeverity(config, k) === 'high' && userValidators[k] !== false) {
|
|
387
|
+
enabled = true;
|
|
388
|
+
}
|
|
389
|
+
out[k] = enabled;
|
|
390
|
+
}
|
|
344
391
|
return out;
|
|
345
392
|
}
|
|
346
393
|
|
|
@@ -358,16 +405,24 @@ export function runGuard(projectDir, config, flags) {
|
|
|
358
405
|
// scope to this list; others run normally over the whole tree.
|
|
359
406
|
const ref = flags.since || 'HEAD~1';
|
|
360
407
|
const changed = isGitRepo(projectDir) ? changedFilesSince(projectDir, ref) : [];
|
|
408
|
+
const liteVals = liteValidatorsConfig(config);
|
|
409
|
+
// Validators that ran beyond the lite set because they're severity=high.
|
|
410
|
+
const escalated = Object.keys(liteVals).filter(
|
|
411
|
+
k => liteVals[k] && !CHANGED_ONLY_VALIDATORS.includes(k)
|
|
412
|
+
);
|
|
361
413
|
config = {
|
|
362
414
|
...config,
|
|
363
|
-
validators:
|
|
415
|
+
validators: liteVals,
|
|
364
416
|
changedFiles: changed,
|
|
365
417
|
changedSinceRef: ref,
|
|
366
418
|
};
|
|
367
419
|
const label = changed.length > 0
|
|
368
420
|
? `${changed.length} file(s) changed since ${ref}`
|
|
369
421
|
: `no changes since ${ref} — running all ${CHANGED_ONLY_VALIDATORS.length} lite validators on full tree`;
|
|
370
|
-
|
|
422
|
+
const escalatedNote = escalated.length > 0
|
|
423
|
+
? ` ${c.yellow}+ ${escalated.length} high-severity validator(s): ${escalated.join(', ')}${c.reset}`
|
|
424
|
+
: '';
|
|
425
|
+
console.log(`${c.cyan}⚡ docguard guard --changed-only${c.reset} ${c.dim}(${label})${c.reset}${escalatedNote}\n`);
|
|
371
426
|
}
|
|
372
427
|
|
|
373
428
|
const data = runGuardInternal(projectDir, config);
|
|
@@ -440,11 +495,23 @@ export function runGuard(projectDir, config, flags) {
|
|
|
440
495
|
console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
|
|
441
496
|
|
|
442
497
|
if (data.status === 'PASS') {
|
|
443
|
-
|
|
498
|
+
// PASS can still carry raw warnings if a validator was demoted to
|
|
499
|
+
// severity=low — surface that honestly rather than claiming a clean sweep.
|
|
500
|
+
if (data.warnings > 0) {
|
|
501
|
+
console.log(` ${c.green}${c.bold}✅ PASS${c.reset} ${c.green}— ${data.passed}/${data.total} passed (${data.warnings} non-blocking warning(s))${c.reset}`);
|
|
502
|
+
} else {
|
|
503
|
+
console.log(` ${c.green}${c.bold}✅ PASS${c.reset} ${c.green}— All ${data.total} checks passed${c.reset}`);
|
|
504
|
+
}
|
|
444
505
|
} else if (data.status === 'WARN') {
|
|
445
|
-
|
|
506
|
+
// effective* counts are what drove the verdict; raw warnings are still
|
|
507
|
+
// enumerated per-validator above, so nothing is hidden.
|
|
508
|
+
console.log(` ${c.yellow}${c.bold}⚠️ WARN${c.reset} ${c.yellow}— ${data.passed}/${data.total} passed, ${data.effectiveWarnings} warning(s)${c.reset}`);
|
|
446
509
|
} else {
|
|
447
|
-
|
|
510
|
+
// effectiveErrors may include warnings escalated by severity=high, so call
|
|
511
|
+
// them "blocking issue(s)" rather than "error(s)". The severity-override
|
|
512
|
+
// note below spells out any escalation/demotion.
|
|
513
|
+
const warnSuffix = data.effectiveWarnings > 0 ? `, ${data.effectiveWarnings} warning(s)` : '';
|
|
514
|
+
console.log(` ${c.red}${c.bold}❌ FAIL${c.reset} ${c.red}— ${data.passed}/${data.total} passed, ${data.effectiveErrors} blocking issue(s)${warnSuffix}${c.reset}`);
|
|
448
515
|
}
|
|
449
516
|
|
|
450
517
|
// Next step hint — always point to diagnose when issues exist
|
|
@@ -533,6 +600,14 @@ export function runGuard(projectDir, config, flags) {
|
|
|
533
600
|
console.log(`\n ${c.yellow}💡${c.reset} ${c.dim}Enhance DocGuard with Spec Kit: ${c.cyan}uv tool install specify-cli --from git+https://github.com/github/spec-kit.git${c.reset}`);
|
|
534
601
|
}
|
|
535
602
|
|
|
603
|
+
// Typo protection for docguard:validator markers — a mistyped key would
|
|
604
|
+
// otherwise silently fail to suppress the validator.
|
|
605
|
+
if (Array.isArray(data.validatorMarkerWarnings) && data.validatorMarkerWarnings.length > 0) {
|
|
606
|
+
for (const w of data.validatorMarkerWarnings) {
|
|
607
|
+
console.log(`\n ${c.yellow}⚠ ${w}${c.reset}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
536
611
|
// When severity overrides demoted warnings to "low" (or promoted them to
|
|
537
612
|
// "high"), show a one-line note so the user knows the exit code may not
|
|
538
613
|
// match what they expected from reading the warning count.
|
package/cli/commands/hooks.mjs
CHANGED
|
@@ -56,6 +56,7 @@ function spliceManagedBlock(existing, newBody) {
|
|
|
56
56
|
}
|
|
57
57
|
import { resolve } from 'node:path';
|
|
58
58
|
import { c } from '../shared.mjs';
|
|
59
|
+
import { getHooksDir } from '../shared-git.mjs';
|
|
59
60
|
|
|
60
61
|
const HOOKS = {
|
|
61
62
|
'pre-commit': {
|
|
@@ -216,17 +217,20 @@ export function runHooks(projectDir, config, flags) {
|
|
|
216
217
|
console.log(`${c.bold}🪝 DocGuard Hooks — ${config.projectName}${c.reset}`);
|
|
217
218
|
console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
|
|
218
219
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
// Resolve the real hooks dir via git — NOT `<projectDir>/.git/hooks`, which
|
|
221
|
+
// is wrong inside a linked worktree (where `.git` is a file, not a dir) and
|
|
222
|
+
// ignores a custom core.hooksPath.
|
|
223
|
+
const hooksDir = getHooksDir(projectDir);
|
|
224
|
+
if (!hooksDir) {
|
|
222
225
|
console.log(` ${c.red}❌ Not a git repository. Run ${c.cyan}git init${c.red} first.${c.reset}\n`);
|
|
223
226
|
process.exit(1);
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
// Only create the dir when we're actually going to write a hook. Read-only
|
|
230
|
+
// modes (--list, --remove) must not have a filesystem side effect.
|
|
231
|
+
const ensureHooksDir = () => {
|
|
232
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
233
|
+
};
|
|
230
234
|
|
|
231
235
|
// Determine which hooks to install
|
|
232
236
|
let hookTypes = Object.keys(HOOKS);
|
|
@@ -273,6 +277,7 @@ export function runHooks(projectDir, config, flags) {
|
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
// Install mode
|
|
280
|
+
ensureHooksDir();
|
|
276
281
|
let installed = 0;
|
|
277
282
|
let skipped = 0;
|
|
278
283
|
|