docguard-cli 0.14.1 → 0.15.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.
@@ -198,7 +198,7 @@ export function runGuardInternal(projectDir, config) {
198
198
  * Freshness (git log), Traceability (REQ scan), Doc-Quality (prose lint) —
199
199
  * stay off for speed.
200
200
  */
201
- export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface'];
201
+ export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface', 'drift', 'todoTracking'];
202
202
 
203
203
  /**
204
204
  * Build a validators map that enables only the pre-commit-lite set.
@@ -167,6 +167,10 @@ export async function runInit(projectDir, config, flags) {
167
167
  const ptc = typeDefaults[detectedType] || typeDefaults.unknown;
168
168
 
169
169
  const defaultConfig = {
170
+ // v0.15-P4: $schema reference enables VS Code / IDE autocomplete +
171
+ // validation for .docguard.json fields. Picked up by any
172
+ // JSON-Schema-aware editor; ignored by DocGuard itself.
173
+ $schema: 'https://raccioly.github.io/docguard/schemas/docguard-config.schema.json',
170
174
  projectName: config.projectName,
171
175
  version: '0.5',
172
176
  profile: profileName,
@@ -29,13 +29,58 @@ const md = {
29
29
  },
30
30
  };
31
31
 
32
+ /**
33
+ * v0.15-P1: in-process cache. buildMemoryPlan is expensive (~400ms on
34
+ * wu-whatsappinbox, 33% of total guard validator time) because it triggers
35
+ * routes/schemas/screens/frontend scanners — all of which walk the source
36
+ * tree. Within a single guard run, sync, generate, and the Generated-
37
+ * Staleness validator all ask for the SAME plan; without caching they each
38
+ * re-pay the cost.
39
+ *
40
+ * Cache key: projectDir + a config fingerprint that captures the fields the
41
+ * scanners actually consume (sourceRoot, ignore, projectType). Other config
42
+ * mutations (e.g. changedFiles per-validator) don't invalidate the plan.
43
+ *
44
+ * Bypass with `_skipCache: true` in opts — used by tests and any caller that
45
+ * wants a fresh scan.
46
+ */
47
+ const _memoryPlanCache = new Map(); // key → plan
48
+
49
+ export function clearMemoryPlanCache() {
50
+ _memoryPlanCache.clear();
51
+ }
52
+
53
+ function _cacheKey(projectDir, config) {
54
+ return JSON.stringify({
55
+ dir: projectDir,
56
+ sourceRoot: config.sourceRoot,
57
+ ignore: Array.isArray(config.ignore) ? [...config.ignore].sort() : null,
58
+ projectType: config.projectType,
59
+ profile: config.profile,
60
+ });
61
+ }
62
+
32
63
  /**
33
64
  * Build the full memory plan for a project.
34
65
  * @returns {{ profile, surface, docs, agentTasks }}
35
66
  * docs[].sections[]: { id, source:'code', body } OR { id, source:'human', task, grounding }
36
67
  * agentTasks: flattened prose tasks the AI must write.
37
68
  */
38
- export function buildMemoryPlan(projectDir, config = {}) {
69
+ export function buildMemoryPlan(projectDir, config = {}, opts = {}) {
70
+ if (!opts._skipCache) {
71
+ const key = _cacheKey(projectDir, config);
72
+ const cached = _memoryPlanCache.get(key);
73
+ if (cached) return cached;
74
+ }
75
+ const result = _buildMemoryPlanUncached(projectDir, config);
76
+ if (!opts._skipCache) {
77
+ _memoryPlanCache.set(_cacheKey(projectDir, config), result);
78
+ }
79
+ return result;
80
+ }
81
+
82
+ // Original implementation, renamed so the public buildMemoryPlan can wrap it.
83
+ function _buildMemoryPlanUncached(projectDir, config = {}) {
39
84
  const profile = detectProjectProfile(projectDir, config);
40
85
  const primaryFramework = profile.primary?.framework || profile.frameworks[0] || '';
41
86
 
@@ -690,20 +690,45 @@ function readFileSafe(path) {
690
690
  try { return readFileSync(path, 'utf-8'); } catch { return null; }
691
691
  }
692
692
 
693
+ // v0.15-P2: walkDir is called 8 times across schemas.mjs (Pydantic, Mongoose,
694
+ // Prisma, SQLAlchemy, Sequelize, GORM, Sqlx, Hibernate). Each call walks the
695
+ // same tree. Cache the file list per (dir, extension-set) so subsequent
696
+ // callers iterate an array instead of re-traversing.
697
+ //
698
+ // Cache key: just the dir path. The extension filter is constant across all
699
+ // callers (the regex hard-coded below), so a single cache slot per dir works.
700
+ // Lifetime: per-process. `clearWalkDirCache()` invalidates for tests.
701
+ const _walkDirCache = new Map(); // dir → string[] of file paths
702
+
703
+ export function clearWalkDirCache() {
704
+ _walkDirCache.clear();
705
+ }
706
+
707
+ const _CODE_FILE_RE = /\.(js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|kt|rb)$/;
708
+
709
+ function _collectFiles(dir, out) {
710
+ let entries;
711
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
712
+ for (const entry of entries) {
713
+ if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
714
+ const fullPath = join(dir, entry.name);
715
+ if (entry.isDirectory()) {
716
+ _collectFiles(fullPath, out);
717
+ } else if (entry.isFile() && _CODE_FILE_RE.test(entry.name)) {
718
+ out.push(fullPath);
719
+ }
720
+ }
721
+ }
722
+
693
723
  function walkDir(dir, callback) {
694
724
  if (!existsSync(dir)) return;
695
- try {
696
- const entries = readdirSync(dir, { withFileTypes: true });
697
- for (const entry of entries) {
698
- if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
699
- const fullPath = join(dir, entry.name);
700
- if (entry.isDirectory()) {
701
- walkDir(fullPath, callback);
702
- } else if (entry.isFile() && /\.(js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|kt|rb)$/.test(entry.name)) {
703
- callback(fullPath);
704
- }
705
- }
706
- } catch { /* skip */ }
725
+ let files = _walkDirCache.get(dir);
726
+ if (!files) {
727
+ files = [];
728
+ _collectFiles(dir, files);
729
+ _walkDirCache.set(dir, files);
730
+ }
731
+ for (const f of files) callback(f);
707
732
  }
708
733
 
709
734
  /**
@@ -20,21 +20,27 @@ const IGNORE_DIRS = new Set([
20
20
  export function validateDrift(projectDir, config) {
21
21
  const results = { name: 'drift', errors: [], warnings: [], passed: 0, total: 0 };
22
22
 
23
- // Find all // DRIFT: comments in source code
24
- const driftComments = [];
25
- walkDir(projectDir, (filePath) => {
23
+ // v0.15-P3: when config.changedFiles is set (--changed-only mode), only
24
+ // visit the listed paths. Drift comments in unchanged files are still in
25
+ // git so they'll be caught by a full guard run; pre-commit hooks care
26
+ // about NEW drift comments in this commit.
27
+ const scanFile = (filePath) => {
26
28
  const ext = extname(filePath);
27
29
  if (!CODE_EXTENSIONS.has(ext)) return;
28
-
29
- const content = readFileSync(filePath, 'utf-8');
30
-
31
- // Fast early-return: skip expensive string split if no comment exists
30
+ // v0.15 hotfix: test files commonly contain literal `// DRIFT:` inside
31
+ // string fixtures (e.g. `'// DRIFT: a-drift\n'`). Reading the test as
32
+ // source would treat the string as a real drift comment. Skip test
33
+ // files unless the user opts in same pattern TODO-Tracking uses.
34
+ const rel = filePath.replace(projectDir + '/', '');
35
+ const includeTests = config?.drift?.includeTestFiles === true;
36
+ if (!includeTests && /(^|\/)(__tests__|tests?|spec)\/|\.(test|spec)\.[^.]+$/.test(rel)) {
37
+ return;
38
+ }
39
+ let content;
40
+ try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
32
41
  if (!content.includes('DRIFT:')) return;
33
-
34
42
  const lines = content.split('\n');
35
-
36
43
  lines.forEach((line, i) => {
37
- // Match various comment styles: // DRIFT:, # DRIFT:, /* DRIFT:, -- DRIFT:
38
44
  const match = line.match(/(?:\/\/|#|\/\*|\-\-)\s*DRIFT:\s*(.+)/i);
39
45
  if (match) {
40
46
  driftComments.push({
@@ -44,7 +50,16 @@ export function validateDrift(projectDir, config) {
44
50
  });
45
51
  }
46
52
  });
47
- });
53
+ };
54
+
55
+ const driftComments = [];
56
+ if (Array.isArray(config.changedFiles) && config.changedFiles.length > 0) {
57
+ for (const rel of config.changedFiles) {
58
+ scanFile(resolve(projectDir, rel));
59
+ }
60
+ } else {
61
+ walkDir(projectDir, scanFile);
62
+ }
48
63
 
49
64
  if (driftComments.length === 0) {
50
65
  // No // DRIFT: comments to reconcile — not applicable (NOT a pass).
@@ -329,6 +329,20 @@ function isSelfPath(fullPath) {
329
329
  }
330
330
 
331
331
  function findTodos(rootDir, dir, todos, config) {
332
+ // v0.15-P3: when config.changedFiles is set (--changed-only mode), only
333
+ // scan those paths. New TODOs in this commit get caught; pre-existing
334
+ // TODOs in unchanged files are still tracked by full guard runs.
335
+ if (dir === rootDir && Array.isArray(config?.changedFiles) && config.changedFiles.length > 0) {
336
+ for (const rel of config.changedFiles) {
337
+ const full = resolve(rootDir, rel);
338
+ let stat;
339
+ try { stat = statSync(full); } catch { continue; }
340
+ if (!stat.isFile()) continue;
341
+ _scanTodoFile(rootDir, full, todos, config);
342
+ }
343
+ return;
344
+ }
345
+
332
346
  let entries;
333
347
  try { entries = readdirSync(dir); } catch { return; }
334
348
 
@@ -345,47 +359,44 @@ function findTodos(rootDir, dir, todos, config) {
345
359
  if (stat.isDirectory()) {
346
360
  findTodos(rootDir, full, todos, config);
347
361
  } else {
348
- const ext = extname(entry).toLowerCase();
349
- if (!SOURCE_EXTENSIONS.has(ext)) continue;
350
-
351
- const relPath = relative(rootDir, full);
352
-
353
- // Skip test files unless explicitly opted in — test fixture strings
354
- // commonly contain comment markers inside template literals that the
355
- // single-line heuristic can't distinguish from real comments.
356
- if (!includeTests && isTestFilePath(relPath)) continue;
357
-
358
- // Skip the validator's own source file — its docstring legitimately
359
- // names the annotation keywords it scans for.
360
- if (isSelfPath(full)) continue;
361
-
362
- // Apply config ignore patterns (todoIgnore + global ignore)
363
- if (config && shouldIgnore(relPath, config, 'todoIgnore')) continue;
364
-
365
- let content;
366
- try { content = readFileSync(full, 'utf-8'); } catch { continue; }
367
-
368
- // Fast early-return: skip expensive string split if no TODO patterns exist
369
- if (!TODO_PATTERN.test(content)) continue;
370
-
371
- const lines = content.split('\n');
372
-
373
- for (let i = 0; i < lines.length; i++) {
374
- // Restrict scanning to text inside a comment keeps the regex from
375
- // matching its own keyword list when DocGuard reads its own source.
376
- const commentText = commentPortion(lines[i]);
377
- if (commentText === null) continue;
378
- if (TODO_PATTERN.test(commentText)) {
379
- const match = commentText.match(TODO_EXTRACT);
380
- if (match) {
381
- todos.push({
382
- keyword: match[1].toUpperCase(),
383
- text: match[2].trim(),
384
- file: relPath,
385
- line: i + 1,
386
- });
387
- }
388
- }
362
+ _scanTodoFile(rootDir, full, todos, config);
363
+ }
364
+ }
365
+ }
366
+
367
+ /**
368
+ * v0.15-P3: per-file TODO scan extracted so both the full-tree walker and
369
+ * the --changed-only path can reuse it. Honors test-file filtering,
370
+ * self-path skip, ignore patterns, and the TODO regex.
371
+ */
372
+ function _scanTodoFile(rootDir, full, todos, config) {
373
+ const includeTests = config?.todoTracking?.includeTestFiles === true;
374
+ const ext = extname(full).toLowerCase();
375
+ if (!SOURCE_EXTENSIONS.has(ext)) return;
376
+
377
+ const relPath = relative(rootDir, full);
378
+
379
+ if (!includeTests && isTestFilePath(relPath)) return;
380
+ if (isSelfPath(full)) return;
381
+ if (config && shouldIgnore(relPath, config, 'todoIgnore')) return;
382
+
383
+ let content;
384
+ try { content = readFileSync(full, 'utf-8'); } catch { return; }
385
+ if (!TODO_PATTERN.test(content)) return;
386
+
387
+ const lines = content.split('\n');
388
+ for (let i = 0; i < lines.length; i++) {
389
+ const commentText = commentPortion(lines[i]);
390
+ if (commentText === null) continue;
391
+ if (TODO_PATTERN.test(commentText)) {
392
+ const match = commentText.match(TODO_EXTRACT);
393
+ if (match) {
394
+ todos.push({
395
+ keyword: match[1].toUpperCase(),
396
+ text: match[2].trim(),
397
+ file: relPath,
398
+ line: i + 1,
399
+ });
389
400
  }
390
401
  }
391
402
  }
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.14.1"
6
+ version: "0.15.1"
7
7
  description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
8
8
  author: "Ricardo Accioly"
9
9
  repository: "https://github.com/raccioly/docguard"
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.14.1
9
+ version: 0.15.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.14.1 -->
12
+ <!-- docguard:version: 0.15.1 -->
13
13
 
14
14
  # DocGuard Fix Skill
15
15
 
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
7
7
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
8
8
  metadata:
9
9
  author: docguard
10
- version: 0.14.1
10
+ version: 0.15.1
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.14.1 -->
13
+ <!-- docguard:version: 0.15.1 -->
14
14
 
15
15
  # DocGuard Guard Skill
16
16
 
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.14.1
9
+ version: 0.15.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.14.1 -->
12
+ <!-- docguard:version: 0.15.1 -->
13
13
 
14
14
  # DocGuard Review Skill
15
15
 
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.14.1
9
+ version: 0.15.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.14.1 -->
12
+ <!-- docguard:version: 0.15.1 -->
13
13
 
14
14
  # DocGuard Score Skill
15
15
 
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
4
4
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
5
5
  metadata:
6
6
  author: docguard
7
- version: 0.14.1
7
+ version: 0.15.1
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,6 +51,7 @@
51
51
  "commands/",
52
52
  "extensions/",
53
53
  "docs/",
54
+ "schemas/",
54
55
  "STANDARD.md",
55
56
  "PHILOSOPHY.md",
56
57
  "README.md",
@@ -0,0 +1,155 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raccioly.github.io/docguard/schemas/docguard-config.schema.json",
4
+ "title": "DocGuard project config (.docguard.json)",
5
+ "description": "Schema for the .docguard.json file that DocGuard reads at the root of every project. Add `\"$schema\": \"https://raccioly.github.io/docguard/schemas/docguard-config.schema.json\"` to your file to get autocomplete + validation in VS Code and other JSON-Schema-aware editors.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "JSON Schema reference for editor autocomplete. Not consumed by DocGuard itself."
11
+ },
12
+ "version": {
13
+ "type": "string",
14
+ "description": "Schema version (0.1, 0.2, ... 0.5). Bumped when fields are added or behavior changes. Migrate with `docguard upgrade --apply`.",
15
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
16
+ },
17
+ "projectName": {
18
+ "type": "string",
19
+ "description": "Human-friendly project name used in guard output."
20
+ },
21
+ "profile": {
22
+ "type": "string",
23
+ "enum": ["starter", "standard", "enterprise"],
24
+ "description": "Compliance profile. starter = minimal, standard = full CDD, enterprise = adds advanced validators.",
25
+ "default": "standard"
26
+ },
27
+ "projectType": {
28
+ "type": "string",
29
+ "enum": ["cli", "library", "webapp", "api", "unknown"],
30
+ "description": "Project shape. Affects which validators run (e.g. webapp + api need env vars; cli/library can skip)."
31
+ },
32
+ "projectTypeConfig": {
33
+ "type": "object",
34
+ "description": "Per-type behavior knobs that override profile defaults.",
35
+ "properties": {
36
+ "needsEnvVars": { "type": "boolean" },
37
+ "needsEnvExample":{ "type": "boolean" },
38
+ "needsE2E": { "type": "boolean" },
39
+ "needsDatabase": { "type": "boolean" }
40
+ },
41
+ "additionalProperties": true
42
+ },
43
+ "sourceRoot": {
44
+ "type": "string",
45
+ "description": "Subdirectory containing the project's source files (e.g. `backend/src`). Validators scope to this when set."
46
+ },
47
+ "ignore": {
48
+ "type": "array",
49
+ "items": { "type": "string" },
50
+ "description": "Glob patterns for paths every validator should skip. Merged with `.docguardignore` at runtime."
51
+ },
52
+ "securityIgnore": {
53
+ "type": "array",
54
+ "items": { "type": "string" },
55
+ "description": "Glob patterns the Security validator additionally skips (e.g. test fixtures with intentional secrets)."
56
+ },
57
+ "todoIgnore": {
58
+ "type": "array",
59
+ "items": { "type": "string" },
60
+ "description": "Glob patterns the TODO-Tracking validator additionally skips."
61
+ },
62
+ "requiredFiles": {
63
+ "type": "object",
64
+ "description": "Files DocGuard expects to exist. Missing files become Structure validator errors.",
65
+ "properties": {
66
+ "canonical": {
67
+ "type": "array",
68
+ "items": { "type": "string" },
69
+ "description": "Canonical doc paths (e.g. docs-canonical/ARCHITECTURE.md)."
70
+ },
71
+ "agentFile": {
72
+ "type": ["array", "string"],
73
+ "description": "Agent rule file(s) (e.g. AGENTS.md, CLAUDE.md). Array means any one suffices."
74
+ },
75
+ "changelog": { "type": "string" },
76
+ "driftLog": { "type": "string" },
77
+ "root": { "type": "array", "items": { "type": "string" } }
78
+ },
79
+ "additionalProperties": true
80
+ },
81
+ "validators": {
82
+ "type": "object",
83
+ "description": "Enable / disable individual validators. Set to false to skip.",
84
+ "properties": {
85
+ "structure": { "type": "boolean" },
86
+ "docsSync": { "type": "boolean" },
87
+ "drift": { "type": "boolean" },
88
+ "changelog": { "type": "boolean" },
89
+ "testSpec": { "type": "boolean" },
90
+ "environment": { "type": "boolean" },
91
+ "security": { "type": "boolean" },
92
+ "architecture": { "type": "boolean" },
93
+ "freshness": { "type": "boolean" },
94
+ "traceability": { "type": "boolean" },
95
+ "docsDiff": { "type": "boolean" },
96
+ "apiSurface": { "type": "boolean" },
97
+ "metadataSync": { "type": "boolean" },
98
+ "docsCoverage": { "type": "boolean" },
99
+ "docQuality": { "type": "boolean" },
100
+ "todoTracking": { "type": "boolean" },
101
+ "schemaSync": { "type": "boolean" },
102
+ "specKit": { "type": "boolean" },
103
+ "crossReference": { "type": "boolean" },
104
+ "generatedStaleness":{ "type": "boolean" },
105
+ "metricsConsistency":{ "type": "boolean" }
106
+ },
107
+ "additionalProperties": false
108
+ },
109
+ "severity": {
110
+ "type": "object",
111
+ "description": "Per-validator severity overrides. Affects EXIT CODE only — display is unchanged. high = warnings fail CI (exit 1). low = warnings ignored. medium (default) = exit 2.",
112
+ "additionalProperties": {
113
+ "type": "string",
114
+ "enum": ["high", "medium", "low"]
115
+ }
116
+ },
117
+ "draftStalenessDays": {
118
+ "type": "integer",
119
+ "minimum": 1,
120
+ "description": "How many days a `status: draft` doc may sit unmodified before Generated-Staleness warns. Default 14."
121
+ },
122
+ "testPattern": {
123
+ "type": "string",
124
+ "description": "Single glob identifying test files (legacy single-string form)."
125
+ },
126
+ "testPatterns": {
127
+ "type": "array",
128
+ "items": { "type": "string" },
129
+ "description": "Glob patterns identifying test files. Preferred over testPattern."
130
+ },
131
+ "todoTracking": {
132
+ "type": "object",
133
+ "description": "TODO-Tracking validator overrides.",
134
+ "properties": {
135
+ "includeTestFiles": {
136
+ "type": "boolean",
137
+ "description": "If true, scan test files for TODO/FIXME annotations. Off by default to avoid false positives from comment-marker strings inside test fixtures."
138
+ }
139
+ },
140
+ "additionalProperties": true
141
+ },
142
+ "docQuality": {
143
+ "type": "object",
144
+ "description": "Doc-Quality validator overrides.",
145
+ "properties": {
146
+ "deepScan": {
147
+ "type": "boolean",
148
+ "description": "If true and the `understanding` CLI is on PATH, run its 31-metric deep analysis. Off by default."
149
+ }
150
+ },
151
+ "additionalProperties": true
152
+ }
153
+ },
154
+ "additionalProperties": true
155
+ }