@qball-inc/the-bulwark 1.0.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 (175) hide show
  1. package/.claude-plugin/plugin.json +43 -0
  2. package/agents/bulwark-fix-validator.md +633 -0
  3. package/agents/bulwark-implementer.md +391 -0
  4. package/agents/bulwark-issue-analyzer.md +308 -0
  5. package/agents/bulwark-standards-reviewer.md +221 -0
  6. package/agents/plan-creation-architect.md +323 -0
  7. package/agents/plan-creation-eng-lead.md +352 -0
  8. package/agents/plan-creation-po.md +300 -0
  9. package/agents/plan-creation-qa-critic.md +334 -0
  10. package/agents/product-ideation-competitive-analyzer.md +298 -0
  11. package/agents/product-ideation-idea-validator.md +268 -0
  12. package/agents/product-ideation-market-researcher.md +292 -0
  13. package/agents/product-ideation-pattern-documenter.md +308 -0
  14. package/agents/product-ideation-segment-analyzer.md +303 -0
  15. package/agents/product-ideation-strategist.md +259 -0
  16. package/agents/statusline-setup.md +97 -0
  17. package/hooks/hooks.json +59 -0
  18. package/package.json +45 -0
  19. package/scripts/hooks/cleanup-stale.sh +13 -0
  20. package/scripts/hooks/enforce-quality.sh +166 -0
  21. package/scripts/hooks/implementer-quality.sh +256 -0
  22. package/scripts/hooks/inject-protocol.sh +52 -0
  23. package/scripts/hooks/suggest-pipeline.sh +175 -0
  24. package/scripts/hooks/track-pipeline-start.sh +37 -0
  25. package/scripts/hooks/track-pipeline-stop.sh +52 -0
  26. package/scripts/init-rules.sh +35 -0
  27. package/scripts/init.sh +151 -0
  28. package/skills/anthropic-validator/SKILL.md +607 -0
  29. package/skills/anthropic-validator/references/agents-checklist.md +131 -0
  30. package/skills/anthropic-validator/references/commands-checklist.md +102 -0
  31. package/skills/anthropic-validator/references/hooks-checklist.md +151 -0
  32. package/skills/anthropic-validator/references/mcp-checklist.md +136 -0
  33. package/skills/anthropic-validator/references/plugins-checklist.md +148 -0
  34. package/skills/anthropic-validator/references/skills-checklist.md +85 -0
  35. package/skills/assertion-patterns/SKILL.md +296 -0
  36. package/skills/bug-magnet-data/SKILL.md +284 -0
  37. package/skills/bug-magnet-data/context/cli-args.md +91 -0
  38. package/skills/bug-magnet-data/context/db-query.md +104 -0
  39. package/skills/bug-magnet-data/context/file-contents.md +103 -0
  40. package/skills/bug-magnet-data/context/http-body.md +91 -0
  41. package/skills/bug-magnet-data/context/process-spawn.md +123 -0
  42. package/skills/bug-magnet-data/data/booleans/boundaries.yaml +143 -0
  43. package/skills/bug-magnet-data/data/collections/arrays.yaml +114 -0
  44. package/skills/bug-magnet-data/data/collections/objects.yaml +123 -0
  45. package/skills/bug-magnet-data/data/concurrency/race-conditions.yaml +118 -0
  46. package/skills/bug-magnet-data/data/concurrency/state-machines.yaml +115 -0
  47. package/skills/bug-magnet-data/data/dates/boundaries.yaml +137 -0
  48. package/skills/bug-magnet-data/data/dates/invalid.yaml +132 -0
  49. package/skills/bug-magnet-data/data/dates/timezone.yaml +118 -0
  50. package/skills/bug-magnet-data/data/encoding/charset.yaml +79 -0
  51. package/skills/bug-magnet-data/data/encoding/normalization.yaml +105 -0
  52. package/skills/bug-magnet-data/data/formats/email.yaml +154 -0
  53. package/skills/bug-magnet-data/data/formats/json.yaml +187 -0
  54. package/skills/bug-magnet-data/data/formats/url.yaml +165 -0
  55. package/skills/bug-magnet-data/data/language-specific/javascript.yaml +182 -0
  56. package/skills/bug-magnet-data/data/language-specific/python.yaml +174 -0
  57. package/skills/bug-magnet-data/data/language-specific/rust.yaml +148 -0
  58. package/skills/bug-magnet-data/data/numbers/boundaries.yaml +161 -0
  59. package/skills/bug-magnet-data/data/numbers/precision.yaml +89 -0
  60. package/skills/bug-magnet-data/data/numbers/special.yaml +69 -0
  61. package/skills/bug-magnet-data/data/strings/boundaries.yaml +109 -0
  62. package/skills/bug-magnet-data/data/strings/injection.yaml +208 -0
  63. package/skills/bug-magnet-data/data/strings/special-chars.yaml +190 -0
  64. package/skills/bug-magnet-data/data/strings/unicode.yaml +139 -0
  65. package/skills/bug-magnet-data/references/external-lists.md +115 -0
  66. package/skills/bulwark-brainstorm/SKILL.md +563 -0
  67. package/skills/bulwark-brainstorm/references/at-teammate-prompts.md +60 -0
  68. package/skills/bulwark-brainstorm/references/role-critical-analyst.md +78 -0
  69. package/skills/bulwark-brainstorm/references/role-development-lead.md +66 -0
  70. package/skills/bulwark-brainstorm/references/role-product-delivery-lead.md +79 -0
  71. package/skills/bulwark-brainstorm/references/role-product-manager.md +62 -0
  72. package/skills/bulwark-brainstorm/references/role-project-sme.md +59 -0
  73. package/skills/bulwark-brainstorm/references/role-technical-architect.md +66 -0
  74. package/skills/bulwark-research/SKILL.md +298 -0
  75. package/skills/bulwark-research/references/viewpoint-contrarian.md +63 -0
  76. package/skills/bulwark-research/references/viewpoint-direct-investigation.md +62 -0
  77. package/skills/bulwark-research/references/viewpoint-first-principles.md +65 -0
  78. package/skills/bulwark-research/references/viewpoint-practitioner.md +62 -0
  79. package/skills/bulwark-research/references/viewpoint-prior-art.md +66 -0
  80. package/skills/bulwark-scaffold/SKILL.md +330 -0
  81. package/skills/bulwark-statusline/SKILL.md +161 -0
  82. package/skills/bulwark-statusline/scripts/statusline.sh +144 -0
  83. package/skills/bulwark-verify/SKILL.md +519 -0
  84. package/skills/code-review/SKILL.md +428 -0
  85. package/skills/code-review/examples/anti-patterns/linting.ts +181 -0
  86. package/skills/code-review/examples/anti-patterns/security.ts +91 -0
  87. package/skills/code-review/examples/anti-patterns/standards.ts +195 -0
  88. package/skills/code-review/examples/anti-patterns/type-safety.ts +108 -0
  89. package/skills/code-review/examples/recommended/linting.ts +195 -0
  90. package/skills/code-review/examples/recommended/security.ts +154 -0
  91. package/skills/code-review/examples/recommended/standards.ts +231 -0
  92. package/skills/code-review/examples/recommended/type-safety.ts +181 -0
  93. package/skills/code-review/frameworks/angular.md +218 -0
  94. package/skills/code-review/frameworks/django.md +235 -0
  95. package/skills/code-review/frameworks/express.md +207 -0
  96. package/skills/code-review/frameworks/flask.md +298 -0
  97. package/skills/code-review/frameworks/generic.md +146 -0
  98. package/skills/code-review/frameworks/react.md +152 -0
  99. package/skills/code-review/frameworks/vue.md +244 -0
  100. package/skills/code-review/references/linting-patterns.md +221 -0
  101. package/skills/code-review/references/security-patterns.md +125 -0
  102. package/skills/code-review/references/standards-patterns.md +246 -0
  103. package/skills/code-review/references/type-safety-patterns.md +130 -0
  104. package/skills/component-patterns/SKILL.md +131 -0
  105. package/skills/component-patterns/references/pattern-cli-command.md +118 -0
  106. package/skills/component-patterns/references/pattern-database.md +166 -0
  107. package/skills/component-patterns/references/pattern-external-api.md +139 -0
  108. package/skills/component-patterns/references/pattern-file-parser.md +168 -0
  109. package/skills/component-patterns/references/pattern-http-server.md +162 -0
  110. package/skills/component-patterns/references/pattern-process-spawner.md +133 -0
  111. package/skills/continuous-feedback/SKILL.md +327 -0
  112. package/skills/continuous-feedback/references/collect-instructions.md +81 -0
  113. package/skills/continuous-feedback/references/specialize-code-review.md +82 -0
  114. package/skills/continuous-feedback/references/specialize-general.md +98 -0
  115. package/skills/continuous-feedback/references/specialize-test-audit.md +81 -0
  116. package/skills/create-skill/SKILL.md +359 -0
  117. package/skills/create-skill/references/agent-conventions.md +194 -0
  118. package/skills/create-skill/references/agent-template.md +195 -0
  119. package/skills/create-skill/references/content-guidance.md +291 -0
  120. package/skills/create-skill/references/decision-framework.md +124 -0
  121. package/skills/create-skill/references/template-pipeline.md +217 -0
  122. package/skills/create-skill/references/template-reference-heavy.md +111 -0
  123. package/skills/create-skill/references/template-research.md +210 -0
  124. package/skills/create-skill/references/template-script-driven.md +172 -0
  125. package/skills/create-skill/references/template-simple.md +80 -0
  126. package/skills/create-subagent/SKILL.md +353 -0
  127. package/skills/create-subagent/references/agent-conventions.md +268 -0
  128. package/skills/create-subagent/references/content-guidance.md +232 -0
  129. package/skills/create-subagent/references/decision-framework.md +134 -0
  130. package/skills/create-subagent/references/template-single-agent.md +192 -0
  131. package/skills/fix-bug/SKILL.md +241 -0
  132. package/skills/governance-protocol/SKILL.md +116 -0
  133. package/skills/init/SKILL.md +341 -0
  134. package/skills/issue-debugging/SKILL.md +385 -0
  135. package/skills/issue-debugging/references/anti-patterns.md +245 -0
  136. package/skills/issue-debugging/references/debug-report-schema.md +227 -0
  137. package/skills/mock-detection/SKILL.md +511 -0
  138. package/skills/mock-detection/references/false-positive-prevention.md +402 -0
  139. package/skills/mock-detection/references/stub-patterns.md +236 -0
  140. package/skills/pipeline-templates/SKILL.md +215 -0
  141. package/skills/pipeline-templates/references/code-change-workflow.md +277 -0
  142. package/skills/pipeline-templates/references/code-review.md +336 -0
  143. package/skills/pipeline-templates/references/fix-validation.md +421 -0
  144. package/skills/pipeline-templates/references/new-feature.md +335 -0
  145. package/skills/pipeline-templates/references/research-brainstorm.md +161 -0
  146. package/skills/pipeline-templates/references/research-planning.md +257 -0
  147. package/skills/pipeline-templates/references/test-audit.md +389 -0
  148. package/skills/pipeline-templates/references/test-execution-fix.md +238 -0
  149. package/skills/plan-creation/SKILL.md +497 -0
  150. package/skills/product-ideation/SKILL.md +372 -0
  151. package/skills/product-ideation/references/analysis-frameworks.md +161 -0
  152. package/skills/session-handoff/SKILL.md +139 -0
  153. package/skills/session-handoff/references/examples.md +223 -0
  154. package/skills/setup-lsp/SKILL.md +312 -0
  155. package/skills/setup-lsp/references/server-registry.md +85 -0
  156. package/skills/setup-lsp/references/troubleshooting.md +135 -0
  157. package/skills/subagent-output-templating/SKILL.md +415 -0
  158. package/skills/subagent-output-templating/references/examples.md +440 -0
  159. package/skills/subagent-prompting/SKILL.md +364 -0
  160. package/skills/subagent-prompting/references/examples.md +342 -0
  161. package/skills/test-audit/SKILL.md +531 -0
  162. package/skills/test-audit/references/known-limitations.md +41 -0
  163. package/skills/test-audit/references/priority-classification.md +30 -0
  164. package/skills/test-audit/references/prompts/deep-mode-detection.md +83 -0
  165. package/skills/test-audit/references/prompts/synthesis.md +57 -0
  166. package/skills/test-audit/references/rewrite-instructions.md +46 -0
  167. package/skills/test-audit/references/schemas/audit-output.yaml +100 -0
  168. package/skills/test-audit/references/schemas/diagnostic-output.yaml +49 -0
  169. package/skills/test-audit/scripts/data-flow-analyzer.ts +509 -0
  170. package/skills/test-audit/scripts/integration-mock-detector.ts +462 -0
  171. package/skills/test-audit/scripts/package.json +20 -0
  172. package/skills/test-audit/scripts/skip-detector.ts +211 -0
  173. package/skills/test-audit/scripts/verification-counter.ts +295 -0
  174. package/skills/test-classification/SKILL.md +310 -0
  175. package/skills/test-fixture-creation/SKILL.md +295 -0
@@ -0,0 +1,462 @@
1
+ import { Project, Node, CallExpression, SourceFile, SyntaxKind } from 'ts-morph';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+
5
+ // --- Types ---
6
+
7
+ interface SectionInfo {
8
+ name: string;
9
+ type: 'integration' | 'e2e';
10
+ signal: 'keyword_in_name' | 'comment_header' | 'inherited';
11
+ line_start: number;
12
+ line_end: number;
13
+ }
14
+
15
+ interface MockLead {
16
+ line: number;
17
+ type: 'T3';
18
+ confidence: 'high' | 'medium';
19
+ mock_pattern: string;
20
+ enclosing_block: string;
21
+ block_type: 'integration' | 'e2e';
22
+ message: string;
23
+ suggestion: string;
24
+ }
25
+
26
+ interface FileResult {
27
+ file: string;
28
+ sections: SectionInfo[];
29
+ leads: MockLead[];
30
+ summary: {
31
+ sections_found: number;
32
+ integration_sections: number;
33
+ e2e_sections: number;
34
+ leads_count: number;
35
+ mock_calls_in_integration: number;
36
+ mock_calls_in_e2e: number;
37
+ file_scope_mocks: number;
38
+ };
39
+ }
40
+
41
+ interface ErrorOutput {
42
+ error: string;
43
+ file: string;
44
+ }
45
+
46
+ // --- Constants ---
47
+
48
+ // Keywords that signal integration or e2e test sections (case-insensitive)
49
+ const INTEGRATION_KEYWORDS = /\b(integration|integ)\b/i;
50
+ const E2E_KEYWORDS = /\b(e2e|end[- ]?to[- ]?end|acceptance|system)\b/i;
51
+
52
+ // Comment patterns for section headers
53
+ const COMMENT_INTEGRATION_PATTERN = /\b(integration\s*test|integration)\b/i;
54
+ const COMMENT_E2E_PATTERN = /\b(e2e|end[- ]?to[- ]?end|acceptance\s*test|system\s*test)\b/i;
55
+
56
+ // Mock framework patterns — property access form (obj.method)
57
+ const MOCK_PROPERTY_PATTERNS: Record<string, Record<string, string>> = {
58
+ jest: {
59
+ fn: 'jest.fn()',
60
+ mock: 'jest.mock()',
61
+ spyOn: 'jest.spyOn()',
62
+ },
63
+ vi: {
64
+ fn: 'vi.fn()',
65
+ mock: 'vi.mock()',
66
+ spyOn: 'vi.spyOn()',
67
+ },
68
+ sinon: {
69
+ stub: 'sinon.stub()',
70
+ mock: 'sinon.mock()',
71
+ fake: 'sinon.fake()',
72
+ spy: 'sinon.spy()',
73
+ },
74
+ };
75
+
76
+ // --- Section Classification ---
77
+
78
+ /**
79
+ * Classify a describe block's test type based on its name text.
80
+ */
81
+ function classifyByName(name: string): 'integration' | 'e2e' | null {
82
+ if (E2E_KEYWORDS.test(name)) return 'e2e';
83
+ if (INTEGRATION_KEYWORDS.test(name)) return 'integration';
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Check preceding comments/siblings for section header markers.
89
+ * Looks at comments immediately before a describe() call.
90
+ */
91
+ function classifyByPrecedingComments(node: Node): 'integration' | 'e2e' | null {
92
+ // Walk backward through siblings looking for comments
93
+ const parent = node.getParent();
94
+ if (!parent) return null;
95
+
96
+ const children = parent.getChildren();
97
+ const nodeIndex = children.indexOf(node);
98
+
99
+ // Check up to 3 siblings before this node for comments
100
+ for (let i = nodeIndex - 1; i >= Math.max(0, nodeIndex - 3); i--) {
101
+ const sibling = children[i];
102
+ const kind = sibling.getKind();
103
+
104
+ if (kind === SyntaxKind.SingleLineCommentTrivia ||
105
+ kind === SyntaxKind.MultiLineCommentTrivia) {
106
+ const text = sibling.getText();
107
+ if (COMMENT_E2E_PATTERN.test(text)) return 'e2e';
108
+ if (COMMENT_INTEGRATION_PATTERN.test(text)) return 'integration';
109
+ }
110
+ }
111
+
112
+ // Also check leading comment ranges on the statement containing this node
113
+ const statement = findContainingStatement(node);
114
+ if (statement) {
115
+ const leadingComments = statement.getLeadingCommentRanges();
116
+ for (const comment of leadingComments) {
117
+ const text = comment.getText();
118
+ if (COMMENT_E2E_PATTERN.test(text)) return 'e2e';
119
+ if (COMMENT_INTEGRATION_PATTERN.test(text)) return 'integration';
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Find the statement (expression statement) containing a node.
128
+ */
129
+ function findContainingStatement(node: Node): Node | null {
130
+ let current: Node | undefined = node;
131
+ while (current) {
132
+ if (Node.isExpressionStatement(current)) return current;
133
+ current = current.getParent();
134
+ }
135
+ return null;
136
+ }
137
+
138
+ // --- Mock Detection ---
139
+
140
+ /**
141
+ * Check if a call expression is a mock framework call.
142
+ * Returns the pattern string (e.g., "jest.fn()") or null.
143
+ */
144
+ function identifyMockCall(node: CallExpression): string | null {
145
+ const expr = node.getExpression();
146
+
147
+ // Property access: jest.fn(), jest.mock(), sinon.stub(), etc.
148
+ if (Node.isPropertyAccessExpression(expr)) {
149
+ const objText = expr.getExpression().getText();
150
+ const propText = expr.getName();
151
+
152
+ // Direct match: jest.fn(), vi.mock(), sinon.stub()
153
+ const group = MOCK_PROPERTY_PATTERNS[objText];
154
+ if (group && group[propText]) {
155
+ return group[propText];
156
+ }
157
+
158
+ // Chain detection: jest.fn().mockResolvedValue(), jest.fn().mockImplementation()
159
+ // The outer call is .mockResolvedValue(), inner is jest.fn()
160
+ const innerExpr = expr.getExpression();
161
+ if (Node.isCallExpression(innerExpr)) {
162
+ const innerPattern = identifyMockCall(innerExpr);
163
+ if (innerPattern) {
164
+ return `${innerPattern}.${propText}()`;
165
+ }
166
+ }
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ // --- Describe Block Analysis ---
173
+
174
+ interface DescribeBlock {
175
+ name: string;
176
+ type: 'integration' | 'e2e' | null;
177
+ signal: 'keyword_in_name' | 'comment_header' | 'inherited' | null;
178
+ lineStart: number;
179
+ lineEnd: number;
180
+ node: CallExpression;
181
+ }
182
+
183
+ /**
184
+ * Extract the name from a describe() call's first argument.
185
+ */
186
+ function getDescribeName(node: CallExpression): string | null {
187
+ const args = node.getArguments();
188
+ if (args.length > 0 && Node.isStringLiteral(args[0])) {
189
+ return args[0].getLiteralText();
190
+ }
191
+ if (args.length > 0 && Node.isNoSubstitutionTemplateLiteral(args[0])) {
192
+ return args[0].getLiteralText();
193
+ }
194
+ return null;
195
+ }
196
+
197
+ /**
198
+ * Check if a call expression is a describe() call.
199
+ */
200
+ function isDescribeCall(node: CallExpression): boolean {
201
+ const expr = node.getExpression();
202
+ if (Node.isIdentifier(expr)) {
203
+ return expr.getText() === 'describe';
204
+ }
205
+ // describe.skip, describe.only
206
+ if (Node.isPropertyAccessExpression(expr)) {
207
+ const obj = expr.getExpression();
208
+ return Node.isIdentifier(obj) && obj.getText() === 'describe';
209
+ }
210
+ return false;
211
+ }
212
+
213
+ /**
214
+ * Find all describe blocks and classify them.
215
+ */
216
+ function findDescribeBlocks(sourceFile: SourceFile): DescribeBlock[] {
217
+ const blocks: DescribeBlock[] = [];
218
+
219
+ sourceFile.forEachDescendant((node) => {
220
+ if (!Node.isCallExpression(node)) return;
221
+ if (!isDescribeCall(node)) return;
222
+
223
+ const name = getDescribeName(node);
224
+ if (!name) return;
225
+
226
+ const lineStart = node.getStartLineNumber();
227
+ const lineEnd = node.getEndLineNumber();
228
+
229
+ // Classify by name first
230
+ let type = classifyByName(name);
231
+ let signal: DescribeBlock['signal'] = type ? 'keyword_in_name' : null;
232
+
233
+ // If not classified by name, check preceding comments
234
+ if (!type) {
235
+ type = classifyByPrecedingComments(node);
236
+ signal = type ? 'comment_header' : null;
237
+ }
238
+
239
+ blocks.push({ name, type, signal, lineStart, lineEnd, node });
240
+ });
241
+
242
+ return blocks;
243
+ }
244
+
245
+ /**
246
+ * Apply inheritance: nested describe blocks inherit parent type.
247
+ */
248
+ function applyInheritance(blocks: DescribeBlock[]): void {
249
+ for (const block of blocks) {
250
+ if (block.type) continue; // Already classified
251
+
252
+ // Check if this block is nested inside a classified block
253
+ for (const parent of blocks) {
254
+ if (!parent.type) continue;
255
+ if (parent === block) continue;
256
+
257
+ if (block.lineStart >= parent.lineStart && block.lineEnd <= parent.lineEnd) {
258
+ block.type = parent.type;
259
+ block.signal = 'inherited';
260
+ break;
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // --- Main Analysis ---
267
+
268
+ function analyzeFile(filePath: string, project: Project): FileResult | ErrorOutput {
269
+ let sourceFile: SourceFile;
270
+ try {
271
+ sourceFile = project.addSourceFileAtPath(filePath);
272
+ } catch (err) {
273
+ return {
274
+ error: `Failed to parse: ${err instanceof Error ? err.message : String(err)}`,
275
+ file: filePath,
276
+ };
277
+ }
278
+
279
+ // Step 1: Find and classify describe blocks
280
+ const blocks = findDescribeBlocks(sourceFile);
281
+ applyInheritance(blocks);
282
+
283
+ // Step 2: Collect sections (classified blocks only)
284
+ const sections: SectionInfo[] = blocks
285
+ .filter((b) => b.type !== null)
286
+ .map((b) => ({
287
+ name: b.name,
288
+ type: b.type!,
289
+ signal: b.signal!,
290
+ line_start: b.lineStart,
291
+ line_end: b.lineEnd,
292
+ }));
293
+
294
+ // Step 3: Find mock calls within classified sections
295
+ // Only report the outermost call in a chain to avoid duplicates.
296
+ // e.g., jest.fn().mockRejectedValueOnce().mockResolvedValue() is ONE lead, not three.
297
+ const leads: MockLead[] = [];
298
+
299
+ sourceFile.forEachDescendant((node) => {
300
+ if (!Node.isCallExpression(node)) return;
301
+
302
+ const mockPattern = identifyMockCall(node);
303
+ if (!mockPattern) return;
304
+
305
+ // Skip if this CallExpression is consumed by an outer chain call.
306
+ // AST structure: inner CallExpr → PropertyAccessExpr → outer CallExpr
307
+ const parent = node.getParent();
308
+ if (parent && Node.isPropertyAccessExpression(parent)) {
309
+ const grandparent = parent.getParent();
310
+ if (grandparent && Node.isCallExpression(grandparent)) {
311
+ // This node is an inner link in a chain — the outer call will be reported
312
+ return;
313
+ }
314
+ }
315
+
316
+ const line = node.getStartLineNumber();
317
+
318
+ // Check if this mock call is inside a classified section
319
+ for (const section of sections) {
320
+ if (line >= section.line_start && line <= section.line_end) {
321
+ leads.push({
322
+ line,
323
+ type: 'T3',
324
+ confidence: 'high',
325
+ mock_pattern: mockPattern,
326
+ enclosing_block: section.name,
327
+ block_type: section.type,
328
+ message: `Mock call '${mockPattern}' in ${section.type} test block '${section.name}'`,
329
+ suggestion: `${section.type === 'e2e' ? 'E2E' : 'Integration'} tests should use real operations. Replace mock with actual implementation.`,
330
+ });
331
+ break; // Don't double-count if nested sections overlap
332
+ }
333
+ }
334
+ });
335
+
336
+ // Step 3b: Detect file-scope mock calls that affect integration/e2e sections
337
+ // File-scope mocks (jest.mock, vi.mock) outside all describe blocks contaminate ALL tests
338
+ const integrationOrE2eSections = sections.filter(
339
+ (s) => s.type === 'integration' || s.type === 'e2e',
340
+ );
341
+ let fileScopeMockCount = 0;
342
+
343
+ if (integrationOrE2eSections.length > 0) {
344
+ const allBlockRanges = blocks.map((b) => ({ start: b.lineStart, end: b.lineEnd }));
345
+
346
+ sourceFile.forEachDescendant((node) => {
347
+ if (!Node.isCallExpression(node)) return;
348
+
349
+ const mockPattern = identifyMockCall(node);
350
+ if (!mockPattern) return;
351
+
352
+ // Skip chain-inner nodes (same dedup as Step 3)
353
+ const parent = node.getParent();
354
+ if (parent && Node.isPropertyAccessExpression(parent)) {
355
+ const grandparent = parent.getParent();
356
+ if (grandparent && Node.isCallExpression(grandparent)) return;
357
+ }
358
+
359
+ const line = node.getStartLineNumber();
360
+
361
+ // Check if this mock is OUTSIDE all describe blocks (file-scope)
362
+ const isInsideDescribe = allBlockRanges.some(
363
+ (r) => line >= r.start && line <= r.end,
364
+ );
365
+ if (isInsideDescribe) return; // Already handled in Step 3
366
+
367
+ fileScopeMockCount++;
368
+
369
+ // File-scope mock found + integration sections exist → T3 lead per section
370
+ for (const section of integrationOrE2eSections) {
371
+ leads.push({
372
+ line,
373
+ type: 'T3',
374
+ confidence: 'high',
375
+ mock_pattern: mockPattern,
376
+ enclosing_block: `FILE_SCOPE → ${section.name}`,
377
+ block_type: section.type,
378
+ message: `File-scope mock '${mockPattern}' at line ${line} affects ${section.type} section '${section.name}' (lines ${section.line_start}-${section.line_end})`,
379
+ suggestion: `Move to separate test file or scope mock to unit test blocks only. ${section.type === 'e2e' ? 'E2E' : 'Integration'} tests should not be affected by file-scope mocks.`,
380
+ });
381
+ }
382
+ });
383
+ }
384
+
385
+ // Sort leads by line number
386
+ leads.sort((a, b) => a.line - b.line);
387
+
388
+ // Step 4: Compute summary
389
+ const integrationSections = sections.filter((s) => s.type === 'integration').length;
390
+ const e2eSections = sections.filter((s) => s.type === 'e2e').length;
391
+ const mockInIntegration = leads.filter((l) => l.block_type === 'integration').length;
392
+ const mockInE2e = leads.filter((l) => l.block_type === 'e2e').length;
393
+
394
+ return {
395
+ file: filePath,
396
+ sections,
397
+ leads,
398
+ summary: {
399
+ sections_found: sections.length,
400
+ integration_sections: integrationSections,
401
+ e2e_sections: e2eSections,
402
+ leads_count: leads.length,
403
+ mock_calls_in_integration: mockInIntegration,
404
+ mock_calls_in_e2e: mockInE2e,
405
+ file_scope_mocks: fileScopeMockCount,
406
+ },
407
+ };
408
+ }
409
+
410
+ // --- Main ---
411
+
412
+ function main(): void {
413
+ const args = process.argv.slice(2);
414
+
415
+ if (args.length === 0) {
416
+ const error: ErrorOutput = {
417
+ error: 'No file paths provided. Usage: integration-mock-detector.ts <file1> [file2] ...',
418
+ file: '',
419
+ };
420
+ process.stderr.write(JSON.stringify(error) + '\n');
421
+ process.exit(1);
422
+ }
423
+
424
+ const project = new Project({
425
+ tsConfigFilePath: undefined,
426
+ skipAddingFilesFromTsConfig: true,
427
+ compilerOptions: {
428
+ allowJs: true,
429
+ checkJs: false,
430
+ noEmit: true,
431
+ strict: false,
432
+ skipLibCheck: true,
433
+ },
434
+ });
435
+
436
+ // Expand directory args to individual test files
437
+ const filePaths: string[] = [];
438
+ for (const arg of args) {
439
+ const resolved = path.resolve(arg);
440
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
441
+ const entries = fs.readdirSync(resolved).filter(f => /\.(test|spec)\.[tj]sx?$/.test(f));
442
+ filePaths.push(...entries.map(f => path.join(resolved, f)));
443
+ } else {
444
+ filePaths.push(resolved);
445
+ }
446
+ }
447
+
448
+ const results: (FileResult | ErrorOutput)[] = [];
449
+
450
+ for (const filePath of filePaths) {
451
+ const result = analyzeFile(filePath, project);
452
+ results.push(result);
453
+ }
454
+
455
+ if (results.length === 1) {
456
+ process.stdout.write(JSON.stringify(results[0], null, 2) + '\n');
457
+ } else {
458
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
459
+ }
460
+ }
461
+
462
+ main();
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@bulwark/test-audit-scripts",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "AST-based test analysis scripts for test-audit skill",
6
+ "dependencies": {
7
+ "ts-morph": "^27.0.0"
8
+ },
9
+ "devDependencies": {
10
+ "@types/jest": "^29.5.0",
11
+ "jest": "^29.7.0",
12
+ "ts-jest": "^29.1.0",
13
+ "tsx": "^4.19.0",
14
+ "typescript": "^5.3.0"
15
+ },
16
+ "jest": {
17
+ "preset": "ts-jest",
18
+ "testEnvironment": "node"
19
+ }
20
+ }
@@ -0,0 +1,211 @@
1
+ import { Project, SyntaxKind, Node, CallExpression, SourceFile } from 'ts-morph';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+
5
+ // --- Types ---
6
+
7
+ interface SkipMarker {
8
+ type: string;
9
+ line: number;
10
+ test_name: string;
11
+ severity: 'high' | 'medium' | 'low';
12
+ rule: 'T4';
13
+ }
14
+
15
+ interface FileResult {
16
+ file: string;
17
+ markers: SkipMarker[];
18
+ summary: {
19
+ skip_count: number;
20
+ only_count: number;
21
+ todo_count: number;
22
+ };
23
+ }
24
+
25
+ interface ErrorOutput {
26
+ error: string;
27
+ file: string;
28
+ }
29
+
30
+ // --- Constants ---
31
+
32
+ // Maps object.method patterns to marker types and severity
33
+ const PROPERTY_ACCESS_PATTERNS: Record<string, Record<string, { type: string; severity: SkipMarker['severity']; category: 'skip' | 'only' | 'todo' }>> = {
34
+ 'describe': {
35
+ 'skip': { type: 'describe.skip', severity: 'medium', category: 'skip' },
36
+ 'only': { type: 'describe.only', severity: 'high', category: 'only' },
37
+ },
38
+ 'it': {
39
+ 'skip': { type: 'it.skip', severity: 'medium', category: 'skip' },
40
+ 'only': { type: 'it.only', severity: 'high', category: 'only' },
41
+ 'todo': { type: 'it.todo', severity: 'low', category: 'todo' },
42
+ },
43
+ 'test': {
44
+ 'skip': { type: 'test.skip', severity: 'medium', category: 'skip' },
45
+ 'only': { type: 'test.only', severity: 'high', category: 'only' },
46
+ 'todo': { type: 'test.todo', severity: 'low', category: 'todo' },
47
+ },
48
+ };
49
+
50
+ // Direct call patterns (xdescribe, xit, fdescribe, fit)
51
+ const DIRECT_CALL_PATTERNS: Record<string, { type: string; severity: SkipMarker['severity']; category: 'skip' | 'only' | 'todo' }> = {
52
+ 'xdescribe': { type: 'xdescribe', severity: 'medium', category: 'skip' },
53
+ 'xit': { type: 'xit', severity: 'medium', category: 'skip' },
54
+ 'fdescribe': { type: 'fdescribe', severity: 'high', category: 'only' },
55
+ 'fit': { type: 'fit', severity: 'high', category: 'only' },
56
+ };
57
+
58
+ // --- AST Analysis ---
59
+
60
+ function extractTestName(node: CallExpression): string {
61
+ const args = node.getArguments();
62
+ if (args.length > 0 && Node.isStringLiteral(args[0])) {
63
+ return args[0].getLiteralText();
64
+ }
65
+ if (args.length > 0 && Node.isTemplateExpression(args[0])) {
66
+ return args[0].getText();
67
+ }
68
+ if (args.length > 0 && Node.isNoSubstitutionTemplateLiteral(args[0])) {
69
+ return args[0].getLiteralText();
70
+ }
71
+ return '<unnamed>';
72
+ }
73
+
74
+ function analyzeFile(filePath: string, project: Project): FileResult | ErrorOutput {
75
+ let sourceFile: SourceFile;
76
+ try {
77
+ sourceFile = project.addSourceFileAtPath(filePath);
78
+ } catch (err) {
79
+ return {
80
+ error: `Failed to parse: ${err instanceof Error ? err.message : String(err)}`,
81
+ file: filePath,
82
+ };
83
+ }
84
+
85
+ const markers: SkipMarker[] = [];
86
+
87
+ sourceFile.forEachDescendant((node) => {
88
+ if (!Node.isCallExpression(node)) return;
89
+
90
+ const expr = node.getExpression();
91
+
92
+ // Check property access patterns: describe.skip(), it.only(), test.todo(), etc.
93
+ if (Node.isPropertyAccessExpression(expr)) {
94
+ const objText = expr.getExpression().getText();
95
+ const propText = expr.getName();
96
+ const patternGroup = PROPERTY_ACCESS_PATTERNS[objText];
97
+
98
+ if (patternGroup && patternGroup[propText]) {
99
+ const pattern = patternGroup[propText];
100
+ markers.push({
101
+ type: pattern.type,
102
+ line: node.getStartLineNumber(),
103
+ test_name: extractTestName(node),
104
+ severity: pattern.severity,
105
+ rule: 'T4',
106
+ });
107
+ return;
108
+ }
109
+ }
110
+
111
+ // Check direct call patterns: xdescribe(), xit(), fdescribe(), fit()
112
+ if (Node.isIdentifier(expr)) {
113
+ const name = expr.getText();
114
+ const pattern = DIRECT_CALL_PATTERNS[name];
115
+
116
+ if (pattern) {
117
+ markers.push({
118
+ type: pattern.type,
119
+ line: node.getStartLineNumber(),
120
+ test_name: extractTestName(node),
121
+ severity: pattern.severity,
122
+ rule: 'T4',
123
+ });
124
+ }
125
+ }
126
+ });
127
+
128
+ // Sort markers by line number
129
+ markers.sort((a, b) => a.line - b.line);
130
+
131
+ // Compute summary counts
132
+ let skipCount = 0;
133
+ let onlyCount = 0;
134
+ let todoCount = 0;
135
+
136
+ for (const marker of markers) {
137
+ const markerType = marker.type;
138
+ // Determine category from the pattern tables
139
+ if (markerType.includes('.skip') || markerType.startsWith('x')) {
140
+ skipCount++;
141
+ } else if (markerType.includes('.only') || markerType.startsWith('f')) {
142
+ onlyCount++;
143
+ } else if (markerType.includes('.todo')) {
144
+ todoCount++;
145
+ }
146
+ }
147
+
148
+ return {
149
+ file: filePath,
150
+ markers,
151
+ summary: {
152
+ skip_count: skipCount,
153
+ only_count: onlyCount,
154
+ todo_count: todoCount,
155
+ },
156
+ };
157
+ }
158
+
159
+ // --- Main ---
160
+
161
+ function main(): void {
162
+ const args = process.argv.slice(2);
163
+
164
+ if (args.length === 0) {
165
+ const error: ErrorOutput = {
166
+ error: 'No file paths provided. Usage: skip-detector.ts <file1> [file2] ...',
167
+ file: '',
168
+ };
169
+ process.stderr.write(JSON.stringify(error) + '\n');
170
+ process.exit(1);
171
+ }
172
+
173
+ const project = new Project({
174
+ tsConfigFilePath: undefined,
175
+ skipAddingFilesFromTsConfig: true,
176
+ compilerOptions: {
177
+ allowJs: true,
178
+ checkJs: false,
179
+ noEmit: true,
180
+ strict: false,
181
+ skipLibCheck: true,
182
+ },
183
+ });
184
+
185
+ // Expand directory args to individual test files
186
+ const filePaths: string[] = [];
187
+ for (const arg of args) {
188
+ const resolved = path.resolve(arg);
189
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
190
+ const entries = fs.readdirSync(resolved).filter(f => /\.(test|spec)\.[tj]sx?$/.test(f));
191
+ filePaths.push(...entries.map(f => path.join(resolved, f)));
192
+ } else {
193
+ filePaths.push(resolved);
194
+ }
195
+ }
196
+
197
+ const results: (FileResult | ErrorOutput)[] = [];
198
+
199
+ for (const filePath of filePaths) {
200
+ const result = analyzeFile(filePath, project);
201
+ results.push(result);
202
+ }
203
+
204
+ if (results.length === 1) {
205
+ process.stdout.write(JSON.stringify(results[0], null, 2) + '\n');
206
+ } else {
207
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
208
+ }
209
+ }
210
+
211
+ main();