@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,509 @@
1
+ import { Project, Node, CallExpression, SourceFile, Block, SyntaxKind } from 'ts-morph';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+
5
+ // --- Types ---
6
+
7
+ interface Violation {
8
+ line: number;
9
+ type: 'T3+';
10
+ confidence: 'high' | 'medium' | 'low';
11
+ variable: string;
12
+ source: 'object_literal' | 'array_literal' | 'mock_import' | 'spread_object';
13
+ message: string;
14
+ suggestion: string;
15
+ }
16
+
17
+ interface FileResult {
18
+ file: string;
19
+ violations: Violation[];
20
+ }
21
+
22
+ interface ErrorOutput {
23
+ error: string;
24
+ file: string;
25
+ }
26
+
27
+ type SourceKind =
28
+ | 'object_literal'
29
+ | 'array_literal'
30
+ | 'spread_object'
31
+ | 'call_expression'
32
+ | 'new_expression'
33
+ | 'primitive'
34
+ | 'parameter'
35
+ | 'unknown';
36
+
37
+ interface VariableSource {
38
+ kind: SourceKind;
39
+ line: number;
40
+ name: string;
41
+ }
42
+
43
+ // --- Constants ---
44
+
45
+ const TEST_FUNCTIONS = new Set(['it', 'test']);
46
+ const DESCRIBE_FUNCTIONS = new Set(['describe']);
47
+ const ALL_TEST_WRAPPERS = new Set([...TEST_FUNCTIONS, ...DESCRIBE_FUNCTIONS]);
48
+
49
+ const MOCK_NAME_PATTERN = /^(mock|fake|stub|dummy)/i;
50
+
51
+ const SETUP_FUNCTIONS = new Set(['beforeEach', 'afterEach', 'beforeAll', 'afterAll']);
52
+
53
+ // --- AST Helpers ---
54
+
55
+ /**
56
+ * Determines the source kind of an initializer expression.
57
+ */
58
+ function classifyInitializer(initNode: Node): SourceKind {
59
+ // Object literal: { key: value }
60
+ if (Node.isObjectLiteralExpression(initNode)) {
61
+ // Check if it uses spread — still a manually constructed literal
62
+ const hasSpread = initNode.getProperties().some(
63
+ (p) => Node.isSpreadAssignment(p)
64
+ );
65
+ return hasSpread ? 'spread_object' : 'object_literal';
66
+ }
67
+
68
+ // Array literal: [item1, item2]
69
+ if (Node.isArrayLiteralExpression(initNode)) {
70
+ return 'array_literal';
71
+ }
72
+
73
+ // Call expression: someFunction() or await someFunction()
74
+ if (Node.isCallExpression(initNode)) {
75
+ return 'call_expression';
76
+ }
77
+ if (Node.isAwaitExpression(initNode)) {
78
+ const inner = initNode.getExpression();
79
+ if (Node.isCallExpression(inner)) {
80
+ return 'call_expression';
81
+ }
82
+ }
83
+
84
+ // New expression: new Service()
85
+ if (Node.isNewExpression(initNode)) {
86
+ return 'new_expression';
87
+ }
88
+
89
+ // Primitive literals
90
+ if (
91
+ Node.isStringLiteral(initNode) ||
92
+ Node.isNumericLiteral(initNode) ||
93
+ Node.isNoSubstitutionTemplateLiteral(initNode) ||
94
+ Node.isTemplateExpression(initNode) ||
95
+ initNode.getKind() === SyntaxKind.TrueKeyword ||
96
+ initNode.getKind() === SyntaxKind.FalseKeyword ||
97
+ initNode.getKind() === SyntaxKind.NullKeyword ||
98
+ initNode.getKind() === SyntaxKind.UndefinedKeyword
99
+ ) {
100
+ return 'primitive';
101
+ }
102
+
103
+ // As-expression: { ... } as SomeType — unwrap
104
+ if (Node.isAsExpression(initNode)) {
105
+ return classifyInitializer(initNode.getExpression());
106
+ }
107
+
108
+ // Parenthesized: (expr) — unwrap
109
+ if (Node.isParenthesizedExpression(initNode)) {
110
+ return classifyInitializer(initNode.getExpression());
111
+ }
112
+
113
+ // Satisfies expression — unwrap
114
+ if (Node.isSatisfiesExpression(initNode)) {
115
+ return classifyInitializer(initNode.getExpression());
116
+ }
117
+
118
+ return 'unknown';
119
+ }
120
+
121
+ /**
122
+ * Builds a map of variable name -> source info within a block of code.
123
+ */
124
+ function buildVariableMap(block: Node): Map<string, VariableSource> {
125
+ const varMap = new Map<string, VariableSource>();
126
+
127
+ block.forEachDescendant((node) => {
128
+ if (!Node.isVariableDeclaration(node)) return;
129
+
130
+ const name = node.getName();
131
+ const init = node.getInitializer();
132
+ if (!init) return;
133
+
134
+ const kind = classifyInitializer(init);
135
+ varMap.set(name, {
136
+ kind,
137
+ line: node.getStartLineNumber(),
138
+ name,
139
+ });
140
+ });
141
+
142
+ return varMap;
143
+ }
144
+
145
+ /**
146
+ * Checks if a test name or describe block suggests integration context.
147
+ */
148
+ function isIntegrationContext(testName: string, describeNames: string[]): boolean {
149
+ const allNames = [testName, ...describeNames].join(' ').toLowerCase();
150
+ return (
151
+ allNames.includes('integration') ||
152
+ allNames.includes('workflow') ||
153
+ allNames.includes('e2e') ||
154
+ allNames.includes('end-to-end') ||
155
+ allNames.includes('end to end')
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Extracts the test name from a test/it call expression.
161
+ */
162
+ function extractTestName(node: CallExpression): string {
163
+ const args = node.getArguments();
164
+ if (args.length > 0 && Node.isStringLiteral(args[0])) {
165
+ return args[0].getLiteralText();
166
+ }
167
+ if (args.length > 0 && Node.isNoSubstitutionTemplateLiteral(args[0])) {
168
+ return args[0].getLiteralText();
169
+ }
170
+ return '<unnamed>';
171
+ }
172
+
173
+ /**
174
+ * Gets the callback body from a test/describe function call.
175
+ */
176
+ function getCallbackBody(node: CallExpression): Node | undefined {
177
+ const args = node.getArguments();
178
+ for (const arg of args) {
179
+ if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
180
+ return arg.getBody();
181
+ }
182
+ }
183
+ return undefined;
184
+ }
185
+
186
+ /**
187
+ * Collects enclosing describe names for context.
188
+ */
189
+ function getDescribeAncestors(node: Node): string[] {
190
+ const names: string[] = [];
191
+ let current = node.getParent();
192
+ while (current) {
193
+ if (Node.isCallExpression(current)) {
194
+ const expr = current.getExpression();
195
+ const text = Node.isIdentifier(expr) ? expr.getText() : '';
196
+ if (DESCRIBE_FUNCTIONS.has(text)) {
197
+ const dArgs = current.getArguments();
198
+ if (dArgs.length > 0 && Node.isStringLiteral(dArgs[0])) {
199
+ names.push(dArgs[0].getLiteralText());
200
+ }
201
+ }
202
+ // Also check describe.skip, describe.only
203
+ if (Node.isPropertyAccessExpression(expr)) {
204
+ const objText = expr.getExpression().getText();
205
+ if (DESCRIBE_FUNCTIONS.has(objText)) {
206
+ const dArgs = current.getArguments();
207
+ if (dArgs.length > 0 && Node.isStringLiteral(dArgs[0])) {
208
+ names.push(dArgs[0].getLiteralText());
209
+ }
210
+ }
211
+ }
212
+ }
213
+ current = current.getParent();
214
+ }
215
+ return names;
216
+ }
217
+
218
+ /**
219
+ * Gets the root identifier name from a property access chain.
220
+ * e.g., mockOrder.id → 'mockOrder', mockOrder.items[0].name → 'mockOrder'
221
+ */
222
+ function getRootIdentifier(node: Node): string | undefined {
223
+ if (Node.isIdentifier(node)) {
224
+ return node.getText();
225
+ }
226
+ if (Node.isPropertyAccessExpression(node)) {
227
+ return getRootIdentifier(node.getExpression());
228
+ }
229
+ if (Node.isElementAccessExpression(node)) {
230
+ return getRootIdentifier(node.getExpression());
231
+ }
232
+ return undefined;
233
+ }
234
+
235
+ /**
236
+ * Flags a variable as a violation if it traces back to an object/array literal.
237
+ */
238
+ function flagVariableIfLiteral(
239
+ varName: string,
240
+ varMap: Map<string, VariableSource>,
241
+ flaggedVars: Set<string>,
242
+ isIntegration: boolean,
243
+ violations: Violation[],
244
+ ): void {
245
+ const source = varMap.get(varName);
246
+ if (!source) return;
247
+ if (flaggedVars.has(varName)) return;
248
+
249
+ if (
250
+ source.kind === 'object_literal' ||
251
+ source.kind === 'array_literal' ||
252
+ source.kind === 'spread_object'
253
+ ) {
254
+ const isMockName = MOCK_NAME_PATTERN.test(varName);
255
+ const confidence = isMockName ? 'high' : isIntegration ? 'high' : 'medium';
256
+ violations.push({
257
+ line: source.line,
258
+ type: 'T3+',
259
+ confidence,
260
+ variable: varName,
261
+ source: source.kind,
262
+ message: `Variable '${varName}' is manually constructed — breaks integration chain`,
263
+ suggestion: 'Replace with factory function or upstream function output',
264
+ });
265
+ flaggedVars.add(varName);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Finds all function call arguments within a test body and checks
271
+ * if any reference variables that trace back to object/array literals.
272
+ */
273
+ function findViolationsInTestBody(
274
+ body: Node,
275
+ varMap: Map<string, VariableSource>,
276
+ testName: string,
277
+ describeNames: string[],
278
+ ): Violation[] {
279
+ const violations: Violation[] = [];
280
+ const flaggedVars = new Set<string>();
281
+ const isIntegration = isIntegrationContext(testName, describeNames);
282
+
283
+ body.forEachDescendant((node) => {
284
+ if (!Node.isCallExpression(node)) return;
285
+
286
+ const expr = node.getExpression();
287
+
288
+ // Skip assertion calls — variables in expect() aren't violations
289
+ if (Node.isIdentifier(expr) && expr.getText() === 'expect') return;
290
+ if (Node.isPropertyAccessExpression(expr)) {
291
+ // Skip chained assertion methods like .toBe(), .toEqual()
292
+ const rootObj = getRootObject(expr);
293
+ if (rootObj === 'expect') return;
294
+ }
295
+
296
+ // Skip setup functions
297
+ if (Node.isIdentifier(expr) && SETUP_FUNCTIONS.has(expr.getText())) return;
298
+
299
+ // Check each argument for references to manually-constructed variables.
300
+ for (const arg of node.getArguments()) {
301
+ // Case 1: Variable passed directly — e.g., processOrder(mockOrderData)
302
+ if (Node.isIdentifier(arg)) {
303
+ flagVariableIfLiteral(arg.getText(), varMap, flaggedVars, isIntegration, violations);
304
+ continue;
305
+ }
306
+
307
+ // Case 2: Inline object whose property values reference manually-constructed
308
+ // variables via property access — e.g., processPayment({ orderId: mockOrder.id })
309
+ // Pure-primitive inline objects like createOrder({ customerId: 'x' }) are NOT flagged.
310
+ if (Node.isObjectLiteralExpression(arg)) {
311
+ for (const prop of arg.getProperties()) {
312
+ if (!Node.isPropertyAssignment(prop)) continue;
313
+ const init = prop.getInitializer();
314
+ if (!init) continue;
315
+ // Check for mockOrder.id pattern
316
+ if (Node.isPropertyAccessExpression(init)) {
317
+ const rootName = getRootIdentifier(init);
318
+ if (rootName) {
319
+ flagVariableIfLiteral(rootName, varMap, flaggedVars, isIntegration, violations);
320
+ }
321
+ }
322
+ // Check for direct variable reference as property value: { data: mockOrder }
323
+ if (Node.isIdentifier(init)) {
324
+ flagVariableIfLiteral(init.getText(), varMap, flaggedVars, isIntegration, violations);
325
+ }
326
+ }
327
+ continue;
328
+ }
329
+
330
+ // Case 3: Property access as direct argument — e.g., releaseReservation(mockOrder.id)
331
+ if (Node.isPropertyAccessExpression(arg)) {
332
+ const rootName = getRootIdentifier(arg);
333
+ if (rootName) {
334
+ flagVariableIfLiteral(rootName, varMap, flaggedVars, isIntegration, violations);
335
+ }
336
+ }
337
+ }
338
+ });
339
+
340
+ return violations;
341
+ }
342
+
343
+ /**
344
+ * Gets the root object name from a property access chain.
345
+ * e.g., expect(x).toBe(y) → 'expect'
346
+ */
347
+ function getRootObject(node: Node): string {
348
+ if (Node.isIdentifier(node)) {
349
+ return node.getText();
350
+ }
351
+ if (Node.isPropertyAccessExpression(node)) {
352
+ return getRootObject(node.getExpression());
353
+ }
354
+ if (Node.isCallExpression(node)) {
355
+ return getRootObject(node.getExpression());
356
+ }
357
+ return '';
358
+ }
359
+
360
+ // --- File Analysis ---
361
+
362
+ function analyzeFile(filePath: string, project: Project): FileResult | ErrorOutput {
363
+ let sourceFile: SourceFile;
364
+ try {
365
+ sourceFile = project.addSourceFileAtPath(filePath);
366
+ } catch (err) {
367
+ return {
368
+ error: `Failed to parse: ${err instanceof Error ? err.message : String(err)}`,
369
+ file: filePath,
370
+ };
371
+ }
372
+
373
+ const violations: Violation[] = [];
374
+
375
+ // Find all test/it calls and analyze their bodies
376
+ sourceFile.forEachDescendant((node) => {
377
+ if (!Node.isCallExpression(node)) return;
378
+
379
+ const expr = node.getExpression();
380
+ let funcName = '';
381
+
382
+ if (Node.isIdentifier(expr)) {
383
+ funcName = expr.getText();
384
+ } else if (Node.isPropertyAccessExpression(expr)) {
385
+ // Handle it.skip, test.only, etc.
386
+ funcName = expr.getExpression().getText();
387
+ }
388
+
389
+ if (!TEST_FUNCTIONS.has(funcName)) return;
390
+
391
+ const testName = extractTestName(node);
392
+ const describeNames = getDescribeAncestors(node);
393
+ const body = getCallbackBody(node);
394
+ if (!body) return;
395
+
396
+ // Build variable map for this test body
397
+ const varMap = buildVariableMap(body);
398
+
399
+ // Also include variables from enclosing scope (beforeEach, describe-level)
400
+ // by walking up to the nearest describe body
401
+ const describeBody = findEnclosingDescribeBody(node);
402
+ if (describeBody) {
403
+ const outerVars = buildVariableMap(describeBody);
404
+ // Only add outer vars that aren't already in the test body map
405
+ for (const [name, source] of outerVars) {
406
+ if (!varMap.has(name)) {
407
+ varMap.set(name, source);
408
+ }
409
+ }
410
+ }
411
+
412
+ const testViolations = findViolationsInTestBody(body, varMap, testName, describeNames);
413
+ violations.push(...testViolations);
414
+ });
415
+
416
+ // Deduplicate by variable + line (same var flagged from multiple tests)
417
+ const seen = new Set<string>();
418
+ const deduped = violations.filter((v) => {
419
+ const key = `${v.variable}:${v.line}`;
420
+ if (seen.has(key)) return false;
421
+ seen.add(key);
422
+ return true;
423
+ });
424
+
425
+ // Sort by line number
426
+ deduped.sort((a, b) => a.line - b.line);
427
+
428
+ return {
429
+ file: filePath,
430
+ violations: deduped,
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Finds the enclosing describe callback body for a test node.
436
+ */
437
+ function findEnclosingDescribeBody(node: Node): Node | undefined {
438
+ let current = node.getParent();
439
+ while (current) {
440
+ if (Node.isCallExpression(current)) {
441
+ const expr = current.getExpression();
442
+ let funcName = '';
443
+ if (Node.isIdentifier(expr)) {
444
+ funcName = expr.getText();
445
+ } else if (Node.isPropertyAccessExpression(expr)) {
446
+ funcName = expr.getExpression().getText();
447
+ }
448
+ if (DESCRIBE_FUNCTIONS.has(funcName)) {
449
+ return getCallbackBody(current);
450
+ }
451
+ }
452
+ current = current.getParent();
453
+ }
454
+ return undefined;
455
+ }
456
+
457
+ // --- Main ---
458
+
459
+ function main(): void {
460
+ const args = process.argv.slice(2);
461
+
462
+ if (args.length === 0) {
463
+ const error: ErrorOutput = {
464
+ error: 'No file paths provided. Usage: data-flow-analyzer.ts <file1> [file2] ...',
465
+ file: '',
466
+ };
467
+ process.stderr.write(JSON.stringify(error) + '\n');
468
+ process.exit(1);
469
+ }
470
+
471
+ const project = new Project({
472
+ tsConfigFilePath: undefined,
473
+ skipAddingFilesFromTsConfig: true,
474
+ compilerOptions: {
475
+ allowJs: true,
476
+ checkJs: false,
477
+ noEmit: true,
478
+ strict: false,
479
+ skipLibCheck: true,
480
+ },
481
+ });
482
+
483
+ // Expand directory args to individual test files
484
+ const filePaths: string[] = [];
485
+ for (const arg of args) {
486
+ const resolved = path.resolve(arg);
487
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
488
+ const entries = fs.readdirSync(resolved).filter(f => /\.(test|spec)\.[tj]sx?$/.test(f));
489
+ filePaths.push(...entries.map(f => path.join(resolved, f)));
490
+ } else {
491
+ filePaths.push(resolved);
492
+ }
493
+ }
494
+
495
+ const results: (FileResult | ErrorOutput)[] = [];
496
+
497
+ for (const filePath of filePaths) {
498
+ const result = analyzeFile(filePath, project);
499
+ results.push(result);
500
+ }
501
+
502
+ if (results.length === 1) {
503
+ process.stdout.write(JSON.stringify(results[0], null, 2) + '\n');
504
+ } else {
505
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
506
+ }
507
+ }
508
+
509
+ main();