@devrik-tools/claude-gates 0.4.0 → 0.7.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.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +39 -4
  3. package/README.md +34 -5
  4. package/cli/config.mjs +126 -124
  5. package/cli/init.mjs +303 -276
  6. package/cli/install.mjs +281 -175
  7. package/cli/materialize.mjs +103 -102
  8. package/cli/registry.mjs +139 -136
  9. package/cli/smoke-fixtures.json +65 -0
  10. package/cli/task.mjs +140 -140
  11. package/package.json +1 -1
  12. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  13. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  14. package/plugins/gates/hooks/doctor.mjs +207 -207
  15. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  16. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  17. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  18. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  19. package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
  20. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  21. package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
  22. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  23. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  24. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  25. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  26. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  27. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  28. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  29. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  30. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  31. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  32. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  33. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  34. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  35. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  36. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  37. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  38. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  39. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  40. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  41. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  42. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  43. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  44. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  45. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  46. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  47. package/plugins/gates/hooks/hooks.json +61 -0
  48. package/plugins/gates/hooks/lib/config.mjs +179 -172
  49. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  50. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  51. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  52. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  53. package/plugins/tasks/hooks/hooks.json +26 -26
  54. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  55. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  56. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  57. package/registry.json +192 -1
@@ -1,187 +1,187 @@
1
- // test-matrix — denies an implementation delegation whose brief touches a domain that
2
- // makes a test type mandatory (money/auth/persistence → E2E; UI → visual/QA; always →
3
- // unit/mutation) without that type declared in the prompt. Migrated from
4
- // ~/.claude/hooks/guard-matriz-de-tests.mjs.
5
- //
6
- // ── What a project can configure (params) ───────────────────────────────────────────
7
- // e2eSignals regex sources (matched case-insensitively) that make an E2E row
8
- // mandatory when found in the brief (money, auth, persistence...).
9
- // Replaces the built-in list wholesale.
10
- // visualSignals regex sources that make a visual/QA row mandatory (UI signals).
11
- // Replaces the built-in list wholesale.
12
- // The defaults live here, in the source, so a project reads them and knows exactly what
13
- // its override replaces.
14
- //
15
- // ── Off by default, and quiet outside its narrow trigger ───────────────────────────
16
- // Only STANDARD/HIGH-RISK implementation delegations on builder subagents, not about
17
- // the harness itself, reach the check. A brief that only describes work never triggers.
18
-
19
- import {
20
- runGate,
21
- deny,
22
- toolInGroups,
23
- delegationPromptOf,
24
- } from '../../lib/hook-io.mjs';
25
- import { RISK_SIGNAL_SOURCES } from '../../lib/signals.mjs';
26
-
27
- const GATE_ID = 'test-matrix';
28
- const CONFIG_KEY = 'requireTestMatrixWhenImplementing';
29
-
30
- const DEFAULT_EXEMPT_SUBAGENTS = [
31
- 'scout',
32
- 'explore',
33
- 'plan',
34
- 'revision',
35
- 'contraste',
36
- 'test-planner',
37
- 'qa',
38
- 'ui',
39
- 'ux',
40
- ];
41
-
42
- // Money/auth signals shared with RISK_SIGNAL (lib/signals.mjs, ES+EN), plus this gate's
43
- // own persistence-domain terms (also bilingual) — a project override still replaces the
44
- // whole list wholesale, same as before.
45
- const DEFAULT_E2E_SIGNALS = [
46
- ...RISK_SIGNAL_SOURCES.slice(0, 2), // money terms, auth terms
47
- 'login|session|token|permission|role|sesi[oó]n|permiso|rol',
48
- 'persist|database|migration|transaction|persistencia|base de datos|migraci[oó]n|transacci[oó]n',
49
- ];
50
-
51
- const DEFAULT_VISUAL_SIGNALS = [
52
- 'ui|interface|interfaz|component|componente|screen|pantalla|view|vista',
53
- 'form|formulario|button|bot[oó]n|modal|layout|style|estilo',
54
- 'responsive|mobile|m[oó]vil|visual|\\.vue|\\.tsx?|\\.jsx?',
55
- ];
56
-
57
- function withWordBoundary(alternation) {
58
- return new RegExp(
59
- `(?:^|[^\\p{L}\\p{N}_])(?:${alternation})(?:[^\\p{L}\\p{N}_]|$)`,
60
- 'iu',
61
- );
62
- }
63
-
64
- const IMPLEMENTATION_VERBS = withWordBoundary(
65
- 'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
66
- 'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|' +
67
- 'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
68
- 'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
69
- 'escrib(í|e)|escribir|implement\\w*|writ(?:e|ing)|creat\\w*|fix\\w*|build\\w*|refactor\\w*|migrat\\w*|' +
70
- 'add\\w*|remov\\w*|delet\\w*|modify|modifies|modifying|rewrit\\w*',
71
- );
72
-
73
- /** Declared LEVEL near the word "level"/"classification" in Spanish or English, matching
74
- * the plugin-wide convention (see risk-level.mjs). */
75
- const DEMANDING_LEVEL_PATTERN =
76
- /(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(STANDARD|HIGH-RISK)\b/iu;
77
- const EXEMPT_LEVEL_PATTERN =
78
- /(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO)\b/iu;
79
-
80
- const HARNESS_PATTERN =
81
- /(\.claude[\\/]|hooks[\\/]|plugins[\\/]gates|settings\.json|[\\/]agents[\\/]\w|[\\/]rules[\\/]\w|[\\/]skills[\\/]\w)/i;
82
-
83
- const DETECTS_E2E = /\b(e2e|end[- ]to[- ]end|playwright)\b/i;
84
- const DETECTS_VISUAL = /\b(visual|qa\b|screenshot|snapshot)\b/i;
85
- const DETECTS_UNIT_OR_MUTATION =
86
- /\b(unit\b|units\b|unitari[ao]s?|vitest|jest|mutation|mutaci[oó]n|mutant|stryker)\b/i;
87
- const DETECTS_NEGATIVE_CASES = withWordBoundary(
88
- 'negative|edge|empty|error|no data|zero|count 0|failure|invalid|limit|' +
89
- 'negativ[ao]s?|borde|vac[ií]o|sin datos|cero|falla|inv[aá]lid[ao]|l[ií]mite',
90
- );
91
-
92
- function joinSignals(signals) {
93
- return withWordBoundary(signals.join('|'));
94
- }
95
-
96
- function isExempt(toolInput, prompt, exemptSubagents) {
97
- const subagentType = String(
98
- toolInput.subagent_type ?? toolInput.subagentType ?? '',
99
- ).toLowerCase();
100
- if (exemptSubagents.includes(subagentType)) return true;
101
- if (EXEMPT_LEVEL_PATTERN.test(prompt)) return true;
102
- if (!DEMANDING_LEVEL_PATTERN.test(prompt)) return true;
103
- if (!IMPLEMENTATION_VERBS.test(prompt)) return true;
104
- if (HARNESS_PATTERN.test(prompt)) return true;
105
- return false;
106
- }
107
-
108
- /** Which mandatory test types are missing from the prompt, given the brief text. */
109
- function missingTypes(prompt, e2ePattern, visualPattern) {
110
- const missing = [];
111
-
112
- if (!DETECTS_UNIT_OR_MUTATION.test(prompt)) {
113
- missing.push({
114
- type: 'unit/mutation',
115
- line: '- unit/mutation: yes — <what it covers> (or: not applicable — <reason>)',
116
- });
117
- }
118
-
119
- if (e2ePattern.test(prompt) && !DETECTS_E2E.test(prompt)) {
120
- missing.push({
121
- type: 'E2E',
122
- line: '- E2E: yes — <end-to-end flow verified> (mandatory: the requirement touches money/auth/persistence)',
123
- });
124
- }
125
-
126
- if (visualPattern.test(prompt) && !DETECTS_VISUAL.test(prompt)) {
127
- missing.push({
128
- type: 'visual/QA',
129
- line: '- visual/QA: yes — <screen/state reviewed in the browser> (mandatory: the requirement touches UI)',
130
- });
131
- }
132
-
133
- const declaresAnyType =
134
- DETECTS_UNIT_OR_MUTATION.test(prompt) ||
135
- DETECTS_E2E.test(prompt) ||
136
- DETECTS_VISUAL.test(prompt);
137
- if (declaresAnyType && !DETECTS_NEGATIVE_CASES.test(prompt)) {
138
- missing.push({
139
- type: 'negative/edge cases',
140
- line: '- negative/edge cases: <no data / empty / error / zero value / invalid input tested> (the happy path is not enough)',
141
- });
142
- }
143
-
144
- return missing;
145
- }
146
-
147
- runGate(
148
- {
149
- id: GATE_ID,
150
- configKey: CONFIG_KEY,
151
- enabledByDefault: false,
152
- defaultParams: {
153
- exemptSubagents: DEFAULT_EXEMPT_SUBAGENTS,
154
- e2eSignals: DEFAULT_E2E_SIGNALS,
155
- visualSignals: DEFAULT_VISUAL_SIGNALS,
156
- },
157
- },
158
- ({ toolName, toolInput, parameters }) => {
159
- if (!toolInGroups(toolName, ['delegation'])) return;
160
-
161
- const prompt = delegationPromptOf(toolInput);
162
- if (!prompt.trim()) return;
163
-
164
- const exemptSubagents =
165
- parameters.exemptSubagents ?? DEFAULT_EXEMPT_SUBAGENTS;
166
- if (isExempt(toolInput, prompt, exemptSubagents)) return;
167
-
168
- const e2eSignals = parameters.e2eSignals ?? DEFAULT_E2E_SIGNALS;
169
- const visualSignals = parameters.visualSignals ?? DEFAULT_VISUAL_SIGNALS;
170
- const e2ePattern = joinSignals(e2eSignals);
171
- const visualPattern = joinSignals(visualSignals);
172
-
173
- const missing = missingTypes(prompt, e2ePattern, visualPattern);
174
- if (missing.length === 0) return;
175
-
176
- const lines = missing.map((entry) => ` ${entry.line}`).join('\n');
177
- deny(
178
- GATE_ID,
179
- `Missing ${missing.length} test type(s) the requirement makes mandatory. Test types ` +
180
- 'are chosen ACCORDING TO THE REQUIREMENT: E2E if it touches money/auth/persistence, ' +
181
- 'visual/QA if it touches UI, and always unit or mutation. ' +
182
- `PASTE THIS SECTION INTO THE PROMPT (adjust yes/no with judgment) AND RELAUNCH:\n` +
183
- `TEST MATRIX (by type, according to the requirement):\n${lines}\n` +
184
- 'A type that truly does not apply is declared "no — <reason>", never silently omitted.',
185
- );
186
- },
187
- );
1
+ // test-matrix — denies an implementation delegation whose brief touches a domain that
2
+ // makes a test type mandatory (money/auth/persistence → E2E; UI → visual/QA; always →
3
+ // unit/mutation) without that type declared in the prompt. Migrated from
4
+ // ~/.claude/hooks/guard-matriz-de-tests.mjs.
5
+ //
6
+ // ── What a project can configure (params) ───────────────────────────────────────────
7
+ // e2eSignals regex sources (matched case-insensitively) that make an E2E row
8
+ // mandatory when found in the brief (money, auth, persistence...).
9
+ // Replaces the built-in list wholesale.
10
+ // visualSignals regex sources that make a visual/QA row mandatory (UI signals).
11
+ // Replaces the built-in list wholesale.
12
+ // The defaults live here, in the source, so a project reads them and knows exactly what
13
+ // its override replaces.
14
+ //
15
+ // ── Off by default, and quiet outside its narrow trigger ───────────────────────────
16
+ // Only STANDARD/HIGH-RISK implementation delegations on builder subagents, not about
17
+ // the harness itself, reach the check. A brief that only describes work never triggers.
18
+
19
+ import {
20
+ runGate,
21
+ deny,
22
+ toolInGroups,
23
+ delegationPromptOf,
24
+ } from '../../lib/hook-io.mjs';
25
+ import { RISK_SIGNAL_SOURCES } from '../../lib/signals.mjs';
26
+
27
+ const GATE_ID = 'test-matrix';
28
+ const CONFIG_KEY = 'requireTestMatrixWhenImplementing';
29
+
30
+ const DEFAULT_EXEMPT_SUBAGENTS = [
31
+ 'scout',
32
+ 'explore',
33
+ 'plan',
34
+ 'revision',
35
+ 'contraste',
36
+ 'test-planner',
37
+ 'qa',
38
+ 'ui',
39
+ 'ux',
40
+ ];
41
+
42
+ // Money/auth signals shared with RISK_SIGNAL (lib/signals.mjs, ES+EN), plus this gate's
43
+ // own persistence-domain terms (also bilingual) — a project override still replaces the
44
+ // whole list wholesale, same as before.
45
+ const DEFAULT_E2E_SIGNALS = [
46
+ ...RISK_SIGNAL_SOURCES.slice(0, 2), // money terms, auth terms
47
+ 'login|session|token|permission|role|sesi[oó]n|permiso|rol',
48
+ 'persist|database|migration|transaction|persistencia|base de datos|migraci[oó]n|transacci[oó]n',
49
+ ];
50
+
51
+ const DEFAULT_VISUAL_SIGNALS = [
52
+ 'ui|interface|interfaz|component|componente|screen|pantalla|view|vista',
53
+ 'form|formulario|button|bot[oó]n|modal|layout|style|estilo',
54
+ 'responsive|mobile|m[oó]vil|visual|\\.vue|\\.tsx?|\\.jsx?',
55
+ ];
56
+
57
+ function withWordBoundary(alternation) {
58
+ return new RegExp(
59
+ `(?:^|[^\\p{L}\\p{N}_])(?:${alternation})(?:[^\\p{L}\\p{N}_]|$)`,
60
+ 'iu',
61
+ );
62
+ }
63
+
64
+ const IMPLEMENTATION_VERBS = withWordBoundary(
65
+ 'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
66
+ 'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|' +
67
+ 'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
68
+ 'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
69
+ 'escrib(í|e)|escribir|implement\\w*|writ(?:e|ing)|creat\\w*|fix\\w*|build\\w*|refactor\\w*|migrat\\w*|' +
70
+ 'add\\w*|remov\\w*|delet\\w*|modify|modifies|modifying|rewrit\\w*',
71
+ );
72
+
73
+ /** Declared LEVEL near the word "level"/"classification" in Spanish or English, matching
74
+ * the plugin-wide convention (see risk-level.mjs). */
75
+ const DEMANDING_LEVEL_PATTERN =
76
+ /(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(STANDARD|HIGH-RISK)\b/iu;
77
+ const EXEMPT_LEVEL_PATTERN =
78
+ /(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO)\b/iu;
79
+
80
+ const HARNESS_PATTERN =
81
+ /(\.claude[\\/]|hooks[\\/]|plugins[\\/]gates|settings\.json|[\\/]agents[\\/]\w|[\\/]rules[\\/]\w|[\\/]skills[\\/]\w)/i;
82
+
83
+ const DETECTS_E2E = /\b(e2e|end[- ]to[- ]end|playwright)\b/i;
84
+ const DETECTS_VISUAL = /\b(visual|qa\b|screenshot|snapshot)\b/i;
85
+ const DETECTS_UNIT_OR_MUTATION =
86
+ /\b(unit\b|units\b|unitari[ao]s?|vitest|jest|mutation|mutaci[oó]n|mutant|stryker)\b/i;
87
+ const DETECTS_NEGATIVE_CASES = withWordBoundary(
88
+ 'negative|edge|empty|error|no data|zero|count 0|failure|invalid|limit|' +
89
+ 'negativ[ao]s?|borde|vac[ií]o|sin datos|cero|falla|inv[aá]lid[ao]|l[ií]mite',
90
+ );
91
+
92
+ function joinSignals(signals) {
93
+ return withWordBoundary(signals.join('|'));
94
+ }
95
+
96
+ function isExempt(toolInput, prompt, exemptSubagents) {
97
+ const subagentType = String(
98
+ toolInput.subagent_type ?? toolInput.subagentType ?? '',
99
+ ).toLowerCase();
100
+ if (exemptSubagents.includes(subagentType)) return true;
101
+ if (EXEMPT_LEVEL_PATTERN.test(prompt)) return true;
102
+ if (!DEMANDING_LEVEL_PATTERN.test(prompt)) return true;
103
+ if (!IMPLEMENTATION_VERBS.test(prompt)) return true;
104
+ if (HARNESS_PATTERN.test(prompt)) return true;
105
+ return false;
106
+ }
107
+
108
+ /** Which mandatory test types are missing from the prompt, given the brief text. */
109
+ function missingTypes(prompt, e2ePattern, visualPattern) {
110
+ const missing = [];
111
+
112
+ if (!DETECTS_UNIT_OR_MUTATION.test(prompt)) {
113
+ missing.push({
114
+ type: 'unit/mutation',
115
+ line: '- unit/mutation: yes — <what it covers> (or: not applicable — <reason>)',
116
+ });
117
+ }
118
+
119
+ if (e2ePattern.test(prompt) && !DETECTS_E2E.test(prompt)) {
120
+ missing.push({
121
+ type: 'E2E',
122
+ line: '- E2E: yes — <end-to-end flow verified> (mandatory: the requirement touches money/auth/persistence)',
123
+ });
124
+ }
125
+
126
+ if (visualPattern.test(prompt) && !DETECTS_VISUAL.test(prompt)) {
127
+ missing.push({
128
+ type: 'visual/QA',
129
+ line: '- visual/QA: yes — <screen/state reviewed in the browser> (mandatory: the requirement touches UI)',
130
+ });
131
+ }
132
+
133
+ const declaresAnyType =
134
+ DETECTS_UNIT_OR_MUTATION.test(prompt) ||
135
+ DETECTS_E2E.test(prompt) ||
136
+ DETECTS_VISUAL.test(prompt);
137
+ if (declaresAnyType && !DETECTS_NEGATIVE_CASES.test(prompt)) {
138
+ missing.push({
139
+ type: 'negative/edge cases',
140
+ line: '- negative/edge cases: <no data / empty / error / zero value / invalid input tested> (the happy path is not enough)',
141
+ });
142
+ }
143
+
144
+ return missing;
145
+ }
146
+
147
+ runGate(
148
+ {
149
+ id: GATE_ID,
150
+ configKey: CONFIG_KEY,
151
+ enabledByDefault: false,
152
+ defaultParams: {
153
+ exemptSubagents: DEFAULT_EXEMPT_SUBAGENTS,
154
+ e2eSignals: DEFAULT_E2E_SIGNALS,
155
+ visualSignals: DEFAULT_VISUAL_SIGNALS,
156
+ },
157
+ },
158
+ ({ toolName, toolInput, parameters }) => {
159
+ if (!toolInGroups(toolName, ['delegation'])) return;
160
+
161
+ const prompt = delegationPromptOf(toolInput);
162
+ if (!prompt.trim()) return;
163
+
164
+ const exemptSubagents =
165
+ parameters.exemptSubagents ?? DEFAULT_EXEMPT_SUBAGENTS;
166
+ if (isExempt(toolInput, prompt, exemptSubagents)) return;
167
+
168
+ const e2eSignals = parameters.e2eSignals ?? DEFAULT_E2E_SIGNALS;
169
+ const visualSignals = parameters.visualSignals ?? DEFAULT_VISUAL_SIGNALS;
170
+ const e2ePattern = joinSignals(e2eSignals);
171
+ const visualPattern = joinSignals(visualSignals);
172
+
173
+ const missing = missingTypes(prompt, e2ePattern, visualPattern);
174
+ if (missing.length === 0) return;
175
+
176
+ const lines = missing.map((entry) => ` ${entry.line}`).join('\n');
177
+ deny(
178
+ GATE_ID,
179
+ `Missing ${missing.length} test type(s) the requirement makes mandatory. Test types ` +
180
+ 'are chosen ACCORDING TO THE REQUIREMENT: E2E if it touches money/auth/persistence, ' +
181
+ 'visual/QA if it touches UI, and always unit or mutation. ' +
182
+ `PASTE THIS SECTION INTO THE PROMPT (adjust yes/no with judgment) AND RELAUNCH:\n` +
183
+ `TEST MATRIX (by type, according to the requirement):\n${lines}\n` +
184
+ 'A type that truly does not apply is declared "no — <reason>", never silently omitted.',
185
+ );
186
+ },
187
+ );