@devrik-tools/claude-gates 0.3.1 → 0.6.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 (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +25 -4
  3. package/README.md +75 -43
  4. package/cli/config.mjs +126 -124
  5. package/cli/index.mjs +154 -104
  6. package/cli/init.mjs +303 -276
  7. package/cli/install.mjs +281 -175
  8. package/cli/materialize.mjs +103 -102
  9. package/cli/registry.mjs +139 -136
  10. package/cli/smoke-fixtures.json +441 -0
  11. package/cli/smoke.mjs +129 -0
  12. package/cli/task.mjs +140 -140
  13. package/package.json +1 -1
  14. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  15. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  16. package/plugins/gates/hooks/doctor.mjs +207 -207
  17. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  18. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  19. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  20. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  21. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  22. package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
  23. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  24. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  25. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  26. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  27. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  28. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  29. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  30. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  31. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  32. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  33. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  34. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  35. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  36. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  37. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  38. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  39. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  40. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  41. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  42. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  43. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  44. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  45. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  46. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  47. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  48. package/plugins/gates/hooks/hooks.json +51 -0
  49. package/plugins/gates/hooks/lib/config.mjs +179 -172
  50. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  51. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  52. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  53. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  54. package/plugins/tasks/hooks/hooks.json +26 -26
  55. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  56. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  57. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  58. package/registry.json +171 -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
+ );