@clear-capabilities/agentic-security-scanner 0.184.0 → 0.186.1

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 (228) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/agentic-security.js +111 -1
  3. package/dist/1128.index.js +912 -0
  4. package/dist/1665.index.js +828 -0
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/5752.index.js +466 -0
  8. package/dist/7405.index.js +1 -1
  9. package/dist/8158.index.js +1713 -0
  10. package/dist/8798.index.js +1 -1
  11. package/dist/9382.index.js +2 -2
  12. package/dist/9579.index.js +1 -1
  13. package/dist/agentic-security.mjs +29 -29
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/dist/{dad58d1113a12c23a142.json → ca62ee93079be4c24eee.json} +29 -12
  16. package/dist/frontend/src/shell.js +12 -2
  17. package/package.json +29 -12
  18. package/src/dataflow/CLAUDE.md +46 -1
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/backward.js +42 -7
  21. package/src/dataflow/cross-service-taint.js +83 -1
  22. package/src/dataflow/demand-query.js +1168 -0
  23. package/src/dataflow/dep-fingerprint.js +718 -0
  24. package/src/dataflow/engine.js +48 -6
  25. package/src/dataflow/exploit-prover.js +6 -2
  26. package/src/dataflow/ifds-precise.js +22 -0
  27. package/src/dataflow/incremental-replay.js +118 -0
  28. package/src/dataflow/incremental-session.js +257 -0
  29. package/src/dataflow/incremental.js +237 -35
  30. package/src/dataflow/index.js +25 -94
  31. package/src/dataflow/ir-cache.js +268 -0
  32. package/src/dataflow/privacy-taint.js +7 -0
  33. package/src/dataflow/summaries.js +72 -6
  34. package/src/dataflow/tabulation.js +80 -24
  35. package/src/engine.js +113 -8
  36. package/src/graph/CLAUDE.md +31 -0
  37. package/src/graph/adapters/artifact.js +81 -0
  38. package/src/graph/adapters/crossrepo.js +75 -0
  39. package/src/graph/adapters/dataflow.js +76 -0
  40. package/src/graph/adapters/provenance.js +92 -0
  41. package/src/graph/adapters/semantic.js +89 -0
  42. package/src/graph/crosswalk-builders.js +152 -0
  43. package/src/graph/crosswalk.js +146 -0
  44. package/src/graph/index.js +15 -0
  45. package/src/graph/observation.js +57 -0
  46. package/src/graph/refs.js +105 -0
  47. package/src/graph/registry.js +129 -0
  48. package/src/graph/relations.js +81 -0
  49. package/src/graph/scc.js +241 -0
  50. package/src/graph/view.js +254 -0
  51. package/src/ir/CLAUDE.md +4 -0
  52. package/src/ir/callgraph.js +36 -8
  53. package/src/ir/index.js +18 -4
  54. package/src/ir/parser-js.js +10 -0
  55. package/src/language/CLAUDE.md +13 -0
  56. package/src/language/nix-option-types.js +170 -0
  57. package/src/language/nix-script-taint.js +46 -3
  58. package/src/language/nix-secrets.js +20 -2
  59. package/src/lineage/CLAUDE.md +72 -8
  60. package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
  61. package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
  62. package/src/lineage/boundary-contract.js +273 -0
  63. package/src/lineage/completion.js +205 -0
  64. package/src/lineage/continuation.js +140 -0
  65. package/src/lineage/control-credit.js +313 -0
  66. package/src/lineage/correlation.js +199 -0
  67. package/src/lineage/coverage.js +8 -1
  68. package/src/lineage/cross-repo-link.js +22 -0
  69. package/src/lineage/cut-planner.js +548 -0
  70. package/src/lineage/driver.js +219 -66
  71. package/src/lineage/engine.js +166 -21
  72. package/src/lineage/export-json.js +5 -0
  73. package/src/lineage/graph-builder.js +84 -7
  74. package/src/lineage/haskell-view.js +59 -0
  75. package/src/lineage/index.js +3 -0
  76. package/src/lineage/nix-view.js +35 -0
  77. package/src/lineage/owned-build.js +77 -0
  78. package/src/lineage/owned-worker-entry.js +22 -0
  79. package/src/lineage/owned-worker.js +95 -0
  80. package/src/lineage/path-evidence.js +123 -0
  81. package/src/lineage/path-predicates.js +529 -0
  82. package/src/lineage/path-query.js +189 -20
  83. package/src/lineage/path-store.js +191 -17
  84. package/src/lineage/scc-solver.js +186 -0
  85. package/src/lineage/smt-orchestrator.js +174 -0
  86. package/src/lineage/smt-z3-backend.js +294 -0
  87. package/src/lineage/source-seeding.js +36 -2
  88. package/src/lineage/summaries.js +66 -4
  89. package/src/lineage/value-versions.js +130 -0
  90. package/src/lineage/witness-selection.js +95 -0
  91. package/src/pipeline/scan-health.js +18 -0
  92. package/src/posture/CLAUDE.md +17 -0
  93. package/src/posture/business-logic.js +16 -7
  94. package/src/posture/dag-commands.js +442 -0
  95. package/src/posture/evidence-bundle.js +1 -1
  96. package/src/posture/finding-defaults.js +19 -0
  97. package/src/posture/git-history.js +91 -1
  98. package/src/posture/witness-bundle.js +573 -0
  99. package/src/posture/witness-runtime.js +276 -0
  100. package/src/report/index.js +2 -0
  101. package/src/sast/CLAUDE.md +2 -0
  102. package/src/sast/_call-args.js +70 -0
  103. package/src/sast/init-scope.js +87 -0
  104. package/src/sast/listen-bind-address.js +49 -0
  105. package/src/sast/redirect-target.js +106 -0
  106. package/src/sast/reflected-input.js +76 -0
  107. package/src/sast/send-file-root.js +27 -0
  108. package/src/sast/sensitive-data-logging.js +38 -1
  109. package/dist/119.index.js +0 -197
  110. package/dist/128.index.js +0 -129
  111. package/dist/1293.index.js +0 -5689
  112. package/dist/1336.index.js +0 -16182
  113. package/dist/1351.index.js +0 -133
  114. package/dist/1387.index.js +0 -271
  115. package/dist/1437.index.js +0 -23
  116. package/dist/1445.index.js +0 -110
  117. package/dist/1505.index.js +0 -151
  118. package/dist/1557.index.js +0 -293
  119. package/dist/1621.index.js +0 -311
  120. package/dist/1751.index.js +0 -160
  121. package/dist/1765.index.js +0 -727
  122. package/dist/1861.index.js +0 -690
  123. package/dist/1906.index.js +0 -433
  124. package/dist/208.index.js +0 -1631
  125. package/dist/2223.index.js +0 -159
  126. package/dist/2238.index.js +0 -295
  127. package/dist/2530.index.js +0 -152
  128. package/dist/2565.index.js +0 -116
  129. package/dist/2574.index.js +0 -127
  130. package/dist/2620.index.js +0 -345
  131. package/dist/2773.index.js +0 -98
  132. package/dist/2775.index.js +0 -32
  133. package/dist/2966.index.js +0 -171
  134. package/dist/2972.index.js +0 -87
  135. package/dist/2991.index.js +0 -131
  136. package/dist/3063.index.js +0 -833
  137. package/dist/3075.index.js +0 -227
  138. package/dist/3121.index.js +0 -297
  139. package/dist/3314.index.js +0 -263
  140. package/dist/3480.index.js +0 -1822
  141. package/dist/3481.index.js +0 -24
  142. package/dist/3506.index.js +0 -268
  143. package/dist/3533.index.js +0 -591
  144. package/dist/3653.index.js +0 -450
  145. package/dist/3740.index.js +0 -722
  146. package/dist/3822.index.js +0 -131
  147. package/dist/3868.index.js +0 -292
  148. package/dist/3918.index.js +0 -192
  149. package/dist/4090.index.js +0 -288
  150. package/dist/4113.index.js +0 -1286
  151. package/dist/4121.index.js +0 -1190
  152. package/dist/4142.index.js +0 -690
  153. package/dist/4168.index.js +0 -509
  154. package/dist/4281.index.js +0 -993
  155. package/dist/4336.index.js +0 -436
  156. package/dist/4387.index.js +0 -532
  157. package/dist/4413.index.js +0 -302
  158. package/dist/4476.index.js +0 -4510
  159. package/dist/4491.index.js +0 -133
  160. package/dist/4538.index.js +0 -185
  161. package/dist/4684.index.js +0 -319
  162. package/dist/4740.index.js +0 -165
  163. package/dist/4780.index.js +0 -209
  164. package/dist/4802.index.js +0 -225
  165. package/dist/4812.index.js +0 -379
  166. package/dist/4835.index.js +0 -126
  167. package/dist/494.index.js +0 -226
  168. package/dist/4991.index.js +0 -549
  169. package/dist/5199.index.js +0 -305
  170. package/dist/5216.index.js +0 -109
  171. package/dist/5439.index.js +0 -301
  172. package/dist/5443.index.js +0 -97
  173. package/dist/5496.index.js +0 -377
  174. package/dist/5738.index.js +0 -240
  175. package/dist/5801.index.js +0 -588
  176. package/dist/5817.index.js +0 -714
  177. package/dist/5858.index.js +0 -223
  178. package/dist/5933.index.js +0 -143
  179. package/dist/601.index.js +0 -23
  180. package/dist/6173.index.js +0 -273
  181. package/dist/6291.index.js +0 -1871
  182. package/dist/6312.index.js +0 -666
  183. package/dist/634.index.js +0 -16182
  184. package/dist/644.index.js +0 -87
  185. package/dist/6508.index.js +0 -283
  186. package/dist/6560.index.js +0 -645
  187. package/dist/6576.index.js +0 -422
  188. package/dist/6579.index.js +0 -271
  189. package/dist/6655.index.js +0 -3221
  190. package/dist/6745.index.js +0 -305
  191. package/dist/6818.index.js +0 -21642
  192. package/dist/6932.index.js +0 -1190
  193. package/dist/6972.index.js +0 -167
  194. package/dist/6995.index.js +0 -163
  195. package/dist/7249.index.js +0 -191
  196. package/dist/7372.index.js +0 -1197
  197. package/dist/7546.index.js +0 -541
  198. package/dist/76.index.js +0 -448
  199. package/dist/7879.index.js +0 -163
  200. package/dist/8025.index.js +0 -130
  201. package/dist/8054.index.js +0 -248
  202. package/dist/808.index.js +0 -1330
  203. package/dist/8097.index.js +0 -201
  204. package/dist/8126.index.js +0 -191
  205. package/dist/8178.index.js +0 -78
  206. package/dist/8382.index.js +0 -93
  207. package/dist/8408.index.js +0 -261
  208. package/dist/8540.index.js +0 -174
  209. package/dist/8635.index.js +0 -802
  210. package/dist/8688.index.js +0 -410
  211. package/dist/8699.index.js +0 -779
  212. package/dist/8891.index.js +0 -457
  213. package/dist/8916.index.js +0 -885
  214. package/dist/9146.index.js +0 -170
  215. package/dist/9168.index.js +0 -841
  216. package/dist/9292.index.js +0 -369
  217. package/dist/9422.index.js +0 -866
  218. package/dist/9440.index.js +0 -453
  219. package/dist/9479.index.js +0 -152
  220. package/dist/9481.index.js +0 -220
  221. package/dist/9539.index.js +0 -100
  222. package/dist/9547.index.js +0 -103
  223. package/dist/9692.index.js +0 -90
  224. package/dist/9800.index.js +0 -462
  225. package/dist/9899.index.js +0 -138
  226. package/dist/9981.index.js +0 -206
  227. package/dist/999.index.js +0 -117
  228. package/dist/9999.index.js +0 -1698
@@ -153,9 +153,13 @@ export function canonicalize(set) {
153
153
  const sorted = [...set].sort((a, b) => a.length - b.length || (a < b ? -1 : 1));
154
154
  const out = new Set();
155
155
  for (const path of sorted) {
156
+ // A kept path covers `path` exactly when it is one of the dot-delimited prefixes of it, so look those up
157
+ // (a handful per path) instead of testing every kept path (the set can hold hundreds in a large function).
156
158
  let covered = false;
157
- for (const existing of out) {
158
- if (pathIsCoveredByPrefix(path, existing)) { covered = true; break; }
159
+ if (typeof path === 'string') {
160
+ for (let i = path.indexOf('.'); i >= 0; i = path.indexOf('.', i + 1)) {
161
+ if (out.has(path.slice(0, i))) { covered = true; break; }
162
+ }
159
163
  }
160
164
  if (!covered) out.add(path);
161
165
  }
@@ -176,6 +180,12 @@ export function hashSet(set) {
176
180
  export function setsEqual(a, b) {
177
181
  if (a === b) return true;
178
182
  if (!a || !b) return false;
183
+ // Identical members are equal under any canonicalization; only a difference needs the canonical forms.
184
+ if (a.size === b.size) {
185
+ let same = true;
186
+ for (const x of a) if (!b.has(x)) { same = false; break; }
187
+ if (same) return true;
188
+ }
179
189
  const ca = canonicalize(a), cb = canonicalize(b);
180
190
  if (ca.size !== cb.size) return false;
181
191
  for (const x of ca) if (!cb.has(x)) return false;
@@ -49,6 +49,15 @@ function _reverseEdges(cfg) {
49
49
  return rev;
50
50
  }
51
51
 
52
+ /**
53
+ * CFG reverse-edge index (node id -> predecessor ids), exported so the demand
54
+ * selector (demand-query.js) and this slicer share one definition of the
55
+ * incoming-edge relation instead of each deriving its own.
56
+ */
57
+ export function reverseEdgesOf(cfg) {
58
+ return _reverseEdges(cfg);
59
+ }
60
+
52
61
  /**
53
62
  * Build a backward slice from a finding's sink site to its source(s).
54
63
  *
@@ -75,10 +84,28 @@ function _reverseEdges(cfg) {
75
84
  * ]
76
85
  */
77
86
  export function sliceBackward(fn, sinkNode, sinkArgPath) {
87
+ return sliceBackwardWithCompletion(fn, sinkNode, sinkArgPath).slice;
88
+ }
89
+
90
+ /**
91
+ * Same slice, plus a completion record (DAG-07 R07.1): { slice, completion }.
92
+ * A slice that stopped at SLICE_BUDGET_NODES is a prefix of the real
93
+ * backward closure; the record carries how many queued steps were left, so a
94
+ * consumer (the SMT infeasibility check among them) can tell "no source
95
+ * found" from "stopped looking". `sliceBackward` itself still returns the
96
+ * bare array for existing callers, with the same record attached as a
97
+ * non-enumerable `completion` property.
98
+ */
99
+ export function sliceBackwardWithCompletion(fn, sinkNode, sinkArgPath) {
78
100
  const out = [];
79
- if (!fn || !sinkNode) return out;
101
+ const completion = { status: 'complete', reasons: [], unexploredFrontierCount: 0, visited: 0, limits: { maxNodes: SLICE_BUDGET_NODES } };
102
+ const finish = () => {
103
+ Object.defineProperty(out, 'completion', { value: completion, enumerable: false, configurable: true });
104
+ return { slice: out, completion };
105
+ };
106
+ if (!fn || !sinkNode) { completion.status = 'unavailable'; completion.reasons = ['no-function-or-sink']; return finish(); }
80
107
  const cfg = fn.cfg;
81
- if (!cfg || !cfg.nodes) return out;
108
+ if (!cfg || !cfg.nodes) { completion.status = 'unavailable'; completion.reasons = ['no-cfg']; return finish(); }
82
109
  const rev = _reverseEdges(cfg);
83
110
 
84
111
  // Map node-id to itself lookup for nodes in this CFG.
@@ -90,7 +117,7 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
90
117
  for (const id of Object.keys(nodes)) {
91
118
  if (nodes[id] === sinkNode) { sinkNid = id; break; }
92
119
  }
93
- if (!sinkNid) return out;
120
+ if (!sinkNid) { completion.status = 'unavailable'; completion.reasons = ['sink-not-in-cfg']; return finish(); }
94
121
 
95
122
  out.push({
96
123
  line: sinkNode.line || 0,
@@ -104,7 +131,12 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
104
131
  let visitedCount = 0;
105
132
 
106
133
  while (work.length) {
107
- if (++visitedCount > SLICE_BUDGET_NODES) break;
134
+ if (++visitedCount > SLICE_BUDGET_NODES) {
135
+ completion.status = 'partial';
136
+ completion.reasons = ['slice-node-budget'];
137
+ completion.unexploredFrontierCount = work.length;
138
+ break;
139
+ }
108
140
  const { nid, queryPath } = work.shift();
109
141
  const key = `${nid}::${queryPath}`;
110
142
  if (visited.has(key)) continue;
@@ -206,8 +238,10 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
206
238
  }
207
239
  }
208
240
 
241
+ completion.visited = Math.min(visitedCount, SLICE_BUDGET_NODES);
209
242
  // Reverse so the trace reads source-first.
210
- return out.reverse();
243
+ out.reverse();
244
+ return finish();
211
245
  }
212
246
 
213
247
  /**
@@ -233,7 +267,7 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
233
267
  if (callGraph && callGraph.functions) {
234
268
  for (const fn of callGraph.functions.values()) fnByQid.set(fn.qid, fn);
235
269
  }
236
- let annotated = 0, skipped = 0, exhausted = false;
270
+ let annotated = 0, skipped = 0, exhausted = false, partialSlices = 0;
237
271
  for (const f of findings) {
238
272
  if (Date.now() > deadline) { exhausted = true; skipped++; continue; }
239
273
  if (!f || !f._funcQid) { skipped++; continue; }
@@ -278,6 +312,7 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
278
312
  if (!slice) slice = candidateSlice;
279
313
  }
280
314
  }
315
+ if (slice && slice.completion && slice.completion.status !== 'complete') partialSlices++;
281
316
  if (slice && slice.length) {
282
317
  f.backwardSlice = slice;
283
318
  f.pathSteps = (f.pathSteps || []).concat(slice.map(s => ({
@@ -291,7 +326,7 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
291
326
  }
292
327
  }
293
328
  Object.defineProperty(findings, '_annotateBackwardSlicesStats', {
294
- value: { annotated, skipped, exhausted, budgetMs },
329
+ value: { annotated, skipped, exhausted, budgetMs, partialSlices },
295
330
  enumerable: false,
296
331
  });
297
332
  return findings;
@@ -63,6 +63,8 @@ import * as path from 'node:path';
63
63
  import * as yaml from '../util/yaml.js';
64
64
 
65
65
  import { statePath } from '../posture/state-dir.js';
66
+ import { runTaintEngine } from './engine.js';
67
+ import { bindBoundaryContracts, boundaryAttribution, BOUNDARY_CONTRACTS_FILENAME, validateBoundaryContract } from '../lineage/boundary-contract.js';
66
68
  const SERVICES_FILE_NAMES = ['services.yml', 'services.yaml'];
67
69
 
68
70
  export function loadServiceGraph(scanRoot) {
@@ -342,4 +344,84 @@ export function runCrossServiceTaint(scanRoot, findings) {
342
344
  return annotateCrossServiceFindings(findings, graph, current);
343
345
  }
344
346
 
345
- export const _internals = { _normalizeGraph, SERVICES_FILE_NAMES, _pathTemplateMatches, _escapeRegex };
347
+ // ─── DAG-08: boundary-contract consumer seeding ──────────────────────────────
348
+ //
349
+ // The SCOPE NOTE above stays true of the `consumes` annotation pass: it never invents a source. This section is the separate,
350
+ // explicit mechanism that does. A versioned boundary contract (lineage/boundary-contract.js) that BINDS to a parser-resolved
351
+ // consumer function and parameter seeds the declared, mapped fields of that parameter as a MODELED untrusted input. The seed is
352
+ // handed to the engine through `fn.boundarySeeds` (read by engine.js's `_unionAnnotationTaint`) for the duration of one run and
353
+ // always removed afterwards, so the shared call graph is never left seeded.
354
+ //
355
+ // A finding produced this way carries `boundary: {..., modeledInput: true, observed: false, exploited: false}`. A declaration
356
+ // establishes where taint is assumed to enter; it never establishes that a value was seen or that the sink is exploitable. A
357
+ // contract that does not bind produces a coverage gap and NO finding.
358
+
359
+ const DEFAULT_MAX_BOUNDARY_RUNS = 64;
360
+
361
+ function loadBoundaryContracts(scanRoot) {
362
+ if (!scanRoot) return { contracts: [], errors: [] };
363
+ const fp = statePath(scanRoot, BOUNDARY_CONTRACTS_FILENAME);
364
+ if (!fs.existsSync(fp)) return { contracts: [], errors: [] };
365
+ try {
366
+ const doc = JSON.parse(fs.readFileSync(fp, 'utf8'));
367
+ const list = Array.isArray(doc) ? doc : Array.isArray(doc && doc.contracts) ? doc.contracts : [];
368
+ return { contracts: list, errors: [] };
369
+ } catch (e) {
370
+ return { contracts: [], errors: [`Failed to parse ${fp}: ${e.message}`] };
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Run the taint engine with each bound contract's consumer fields seeded, one seed at a time so a finding is attributed to the
376
+ * exact consumer field that reached it (a sibling field the contract does not map is never seeded and so never attributed).
377
+ * When `contracts` is null/undefined they are read from `<scanRoot>/.agentic-security/boundary-contracts.json` (opts.scanRoot).
378
+ * @returns {{findings: object[], bindings: object[], gaps: object[], rejected: object[], coverage: object, loadErrors: string[]}}
379
+ */
380
+ export function runBoundaryTaint(perFileIR, callGraph, contracts, opts = {}) {
381
+ let loadErrors = [];
382
+ if (contracts === null || contracts === undefined) { const l = loadBoundaryContracts(opts.scanRoot); contracts = l.contracts; loadErrors = l.errors; }
383
+ const maxRuns = Number.isFinite(opts.maxRuns) ? opts.maxRuns : DEFAULT_MAX_BOUNDARY_RUNS;
384
+ const { bindings, gaps, rejected } = bindBoundaryContracts(contracts, callGraph);
385
+ const fnByQid = new Map();
386
+ for (const f of callGraph.functions.values()) fnByQid.set(f.qid, f);
387
+ const baseline = new Set(runTaintEngine(perFileIR, callGraph, opts.engine || {}).map((f) => f.id));
388
+ const found = new Map();
389
+ let runs = 0; let truncated = false;
390
+ for (const b of bindings) {
391
+ const fn = fnByQid.get(b.qid);
392
+ for (const seed of b.seeds) {
393
+ if (runs >= maxRuns) { truncated = true; break; }
394
+ runs++;
395
+ const prior = fn.boundarySeeds;
396
+ fn.boundarySeeds = [seed];
397
+ let out = [];
398
+ try { out = runTaintEngine(perFileIR, callGraph, opts.engine || {}); } finally { if (prior === undefined) delete fn.boundarySeeds; else fn.boundarySeeds = prior; }
399
+ const mapping = b.mappedFields.find((m) => seed.endsWith(`.${m.to}`) || seed === m.to) || null;
400
+ for (const f of out) {
401
+ if (baseline.has(f.id)) continue;
402
+ let rec = found.get(f.id);
403
+ if (!rec) { rec = { finding: { ...f }, attributions: [] }; found.set(f.id, rec); }
404
+ rec.attributions.push({ binding: b, seed, mapping });
405
+ }
406
+ }
407
+ if (truncated) break;
408
+ }
409
+ const findings = [...found.values()].map(({ finding, attributions }) => {
410
+ const first = attributions[0];
411
+ finding.boundary = {
412
+ ...boundaryAttribution(first.binding),
413
+ fields: attributions.map((a) => ({ seed: a.seed, producerField: a.mapping && a.mapping.from, consumerField: a.mapping && a.mapping.to })),
414
+ discoveredBy: 'boundary-contract-seed',
415
+ };
416
+ finding.boundarySeeded = true;
417
+ return finding;
418
+ }).sort((a, b) => (a.id < b.id ? -1 : 1));
419
+ const allGaps = [...gaps];
420
+ if (truncated) allGaps.push({ kind: 'budget', reason: 'engine-run-budget', detail: `stopped after ${maxRuns} seeded runs; remaining seeds were not analyzed` });
421
+ return {
422
+ findings, bindings, gaps: allGaps, rejected, loadErrors,
423
+ coverage: { contracts: (contracts || []).length, bound: bindings.length, unbound: gaps.filter((g) => g.side === 'consumer').length, rejected: rejected.length, engineRuns: runs, truncated },
424
+ };
425
+ }
426
+
427
+ export const _internals = { _normalizeGraph, SERVICES_FILE_NAMES, _pathTemplateMatches, _escapeRegex, validateBoundaryContract };