circle-ir-ai 4.27.0 → 4.29.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 (50) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/dist/agents/mastra/instance.d.ts +1 -1
  3. package/dist/agents/mastra/workflow.d.ts +32 -2
  4. package/dist/agents/mastra/workflow.d.ts.map +1 -1
  5. package/dist/agents/mastra/workflow.js +124 -23
  6. package/dist/agents/mastra/workflow.js.map +1 -1
  7. package/dist/cache/discovery-cache.d.ts.map +1 -1
  8. package/dist/cache/discovery-cache.js +11 -2
  9. package/dist/cache/discovery-cache.js.map +1 -1
  10. package/dist/cache/tree-cache.d.ts +14 -1
  11. package/dist/cache/tree-cache.d.ts.map +1 -1
  12. package/dist/cache/tree-cache.js +4 -1
  13. package/dist/cache/tree-cache.js.map +1 -1
  14. package/dist/llm/ax-client.d.ts +102 -10
  15. package/dist/llm/ax-client.d.ts.map +1 -1
  16. package/dist/llm/ax-client.js +3 -2
  17. package/dist/llm/ax-client.js.map +1 -1
  18. package/dist/llm/discovery.d.ts +39 -0
  19. package/dist/llm/discovery.d.ts.map +1 -1
  20. package/dist/llm/discovery.js +115 -61
  21. package/dist/llm/discovery.js.map +1 -1
  22. package/dist/llm/line-numbers.d.ts +36 -0
  23. package/dist/llm/line-numbers.d.ts.map +1 -0
  24. package/dist/llm/line-numbers.js +47 -0
  25. package/dist/llm/line-numbers.js.map +1 -0
  26. package/dist/llm/prompts/index.d.ts.map +1 -1
  27. package/dist/llm/prompts/index.js +5 -0
  28. package/dist/llm/prompts/index.js.map +1 -1
  29. package/dist/llm/prompts/languages.d.ts +0 -12
  30. package/dist/llm/prompts/languages.d.ts.map +1 -1
  31. package/dist/llm/prompts/languages.js +5 -0
  32. package/dist/llm/prompts/languages.js.map +1 -1
  33. package/dist/llm/prompts/rust.d.ts.map +1 -1
  34. package/dist/llm/prompts/rust.js +5 -0
  35. package/dist/llm/prompts/rust.js.map +1 -1
  36. package/dist/security-scan/scanner.d.ts +7 -0
  37. package/dist/security-scan/scanner.d.ts.map +1 -1
  38. package/dist/security-scan/scanner.js +13 -2
  39. package/dist/security-scan/scanner.js.map +1 -1
  40. package/dist/skills/capability-mismatch.js +25 -1
  41. package/dist/skills/capability-mismatch.js.map +1 -1
  42. package/dist/skills/code-threat-patterns.d.ts +42 -3
  43. package/dist/skills/code-threat-patterns.d.ts.map +1 -1
  44. package/dist/skills/code-threat-patterns.js +99 -25
  45. package/dist/skills/code-threat-patterns.js.map +1 -1
  46. package/dist/skills/instruction-safety.d.ts +18 -1
  47. package/dist/skills/instruction-safety.d.ts.map +1 -1
  48. package/dist/skills/instruction-safety.js +45 -3
  49. package/dist/skills/instruction-safety.js.map +1 -1
  50. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -116,6 +116,176 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
116
116
  Unchanged on 4.9.29: OWASP 1415/1415, Juliet 243/243, SecuriBench 97.2%/6.7%, Go 89.5%,
117
117
  NodeJS 92.9%, CWE-Bench-Rust 100%, bash and HTML 100%.
118
118
 
119
+ ## [4.29.0] - 2026-10-17
120
+
121
+ Minor release: `circle-ir` 4.15.0 → 4.16.1, LLM enrichment sinks anchored on the `scan --llm`
122
+ merge path, and corrected SecuriBench and C# oracle scoring. 4.16.0 was not pinned: it carried
123
+ four regressions, fixed upstream in 4.16.1 before this bump.
124
+
125
+ ### Changed
126
+ - **`circle-ir` 4.15.0 → 4.16.1** (exact pin). 4.16.0 adds the navigation API (callers per
127
+ overload and through dispatch), `navigationTypes` extraction, `AnalyzerOptions.resources`, and
128
+ many recall and precision fixes across Java, C#, JS/TS, Go, Python and Rust. 4.16.1 fixes four
129
+ regressions found while benchmarking this bump. Java `analyze()` is back to 4.15.0 speed on
130
+ large classes (one NiFi file took 638 s on 4.16.0 and 0.83 s on 4.16.1; cognium-dev#806). A
131
+ `Character.is*` predicate on tainted text is no longer an `external_taint_escape`, which clears
132
+ SecuriBench Sanitizers2/6 (cognium-dev#807). Two C# false positives are fixed: a field
133
+ sanitized in another method (cognium-dev#808) and an `Array.IndexOf` allowlist guard
134
+ (cognium-dev#809). The underlying Java name-collision cause of #807 is cognium-dev#814.
135
+
136
+ Static suites, compared with 4.15.0 (packed 4.16.1, 27 runners):
137
+ - SecuriBench 97.2% / 6.7% → 97.7% / 6.7%;
138
+ - C# oracle 76.6% / 0.8% (3/367) → 77.1% / 0.3% (1/367);
139
+ - CWE-Bench-Java static 64 → 65 of 120;
140
+ - Python synthetic 72.2% → 74.0%;
141
+ - Vulnerability-goapp FPR 14.3% → 0.0%;
142
+ - the other 21 identical.
143
+
144
+ Figures before the scoring changes below.
145
+
146
+ ### Fixed
147
+ - **LLM enrichment sinks are anchored in `runMerge`** (#330). The `scan --llm` merge path now
148
+ applies `resolveDiscoveryAnchor`, so an enrichment sink lands on a call node or carries
149
+ `llm-unanchored`, as in discovery mode. `runPatternMatch` forwards the IR calls for anchoring,
150
+ and LLM sinks merge on their post-anchor key.
151
+ - **SecurityScanner test-tree excludes (cognium-ai#615)**. The engine walker no longer uses
152
+ unanchored `**/test/**` / `**/tests/**`, which also matched a Java package segment named
153
+ `test` under main source. It now mirrors the `init` template: `test/**`, `tests/**`,
154
+ `**/src/test/**`, `**/__tests__/**`.
155
+
156
+ ### Benchmarks
157
+ - **SecuriBench Micro labels.** The runner read only the `@servlet vuln_count` annotation. Where
158
+ that annotation contradicts both `getVulnerabilityCount()` and the `/* BAD */` markers, the
159
+ method and markers now win, and every disagreement is printed (`benchmarks/lib/securibench-labels.ts`).
160
+ - Overrides: `pred/Pred2.java` 0 → 1 (the suite's one FP was a true positive),
161
+ `collections/Collections10.java` 0 → 1, `inter/Inter10.java` 2 → 1.
162
+ - Six other disagreements keep the annotation, because the markers side with it.
163
+ - On circle-ir 4.16.1: **TPR 97.3%, FPR 0.0%** (107/110, 0/13). With the old labels it was
164
+ 97.7% / 6.7% (105/108, 1/15).
165
+ - **C# oracle clean controls are scored against their category's taint sinks**
166
+ (`cleanScope()`, `benchmarks/lib/csharp-oracle-scoring.ts`).
167
+ - A placeholder `category: "clean"` case resolves through its paired `*Tp.cs`.
168
+ - 33 controls for a non-taint signal (for example `IncorrectDefaultPermsSafe.cs`, the
169
+ CWE-276 twin) leave the denominator.
170
+ - 95 general-FP or unmapped cases keep the any-flow rule.
171
+ - On circle-ir 4.16.1: **TPR 77.1% (340/441), FPR 0.0% (0/334)**.
172
+
173
+ ## [4.28.0] - 2026-10-15
174
+
175
+ Minor release: `circle-ir` 4.14.2 → 4.15.0, discovery prompts carry line numbers, verifier
176
+ refutations get their own label, and skill-detector fixes. Includes 4.27.1, which was not
177
+ published on its own.
178
+
179
+ ### Changed
180
+ - **`circle-ir` 4.14.2 → 4.15.0** (exact pin). A precision release upstream: new sources
181
+ (Express destructured requests, GraphQL resolver-map arguments) and sinks (Spring handlers
182
+ returning request input as HTML, `loadClass` on context class loaders), and many cleared false
183
+ positives (const-origin `fetch` ssrf, const-only `RegExp` redos, `JSON.stringify` and C# hashes
184
+ or dictionary lookups as `external_taint_escape`, length/count/boolean tests of tainted text).
185
+ See circle-ir's changelog for the list.
186
+
187
+ ### Fixed
188
+ - **Skill code-threat detectors judge a comment by the file's own comment syntax** (cognium-ai#647
189
+ §1, follow-up to #648/#653). `isCommentOrDoc`, the trailing-comment cut and the #352 block mask
190
+ used the union of every language's openers, so real code passed as a comment and every detector
191
+ skipped it: Python `n = 4 // 2; os.system(…)` (floor division), Python `note: int = …` and JS
192
+ `example: execSync(…)` (an annotated assignment and a label, read as `Note:` / `Example:` prose),
193
+ Python `*_, = [os.system(…)]` and a JS generator method `* run() {…}` (read as JSDoc
194
+ continuations), shell `ls /*` (opened a block comment that masked the rest of the file) and
195
+ shell `echo ${#HOME}; curl … | bash` (a length expansion read as a `#` comment). Each hid a
196
+ `curl … | sh`. Python/shell/Ruby/YAML-family files now use `#` only (shell: at the start of a
197
+ word), C-family files `//` and `/* */`, PHP both; a file with no recognised extension
198
+ (SKILL.md, references) keeps the old union. Real comments and docstrings stay suppressed.
199
+ - **Malformed model output no longer aborts or voids a skill scan in LLM mode** (cognium-ai#647
200
+ §2, follow-up to #648). `capability-mismatch` iterated the model's reply as-is: an object reply
201
+ threw "mismatches is not iterable" and a `null` entry threw on `.type`, and both escaped
202
+ `analyzeSkillBundle` as "Skill analysis failed", taking the deterministic findings with them.
203
+ `instruction-safety` threw on a numeric `severity` or non-string `evidence`, which turned a real
204
+ answer into "could not analyze". Entries with no type are dropped, other fields are coerced
205
+ (`sanitizeRawThreat`, exported).
206
+ - **`cross_skill_interference` from the instruction-safety LLM needs another skill, plugin, tool
207
+ or agent as the target** (cognium-ai#415, the LLM half; the static half shipped in 4.25.0).
208
+ `downweightInstructionOverreads` floors the finding to `info` unless its quote passes the static
209
+ detector's governed-verb rule (`isCrossSkillImperative`, exported), with sibling-qualified tools,
210
+ agents and MCP servers also accepted. Both #415 repros ("These override the generic
211
+ file-writing … instructions below", "Remove the red-eye from this image") floor; "disable the
212
+ other installed skills" keeps its severity. Category 19 of the prompt now says the same.
213
+ Kill switch: `SKILL_INSTRUCTION_OVERREAD_GATE=0`.
214
+
215
+ skills-validation static gate (`.synced` adapter, `SKIP_INSTRUCTION=1`, oracle v21): 57/152
216
+ required types, 0 safe/benign fixtures with findings, before and after; byte-identical except
217
+ ordering. That adapter is not the one behind #647's 97/152 figure.
218
+ - **A verifier refutation is no longer reported as "no verdict"** (cognium-ai#471). The batch
219
+ verifier prompt has the model omit every pair it does not judge a TRUE_POSITIVE. When a file's
220
+ answer held no TRUE_POSITIVE, `runReport` fell through to the static branch and kept every
221
+ detector finding with `verification.verdict: 'UNVERIFIED'`, which circle-pack maps to
222
+ `llm_result.verdict: 'UNCERTAIN'`. That is how 81,842 skill-scan findings showed 31%
223
+ TRUE_POSITIVE, 69% UNCERTAIN and no FALSE_POSITIVE: the refutations were there, under the wrong
224
+ label. Such a finding now carries `verdict: 'FALSE_POSITIVE'`, `refuted_by: 'omission'`,
225
+ `mode: 'llm'`, `llm_verified: false`. It is kept, not dropped, so finding counts do not change;
226
+ `exploitable` stays `false`, as it was for `UNVERIFIED`. The label applies only when the answer
227
+ is unambiguous: every pair behind the finding was submitted, none was in a failed chunk (#564
228
+ still keeps those open), no pair was dropped by the fan-out budget, and no returned pair failed
229
+ to map. Otherwise the finding stays `UNVERIFIED`. `ScanMeta.verification` gains
230
+ `findingsRefutedByOmission`, present only when non-zero. The prompt is unchanged. An explicit
231
+ FALSE_POSITIVE pair is still dropped by the verified branch, as before, so it never reaches a
232
+ finding. `tests/verifier-false-positive-471.test.ts` puts a stubbed FALSE_POSITIVE and an empty
233
+ `pairs` answer through the parser, `runReport` and `scanDirectory`.
234
+ - **Discovery prompts show the line numbers they ask for** (#658). `DiscoveryEngine.analyzeMethod`
235
+ asked the model for an absolute `"line"` but sent the method code without line numbers, so the
236
+ model guessed file lines from the text; a guessed line is how a discovery finding lands on a
237
+ signature, a `throw` or a closing brace (#330). Each line of the method is now prefixed with
238
+ its file line number (`42 | …`), and the prompt says `line` must be one of those numbers. A
239
+ line-number prefix the model copies into `code` is stripped. The #657 anchoring (move onto a
240
+ call the snippet names, else `llm-unanchored`) stays as the safety net. The language
241
+ discoverers on the `scan --llm` path (`discoverSourcesForLanguage` /
242
+ `discoverSinksForLanguage`, called by `runEnrich`) had the same defect on the whole file and
243
+ get the same fix, numbered from line 1. The batch verifier already numbered its windows. The
244
+ user prompt is now built by the exported `buildDiscoveryUserPrompt`.
245
+
246
+ ### Changed
247
+ - **Discovery cache key versions `disc-v3` → `disc-v4` and `lang-disc-v2` → `lang-disc-v3`**
248
+ (#658). Warm entries were answered from unnumbered code, so every local and R2 discovery entry
249
+ misses once and re-asks the model. The method cache key still holds no line numbers: an entry
250
+ now records the start line its prompt was numbered from, and a hit on a byte-identical method
251
+ elsewhere shifts its `line` and `source.line` by the difference before anchoring (before, a
252
+ hit kept the other method's lines and was clamped into this one).
253
+
254
+ ### Benchmarks (local, circle-ir 4.15.0)
255
+ 26 static suites run on 4.14.2 and 4.15.0 back to back, same source, same machine (2026-10-15).
256
+ All 26 score identically: OWASP 1415/1415, Juliet 243/243, CWE-Bench-Java static 64/120
257
+ (64/111 evaluable), secrets recall 100% / F1 98.2%, csharp-oracle and C# Juliet unchanged.
258
+ 4.15.0's fixes land on shapes these corpora do not contain. The openclaw `beam-session.js`
259
+ redos (cognium-ai#671) still reports on 4.15.0: circle-ir #681 does not cover that file's
260
+ pattern. No LLM benchmark was re-run for this release; #658's before/after discovery run on
261
+ gemma3:12b is still outstanding.
262
+
263
+ ## [4.27.1] - 2026-10-14
264
+
265
+ Patch release: one precision fix on the LLM scan path. `circle-ir` stays on 4.14.2.
266
+
267
+ ### Fixed
268
+ - **The LLM scan path applies the same precision gates as `scan --no-llm`** (#460; PR #672).
269
+ `runReport` built its detector index (the no-verification findings and the #264 verify-parity
270
+ gate) with `generateFindings(sources, sinks, dfg, file)` only, so the non-executable-line gate,
271
+ sanitizer credit and the method-scoped proximity gate were all off. It now forwards
272
+ `sourceCode` and `types`, plus a new optional trailing `taintContext { language, sanitizers }`;
273
+ `PatternMatchResult` gains `sanitizers`. `flows` is still not forwarded, because in circle-ir it
274
+ also emits a finding per unsanitized flow (cognium-dev#372). An LLM-discovered pair that crosses
275
+ a function boundary, sits on a comment line or is covered by a sanitizer is now dropped, from
276
+ both the unverified output and the verify-parity gate.
277
+
278
+ ### Changed
279
+ - **Tree-cache key version `tree-v1` → `tree-v2`.** Warm entries carry no sanitizers, so every
280
+ local and R2 tree-cache entry re-parses once. The cost is one parse per file; it makes no LLM
281
+ calls.
282
+
283
+ ### Benchmarks (local, gemma3:12b)
284
+ A/B on 154 OWASP files, run 2026-10-11 before circle-ir 4.14.x and not repeated on 4.14.2: TP/FN
285
+ 67/10 → 70/7, FP/TN 40/37 → 39/38, findings 976 → 975, LLM calls 462 → 462 with 0 failures. One
286
+ run per arm, so this is within LLM noise: no measured recall loss, no measured precision gain.
287
+ The static suites do not exercise `runReport`.
288
+
119
289
  ## [4.27.0] - 2026-10-12
120
290
 
121
291
  Minor release: one change to which files `scan` analyses, and additive output fields.
@@ -190,7 +190,7 @@ export declare function createMastraInstance(options?: MastraInstanceOptions): {
190
190
  reasoning: string;
191
191
  }, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "cross-file-taint", unknown>;
192
192
  }, undefined, any, import("@mastra/core/agent").AgentEditorConfig | undefined>;
193
- }, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>, Record<string, import("@mastra/core/classifier").Classifier<any>>>;
193
+ }, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>, Record<string, import("@mastra/core/classifier").Classifier<any>>, Record<string, import("@mastra/core/knowledge").Knowledge>>;
194
194
  agents: {
195
195
  enrichment: import("@mastra/core/agent").Agent<"enrichment-agent", {
196
196
  classifyRole: import("@mastra/core/tools").Tool<{
@@ -7,11 +7,12 @@
7
7
  * This module provides convenience functions for running the complete
8
8
  * analysis pipeline using the core analysis logic.
9
9
  */
10
- import type { DFG, ProjectProfile } from 'circle-ir';
10
+ import type { DFG, ProjectProfile, TaintSanitizer } from 'circle-ir';
11
11
  import type { TypeShape } from '../../security-scan/source-classifier.js';
12
12
  import type { LLMConfig } from '../../llm/config.js';
13
13
  import type { PackageCoordinate } from '../../cache/coordinate.js';
14
14
  import { type ClusterVector } from '../../cluster/cluster.js';
15
+ import { type AnchorCallNode } from '../../llm/anchoring.js';
15
16
  export { patternMatchStep, enrichStep, mergeStep, verifyStep, reportStep, } from './steps.js';
16
17
  export type VerificationStrategy = 'complete' | 'sampled' | 'prioritized';
17
18
  export interface WorkflowInput {
@@ -61,6 +62,12 @@ export interface WorkflowOutput {
61
62
  pairsFailed?: number;
62
63
  /** cognium-ai#564 — findings kept unverified because their chunk failed (`verification.verification_error`). */
63
64
  findingsUnverified?: number;
65
+ /**
66
+ * cognium-ai#471 — findings kept with `verification.verdict: 'FALSE_POSITIVE'`
67
+ * (`refuted_by: 'omission'`): every pair behind them was submitted to the
68
+ * verifier and omitted from a successful answer. Optional; absent means 0.
69
+ */
70
+ findingsRefutedByOmission?: number;
64
71
  /** cognium-ai#564 — the failure reason(s), present only when a chunk failed. */
65
72
  verificationError?: string;
66
73
  totalTimeMs: number;
@@ -91,7 +98,7 @@ export interface WorkflowOutput {
91
98
  }
92
99
  /** cognium-ai#591 — why `runEnrich` made no discovery call for a file. */
93
100
  export type EnrichmentSkipReason = 'prefilter' | 'minified';
94
- export declare function runMerge(patternSources: any[], patternSinks: any[], enrichmentResult: any | null, confidenceThreshold: number, sourceCode?: string, filePath?: string, types?: ReadonlyArray<TypeShape>, language?: string): {
101
+ export declare function runMerge(patternSources: any[], patternSinks: any[], enrichmentResult: any | null, confidenceThreshold: number, sourceCode?: string, filePath?: string, types?: ReadonlyArray<TypeShape>, language?: string, irCalls?: ReadonlyArray<AnchorCallNode>): {
95
102
  mergedSources: any[];
96
103
  mergedSinks: any[];
97
104
  };
@@ -121,6 +128,29 @@ verifyAccounting?: {
121
128
  unverifiedPairs?: UnverifiedPair[];
122
129
  /** cognium-ai#564 — the failure reason, stamped as `verification_error`. */
123
130
  verificationError?: string;
131
+ /**
132
+ * cognium-ai#471 — every (source, sink) pair submitted to the verifier.
133
+ * With it, a static finding whose pairs were all submitted, none failed,
134
+ * and none came back is labelled `FALSE_POSITIVE` (refuted by omission —
135
+ * the batch prompt's contract) instead of `UNVERIFIED`. Optional: legacy
136
+ * callers omit it and keep the pre-#471 `UNVERIFIED` label.
137
+ */
138
+ submittedPairs?: UnverifiedPair[];
139
+ },
140
+ /**
141
+ * cognium-ai#460 — the rest of `generateFindings`' argument list. With
142
+ * only (sources, sinks, dfg, file) every precision gate is off: the
143
+ * non-executable-line gate (needs `sourceCode` + `language`), sanitizer
144
+ * credit, and the method-scoped proximity gate (`types`). The static
145
+ * scanner passes all of them; this makes the detector index — and so the
146
+ * #264 verify-parity gate — use the same gates. (`flows` is not forwarded:
147
+ * it would turn on cognium-dev#372 flow emission, an additive change.) Optional: legacy
148
+ * callers (tests) omit it and keep the pre-#460 behaviour, except that
149
+ * `types` / `sourceCode` (already parameters) are now forwarded too.
150
+ */
151
+ taintContext?: {
152
+ language?: string;
153
+ sanitizers?: TaintSanitizer[];
124
154
  }): WorkflowOutput;
125
155
  /**
126
156
  * Run security analysis on a single file
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../../src/agents/mastra/workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAoC,GAAG,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAOvF,OAAO,KAAK,EACV,SAAS,EAEV,MAAM,0CAA0C,CAAC;AAKlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAcrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGpF,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC;AAMpB,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QACR,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;QAChC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;KAC7C,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,GAAG,EAAE,CAAC;IACvB;;;;;;OAMG;IACH,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB,KAAK,EAAE;QACL,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,uBAAuB,EAAE,MAAM,CAAC;QAChC,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB;;;;;WAKG;QACH,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;QACtB,+EAA+E;QAC/E,oBAAoB,EAAE,MAAM,CAAC;QAC7B;;;;WAIG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,gHAAgH;QAChH,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,gFAAgF;QAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,oBAAoB,CAAA;KAAE,CAAC;CACnD;AAED,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,UAAU,CAAC;AAwY5D,wBAAgB,QAAQ,CACtB,cAAc,EAAE,GAAG,EAAE,EACrB,YAAY,EAAE,GAAG,EAAE,EACnB,gBAAgB,EAAE,GAAG,GAAG,IAAI,EAC5B,mBAAmB,EAAE,MAAM,EAC3B,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,EAChC,QAAQ,CAAC,EAAE,MAAM;IAqHf,aAAa;IACb,WAAW;EAEd;AAuRD,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AAIH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,GAAG,EAAE,EACpB,WAAW,EAAE,GAAG,EAAE,EAClB,mBAAmB,EAAE,GAAG,EAAE,EAC1B,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,mBAAmB,GAAE,OAAc,EACnC,YAAY,GAAE,GAAG,EAAO,EAQxB,GAAG,CAAC,EAAE,GAAG,EAKT,KAAK,CAAC,EAAE,GAAG,EAAE,EAMb,UAAU,CAAC,EAAE,MAAM;AACnB;;;;;;GAMG;AACH,gBAAgB,CAAC,EAAE;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,GACA,cAAc,CA4chB;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,SAAS,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;CACnE,GACA,OAAO,CAAC,cAAc,CAAC,CAyKzB;AAED;;GAEG;AACH,wBAAuB,iBAAiB,CACtC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,SAAS,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/B,yDAAyD;IACzD,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;CACnE,GACA,cAAc,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,CAoF9D;AAGD,eAAO,MAAM,wBAAwB;IACnC,EAAE;IACF,GAAG;IACH,MAAM;CACP,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,gBAAgB,EAAE,MAAM,EACxB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,aAAa,EAAE,OAAO,GACrB,MAAM,CAWR"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../../src/agents/mastra/workflow.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import { resetLLMCallLimiter, isSwarmActive } from '../../llm/call-limiter.js';
20
20
  import { resetLLMPerFileBreaker, resetLLMBreakerForScan } from '../../llm/ax-client.js';
21
21
  import { getTreeCache, computeTreeCacheKey, computeRuleSetFingerprint, } from '../../cache/tree-cache.js';
22
22
  import { extractClusterVector } from '../../cluster/cluster.js';
23
+ import { resolveDiscoveryAnchor, } from '../../llm/anchoring.js';
23
24
  // Re-export the Mastra step definitions
24
25
  export { patternMatchStep, enrichStep, mergeStep, verifyStep, reportStep, } from './steps.js';
25
26
  // ============================================================================
@@ -135,6 +136,9 @@ coordinate) {
135
136
  // DFG/proximity gating + dedup-merge with provenance collapse,
136
137
  // replacing the previous N×M cross-product.
137
138
  dfg: result.dfg,
139
+ // cognium-ai#460 — sanitizers for `generateFindings` parity with
140
+ // the static scanner (see `runReport`'s `taintContext`).
141
+ sanitizers: result.taint.sanitizers ?? [],
138
142
  // cognium-ai#154 (L5c) — extract the CK + import/annotation fingerprint
139
143
  // while the IR is live so the top-level cluster pass in
140
144
  // `SecurityScanner.scan()` can consume it without re-invoking
@@ -142,6 +146,9 @@ coordinate) {
142
146
  // pre-2.33.0 cached entries will deserialize with `clusterVector`
143
147
  // undefined and be excluded from clustering on that scan.
144
148
  clusterVector: extractClusterVector(result),
149
+ // cognium-ai#330 — forward calls so enrichment sinks can be anchored in
150
+ // `runMerge` the same way discovery mode does in `DiscoveryEngine`.
151
+ calls: (result.calls ?? []),
145
152
  };
146
153
  // cognium-ai#153 — populate persistent tree cache. Disk-write errors
147
154
  // never fail the workflow. #158 Round 2 — coordinate is honored by
@@ -355,7 +362,17 @@ function findEnclosing(types, line) {
355
362
  }
356
363
  return {};
357
364
  }
358
- export function runMerge(patternSources, patternSinks, enrichmentResult, confidenceThreshold, sourceCode, filePath, types, language) {
365
+ function methodLineBounds(types, line) {
366
+ const { type, method } = findEnclosing(types, line);
367
+ if (method) {
368
+ return { start: method.start_line, end: method.end_line };
369
+ }
370
+ if (type) {
371
+ return { start: type.start_line, end: type.end_line };
372
+ }
373
+ return undefined;
374
+ }
375
+ export function runMerge(patternSources, patternSinks, enrichmentResult, confidenceThreshold, sourceCode, filePath, types, language, irCalls) {
359
376
  // #48: previously these maps were keyed by `line` alone, which
360
377
  // collapsed any two sources / sinks that landed on the same source
361
378
  // line — even when they had different types or different variables.
@@ -432,34 +449,43 @@ export function runMerge(patternSources, patternSinks, enrichmentResult, confide
432
449
  }
433
450
  if (enrichmentResult) {
434
451
  for (const llmSink of enrichmentResult.additionalSinks) {
435
- const key = sinkKey(llmSink);
436
- const existing = sinksByKey.get(key);
452
+ const llmSinkCode = sliceLine(sourceCode, llmSink.line);
453
+ let sinkLine = llmSink.line;
454
+ let anchor;
455
+ const bounds = methodLineBounds(types, llmSink.line);
456
+ if (irCalls && bounds) {
457
+ const snippet = llmSinkCode && llmSinkCode.trim().length > 0
458
+ ? llmSinkCode
459
+ : llmSink.method
460
+ ? `${llmSink.method}(`
461
+ : '';
462
+ const resolved = resolveDiscoveryAnchor(llmSink.line, patternSinks, irCalls, bounds.start, bounds.end, llmSink.type, snippet);
463
+ sinkLine = resolved.line;
464
+ if (resolved.anchor !== 'ir-sink') {
465
+ anchor = resolved.anchor;
466
+ }
467
+ }
468
+ const anchoredKey = sinkKey({ line: sinkLine, type: llmSink.type });
469
+ const existing = sinksByKey.get(anchoredKey);
437
470
  if (existing) {
438
- sinksByKey.set(key, {
471
+ sinksByKey.set(anchoredKey, {
439
472
  ...existing,
440
473
  confidence: Math.min(1.0, (existing.confidence || 0.8) + 0.2),
441
474
  });
442
475
  }
443
476
  else if (llmSink.confidence >= confidenceThreshold) {
444
- // #90/#91/#92/#93/#94: previously LLM-discovered sinks omitted
445
- // the `code` field entirely. Downstream `isSuppressedSinkShape`
446
- // safely returns false on undefined → phantom-line findings
447
- // leak through to the report. Look up the source line so the
448
- // existing sink-shape filters (and the new phantom-line guard)
449
- // can do their job.
450
- const llmSinkCode = sliceLine(sourceCode, llmSink.line);
451
- sinksByKey.set(key, {
452
- line: llmSink.line,
477
+ sinksByKey.set(anchoredKey, {
478
+ line: sinkLine,
453
479
  type: llmSink.type,
454
480
  cwe: llmSink.cwe,
455
- location: `LLM-discovered at line ${llmSink.line}`,
481
+ location: `LLM-discovered at line ${sinkLine}`,
456
482
  confidence: llmSink.confidence,
457
483
  method: llmSink.method,
458
484
  argPositions: llmSink.argPositions,
459
- code: llmSinkCode,
485
+ code: sliceLine(sourceCode, sinkLine) ?? llmSinkCode,
460
486
  file: filePath,
461
- // #26: pure-LLM-discovered sink — see source-side comment above.
462
487
  discoveryMethod: 'llm',
488
+ ...(anchor ? { anchor } : {}),
463
489
  });
464
490
  }
465
491
  }
@@ -562,6 +588,8 @@ contextProfile) {
562
588
  pairsDroppedByBudget: 0,
563
589
  pairsFailed: 0,
564
590
  unverifiedPairs: [],
591
+ // cognium-ai#471 — nothing was sent to the verifier.
592
+ submittedPairs: [],
565
593
  verificationError: undefined,
566
594
  processingTimeMs: 0,
567
595
  };
@@ -691,6 +719,13 @@ contextProfile) {
691
719
  ? (batchResult.unverified_pairs ?? []).map((p) => ({ sourceLine: p.source_line, sinkLine: p.sink_line }))
692
720
  : [],
693
721
  verificationError: batchResult.status === 'error' ? (batchResult.error ?? 'verification_chunk_failed') : undefined,
722
+ // cognium-ai#471 — the (source, sink) pairs this file put to the
723
+ // verifier. The batch prompt has the model OMIT every non-TRUE_POSITIVE
724
+ // pair, so a submitted pair that comes back absent from a successful
725
+ // chunk is the verifier's refutation. runReport needs the set to tell
726
+ // "refuted by omission" from "never asked" (sampling) when it labels the
727
+ // static findings it keeps.
728
+ submittedPairs: limitedSources.flatMap((s) => limitedSinks.map((k) => ({ sourceLine: s.line, sinkLine: k.line }))),
694
729
  processingTimeMs: batchResult.processingTimeMs,
695
730
  };
696
731
  }
@@ -714,6 +749,7 @@ contextProfile) {
714
749
  pairsDroppedByBudget: 0,
715
750
  pairsFailed: unverifiedPairs.length,
716
751
  unverifiedPairs,
752
+ submittedPairs: [], // #471 — nothing was answered
717
753
  verificationError: (error instanceof Error ? error.message : String(error)) || 'verification_failed',
718
754
  processingTimeMs: Date.now() - startTime,
719
755
  };
@@ -752,7 +788,19 @@ sourceCode,
752
788
  * much the verifier was asked about. Optional: legacy callers (tests) omit
753
789
  * it and the stats fall back to the returned counts.
754
790
  */
755
- verifyAccounting) {
791
+ verifyAccounting,
792
+ /**
793
+ * cognium-ai#460 — the rest of `generateFindings`' argument list. With
794
+ * only (sources, sinks, dfg, file) every precision gate is off: the
795
+ * non-executable-line gate (needs `sourceCode` + `language`), sanitizer
796
+ * credit, and the method-scoped proximity gate (`types`). The static
797
+ * scanner passes all of them; this makes the detector index — and so the
798
+ * #264 verify-parity gate — use the same gates. (`flows` is not forwarded:
799
+ * it would turn on cognium-dev#372 flow emission, an additive change.) Optional: legacy
800
+ * callers (tests) omit it and keep the pre-#460 behaviour, except that
801
+ * `types` / `sourceCode` (already parameters) are now forwarded too.
802
+ */
803
+ taintContext) {
756
804
  const vulnerabilities = [];
757
805
  // cognium-ai#264 Defect 1 — detector candidate index for verify-path parity.
758
806
  // `generateFindings()` applies `canSourceReachSink` (type coverage) + the
@@ -766,7 +814,7 @@ verifyAccounting) {
766
814
  // gap #258 left open (path_traversal's ceiling is `high`, so the rule cap alone
767
815
  // still reads C+H). Computed once and shared with the no-verification branch.
768
816
  const effectiveDfg = dfg ?? { defs: [], uses: [], chains: [] };
769
- const detectorFindings = generateFindings(mergedSources, mergedSinks, effectiveDfg, filePath);
817
+ const detectorFindings = generateFindings(mergedSources, mergedSinks, effectiveDfg, filePath, sourceCode, taintContext?.language, taintContext?.sanitizers ?? [], (types ?? []));
770
818
  const detectorSevRank = { critical: 4, high: 3, medium: 2, low: 1 };
771
819
  const detectorSinkSeverity = new Map();
772
820
  for (const df of detectorFindings) {
@@ -1033,6 +1081,32 @@ verifyAccounting) {
1033
1081
  const srcs = finding.evidence?.sources ?? [{ line: finding.source.line }];
1034
1082
  return srcs.some((s) => failedPairKeys.has(`${s.line}:${finding.sink.line}`));
1035
1083
  };
1084
+ // cognium-ai#471 — a static finding the verifier was asked about and did not
1085
+ // confirm. The batch prompt has the model OMIT every non-TRUE_POSITIVE pair
1086
+ // ("an omitted pair is counted as assessed-and-safe"), so when a file's
1087
+ // verifier answer contains no TRUE_POSITIVE this branch used to keep every
1088
+ // static finding as `UNVERIFIED` — the verifier's refutation was relabelled
1089
+ // "no verdict", which circle-pack maps to `llm_result.verdict: UNCERTAIN`.
1090
+ // That is how 81,842 skill-scan findings produced 0 FALSE_POSITIVE.
1091
+ //
1092
+ // A finding is refuted by omission only when the answer is unambiguous:
1093
+ // - the caller passed the submitted-pair set (legacy callers: no change);
1094
+ // - no pair was dropped by our fan-out budget, and none of the pairs the
1095
+ // model returned failed to map (a hallucinated line could be a TP we
1096
+ // cannot place);
1097
+ // - every contributing (source, sink) pair was submitted (a pair the
1098
+ // sampler never sent was never asked about);
1099
+ // - no contributing pair sits in a failed chunk (#564 keeps those open).
1100
+ const submittedPairKeys = new Set((verifyAccounting?.submittedPairs ?? []).map((p) => `${p.sourceLine}:${p.sinkLine}`));
1101
+ const omissionIsRefutation = submittedPairKeys.size > 0 &&
1102
+ (verifyAccounting?.pairsDroppedByBudget ?? 0) === 0 &&
1103
+ (verifyAccounting?.pairsUnmapped ?? 0) === 0;
1104
+ const refutedByOmission = (finding) => {
1105
+ if (!omissionIsRefutation || hitsFailedPair(finding))
1106
+ return false;
1107
+ const srcs = finding.evidence?.sources ?? [{ line: finding.source.line }];
1108
+ return srcs.length > 0 && srcs.every((s) => submittedPairKeys.has(`${s.line}:${finding.sink.line}`));
1109
+ };
1036
1110
  const verifiedBranch = verificationEnabled && verificationResults.length > 0;
1037
1111
  const verifiedLocs = new Set(vulnerabilities.map((v) => `${v.sink?.line}:${v.type}`));
1038
1112
  if (!verifiedBranch || failedPairKeys.size > 0) {
@@ -1151,7 +1225,25 @@ verifyAccounting) {
1151
1225
  mode: 'attempted',
1152
1226
  verification_error: verificationError ?? 'verification_chunk_failed',
1153
1227
  }
1154
- : { verdict: 'UNVERIFIED', attempted: true, llm_verified: false, mode: 'attempted' }, // #331 three-state
1228
+ : refutedByOmission(finding)
1229
+ ? {
1230
+ // cognium-ai#471 — the verifier answered, and its answer
1231
+ // (per the batch prompt's omit-non-TP contract) is "not a
1232
+ // true positive". Kept, not dropped: dropping would change
1233
+ // recall on every LLM scan, which is a product decision;
1234
+ // labelling it lets consumers see the refutation.
1235
+ verdict: 'FALSE_POSITIVE',
1236
+ attempted: true,
1237
+ llm_verified: false,
1238
+ mode: 'llm',
1239
+ refuted_by: 'omission',
1240
+ reasoning_source: 'template',
1241
+ reasoning: 'Submitted to the LLM verifier, which did not confirm it: the batch verifier ' +
1242
+ 'omits every pair it does not judge a TRUE_POSITIVE (cognium-ai#471).',
1243
+ confidence: 0,
1244
+ exploitability: 'none',
1245
+ }
1246
+ : { verdict: 'UNVERIFIED', attempted: true, llm_verified: false, mode: 'attempted' }, // #331 three-state
1155
1247
  // #26: surface the provenance label so downstream consumers
1156
1248
  // (cognium-ai CLI output, SARIF emitter) can distinguish
1157
1249
  // static-only / LLM-only / mixed findings without re-deriving.
@@ -1177,6 +1269,9 @@ verifyAccounting) {
1177
1269
  // cognium-ai#564 — failed-chunk pairs, and the findings kept for them.
1178
1270
  pairsFailed: verifyAccounting?.pairsFailed ?? 0,
1179
1271
  findingsUnverified: vulnerabilities.filter((v) => v.verification?.verification_error !== undefined).length,
1272
+ // cognium-ai#471 — findings kept with `verdict: 'FALSE_POSITIVE'`
1273
+ // because the verifier omitted (refuted) every pair behind them.
1274
+ findingsRefutedByOmission: vulnerabilities.filter((v) => v.verification?.refuted_by === 'omission').length,
1180
1275
  ...(verificationError !== undefined ? { verificationError } : {}),
1181
1276
  totalTimeMs: patternMatchTimeMs + enrichTimeMs + verifyTimeMs,
1182
1277
  },
@@ -1255,7 +1350,7 @@ export async function analyzeFile(filePath, sourceCode, options) {
1255
1350
  enrichResult = await runEnrich(filePath, sourceCode, patternResult.types, patternResult.imports, patternResult.patternSources, patternResult.patternSinks, enableEnrichment, language, llmConfig);
1256
1351
  }
1257
1352
  const mergeResult = runMerge(patternResult.patternSources, patternResult.patternSinks, enrichResult.enrichmentResult, confidenceThreshold, sourceCode, filePath, patternResult.types, // cognium-ai#128 gate
1258
- language);
1353
+ language, patternResult.calls);
1259
1354
  // L2 — same pattern as enrich: wrap when the operator has opted in
1260
1355
  // via CP_VERIFY_TIMEOUT_MS.
1261
1356
  let verifyResult;
@@ -1278,7 +1373,10 @@ export async function analyzeFile(filePath, sourceCode, options) {
1278
1373
  pairsFailed: verifyResult.pairsFailed,
1279
1374
  unverifiedPairs: verifyResult.unverifiedPairs,
1280
1375
  verificationError: verifyResult.verificationError,
1281
- });
1376
+ submittedPairs: verifyResult.submittedPairs, // cognium-ai#471
1377
+ },
1378
+ // cognium-ai#460 — full generateFindings argument list.
1379
+ { language, sanitizers: patternResult.sanitizers });
1282
1380
  // cognium-ai#154 (L5c) — surface the cluster vector extracted while the
1283
1381
  // IR was live in `runPatternMatch`. Keeps `runReport`'s signature
1284
1382
  // untouched (its job is source×sink → vuln synthesis; the vector is
@@ -1313,7 +1411,7 @@ export async function* analyzeFileStream(filePath, sourceCode, options) {
1313
1411
  yield { step: 'enrich', status: 'completed', data: enrichResult };
1314
1412
  yield { step: 'merge', status: 'running' };
1315
1413
  const mergeResult = runMerge(patternResult.patternSources, patternResult.patternSinks, enrichResult.enrichmentResult, 0.7, sourceCode, filePath, patternResult.types, // cognium-ai#128 gate
1316
- language);
1414
+ language, patternResult.calls);
1317
1415
  yield { step: 'merge', status: 'completed', data: mergeResult };
1318
1416
  yield { step: 'verify', status: 'running' };
1319
1417
  const verifyResult = await runVerify(filePath, sourceCode, mergeResult.mergedSources, mergeResult.mergedSinks, patternResult.types, enableVerification, maxVerificationTargets, verificationStrategy, llmConfig, language, contextProfile);
@@ -1331,7 +1429,10 @@ export async function* analyzeFileStream(filePath, sourceCode, options) {
1331
1429
  pairsFailed: verifyResult.pairsFailed,
1332
1430
  unverifiedPairs: verifyResult.unverifiedPairs,
1333
1431
  verificationError: verifyResult.verificationError,
1334
- });
1432
+ submittedPairs: verifyResult.submittedPairs, // cognium-ai#471
1433
+ },
1434
+ // cognium-ai#460 — full generateFindings argument list.
1435
+ { language, sanitizers: patternResult.sanitizers });
1335
1436
  yield { step: 'report', status: 'completed', data: reportResult };
1336
1437
  }
1337
1438
  // Legacy export for compatibility