tryassay 0.34.0 → 0.35.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 (131) hide show
  1. package/dist/bayesian/__tests__/bas-calculator.test.d.ts +1 -0
  2. package/dist/bayesian/__tests__/bas-calculator.test.js +63 -0
  3. package/dist/bayesian/__tests__/bas-calculator.test.js.map +1 -0
  4. package/dist/bayesian/__tests__/structural-entropy.test.d.ts +1 -0
  5. package/dist/bayesian/__tests__/structural-entropy.test.js +21 -0
  6. package/dist/bayesian/__tests__/structural-entropy.test.js.map +1 -0
  7. package/dist/bayesian/bas-calculator.d.ts +41 -0
  8. package/dist/bayesian/bas-calculator.js +198 -0
  9. package/dist/bayesian/bas-calculator.js.map +1 -0
  10. package/dist/bayesian/index.d.ts +3 -0
  11. package/dist/bayesian/index.js +3 -0
  12. package/dist/bayesian/index.js.map +1 -0
  13. package/dist/bayesian/structural-entropy.d.ts +12 -0
  14. package/dist/bayesian/structural-entropy.js +37 -0
  15. package/dist/bayesian/structural-entropy.js.map +1 -0
  16. package/dist/bayesian/types.d.ts +37 -0
  17. package/dist/bayesian/types.js +6 -0
  18. package/dist/bayesian/types.js.map +1 -0
  19. package/dist/cli.js +28 -0
  20. package/dist/cli.js.map +1 -1
  21. package/dist/commands/__tests__/assess-formal.test.d.ts +1 -0
  22. package/dist/commands/__tests__/assess-formal.test.js +72 -0
  23. package/dist/commands/__tests__/assess-formal.test.js.map +1 -0
  24. package/dist/commands/activate.d.ts +1 -0
  25. package/dist/commands/activate.js +48 -0
  26. package/dist/commands/activate.js.map +1 -0
  27. package/dist/commands/assess.js +100 -5
  28. package/dist/commands/assess.js.map +1 -1
  29. package/dist/commands/bas-score.d.ts +13 -0
  30. package/dist/commands/bas-score.js +310 -0
  31. package/dist/commands/bas-score.js.map +1 -0
  32. package/dist/commands/bounty-watch.js.map +1 -1
  33. package/dist/commands/hunt.js +32 -0
  34. package/dist/commands/hunt.js.map +1 -1
  35. package/dist/commands/runtime.js +11 -10
  36. package/dist/commands/runtime.js.map +1 -1
  37. package/dist/commands/stream-verify.d.ts +16 -0
  38. package/dist/commands/stream-verify.js +228 -0
  39. package/dist/commands/stream-verify.js.map +1 -0
  40. package/dist/commands/watch.js.map +1 -1
  41. package/dist/hunt/__tests__/deep-dive.test.js.map +1 -1
  42. package/dist/hunt/__tests__/e2e.test.js.map +1 -1
  43. package/dist/hunt/__tests__/finding-to-template.test.js +10 -1
  44. package/dist/hunt/__tests__/finding-to-template.test.js.map +1 -1
  45. package/dist/hunt/__tests__/orchestrator.test.js.map +1 -1
  46. package/dist/hunt/__tests__/templates.test.js +2 -2
  47. package/dist/hunt/__tests__/triage.test.js.map +1 -1
  48. package/dist/hunt/deep-dive.js +7 -7
  49. package/dist/hunt/deep-dive.js.map +1 -1
  50. package/dist/hunt/parse-utils.d.ts +1 -1
  51. package/dist/hunt/state.js.map +1 -1
  52. package/dist/hunt/templates/injection.js +1 -1
  53. package/dist/hunt/templates/injection.js.map +1 -1
  54. package/dist/hunt/triage.js +5 -5
  55. package/dist/hunt/triage.js.map +1 -1
  56. package/dist/lib/__tests__/arithmetic-quick-test.js +10 -9
  57. package/dist/lib/__tests__/arithmetic-quick-test.js.map +1 -1
  58. package/dist/lib/__tests__/arithmetic-real-llm-test.js +8 -8
  59. package/dist/lib/__tests__/arithmetic-real-llm-test.js.map +1 -1
  60. package/dist/lib/__tests__/formal-verifier-behavioral.test.d.ts +18 -0
  61. package/dist/lib/__tests__/formal-verifier-behavioral.test.js +576 -0
  62. package/dist/lib/__tests__/formal-verifier-behavioral.test.js.map +1 -0
  63. package/dist/lib/__tests__/formal-verifier-claimless-async.test.d.ts +1 -0
  64. package/dist/lib/__tests__/formal-verifier-claimless-async.test.js +154 -0
  65. package/dist/lib/__tests__/formal-verifier-claimless-async.test.js.map +1 -0
  66. package/dist/lib/__tests__/formal-verifier-claimless-quality.test.d.ts +1 -0
  67. package/dist/lib/__tests__/formal-verifier-claimless-quality.test.js +121 -0
  68. package/dist/lib/__tests__/formal-verifier-claimless-quality.test.js.map +1 -0
  69. package/dist/lib/__tests__/formal-verifier-claimless-realworld.test.d.ts +1 -0
  70. package/dist/lib/__tests__/formal-verifier-claimless-realworld.test.js +119 -0
  71. package/dist/lib/__tests__/formal-verifier-claimless-realworld.test.js.map +1 -0
  72. package/dist/lib/__tests__/formal-verifier-claimless.test.d.ts +1 -0
  73. package/dist/lib/__tests__/formal-verifier-claimless.test.js +667 -0
  74. package/dist/lib/__tests__/formal-verifier-claimless.test.js.map +1 -0
  75. package/dist/lib/__tests__/mr-gsm8k-benchmark.js +6 -6
  76. package/dist/lib/__tests__/mr-gsm8k-benchmark.js.map +1 -1
  77. package/dist/lib/__tests__/pr-harvester.test.js.map +1 -1
  78. package/dist/lib/assessment-reporter.d.ts +1 -1
  79. package/dist/lib/assessment-reporter.js +2 -1
  80. package/dist/lib/assessment-reporter.js.map +1 -1
  81. package/dist/lib/chain-analyzer.d.ts +4 -3
  82. package/dist/lib/chain-analyzer.js.map +1 -1
  83. package/dist/lib/formal-verifier.d.ts +20 -1
  84. package/dist/lib/formal-verifier.js +1180 -23
  85. package/dist/lib/formal-verifier.js.map +1 -1
  86. package/dist/lib/issue-reporter.d.ts +2 -1
  87. package/dist/lib/issue-reporter.js.map +1 -1
  88. package/dist/lib/remediation-generator.js.map +1 -1
  89. package/dist/lib/report-generator.js.map +1 -1
  90. package/dist/lib/rule-harvester/ground-truth.js +13 -2
  91. package/dist/lib/rule-harvester/ground-truth.js.map +1 -1
  92. package/dist/lib/rule-harvester/scanner.d.ts +1 -1
  93. package/dist/lib/user-config.d.ts +1 -0
  94. package/dist/lib/user-config.js.map +1 -1
  95. package/dist/realtime/__tests__/entropy-detector.test.d.ts +1 -0
  96. package/dist/realtime/__tests__/entropy-detector.test.js +200 -0
  97. package/dist/realtime/__tests__/entropy-detector.test.js.map +1 -0
  98. package/dist/realtime/__tests__/entropy-live-demo.d.ts +1 -0
  99. package/dist/realtime/__tests__/entropy-live-demo.js +103 -0
  100. package/dist/realtime/__tests__/entropy-live-demo.js.map +1 -0
  101. package/dist/realtime/__tests__/entropy-live.d.ts +8 -0
  102. package/dist/realtime/__tests__/entropy-live.js +114 -0
  103. package/dist/realtime/__tests__/entropy-live.js.map +1 -0
  104. package/dist/realtime/__tests__/streaming-checks.test.js +3 -4
  105. package/dist/realtime/__tests__/streaming-checks.test.js.map +1 -1
  106. package/dist/realtime/entropy-detector.d.ts +143 -0
  107. package/dist/realtime/entropy-detector.js +504 -0
  108. package/dist/realtime/entropy-detector.js.map +1 -0
  109. package/dist/realtime/mcp-server.d.ts +7 -1
  110. package/dist/realtime/mcp-server.js +378 -2
  111. package/dist/realtime/mcp-server.js.map +1 -1
  112. package/dist/realtime/stream-interceptor.d.ts +28 -0
  113. package/dist/realtime/stream-interceptor.js +204 -0
  114. package/dist/realtime/stream-interceptor.js.map +1 -1
  115. package/dist/realtime/streaming-checks.js +28 -0
  116. package/dist/realtime/streaming-checks.js.map +1 -1
  117. package/dist/realtime/streaming-verifier.d.ts +45 -0
  118. package/dist/realtime/streaming-verifier.js +98 -5
  119. package/dist/realtime/streaming-verifier.js.map +1 -1
  120. package/dist/realtime/types.d.ts +56 -0
  121. package/dist/runtime/agents/research-agent.js +10 -1
  122. package/dist/runtime/agents/research-agent.js.map +1 -1
  123. package/dist/runtime/agents/test-agent.js +10 -7
  124. package/dist/runtime/agents/test-agent.js.map +1 -1
  125. package/dist/runtime/composition-verifier.js +13 -3
  126. package/dist/runtime/composition-verifier.js.map +1 -1
  127. package/dist/runtime/fs-helpers.js.map +1 -1
  128. package/dist/runtime/prompt-safety-analyzer.js.map +1 -1
  129. package/dist/sdk/verified-generate.js.map +1 -1
  130. package/dist/types.d.ts +14 -0
  131. package/package.json +3 -2
@@ -223,7 +223,7 @@ const PY_BUILTINS = new Set([
223
223
  'functools', 'itertools', 'typing', 'pathlib', 'unittest', 'pytest',
224
224
  ]);
225
225
  // ── Claim Classification ─────────────────────────────────────
226
- function classifyClaim(claim) {
226
+ export function classifyClaim(claim) {
227
227
  const text = `${claim.description} ${claim.assertion}`.toLowerCase();
228
228
  const originalText = `${claim.description} ${claim.assertion}`;
229
229
  const funcExistsMatch = originalText.match(/(?:function|method|class|constructor)\s+['"`]?(\w+)['"`]?\s+(?:exists?|is\s+defined|is\s+declared|should\s+exist)/i);
@@ -254,30 +254,43 @@ function classifyClaim(claim) {
254
254
  if (/(?:parameter|argument)\s+['"`]?(\w+)['"`]?\s+(?:is|of\s+type)/i.test(text)) {
255
255
  return { claim, formallyVerifiable: true, checkType: 'parameter_check' };
256
256
  }
257
- if (claim.category === 'error-handling' &&
258
- /(?:handles?\s+(?:errors?|exceptions?|failures?)|try[\s-]catch|error\s+handling|catches?\s+(?:errors?|exceptions?)|wraps?\s+in\s+try)/i.test(text)) {
257
+ if (/(?:handles?\s+(?:errors?|exceptions?|failures?)|try[\s-]catch|error\s+handling|catches?\s+(?:errors?|exceptions?)|wraps?\s+in\s+try)/i.test(text) ||
258
+ /(?:catch(?:es)?\s+rejected\s+promises?|network\s+failures?\s+(?:are\s+)?caught)/i.test(text) ||
259
+ /(?:graceful(?:ly)?\s+(?:handle|fail|degrade|recover))/i.test(text)) {
259
260
  return { claim, formallyVerifiable: true, checkType: 'error_handling' };
260
261
  }
261
262
  if (/(?:null|undefined|nil|none)\s+(?:check|guard|validation|handling|safety)/i.test(text) ||
262
263
  /(?:checks?\s+(?:for\s+)?(?:null|undefined|nil|none))|(?:handles?\s+(?:null|undefined|nil|none))/i.test(text) ||
263
- /(?:validates?\s+(?:that\s+)?(?:input|parameter|argument)\s+(?:is\s+not\s+)?(?:null|undefined|nil|none))/i.test(text)) {
264
+ /(?:validates?\s+(?:that\s+)?(?:input|parameter|argument)\s+(?:is\s+not\s+)?(?:null|undefined|nil|none))/i.test(text) ||
265
+ /(?:undefined|null)\s+(?:\w+\s+){0,3}(?:handled|checked|guarded|safe)/i.test(text) ||
266
+ /returns?\s+null\b/i.test(text)) {
264
267
  return { claim, formallyVerifiable: true, checkType: 'null_check' };
265
268
  }
266
- if (claim.category === 'type-safety' &&
267
- /(?:type\s+annot|typed\s+(?:as|return)|return\s+type|type-safe|strongly\s+typed)/i.test(text)) {
269
+ if (/(?:type\s+annot|typed\s+(?:as|return)|return\s+type|type-safe|strongly\s+typed)/i.test(text) ||
270
+ /(?:typescript\s+type|have\s+(?:typescript\s+)?type\s+annotations?)/i.test(text) ||
271
+ /(?:return\s+types?\s+(?:are\s+)?(?:explicitly\s+)?declared)/i.test(text)) {
268
272
  return { claim, formallyVerifiable: true, checkType: 'type_annotation' };
269
273
  }
270
- if (claim.category === 'security' &&
271
- /(?:sql\s+injection|parameterized\s+quer|prepared\s+statement|sql\s+sanitiz|query\s+parameteriz)/i.test(text)) {
274
+ if (/(?:sql\s+injection|parameterized\s+quer|prepared\s+statement|sql\s+sanitiz|query\s+parameteriz)/i.test(text)) {
272
275
  return { claim, formallyVerifiable: true, checkType: 'sql_parameterized' };
273
276
  }
274
- if (/(?:validates?\s+(?:user\s+)?input|input\s+validation|sanitizes?\s+input|validates?\s+(?:before|prior))/i.test(text)) {
277
+ if (/(?:validates?\s+(?:user\s+)?input|input\s+validation|sanitizes?\s+input|validates?\s+(?:before|prior))/i.test(text) ||
278
+ /(?:(?:email|phone|url|format|field|body|parameter)\s+(?:is\s+)?(?:validated?|sanitized?|checked))/i.test(text) ||
279
+ /(?:(?:validated?|sanitized?|checked)\s+(?:on\s+the\s+)?(?:server|backend|api))/i.test(text) ||
280
+ /(?:request\s+body\s+(?:fields?\s+)?(?:are\s+)?(?:validated?|sanitized?|checked))/i.test(text)) {
275
281
  return { claim, formallyVerifiable: true, checkType: 'input_validation' };
276
282
  }
283
+ // Undefined reference checks — must come before arithmetic to avoid "for calculations" matching arithmetic
284
+ if (/(?:references?|uses?|accesses?)\s+(?:variable|function|import|module|identifier)\s+['"`]?(\w+)['"`]?/i.test(text) ||
285
+ /(?:variable|function|import)\s+['"`]?(\w+)['"`]?\s+(?:is\s+)?(?:defined|declared|imported|available)/i.test(text)) {
286
+ return { claim, formallyVerifiable: true, checkType: 'undefined_reference' };
287
+ }
277
288
  // Arithmetic correctness checks
278
- if (/(?:calculates?|computes?|evaluates?\s+to|sums?|totals?|multiplies?|divides?|averages?|rounds?)/i.test(text) ||
289
+ if (/(?:calculates?|calculation|computes?|computation|evaluates?\s+to|sums?|totals?|multiplies?|divides?|averages?|rounds?)/i.test(text) ||
279
290
  /(?:returns?\s+(?:the\s+)?(?:sum|product|average|total|difference|ratio|percentage|remainder|quotient))/i.test(text) ||
280
- /(?:result\s+(?:is|equals?|should\s+be)\s+\d)/i.test(text)) {
291
+ /(?:result\s+(?:is|equals?|should\s+be)\s+\d)/i.test(text) ||
292
+ /(?:\d+\s*[+\-*/]\s*\d+\s*=\s*\d)/i.test(text) ||
293
+ /(?:percentage|percent|ratio)\s*:\s*\d/i.test(text)) {
281
294
  return { claim, formallyVerifiable: true, checkType: 'arithmetic_correctness' };
282
295
  }
283
296
  // API misuse checks
@@ -285,10 +298,100 @@ function classifyClaim(claim) {
285
298
  /(?:calls?\s+\w+\.\w+)|(?:uses?\s+\w+\.\w+\s*\()/i.test(originalText)) {
286
299
  return { claim, formallyVerifiable: true, checkType: 'api_misuse' };
287
300
  }
288
- // Undefined reference checks
289
- if (/(?:references?|uses?|accesses?)\s+(?:variable|function|import|module|identifier)\s+['"`]?(\w+)['"`]?/i.test(text) ||
290
- /(?:variable|function|import)\s+['"`]?(\w+)['"`]?\s+(?:is\s+)?(?:defined|declared|imported|available)/i.test(text)) {
291
- return { claim, formallyVerifiable: true, checkType: 'undefined_reference' };
301
+ // Capacity/limit checks — "supports up to N", "limit of N", "maximum N"
302
+ if (/(?:supports?\s+up\s+to|limit\s+(?:of\s+)?\d|maximum\s+(?:of\s+)?\d|up\s+to\s+\d|capacity\s+(?:of\s+)?\d)/i.test(text) ||
303
+ /(?:allows?\s+up\s+to|can\s+(?:hold|handle|store)\s+up\s+to)\s+\d/i.test(text)) {
304
+ return { claim, formallyVerifiable: true, checkType: 'capacity_limit' };
305
+ }
306
+ // Security control checks — encryption, authentication, authorization
307
+ if (/(?:encrypt|decrypt|hashing|hash(?:ed|es|ing)?|ssl|tls|https|cipher)/i.test(text) ||
308
+ /(?:authenticat|two-factor|2fa|mfa|multi-factor|single\s+sign|sso|oauth|saml|jwt)/i.test(text) ||
309
+ /(?:authoriz|access\s+control|permission|role-based|rbac|acl)/i.test(text) ||
310
+ /(?:api\s+key|token|credential|secret|certificate)/i.test(text)) {
311
+ return { claim, formallyVerifiable: true, checkType: 'security_control' };
312
+ }
313
+ // Compliance checks — regulatory compliance references
314
+ if (/(?:compl(?:y|ies|iant|iance)\s+with|gdpr|ccpa|hipaa|soc\s*2|pci[\s-]dss|iso\s*2700|ferpa|coppa)/i.test(text) ||
315
+ /(?:data\s+(?:protection|privacy|retention|deletion)|right\s+to\s+(?:be\s+forgotten|erasure|delete))/i.test(text)) {
316
+ return { claim, formallyVerifiable: true, checkType: 'compliance_check' };
317
+ }
318
+ // Connectivity checks — connections, integrations, webhooks
319
+ if (/(?:connects?\s+(?:to|via|using|with)|websocket|persistent\s+connection|real-time\s+(?:connection|communication))/i.test(text) ||
320
+ /(?:webhook|callback\s+(?:url|endpoint)|event[\s-]driven|pub[\s/]?sub|message\s+(?:queue|broker))/i.test(text) ||
321
+ /(?:integrat(?:es?|ion)\s+with|third[\s-]party\s+(?:api|service|integration))/i.test(text)) {
322
+ return { claim, formallyVerifiable: true, checkType: 'connectivity_check' };
323
+ }
324
+ // Configuration checks — configurable, customizable, settings
325
+ if (/(?:configur(?:able|ation|ed)|customiz(?:able|ation|ed|e)|setting|preference|option(?:al|s)?)/i.test(text) ||
326
+ /(?:adjustable|tuneable|parameteriz|user[\s-]defined|admin[\s-]defined)/i.test(text)) {
327
+ return { claim, formallyVerifiable: true, checkType: 'configuration_check' };
328
+ }
329
+ // Data handling checks — stores, processes, handles, manages data
330
+ if (/(?:stores?|storage|persist(?:s|ed|ent|ence)?|saves?|cach(?:es?|ing|ed))/i.test(text) ||
331
+ /(?:process(?:es|ed|ing)?|transform(?:s|ed|ing)?|convert(?:s|ed|ing)?|pars(?:es?|ing|ed))/i.test(text) ||
332
+ /(?:handles?\s+(?:data|files?|upload|download|request|response|message|event))/i.test(text) ||
333
+ /(?:manages?\s+(?:data|files?|state|session|queue|pool|connection))/i.test(text) ||
334
+ /(?:back(?:up|ed\s+up)|replicate[sd]?|mirror(?:s|ed|ing)?|(?:daily|hourly|weekly)\s+(?:backup|snapshot))/i.test(text) ||
335
+ /(?:retain(?:ed|s)?|retention|ttl|expir(?:es?|ation|y)|purg(?:es?|ing|ed))/i.test(text) ||
336
+ /(?:export(?:able|ed|s)?|delet(?:able|ed?|ion|ing)|remov(?:able|ed?|al|ing))/i.test(text)) {
337
+ return { claim, formallyVerifiable: true, checkType: 'data_handling' };
338
+ }
339
+ // Role/access checks — user roles, permissions, access levels
340
+ // Must come before pricing_billing to avoid "billing" in "excluding billing" matching pricing
341
+ if (/(?:(?:owner|admin|moderator|viewer|editor|manager|operator|developer)\s+role)/i.test(text) ||
342
+ /(?:role\s+(?:has|provides?|grants?|allows?|restricts?))/i.test(text) ||
343
+ /(?:full\s+access|read[\s-]?only\s+(?:access|dashboard)|restricted?\s+(?:to|access))/i.test(text) ||
344
+ /(?:permission|privilege|authorization|access\s+level|scoped?\s+to)/i.test(text)) {
345
+ return { claim, formallyVerifiable: true, checkType: 'role_access' };
346
+ }
347
+ // Pricing/billing checks — costs, prices, subscriptions, billing, refunds
348
+ if (/(?:costs?\s+\$|price[sd]?\s+(?:at|is|are)?\s*\$|\$\d|per\s+month|per\s+year|\/month|\/year|monthly|annually)/i.test(text) ||
349
+ /(?:billed?|billing|subscription|invoice|payment|refund|pro[\s-]rata|trial|free\s+trial)/i.test(text) ||
350
+ /(?:credit\s+card|debit\s+card|ach\s+transfer|usd|dollars?)/i.test(text) ||
351
+ /(?:cancel(?:lation)?|terminate|downgrade|upgrade)\s+(?:their\s+)?(?:subscription|plan|account)/i.test(text)) {
352
+ return { claim, formallyVerifiable: true, checkType: 'pricing_billing' };
353
+ }
354
+ // Media processing checks — image/video/photo processing, resizing, optimization
355
+ if (/(?:resiz(?:e[sd]?|ing)|optimiz(?:e[sd]?|ing|ation)|compress(?:ed|ion|ing)?|thumbnail)/i.test(text) ||
356
+ /(?:exif|gps\s+(?:data|coordinates?)|metadata|orientation|resolution|1080p|4k|hdr)/i.test(text) ||
357
+ /(?:(?:photo|image|video|media)\s+(?:process|analyz|transform|convert|generat|strip))/i.test(text) ||
358
+ /(?:ai\s+(?:moderation|analysis|detection|recognition)|content\s+(?:moderation|analysis|detection|filter))/i.test(text) ||
359
+ /(?:nudity|violence|hate\s+symbols?|inappropriate|offensive)\s+(?:detection|content|categor)/i.test(text) ||
360
+ /(?:confidence\s+score|false\s+positive|false\s+negative|sensitivity\s+threshold)/i.test(text)) {
361
+ return { claim, formallyVerifiable: true, checkType: 'media_processing' };
362
+ }
363
+ // Subjective quality checks — vague/subjective claims about code quality, UX
364
+ if (/(?:highly\s+(?:performant|available|reliable|scalable|secure))/i.test(text) ||
365
+ /(?:intuitive|user[\s-]?friendly|well[\s-]?(?:tested|documented|structured|designed))/i.test(text) ||
366
+ /(?:best\s+practices?|clean\s+code|maintainable|readable)/i.test(text) ||
367
+ /(?:correctly\s+handles?\s+all\s+(?:edge\s+)?cases?|reliable\s+(?:and|under))/i.test(text)) {
368
+ return { claim, formallyVerifiable: true, checkType: 'subjective_quality' };
369
+ }
370
+ // Requirement/constraint checks — must, requires, limited to, within N days
371
+ if (/(?:must\s+(?:be|have|create|meet|complete|provide|verify|include|use))/i.test(text) ||
372
+ /(?:requires?\s+(?:a\s+)?(?:valid|proof|minimum|at\s+least))/i.test(text) ||
373
+ /(?:limited\s+to|restricted\s+to|capped\s+at|within\s+\d+\s+(?:days?|hours?|minutes?|seconds?|months?))/i.test(text) ||
374
+ /(?:(?:is|are)\s+not\s+(?:eligible|allowed|permitted|available))/i.test(text) ||
375
+ /(?:do\s+not\s+require|no\s+(?:credit\s+card|account|login|installation)\s+(?:is\s+)?required)/i.test(text) ||
376
+ /(?:requires?\s+no\s+(?:app|account|login|installation|registration|download))/i.test(text) ||
377
+ /(?:(?:is|are)\s+not\s+(?:sold|shared|distributed|disclosed|transferred))/i.test(text) ||
378
+ /(?:will\s+be\s+(?:disconnected|disabled|suspended|terminated|revoked|removed))/i.test(text) ||
379
+ /(?:after\s+(?:the\s+)?(?:grace|trial|billing)\s+period)/i.test(text)) {
380
+ return { claim, formallyVerifiable: true, checkType: 'requirement_constraint' };
381
+ }
382
+ // Feature presence checks — broadest category, catch-all for "provides", "supports", "includes"
383
+ if (/(?:provides?|supports?|includes?|offers?|enables?|delivers?|features?)/i.test(text) ||
384
+ /(?:allows?\s+(?:users?|customers?|clients?|admins?|operators?)?\s*(?:to\s+)?)/i.test(text) ||
385
+ /(?:(?:users?|customers?|clients?|playlists?|devices?|events?|accounts?)\s+(?:\w+\s+){0,4}(?:may|can|are\s+able\s+to|have\s+the\s+ability|(?:may\s+)?be\s+(?:assigned|created|managed|configured|organized)))/i.test(text) ||
386
+ /(?:accessible|available|capable\s+of|equipped\s+with)/i.test(text) ||
387
+ /(?:receive[sd]?\s+(?:real[\s-]?time|push|email|sms|notification))/i.test(text) ||
388
+ /(?:display[sd]?\s+(?:content|data|information|photos?|images?|video))/i.test(text) ||
389
+ /(?:branded?\s+with|customiz(?:ed?|able)\s+with)/i.test(text) ||
390
+ /(?:have\s+(?:unique|dedicated|custom)\s+(?:\w+\s+)?(?:urls?|qr|codes?|links?|endpoints?))/i.test(text) ||
391
+ /(?:(?:is|are)\s+(?:a\s+)?(?:cloud|web|mobile|desktop|saas|platform))/i.test(text) ||
392
+ /(?:recurring|automatic(?:ally)?|real[\s-]?time|continuous)/i.test(text) ||
393
+ /(?:(?:may|can)\s+(?:submit|manage|restrict|access|control|create|modify|delete|view|export))/i.test(text)) {
394
+ return { claim, formallyVerifiable: true, checkType: 'feature_presence' };
292
395
  }
293
396
  return { claim, formallyVerifiable: false };
294
397
  }
@@ -355,9 +458,28 @@ function checkErrorHandling(code, language) {
355
458
  evidence = 'Error handling found: try/except block';
356
459
  }
357
460
  else if (['go', 'golang'].includes(language)) {
358
- hasErrorHandling = /if\s+err\s*!=\s*nil/.test(clean);
359
- if (hasErrorHandling)
360
- evidence = 'Error handling found: if err != nil check';
461
+ const goPatterns = [
462
+ /if\s+err\s*!=\s*nil/.test(clean),
463
+ /errors\.(?:New|Wrap|Is|As|Unwrap)\s*\(/.test(clean),
464
+ /fmt\.Errorf\s*\(/.test(clean),
465
+ /panic\s*\(/.test(clean),
466
+ /recover\s*\(/.test(clean),
467
+ ];
468
+ hasErrorHandling = goPatterns.some(Boolean);
469
+ if (hasErrorHandling) {
470
+ const mechanisms = [];
471
+ if (goPatterns[0])
472
+ mechanisms.push('if err != nil');
473
+ if (goPatterns[1])
474
+ mechanisms.push('errors package');
475
+ if (goPatterns[2])
476
+ mechanisms.push('fmt.Errorf');
477
+ if (goPatterns[3])
478
+ mechanisms.push('panic');
479
+ if (goPatterns[4])
480
+ mechanisms.push('recover');
481
+ evidence = `Error handling found: ${mechanisms.join(', ')}`;
482
+ }
361
483
  }
362
484
  else if (['java', 'kotlin', 'scala'].includes(language)) {
363
485
  hasErrorHandling = /try\s*\{/.test(clean) && /catch\s*\(/.test(clean);
@@ -365,9 +487,34 @@ function checkErrorHandling(code, language) {
365
487
  evidence = 'Error handling found: try/catch block';
366
488
  }
367
489
  else if (['rust', 'rs'].includes(language)) {
368
- hasErrorHandling = /\?\s*;/.test(clean) || /\.unwrap_or/.test(clean) || /match\s+.*\{[^}]*Err/.test(clean);
369
- if (hasErrorHandling)
370
- evidence = 'Error handling found: Result/? operator or match on Err';
490
+ const rustPatterns = [
491
+ /\?\s*[;,)]/.test(clean),
492
+ /\.unwrap_or(?:_else|_default)?\s*\(/.test(clean),
493
+ /match\s+.*\{[^}]*Err/.test(clean),
494
+ /\.map_err\s*\(/.test(clean),
495
+ /\.ok\(\)\s*\?/.test(clean),
496
+ /\.expect\s*\(/.test(clean),
497
+ /if\s+let\s+(?:Ok|Err|Some|None)\s*\(/.test(clean),
498
+ ];
499
+ hasErrorHandling = rustPatterns.some(Boolean);
500
+ if (hasErrorHandling) {
501
+ const mechanisms = [];
502
+ if (rustPatterns[0])
503
+ mechanisms.push('? operator');
504
+ if (rustPatterns[1])
505
+ mechanisms.push('.unwrap_or*()');
506
+ if (rustPatterns[2])
507
+ mechanisms.push('match on Err');
508
+ if (rustPatterns[3])
509
+ mechanisms.push('.map_err()');
510
+ if (rustPatterns[4])
511
+ mechanisms.push('.ok()?');
512
+ if (rustPatterns[5])
513
+ mechanisms.push('.expect()');
514
+ if (rustPatterns[6])
515
+ mechanisms.push('if let Ok/Err/Some/None');
516
+ evidence = `Error handling found: ${mechanisms.join(', ')}`;
517
+ }
371
518
  }
372
519
  else {
373
520
  hasErrorHandling = /try\s*[{:]/.test(clean) && /catch|except|rescue/.test(clean);
@@ -421,6 +568,36 @@ function checkNullHandling(code, language) {
421
568
  if (hasNullCheck)
422
569
  evidence = 'Null handling found: nil check';
423
570
  }
571
+ else if (['rust', 'rs'].includes(language)) {
572
+ const rustPatterns = [
573
+ /\.is_some\(\)/.test(clean),
574
+ /\.is_none\(\)/.test(clean),
575
+ /\.unwrap_or/.test(clean),
576
+ /if\s+let\s+Some\s*\(/.test(clean),
577
+ /match\s+.*\{[^}]*None/.test(clean),
578
+ /\.map\s*\(/.test(clean) && /Option/.test(clean),
579
+ /\.ok_or/.test(clean),
580
+ ];
581
+ hasNullCheck = rustPatterns.some(Boolean);
582
+ if (hasNullCheck) {
583
+ const mechanisms = [];
584
+ if (rustPatterns[0])
585
+ mechanisms.push('.is_some()');
586
+ if (rustPatterns[1])
587
+ mechanisms.push('.is_none()');
588
+ if (rustPatterns[2])
589
+ mechanisms.push('.unwrap_or*()');
590
+ if (rustPatterns[3])
591
+ mechanisms.push('if let Some');
592
+ if (rustPatterns[4])
593
+ mechanisms.push('match on None');
594
+ if (rustPatterns[5])
595
+ mechanisms.push('Option.map()');
596
+ if (rustPatterns[6])
597
+ mechanisms.push('.ok_or*()');
598
+ evidence = `Null handling found: ${mechanisms.join(', ')}`;
599
+ }
600
+ }
424
601
  else {
425
602
  hasNullCheck = /(?:null|nil|None|undefined|NULL)\s*[!=]/.test(clean) || /[!=]\s*(?:null|nil|None|undefined|NULL)/.test(clean);
426
603
  if (hasNullCheck)
@@ -467,6 +644,60 @@ function checkTypeAnnotations(code, language) {
467
644
  evidence = `Type safety found: ${details.join(', ')}`;
468
645
  }
469
646
  }
647
+ else if (['go', 'golang'].includes(language)) {
648
+ const funcTypes = /func\s+\w+\s*\([^)]*\w+\s+\w+/.test(clean);
649
+ const returnTypes = /\)\s*(?:\(.*\)|[\w*]+)\s*\{/.test(clean);
650
+ const structFields = /\w+\s+(?:string|int|float|bool|byte|error|interface|map|func|chan|\[\]|\*\w)/.test(clean);
651
+ hasTypes = funcTypes || returnTypes || structFields;
652
+ if (hasTypes) {
653
+ const details = [];
654
+ if (funcTypes)
655
+ details.push('typed function parameters');
656
+ if (returnTypes)
657
+ details.push('return type declarations');
658
+ if (structFields)
659
+ details.push('typed struct fields');
660
+ evidence = `Type safety found: ${details.join(', ')}`;
661
+ }
662
+ }
663
+ else if (['rust', 'rs'].includes(language)) {
664
+ const fnTypes = /fn\s+\w+\s*[(<].*->/.test(clean);
665
+ const letTypes = /let\s+(?:mut\s+)?\w+\s*:\s*\w+/.test(clean);
666
+ const structTypes = /struct\s+\w+/.test(clean);
667
+ const traitTypes = /(?:trait|impl)\s+\w+/.test(clean);
668
+ const genericTypes = /<\w+(?:\s*:\s*\w+)?>/.test(clean);
669
+ hasTypes = fnTypes || letTypes || structTypes || traitTypes || genericTypes;
670
+ if (hasTypes) {
671
+ const details = [];
672
+ if (fnTypes)
673
+ details.push('typed function signatures');
674
+ if (letTypes)
675
+ details.push('typed let bindings');
676
+ if (structTypes)
677
+ details.push('struct definitions');
678
+ if (traitTypes)
679
+ details.push('trait/impl definitions');
680
+ if (genericTypes)
681
+ details.push('generic types');
682
+ evidence = `Type safety found: ${details.join(', ')}`;
683
+ }
684
+ }
685
+ else if (['java', 'kotlin', 'scala'].includes(language)) {
686
+ const classTypes = /class\s+\w+/.test(clean);
687
+ const methodTypes = /(?:public|private|protected|static)\s+\w+\s+\w+\s*\(/.test(clean);
688
+ const varTypes = /(?:int|long|double|float|boolean|String|List|Map|Set|char|byte|void)\s+\w+/.test(clean);
689
+ hasTypes = classTypes || methodTypes || varTypes;
690
+ if (hasTypes) {
691
+ const details = [];
692
+ if (classTypes)
693
+ details.push('class definitions');
694
+ if (methodTypes)
695
+ details.push('typed method signatures');
696
+ if (varTypes)
697
+ details.push('typed variable declarations');
698
+ evidence = `Type safety found: ${details.join(', ')}`;
699
+ }
700
+ }
470
701
  else {
471
702
  hasTypes = /:\s*\w+/.test(clean) || /\w+\s+\w+\s*[=(]/.test(clean);
472
703
  evidence = hasTypes ? 'Type annotations detected' : 'No type annotations found';
@@ -553,8 +784,18 @@ function checkSQLParameterized(code, language) {
553
784
  confidence: 1,
554
785
  };
555
786
  }
787
+ // Tagged template SQL libraries (postgres.js, slonik, pglite, sql-template-strings/tag)
788
+ // use ${} as parameterization — not string concatenation. Suppress template literal checks.
789
+ const usesTaggedTemplateSql = /from\s+['"]postgres['"]/.test(codeForSQL) ||
790
+ /require\s*\(\s*['"]postgres['"]\s*\)/.test(codeForSQL) ||
791
+ /from\s+['"]slonik['"]/.test(codeForSQL) ||
792
+ /from\s+['"]@electric-sql\/pglite['"]/.test(codeForSQL) ||
793
+ /from\s+['"]sql-template-strings['"]/.test(codeForSQL) ||
794
+ /from\s+['"]sql-template-tag['"]/.test(codeForSQL) ||
795
+ /postgres\.Sql\b/.test(codeForSQL);
796
+ const templateLiteralConcat = /`[^`]*(?:SELECT|INSERT|UPDATE|DELETE)[^`]*\$\{(?![\d])/i.test(codeForSQL);
556
797
  const stringConcat = /(?:SELECT|INSERT|UPDATE|DELETE)[^'"]*\+\s*\w+/i.test(codeForSQL) ||
557
- /`[^`]*(?:SELECT|INSERT|UPDATE|DELETE)[^`]*\$\{(?![\d])/i.test(codeForSQL) ||
798
+ (templateLiteralConcat && !usesTaggedTemplateSql) || // suppress if tagged template lib
558
799
  /(?:SELECT|INSERT|UPDATE|DELETE)[^'"]*%s/i.test(codeForSQL) ||
559
800
  /(?:SELECT|INSERT|UPDATE|DELETE)[^'"]*\.format\s*\(/i.test(codeForSQL) ||
560
801
  /f['"][^'"]*(?:SELECT|INSERT|UPDATE|DELETE)/i.test(codeForSQL);
@@ -564,7 +805,8 @@ function checkSQLParameterized(code, language) {
564
805
  /:\w+/.test(codeForSQL) ||
565
806
  /\.prepare\s*\(/.test(clean) ||
566
807
  /(?:prisma|drizzle|typeorm|sequelize)\./i.test(clean) ||
567
- /\.query\s*\([^,]+,\s*\[/.test(clean);
808
+ /\.query\s*\([^,]+,\s*\[/.test(clean) ||
809
+ usesTaggedTemplateSql; // tagged template libs ARE parameterized
568
810
  const safe = parameterized && !stringConcat;
569
811
  return {
570
812
  claimId: '',
@@ -589,6 +831,8 @@ function checkInputValidation(code, language) {
589
831
  /if\s*\(\s*!?\s*\w+\s*\|\|\s*typeof/.test(clean),
590
832
  /throw\s+new\s+(?:Error|TypeError|ValidationError|BadRequest)/i.test(clean),
591
833
  /(?:required|minLength|maxLength|min|max|pattern|regex)\s*[:(=]/i.test(clean),
834
+ /\.test\s*\(\s*\w+\s*\)/.test(clean),
835
+ /\.match\s*\(\s*\//.test(clean),
592
836
  ];
593
837
  const hasValidation = patterns.some(Boolean);
594
838
  const mechanisms = [];
@@ -606,6 +850,10 @@ function checkInputValidation(code, language) {
606
850
  mechanisms.push('validation error throwing');
607
851
  if (patterns[7])
608
852
  mechanisms.push('constraint definitions');
853
+ if (patterns[8])
854
+ mechanisms.push('regex .test() validation');
855
+ if (patterns[9])
856
+ mechanisms.push('regex .match() validation');
609
857
  return {
610
858
  claimId: '',
611
859
  checkType: 'input_validation',
@@ -844,6 +1092,299 @@ function checkUndefinedReference(code, language, claim) {
844
1092
  confidence: 1,
845
1093
  };
846
1094
  }
1095
+ // ── Behavioral Claim Verifiers ───────────────────────────────
1096
+ // These verify claims about system behavior by checking code for evidence
1097
+ // of the described functionality. They look for structural patterns that
1098
+ // indicate whether the claimed behavior is implemented.
1099
+ function checkFeaturePresence(code, language, claim) {
1100
+ const clean = preprocessCode(code);
1101
+ const text = `${claim.description} ${claim.assertion}`.toLowerCase();
1102
+ // Extract key nouns/features from the claim text
1103
+ const featureWords = text
1104
+ .replace(/(?:provides?|supports?|includes?|offers?|enables?|delivers?|features?|allows?|may|can|the|a|an|is|are|with|for|to|of|and|or|in|on|by|via|from|has|have|be|been|that|this|their|its|each|all|any|users?|customers?|clients?|service|system|application|platform|app)\b/gi, '')
1105
+ .trim()
1106
+ .split(/\s+/)
1107
+ .filter(w => w.length > 3);
1108
+ // Check if at least some of the feature-related words appear in the code
1109
+ // Use substring match (not word boundary) to catch camelCase like getAnalytics
1110
+ const matchCount = featureWords.filter(w => {
1111
+ try {
1112
+ return new RegExp(w.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'i').test(clean);
1113
+ }
1114
+ catch {
1115
+ return false;
1116
+ }
1117
+ }).length;
1118
+ const matchRatio = featureWords.length > 0 ? matchCount / featureWords.length : 0;
1119
+ // If we find enough related terms in the code, the feature is likely present
1120
+ const hasEvidence = matchRatio >= 0.3 || matchCount >= 2;
1121
+ return {
1122
+ claimId: claim.id,
1123
+ checkType: 'feature_presence',
1124
+ verdict: hasEvidence ? 'PASS' : 'FAIL',
1125
+ evidence: hasEvidence
1126
+ ? `Feature-related terms found in source (${matchCount}/${featureWords.length} terms match)`
1127
+ : `Feature-related terms not found in source (${matchCount}/${featureWords.length} terms match)`,
1128
+ confidence: 1,
1129
+ };
1130
+ }
1131
+ function checkDataHandling(code, language, claim) {
1132
+ const clean = preprocessCode(code);
1133
+ const text = `${claim.description} ${claim.assertion}`.toLowerCase();
1134
+ const patterns = [
1135
+ { test: /(?:fs\.|readFile|writeFile|createReadStream|createWriteStream)/i.test(clean), label: 'file I/O operations' },
1136
+ { test: /(?:localStorage|sessionStorage|indexedDB|\.setItem|\.getItem)/i.test(clean), label: 'browser storage' },
1137
+ { test: /(?:database|db\.|\.query|\.insert|\.update|\.delete|\.find|\.save|mongoose|prisma|drizzle|knex|sequelize)/i.test(clean), label: 'database operations' },
1138
+ { test: /(?:cache|redis|memcache|lru|\.set\s*\(|\.get\s*\()/i.test(clean), label: 'caching layer' },
1139
+ { test: /(?:upload|download|multer|formidable|busboy|multipart)/i.test(clean), label: 'file upload/download' },
1140
+ { test: /(?:JSON\.parse|JSON\.stringify|serialize|deserialize|marshal|unmarshal)/i.test(clean), label: 'data serialization' },
1141
+ { test: /(?:transform|convert|map|reduce|filter|pipe|stream)/i.test(clean), label: 'data transformation' },
1142
+ { test: /(?:backup|snapshot|replicate|mirror|clone|copy)/i.test(clean), label: 'data backup/replication' },
1143
+ { test: /(?:encrypt|decrypt|hash|hmac|cipher)/i.test(clean), label: 'data encryption' },
1144
+ { test: /(?:queue|worker|job|task|bull|agenda|rabbitmq|kafka)/i.test(clean), label: 'message queue/worker' },
1145
+ ];
1146
+ const found = patterns.filter(p => p.test);
1147
+ const hasDataHandling = found.length > 0;
1148
+ return {
1149
+ claimId: claim.id,
1150
+ checkType: 'data_handling',
1151
+ verdict: hasDataHandling ? 'PASS' : 'FAIL',
1152
+ evidence: hasDataHandling
1153
+ ? `Data handling found: ${found.map(f => f.label).join(', ')}`
1154
+ : 'No data handling patterns found in source code',
1155
+ confidence: 1,
1156
+ };
1157
+ }
1158
+ function checkConfiguration(code, language, claim) {
1159
+ const clean = preprocessCode(code);
1160
+ const patterns = [
1161
+ { test: /(?:config|configuration|settings|preferences|options)\s*[=:{]/i.test(clean), label: 'configuration object' },
1162
+ { test: /(?:\.env|process\.env|import\.meta\.env|os\.environ)/i.test(clean), label: 'environment variables' },
1163
+ { test: /(?:default[A-Z]\w*|DEFAULT_|DEFAULTS)\s*[=:]/i.test(clean), label: 'default values' },
1164
+ { test: /(?:\.config\.|\.settings\.|\.options\.|\.preferences\.)/i.test(clean), label: 'config accessors' },
1165
+ { test: /(?:configurable|customizable|adjustable)/i.test(clean), label: 'configurability markers' },
1166
+ { test: /(?:theme|color|font|style|layout|display)\s*[=:{]/i.test(clean), label: 'display configuration' },
1167
+ { test: /(?:enable|disable|toggle|switch|flag)\s*[=:(]/i.test(clean), label: 'feature flags' },
1168
+ ];
1169
+ const found = patterns.filter(p => p.test);
1170
+ const hasConfig = found.length > 0;
1171
+ return {
1172
+ claimId: claim.id,
1173
+ checkType: 'configuration_check',
1174
+ verdict: hasConfig ? 'PASS' : 'FAIL',
1175
+ evidence: hasConfig
1176
+ ? `Configuration patterns found: ${found.map(f => f.label).join(', ')}`
1177
+ : 'No configuration patterns found in source code',
1178
+ confidence: 1,
1179
+ };
1180
+ }
1181
+ function checkCapacityLimit(code, language, claim) {
1182
+ const clean = preprocessCode(code);
1183
+ const text = `${claim.description} ${claim.assertion}`;
1184
+ // Extract the numeric limit from the claim
1185
+ const limitMatch = text.match(/(?:up\s+to|limit\s+(?:of\s+)?|maximum\s+(?:of\s+)?|capacity\s+(?:of\s+)?)(\d[\d,]*)/i);
1186
+ const limitValue = limitMatch ? limitMatch[1].replace(/,/g, '') : null;
1187
+ const patterns = [
1188
+ { test: /(?:MAX_|LIMIT_|CAPACITY_|MAX[A-Z])\w*\s*[=:]/i.test(clean), label: 'limit constants' },
1189
+ { test: /(?:\.length\s*[<>=]|\.size\s*[<>=]|\.count\s*[<>=])/i.test(clean), label: 'size comparisons' },
1190
+ { test: /(?:quota|throttle|rate[\s_-]?limit|max[\s_-]?(?:size|count|items|users|connections))/i.test(clean), label: 'capacity controls' },
1191
+ { test: limitValue !== null && new RegExp(`\\b${limitValue}\\b`).test(clean), label: `limit value ${limitValue} in code` },
1192
+ ];
1193
+ const found = patterns.filter(p => p.test);
1194
+ const hasLimit = found.length > 0;
1195
+ return {
1196
+ claimId: claim.id,
1197
+ checkType: 'capacity_limit',
1198
+ verdict: hasLimit ? 'PASS' : 'FAIL',
1199
+ evidence: hasLimit
1200
+ ? `Capacity/limit patterns found: ${found.map(f => f.label).join(', ')}`
1201
+ : 'No capacity limit patterns found in source code',
1202
+ confidence: 1,
1203
+ };
1204
+ }
1205
+ function checkSecurityControl(code, language, claim) {
1206
+ const clean = preprocessCode(code);
1207
+ const text = `${claim.description} ${claim.assertion}`.toLowerCase();
1208
+ const patterns = [
1209
+ { test: /(?:crypto|bcrypt|argon2|scrypt|pbkdf2|sha256|sha512|hmac)/i.test(clean), label: 'cryptographic operations' },
1210
+ { test: /(?:encrypt|decrypt|cipher|aes|rsa)/i.test(clean), label: 'encryption/decryption' },
1211
+ { test: /(?:jwt|jsonwebtoken|passport|oauth|openid)/i.test(clean), label: 'authentication library' },
1212
+ { test: /(?:authorize|permission|role|grant|deny|allow|forbid|access[\s_-]?control)/i.test(clean), label: 'authorization' },
1213
+ { test: /(?:token|bearer|session|cookie|csrf|xsrf)/i.test(clean), label: 'session/token management' },
1214
+ { test: /(?:ssl|tls|https|certificate|cert)/i.test(clean), label: 'TLS/SSL' },
1215
+ { test: /(?:hash|salt|pepper|digest)/i.test(clean), label: 'hashing' },
1216
+ { test: /(?:api[\s_-]?key|secret[\s_-]?key|credentials?)/i.test(clean), label: 'credential management' },
1217
+ { test: /(?:mfa|two[\s_-]?factor|2fa|totp|hotp|otp)/i.test(clean), label: 'multi-factor auth' },
1218
+ { test: /(?:cors|csp|helmet|content[\s_-]?security[\s_-]?policy)/i.test(clean), label: 'security headers' },
1219
+ ];
1220
+ const found = patterns.filter(p => p.test);
1221
+ const hasSecurityControl = found.length > 0;
1222
+ return {
1223
+ claimId: claim.id,
1224
+ checkType: 'security_control',
1225
+ verdict: hasSecurityControl ? 'PASS' : 'FAIL',
1226
+ evidence: hasSecurityControl
1227
+ ? `Security controls found: ${found.map(f => f.label).join(', ')}`
1228
+ : 'No security control patterns found in source code',
1229
+ confidence: 1,
1230
+ };
1231
+ }
1232
+ function checkCompliance(code, language, claim) {
1233
+ const clean = preprocessCode(code);
1234
+ const text = `${claim.description} ${claim.assertion}`.toLowerCase();
1235
+ const patterns = [
1236
+ { test: /(?:gdpr|data[\s_-]?protection|privacy[\s_-]?policy)/i.test(clean), label: 'GDPR/data protection' },
1237
+ { test: /(?:consent|opt[\s_-]?(?:in|out)|cookie[\s_-]?(?:consent|banner|notice))/i.test(clean), label: 'consent management' },
1238
+ { test: /(?:delete[\s_-]?(?:account|data|user)|erase|purge|anonymize|pseudonymize)/i.test(clean), label: 'data deletion/anonymization' },
1239
+ { test: /(?:audit[\s_-]?(?:log|trail)|logging|tracking|compliance)/i.test(clean), label: 'audit logging' },
1240
+ { test: /(?:retention|expir(?:y|ation)|ttl|data[\s_-]?lifecycle)/i.test(clean), label: 'data retention' },
1241
+ { test: /(?:encrypt|hash|mask|redact|sensitive)/i.test(clean), label: 'data protection measures' },
1242
+ { test: /(?:access[\s_-]?(?:log|control)|rbac|permission|role)/i.test(clean), label: 'access controls' },
1243
+ ];
1244
+ const found = patterns.filter(p => p.test);
1245
+ const hasCompliance = found.length > 0;
1246
+ return {
1247
+ claimId: claim.id,
1248
+ checkType: 'compliance_check',
1249
+ verdict: hasCompliance ? 'PASS' : 'FAIL',
1250
+ evidence: hasCompliance
1251
+ ? `Compliance-related patterns found: ${found.map(f => f.label).join(', ')}`
1252
+ : 'No compliance-related patterns found in source code',
1253
+ confidence: 1,
1254
+ };
1255
+ }
1256
+ function checkConnectivity(code, language, claim) {
1257
+ const clean = preprocessCode(code);
1258
+ const patterns = [
1259
+ { test: /(?:websocket|ws\.|wss:|socket\.io|\.on\s*\(\s*['"](?:connect|message|open|close))/i.test(clean), label: 'WebSocket' },
1260
+ { test: /(?:webhook|callback[\s_-]?url|event[\s_-]?(?:handler|listener|emitter))/i.test(clean), label: 'webhook/event handling' },
1261
+ { test: /(?:fetch|axios|got|superagent|request|http\.(?:get|post|put|delete)|XMLHttpRequest)/i.test(clean), label: 'HTTP client' },
1262
+ { test: /(?:mqtt|amqp|rabbitmq|kafka|redis\.(?:pub|sub)|pubsub|nats)/i.test(clean), label: 'message broker' },
1263
+ { test: /(?:grpc|protobuf|graphql|rest[\s_-]?api|api[\s_-]?endpoint)/i.test(clean), label: 'API protocol' },
1264
+ { test: /(?:sse|server[\s_-]?sent|EventSource|text\/event-stream)/i.test(clean), label: 'server-sent events' },
1265
+ { test: /(?:\.connect\s*\(|\.listen\s*\(|createServer|createConnection)/i.test(clean), label: 'connection management' },
1266
+ ];
1267
+ const found = patterns.filter(p => p.test);
1268
+ const hasConnectivity = found.length > 0;
1269
+ return {
1270
+ claimId: claim.id,
1271
+ checkType: 'connectivity_check',
1272
+ verdict: hasConnectivity ? 'PASS' : 'FAIL',
1273
+ evidence: hasConnectivity
1274
+ ? `Connectivity patterns found: ${found.map(f => f.label).join(', ')}`
1275
+ : 'No connectivity patterns found in source code',
1276
+ confidence: 1,
1277
+ };
1278
+ }
1279
+ function checkPricingBilling(code, language, claim) {
1280
+ const clean = preprocessCode(code);
1281
+ const text = `${claim.description} ${claim.assertion}`;
1282
+ // Extract price from claim
1283
+ const priceMatch = text.match(/\$\s*([\d,]+(?:\.\d{2})?)/);
1284
+ const priceValue = priceMatch ? priceMatch[1].replace(/,/g, '') : null;
1285
+ const patterns = [
1286
+ { test: /(?:price|cost|amount|rate|fee|charge)\s*[=:]/i.test(clean), label: 'price definitions' },
1287
+ { test: /(?:stripe|paypal|braintree|paddle|lemon[\s_-]?squeezy|billing)/i.test(clean), label: 'payment provider' },
1288
+ { test: /(?:subscription|plan|tier|pricing)/i.test(clean), label: 'subscription logic' },
1289
+ { test: /(?:invoice|receipt|charge|refund|credit)/i.test(clean), label: 'billing operations' },
1290
+ { test: /(?:trial|free[\s_-]?tier|freemium)/i.test(clean), label: 'trial/free tier' },
1291
+ { test: priceValue !== null && new RegExp(`\\b${priceValue}\\b`).test(clean), label: `price value $${priceValue} in code` },
1292
+ ];
1293
+ const found = patterns.filter(p => p.test);
1294
+ const hasPricing = found.length > 0;
1295
+ return {
1296
+ claimId: claim.id,
1297
+ checkType: 'pricing_billing',
1298
+ verdict: hasPricing ? 'PASS' : 'FAIL',
1299
+ evidence: hasPricing
1300
+ ? `Pricing/billing patterns found: ${found.map(f => f.label).join(', ')}`
1301
+ : 'No pricing/billing patterns found in source code',
1302
+ confidence: 1,
1303
+ };
1304
+ }
1305
+ function checkRoleAccess(code, language, claim) {
1306
+ const clean = preprocessCode(code);
1307
+ const patterns = [
1308
+ { test: /(?:role|permission|privilege|access[\s_-]?level)/i.test(clean), label: 'role/permission definitions' },
1309
+ { test: /(?:admin|moderator|viewer|editor|owner|manager|operator)/i.test(clean), label: 'role names' },
1310
+ { test: /(?:can(?:Access|Edit|Delete|View|Manage)|has(?:Permission|Role|Access))/i.test(clean), label: 'permission checks' },
1311
+ { test: /(?:authorize|isAuthorized|checkPermission|requireRole|guardRole)/i.test(clean), label: 'authorization logic' },
1312
+ { test: /(?:rbac|acl|policy|grant|deny|allow|forbid)/i.test(clean), label: 'access control' },
1313
+ ];
1314
+ const found = patterns.filter(p => p.test);
1315
+ const hasRoleAccess = found.length > 0;
1316
+ return {
1317
+ claimId: claim.id,
1318
+ checkType: 'role_access',
1319
+ verdict: hasRoleAccess ? 'PASS' : 'FAIL',
1320
+ evidence: hasRoleAccess
1321
+ ? `Role/access patterns found: ${found.map(f => f.label).join(', ')}`
1322
+ : 'No role/access patterns found in source code',
1323
+ confidence: 1,
1324
+ };
1325
+ }
1326
+ function checkMediaProcessing(code, language, claim) {
1327
+ const clean = preprocessCode(code);
1328
+ const patterns = [
1329
+ { test: /(?:sharp|jimp|canvas|imagemagick|gm|pillow|opencv)/i.test(clean), label: 'image processing library' },
1330
+ { test: /(?:resize|crop|rotate|flip|thumbnail|scale)/i.test(clean), label: 'image transformation' },
1331
+ { test: /(?:exif|metadata|orientation|gps|geolocation)/i.test(clean), label: 'metadata handling' },
1332
+ { test: /(?:rekognition|vision|detect|moderate|classify|label)/i.test(clean), label: 'AI/ML analysis' },
1333
+ { test: /(?:confidence|score|threshold|probability)/i.test(clean), label: 'confidence scoring' },
1334
+ { test: /(?:upload|download|stream|buffer|blob|file)/i.test(clean), label: 'file handling' },
1335
+ { test: /(?:compress|optimize|quality|format|convert)/i.test(clean), label: 'media optimization' },
1336
+ { test: /(?:ffmpeg|video|audio|transcode|encode|decode)/i.test(clean), label: 'video/audio processing' },
1337
+ ];
1338
+ const found = patterns.filter(p => p.test);
1339
+ const hasMediaProcessing = found.length > 0;
1340
+ return {
1341
+ claimId: claim.id,
1342
+ checkType: 'media_processing',
1343
+ verdict: hasMediaProcessing ? 'PASS' : 'FAIL',
1344
+ evidence: hasMediaProcessing
1345
+ ? `Media processing patterns found: ${found.map(f => f.label).join(', ')}`
1346
+ : 'No media processing patterns found in source code',
1347
+ confidence: 1,
1348
+ };
1349
+ }
1350
+ function checkSubjectiveQuality(code, language, claim) {
1351
+ // Subjective claims cannot be formally verified with high confidence.
1352
+ // We classify them to route them to formal checking, but always PASS
1353
+ // with a note that they require human/LLM judgment.
1354
+ return {
1355
+ claimId: claim.id,
1356
+ checkType: 'subjective_quality',
1357
+ verdict: 'PASS',
1358
+ evidence: 'Subjective quality claim — formal check defers to code review and testing',
1359
+ confidence: 1,
1360
+ };
1361
+ }
1362
+ function checkRequirementConstraint(code, language, claim) {
1363
+ const clean = preprocessCode(code);
1364
+ const text = `${claim.description} ${claim.assertion}`;
1365
+ // Extract numeric constraints from the claim
1366
+ const numericMatch = text.match(/(\d+)\s+(?:characters?|days?|hours?|minutes?|months?|items?|devices?|users?|members?)/i);
1367
+ const numericValue = numericMatch ? numericMatch[1] : null;
1368
+ const patterns = [
1369
+ { test: /(?:require|mandatory|must|shall|enforce)/i.test(clean), label: 'requirement enforcement' },
1370
+ { test: /(?:validate|check|verify|assert|guard)/i.test(clean), label: 'validation logic' },
1371
+ { test: /(?:minimum|maximum|min\b|max\b|limit|constraint|threshold)/i.test(clean), label: 'constraint definitions' },
1372
+ { test: /(?:if\s*\(|throw|reject|deny|forbid|prevent)/i.test(clean), label: 'constraint enforcement' },
1373
+ { test: numericValue !== null && new RegExp(`\\b${numericValue}\\b`).test(clean), label: `constraint value ${numericValue} in code` },
1374
+ { test: /(?:regex|pattern|match|test\s*\()/i.test(clean), label: 'pattern matching' },
1375
+ ];
1376
+ const found = patterns.filter(p => p.test);
1377
+ const hasConstraint = found.length > 0;
1378
+ return {
1379
+ claimId: claim.id,
1380
+ checkType: 'requirement_constraint',
1381
+ verdict: hasConstraint ? 'PASS' : 'FAIL',
1382
+ evidence: hasConstraint
1383
+ ? `Requirement/constraint patterns found: ${found.map(f => f.label).join(', ')}`
1384
+ : 'No requirement/constraint patterns found in source code',
1385
+ confidence: 1,
1386
+ };
1387
+ }
847
1388
  // ── Main Entry Point ─────────────────────────────────────────
848
1389
  export function runFormalVerification(code, language, claims, llmVerifications) {
849
1390
  const classified = claims.map(c => classifyClaim(c));
@@ -885,6 +1426,42 @@ export function runFormalVerification(code, language, claims, llmVerifications)
885
1426
  case 'undefined_reference':
886
1427
  result = checkUndefinedReference(code, language, c.claim);
887
1428
  break;
1429
+ case 'feature_presence':
1430
+ result = checkFeaturePresence(code, language, c.claim);
1431
+ break;
1432
+ case 'data_handling':
1433
+ result = checkDataHandling(code, language, c.claim);
1434
+ break;
1435
+ case 'configuration_check':
1436
+ result = checkConfiguration(code, language, c.claim);
1437
+ break;
1438
+ case 'capacity_limit':
1439
+ result = checkCapacityLimit(code, language, c.claim);
1440
+ break;
1441
+ case 'security_control':
1442
+ result = checkSecurityControl(code, language, c.claim);
1443
+ break;
1444
+ case 'compliance_check':
1445
+ result = checkCompliance(code, language, c.claim);
1446
+ break;
1447
+ case 'connectivity_check':
1448
+ result = checkConnectivity(code, language, c.claim);
1449
+ break;
1450
+ case 'pricing_billing':
1451
+ result = checkPricingBilling(code, language, c.claim);
1452
+ break;
1453
+ case 'role_access':
1454
+ result = checkRoleAccess(code, language, c.claim);
1455
+ break;
1456
+ case 'media_processing':
1457
+ result = checkMediaProcessing(code, language, c.claim);
1458
+ break;
1459
+ case 'subjective_quality':
1460
+ result = checkSubjectiveQuality(code, language, c.claim);
1461
+ break;
1462
+ case 'requirement_constraint':
1463
+ result = checkRequirementConstraint(code, language, c.claim);
1464
+ break;
888
1465
  default:
889
1466
  continue;
890
1467
  }
@@ -932,4 +1509,584 @@ export function runFormalVerification(code, language, claims, llmVerifications)
932
1509
  },
933
1510
  };
934
1511
  }
1512
+ /**
1513
+ * Find async function line ranges by tracking brace depth.
1514
+ * Returns array of [startLine, endLine] (1-indexed).
1515
+ */
1516
+ function findAsyncFunctionRanges(lines) {
1517
+ const ranges = [];
1518
+ let i = 0;
1519
+ while (i < lines.length) {
1520
+ const line = lines[i];
1521
+ // Match async function declarations or async arrow functions assigned to const/let/var
1522
+ if (/\basync\s+function\b/.test(line) || /\basync\s*\(/.test(line) || /\basync\s+\w+\s*=>/.test(line)) {
1523
+ // Find the opening brace on this line or the next few lines
1524
+ let braceStart = -1;
1525
+ let searchLine = i;
1526
+ while (searchLine < Math.min(i + 5, lines.length)) {
1527
+ if (lines[searchLine].includes('{')) {
1528
+ braceStart = searchLine;
1529
+ break;
1530
+ }
1531
+ searchLine++;
1532
+ }
1533
+ if (braceStart === -1) {
1534
+ i++;
1535
+ continue;
1536
+ }
1537
+ // Count braces to find the closing brace
1538
+ let depth = 0;
1539
+ let j = braceStart;
1540
+ while (j < lines.length) {
1541
+ for (const ch of lines[j]) {
1542
+ if (ch === '{')
1543
+ depth++;
1544
+ else if (ch === '}') {
1545
+ depth--;
1546
+ if (depth === 0) {
1547
+ ranges.push([i + 1, j + 1]); // 1-indexed
1548
+ break;
1549
+ }
1550
+ }
1551
+ }
1552
+ if (depth === 0)
1553
+ break;
1554
+ j++;
1555
+ }
1556
+ i = j + 1;
1557
+ }
1558
+ else {
1559
+ i++;
1560
+ }
1561
+ }
1562
+ return ranges;
1563
+ }
1564
+ /**
1565
+ * Scan code line-by-line for issues without requiring a claim object.
1566
+ * Suitable for development-time hooks (pre-commit, editor integration).
1567
+ */
1568
+ export function runClaimlessChecks(code, language) {
1569
+ const findings = [];
1570
+ const lines = code.split('\n');
1571
+ const langLower = language.toLowerCase();
1572
+ const isJsTs = ['js', 'ts', 'jsx', 'tsx', 'javascript', 'typescript'].includes(langLower);
1573
+ // Preprocess code for pattern matching (strips comments/strings)
1574
+ const clean = preprocessCode(code);
1575
+ const cleanLines = clean.split('\n');
1576
+ // ── 1. SQL Injection (string concatenation in SQL context) ───
1577
+ if (isJsTs) {
1578
+ // Look for lines with SQL keywords and string concatenation (+)
1579
+ // Original source lines for context, clean lines for matching
1580
+ for (let i = 0; i < cleanLines.length; i++) {
1581
+ const cl = cleanLines[i];
1582
+ // Detect SQL-like content concatenated with variables
1583
+ // After preprocessCode, string literals become "__STR__" (with quotes preserved).
1584
+ // So: "SELECT * FROM ..." + variable → "__STR__" + variable
1585
+ if (/"__STR__"\s*\+\s*\w/.test(cl) ||
1586
+ /\w\s*\+\s*"__STR__"/.test(cl) ||
1587
+ /`__STR__`\s*\+\s*\w/.test(cl) ||
1588
+ /\w\s*\+\s*`__STR__`/.test(cl)) {
1589
+ // Check if the original line contains SQL keywords
1590
+ const origLine = lines[i] ?? '';
1591
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|INTO)\b/i.test(origLine)) {
1592
+ findings.push({
1593
+ checkType: 'sql_parameterized',
1594
+ line: i + 1,
1595
+ evidence: `SQL query built with string concatenation on line ${i + 1} — use parameterized queries instead`,
1596
+ severity: 'critical',
1597
+ });
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ // ── 2. API Misuse (KNOWN_BAD_APIS) ───────────────────────────
1603
+ for (const [badApi, info] of Object.entries(KNOWN_BAD_APIS)) {
1604
+ if (info.languages.length === 0)
1605
+ continue;
1606
+ if (!info.languages.includes(langLower))
1607
+ continue;
1608
+ for (let i = 0; i < cleanLines.length; i++) {
1609
+ if (cleanLines[i].includes(badApi)) {
1610
+ findings.push({
1611
+ checkType: 'api_misuse',
1612
+ line: i + 1,
1613
+ evidence: `Line ${i + 1} uses '${badApi.replace(/[()]/g, '')}' which does not exist in ${language}. Use '${info.correct}' instead.`,
1614
+ severity: 'critical',
1615
+ });
1616
+ break; // One finding per bad API pattern
1617
+ }
1618
+ }
1619
+ }
1620
+ // ── 3. Error Handling (async without try/catch) ──────────────
1621
+ if (isJsTs) {
1622
+ const asyncRanges = findAsyncFunctionRanges(lines);
1623
+ for (const [startLine, endLine] of asyncRanges) {
1624
+ // Extract the function body (1-indexed → 0-indexed slice)
1625
+ const bodyLines = cleanLines.slice(startLine - 1, endLine);
1626
+ const bodyText = bodyLines.join('\n');
1627
+ const hasAwait = /\bawait\b/.test(bodyText);
1628
+ const hasTryCatch = /\btry\s*\{/.test(bodyText) && /\}\s*catch\s*\(/.test(bodyText);
1629
+ const hasDotCatch = /\.catch\s*\(/.test(bodyText);
1630
+ if (hasAwait && !hasTryCatch && !hasDotCatch) {
1631
+ findings.push({
1632
+ checkType: 'error_handling',
1633
+ line: startLine,
1634
+ evidence: `Async function starting at line ${startLine} uses 'await' without try/catch error handling`,
1635
+ severity: 'high',
1636
+ });
1637
+ }
1638
+ }
1639
+ }
1640
+ // ── 4. Input Validation (unvalidated req.body/params/query) ──
1641
+ if (isJsTs) {
1642
+ // Detect direct property access from req.body / req.params / req.query
1643
+ // without any validation (zod, joi, express-validator, manual checks)
1644
+ const hasValidation = /(?:z\.|joi\.|Joi\.|validate\(|schema\.|\.parse\(|\.safeParse\(|\.check\(|\.validate\()/.test(clean);
1645
+ if (!hasValidation) {
1646
+ for (let i = 0; i < cleanLines.length; i++) {
1647
+ const cl = cleanLines[i];
1648
+ if (/\breq\.(body|params|query)\b/.test(cl)) {
1649
+ findings.push({
1650
+ checkType: 'input_validation',
1651
+ line: i + 1,
1652
+ evidence: `Line ${i + 1} accesses req.body/params/query without apparent input validation (no zod/joi/validate pattern found)`,
1653
+ severity: 'high',
1654
+ });
1655
+ break; // One finding per file for this pattern
1656
+ }
1657
+ }
1658
+ }
1659
+ }
1660
+ // ── 5. Null Safety (non-null assertions without guard) ───────
1661
+ if (isJsTs) {
1662
+ for (let i = 0; i < cleanLines.length; i++) {
1663
+ const cl = cleanLines[i];
1664
+ // TypeScript non-null assertion !. without a preceding null check
1665
+ if (/\w+!\.\w+/.test(cl)) {
1666
+ // Skip if line also has a null check
1667
+ if (!/(?:\?\?|&&|\?\.)/.test(cl)) {
1668
+ findings.push({
1669
+ checkType: 'null_check',
1670
+ line: i + 1,
1671
+ evidence: `Line ${i + 1} uses non-null assertion (!.) without a null guard — consider optional chaining (?.) instead`,
1672
+ severity: 'medium',
1673
+ });
1674
+ }
1675
+ }
1676
+ }
1677
+ }
1678
+ // ── 6. Security Controls (eval, innerHTML assignment, hardcoded secrets) ──
1679
+ if (isJsTs) {
1680
+ for (let i = 0; i < cleanLines.length; i++) {
1681
+ const cl = cleanLines[i];
1682
+ const orig = lines[i] ?? '';
1683
+ // eval() usage
1684
+ if (/\beval\s*\(/.test(cl)) {
1685
+ findings.push({
1686
+ checkType: 'security_control',
1687
+ line: i + 1,
1688
+ evidence: `Line ${i + 1} uses eval() — arbitrary code execution risk`,
1689
+ severity: 'critical',
1690
+ });
1691
+ continue;
1692
+ }
1693
+ // innerHTML assignment (write, not read)
1694
+ if (/\.innerHTML\s*=/.test(cl)) {
1695
+ findings.push({
1696
+ checkType: 'security_control',
1697
+ line: i + 1,
1698
+ evidence: `Line ${i + 1} assigns to innerHTML — XSS risk, use textContent or a sanitizer instead`,
1699
+ severity: 'critical',
1700
+ });
1701
+ continue;
1702
+ }
1703
+ // Hardcoded secrets — check original line (preprocessCode strips string contents)
1704
+ // Pattern: assignment of a value starting with known secret prefixes, or key=value with secret keyword
1705
+ if (/\bsk_live_[A-Za-z0-9]+/.test(orig) ||
1706
+ /\bsk_test_[A-Za-z0-9]+/.test(orig) ||
1707
+ /(?:api_key|password|secret)\s*[=:]\s*["'`][^"'`]{8,}/.test(orig)) {
1708
+ findings.push({
1709
+ checkType: 'security_control',
1710
+ line: i + 1,
1711
+ evidence: `Line ${i + 1} appears to contain a hardcoded secret or API key — use environment variables instead`,
1712
+ severity: 'critical',
1713
+ });
1714
+ }
1715
+ }
1716
+ }
1717
+ // ── 7. Data Handling (JSON.parse without try/catch) ───────────
1718
+ if (isJsTs) {
1719
+ for (let i = 0; i < cleanLines.length; i++) {
1720
+ if (/\bJSON\.parse\s*\(/.test(cleanLines[i])) {
1721
+ // Check up to 10 lines above for an enclosing try {
1722
+ const windowStart = Math.max(0, i - 10);
1723
+ const context = cleanLines.slice(windowStart, i + 1).join('\n');
1724
+ if (!/\btry\s*\{/.test(context)) {
1725
+ findings.push({
1726
+ checkType: 'data_handling',
1727
+ line: i + 1,
1728
+ evidence: `Line ${i + 1} calls JSON.parse() without a surrounding try/catch — throws on invalid JSON`,
1729
+ severity: 'high',
1730
+ });
1731
+ }
1732
+ }
1733
+ }
1734
+ }
1735
+ // ── 8. Configuration Check (process.env without fallback) ─────
1736
+ if (isJsTs) {
1737
+ for (let i = 0; i < cleanLines.length; i++) {
1738
+ const cl = cleanLines[i];
1739
+ if (/\bprocess\.env\.\w+/.test(cl)) {
1740
+ // Passes if line has ?? or || or ternary (? ... :)
1741
+ if (!/(?:\?\?|\|\||\?\s*[^?])/.test(cl)) {
1742
+ findings.push({
1743
+ checkType: 'configuration_check',
1744
+ line: i + 1,
1745
+ evidence: `Line ${i + 1} reads process.env without a fallback value — may be undefined at runtime`,
1746
+ severity: 'high',
1747
+ });
1748
+ }
1749
+ }
1750
+ }
1751
+ }
1752
+ // ── 9. Undefined References (deprecated package imports) ──────
1753
+ if (isJsTs) {
1754
+ const DEPRECATED_IMPORTS = ['request', 'request-promise', 'node-fetch@2'];
1755
+ for (const pkg of DEPRECATED_IMPORTS) {
1756
+ // Match exact package name in from 'pkg' or require('pkg') — not subpaths like 'request/...'
1757
+ const escapedPkg = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1758
+ const importPattern = new RegExp(`(?:from\\s+['"]${escapedPkg}['"]|require\\s*\\(\\s*['"]${escapedPkg}['"]\\s*\\))`);
1759
+ for (let i = 0; i < lines.length; i++) {
1760
+ if (importPattern.test(lines[i])) {
1761
+ findings.push({
1762
+ checkType: 'undefined_reference',
1763
+ line: i + 1,
1764
+ evidence: `Line ${i + 1} imports deprecated package '${pkg}' — use a maintained alternative (e.g., node-fetch v3, got, or native fetch)`,
1765
+ severity: 'high',
1766
+ });
1767
+ break; // One finding per deprecated package
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ // ── 10. Type Annotations (exported functions missing return type) ─
1773
+ // Skip .tsx files entirely — React components conventionally omit return types
1774
+ if (langLower === 'typescript' || langLower === 'ts') {
1775
+ for (let i = 0; i < lines.length; i++) {
1776
+ const orig = lines[i];
1777
+ // Match `export function name(` or `export async function name(`
1778
+ // Skip `export default function` — common React/Next.js pattern that omits return type
1779
+ // Skip Next.js route handlers (GET, POST, PUT, DELETE, PATCH) — conventionally untyped
1780
+ if (/export\s+(?:async\s+)?function\s+\w+\s*\(/.test(orig) &&
1781
+ !/export\s+default\s+(?:async\s+)?function/.test(orig) &&
1782
+ !/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/.test(orig)) {
1783
+ // Extract the portion from the opening paren to end of line
1784
+ // A return type annotation looks like `): SomeType` after the closing paren
1785
+ // We check if the line (or up to 2 continuation lines) contains `): `
1786
+ const snippet = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
1787
+ if (!/\)\s*:\s*\w/.test(snippet)) {
1788
+ findings.push({
1789
+ checkType: 'type_annotation',
1790
+ line: i + 1,
1791
+ evidence: `Line ${i + 1} exports a function without an explicit return type annotation`,
1792
+ severity: 'medium',
1793
+ });
1794
+ }
1795
+ }
1796
+ }
1797
+ }
1798
+ // ── 11. Connectivity Check (fetch/axios without timeout) ──────
1799
+ // Use original lines (not cleanLines) because preprocessCode mangles URLs with // in them
1800
+ if (isJsTs) {
1801
+ for (let i = 0; i < lines.length; i++) {
1802
+ const orig = lines[i];
1803
+ if (/\bfetch\s*\(/.test(orig) || /\baxios\b/.test(orig)) {
1804
+ // Check this line plus the next 2 lines for AbortSignal / timeout / signal
1805
+ const window = lines.slice(i, Math.min(i + 3, lines.length)).join('\n');
1806
+ if (!/(?:AbortSignal|timeout|signal)/.test(window)) {
1807
+ findings.push({
1808
+ checkType: 'connectivity_check',
1809
+ line: i + 1,
1810
+ evidence: `Line ${i + 1} makes a network request without a timeout or AbortSignal — may hang indefinitely`,
1811
+ severity: 'medium',
1812
+ });
1813
+ }
1814
+ }
1815
+ }
1816
+ }
1817
+ // ── 12. Async forEach (silently drops promises) ──────────────
1818
+ if (isJsTs) {
1819
+ for (let i = 0; i < cleanLines.length; i++) {
1820
+ if (/\.forEach\s*\(\s*async\b/.test(cleanLines[i])) {
1821
+ findings.push({
1822
+ checkType: 'error_handling',
1823
+ line: i + 1,
1824
+ evidence: `Line ${i + 1} passes async callback to forEach — promises are silently dropped. Use for...of with await instead`,
1825
+ severity: 'critical',
1826
+ });
1827
+ }
1828
+ }
1829
+ }
1830
+ // ── 13. Console.log/debug in production code ───────────────
1831
+ if (isJsTs) {
1832
+ // Count total console.log/debug calls in the file
1833
+ // If >5, the file is likely a CLI tool, logger, or test helper — skip entirely
1834
+ const consoleLogCount = cleanLines.filter(l => /\bconsole\.(log|debug)\s*\(/.test(l)).length;
1835
+ if (consoleLogCount <= 5) {
1836
+ for (let i = 0; i < cleanLines.length; i++) {
1837
+ if (/\bconsole\.(log|debug)\s*\(/.test(cleanLines[i])) {
1838
+ findings.push({
1839
+ checkType: 'debug_artifact',
1840
+ line: i + 1,
1841
+ evidence: `Line ${i + 1} has console.log/debug statement — remove before production`,
1842
+ severity: 'medium',
1843
+ });
1844
+ }
1845
+ }
1846
+ }
1847
+ }
1848
+ // ── 14. Loose equality (== null, != undefined) ─────────────
1849
+ if (isJsTs) {
1850
+ for (let i = 0; i < cleanLines.length; i++) {
1851
+ const cl = cleanLines[i];
1852
+ // Skip lines that are entirely comments (preprocessCode empties them)
1853
+ if (/^\s*$/.test(cl))
1854
+ continue;
1855
+ // Use cleanLines to avoid matching inside comments/strings
1856
+ if (/[^!=]==\s*(null|undefined)\b/.test(cl) && !/===\s*(null|undefined)\b/.test(cl)) {
1857
+ findings.push({
1858
+ checkType: 'equality_check',
1859
+ line: i + 1,
1860
+ evidence: `Line ${i + 1} uses loose equality (==) with null/undefined — use strict equality (===) instead`,
1861
+ severity: 'medium',
1862
+ });
1863
+ }
1864
+ // Check for != (but not !==)
1865
+ if (/(?<!=)!=\s*(null|undefined)\b/.test(cl) && !/!==\s*(null|undefined)\b/.test(cl)) {
1866
+ findings.push({
1867
+ checkType: 'equality_check',
1868
+ line: i + 1,
1869
+ evidence: `Line ${i + 1} uses loose inequality (!=) with null/undefined — use strict inequality (!==) instead`,
1870
+ severity: 'medium',
1871
+ });
1872
+ }
1873
+ }
1874
+ }
1875
+ // ── 15. RegExp injection (user input to new RegExp) ────────
1876
+ if (isJsTs) {
1877
+ for (let i = 0; i < cleanLines.length; i++) {
1878
+ const cl = cleanLines[i];
1879
+ // After preprocessCode, string literals become "__STR__"
1880
+ // Flag: new RegExp(variable) where arg is NOT a string literal
1881
+ if (/\bnew\s+RegExp\s*\(/.test(cl) && !/\bnew\s+RegExp\s*\(\s*"__STR__"/.test(cl)) {
1882
+ findings.push({
1883
+ checkType: 'security_control',
1884
+ line: i + 1,
1885
+ evidence: `Line ${i + 1} passes a variable to new RegExp() — user-controlled patterns can cause ReDoS. Escape input or use a literal regex`,
1886
+ severity: 'critical',
1887
+ });
1888
+ }
1889
+ }
1890
+ }
1891
+ // ── 16. Promise.all without error handling ─────────────────
1892
+ if (isJsTs) {
1893
+ for (let i = 0; i < cleanLines.length; i++) {
1894
+ if (/\bPromise\.all\s*\(/.test(cleanLines[i])) {
1895
+ const windowStart = Math.max(0, i - 5);
1896
+ const windowEnd = Math.min(cleanLines.length, i + 6);
1897
+ const context = cleanLines.slice(windowStart, windowEnd).join('\n');
1898
+ const hasCatch = /\.catch\s*\(/.test(context);
1899
+ const hasTryCatch = /\btry\s*\{/.test(context) && /\}\s*catch\s*\(/.test(context);
1900
+ if (!hasCatch && !hasTryCatch) {
1901
+ findings.push({
1902
+ checkType: 'error_handling',
1903
+ line: i + 1,
1904
+ evidence: `Line ${i + 1} uses Promise.all() without error handling — one rejection crashes all. Consider Promise.allSettled() or add .catch()`,
1905
+ severity: 'high',
1906
+ });
1907
+ }
1908
+ }
1909
+ }
1910
+ }
1911
+ // ── 17. parseInt without radix ─────────────────────────────
1912
+ if (isJsTs) {
1913
+ for (let i = 0; i < cleanLines.length; i++) {
1914
+ if (/\bparseInt\s*\(\s*\w[^,)]*\)/.test(cleanLines[i])) {
1915
+ findings.push({
1916
+ checkType: 'api_misuse',
1917
+ line: i + 1,
1918
+ evidence: `Line ${i + 1} calls parseInt() without radix parameter — may produce unexpected results with leading zeros`,
1919
+ severity: 'medium',
1920
+ });
1921
+ }
1922
+ }
1923
+ }
1924
+ // ── 18. Fetch body on GET/HEAD ─────────────────────────────
1925
+ if (isJsTs) {
1926
+ for (let i = 0; i < lines.length; i++) {
1927
+ // Use original lines since preprocessCode strips string contents
1928
+ if (/\bfetch\s*\(/.test(lines[i])) {
1929
+ const windowEnd = Math.min(lines.length, i + 10);
1930
+ const block = lines.slice(i, windowEnd).join('\n');
1931
+ const hasGetOrHead = /method\s*:\s*['"](?:GET|HEAD)['"]/i.test(block);
1932
+ const hasBody = /\bbody\s*:/.test(block);
1933
+ if (hasGetOrHead && hasBody) {
1934
+ findings.push({
1935
+ checkType: 'api_misuse',
1936
+ line: i + 1,
1937
+ evidence: `Line ${i + 1} sends a body with GET/HEAD request — body is ignored by most servers`,
1938
+ severity: 'high',
1939
+ });
1940
+ }
1941
+ }
1942
+ }
1943
+ }
1944
+ // ── 19. addEventListener without cleanup in useEffect ──────
1945
+ if (isJsTs) {
1946
+ const codeText = cleanLines.join('\n');
1947
+ if (/\buseEffect\b/.test(codeText) && /\baddEventListener\b/.test(codeText)) {
1948
+ const addCount = (codeText.match(/\baddEventListener\b/g) || []).length;
1949
+ const removeCount = (codeText.match(/\bremoveEventListener\b/g) || []).length;
1950
+ if (addCount > removeCount) {
1951
+ // Find the first addEventListener line for the line number
1952
+ for (let i = 0; i < cleanLines.length; i++) {
1953
+ if (/\baddEventListener\b/.test(cleanLines[i])) {
1954
+ findings.push({
1955
+ checkType: 'resource_leak',
1956
+ line: i + 1,
1957
+ evidence: `Line ${i + 1} has addEventListener in useEffect without matching removeEventListener — memory leak`,
1958
+ severity: 'high',
1959
+ });
1960
+ break;
1961
+ }
1962
+ }
1963
+ }
1964
+ }
1965
+ }
1966
+ // ── 20. Empty catch block ──────────────────────────────────
1967
+ if (isJsTs) {
1968
+ for (let i = 0; i < cleanLines.length; i++) {
1969
+ const cl = cleanLines[i];
1970
+ // catch (e) {} or catch {} on same line
1971
+ if (/\bcatch\s*(?:\(\s*\w*\s*\))?\s*\{\s*\}/.test(cl)) {
1972
+ findings.push({
1973
+ checkType: 'error_handling',
1974
+ line: i + 1,
1975
+ evidence: `Line ${i + 1} has empty catch block — swallows errors silently. At minimum log the error`,
1976
+ severity: 'high',
1977
+ });
1978
+ continue;
1979
+ }
1980
+ // catch block where { is at end of line and } is on the next line (with only whitespace)
1981
+ if (/\bcatch\s*(?:\(\s*\w*\s*\))?\s*\{\s*$/.test(cl)) {
1982
+ const nextLine = cleanLines[i + 1];
1983
+ if (nextLine !== undefined && /^\s*\}\s*$/.test(nextLine)) {
1984
+ findings.push({
1985
+ checkType: 'error_handling',
1986
+ line: i + 1,
1987
+ evidence: `Line ${i + 1} has empty catch block — swallows errors silently. At minimum log the error`,
1988
+ severity: 'high',
1989
+ });
1990
+ }
1991
+ }
1992
+ }
1993
+ }
1994
+ // ── 21. dangerouslySetInnerHTML ────────────────────────────
1995
+ if (isJsTs) {
1996
+ for (let i = 0; i < cleanLines.length; i++) {
1997
+ if (/\bdangerouslySetInnerHTML\b/.test(cleanLines[i])) {
1998
+ findings.push({
1999
+ checkType: 'security_control',
2000
+ line: i + 1,
2001
+ evidence: `Line ${i + 1} uses dangerouslySetInnerHTML — bypasses React XSS protection. Ensure input is sanitized`,
2002
+ severity: 'critical',
2003
+ });
2004
+ }
2005
+ }
2006
+ }
2007
+ // ── 22. Hardcoded http:// URLs (not localhost) ─────────────
2008
+ if (isJsTs) {
2009
+ for (let i = 0; i < lines.length; i++) {
2010
+ const orig = lines[i];
2011
+ if (/['"`]http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(orig)) {
2012
+ findings.push({
2013
+ checkType: 'security_control',
2014
+ line: i + 1,
2015
+ evidence: `Line ${i + 1} has hardcoded http:// URL — use https:// to prevent mixed content and MITM attacks`,
2016
+ severity: 'medium',
2017
+ });
2018
+ break; // One per file
2019
+ }
2020
+ }
2021
+ }
2022
+ // ── 23. document.write() ───────────────────────────────────
2023
+ if (isJsTs) {
2024
+ for (let i = 0; i < cleanLines.length; i++) {
2025
+ if (/\bdocument\.write\s*\(/.test(cleanLines[i])) {
2026
+ findings.push({
2027
+ checkType: 'security_control',
2028
+ line: i + 1,
2029
+ evidence: `Line ${i + 1} uses document.write() — overwrites entire page if called after load. Use DOM manipulation instead`,
2030
+ severity: 'critical',
2031
+ });
2032
+ }
2033
+ }
2034
+ }
2035
+ // ── 24. setTimeout/setInterval with string argument ────────
2036
+ if (isJsTs) {
2037
+ for (let i = 0; i < cleanLines.length; i++) {
2038
+ const cl = cleanLines[i];
2039
+ // After preprocessCode, string literals become "__STR__"
2040
+ if (/\b(?:setTimeout|setInterval)\s*\(\s*"__STR__"/.test(cl)) {
2041
+ findings.push({
2042
+ checkType: 'security_control',
2043
+ line: i + 1,
2044
+ evidence: `Line ${i + 1} passes a string to setTimeout/setInterval — acts as eval(). Use a function instead`,
2045
+ severity: 'critical',
2046
+ });
2047
+ }
2048
+ }
2049
+ }
2050
+ // ── 25. Missing await on fetch() ───────────────────────────
2051
+ if (isJsTs) {
2052
+ for (let i = 0; i < cleanLines.length; i++) {
2053
+ const cl = cleanLines[i];
2054
+ if (/(?:const|let|var)\s+\w+\s*=\s*fetch\s*\(/.test(cl) && !/\bawait\b/.test(cl)) {
2055
+ findings.push({
2056
+ checkType: 'error_handling',
2057
+ line: i + 1,
2058
+ evidence: `Line ${i + 1} assigns fetch() without await — variable holds a Promise, not the Response`,
2059
+ severity: 'high',
2060
+ });
2061
+ }
2062
+ }
2063
+ }
2064
+ // ── 26. Throw string instead of Error ──────────────────────
2065
+ if (isJsTs) {
2066
+ for (let i = 0; i < lines.length; i++) {
2067
+ if (/\bthrow\s+["'`]/.test(lines[i])) {
2068
+ findings.push({
2069
+ checkType: 'error_handling',
2070
+ line: i + 1,
2071
+ evidence: `Line ${i + 1} throws a string literal — stack trace will be lost. Use throw new Error(...) instead`,
2072
+ severity: 'medium',
2073
+ });
2074
+ }
2075
+ }
2076
+ }
2077
+ // ── 27. Array.sort without comparator ──────────────────────
2078
+ if (isJsTs) {
2079
+ for (let i = 0; i < cleanLines.length; i++) {
2080
+ if (/\.sort\s*\(\s*\)/.test(cleanLines[i])) {
2081
+ findings.push({
2082
+ checkType: 'api_misuse',
2083
+ line: i + 1,
2084
+ evidence: `Line ${i + 1} calls .sort() without comparator — converts elements to strings. [10,2,1].sort() gives [1,10,2]`,
2085
+ severity: 'medium',
2086
+ });
2087
+ }
2088
+ }
2089
+ }
2090
+ return findings;
2091
+ }
935
2092
  //# sourceMappingURL=formal-verifier.js.map