clinkx 1.0.0 → 1.1.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 (333) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +184 -112
  3. package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
  4. package/clinkx-workflows/dist/chairman-knobs.js +27 -0
  5. package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
  6. package/clinkx-workflows/dist/clink-client.d.ts +10 -0
  7. package/clinkx-workflows/dist/clink-client.js +39 -1
  8. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  9. package/clinkx-workflows/dist/config.d.ts +29 -1
  10. package/clinkx-workflows/dist/config.js +35 -2
  11. package/clinkx-workflows/dist/config.js.map +1 -1
  12. package/clinkx-workflows/dist/context-preflight.d.ts +5 -0
  13. package/clinkx-workflows/dist/context-preflight.js.map +1 -1
  14. package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
  15. package/clinkx-workflows/dist/council-execution-policy.js +110 -0
  16. package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
  17. package/clinkx-workflows/dist/council-expander.d.ts +9 -8
  18. package/clinkx-workflows/dist/council-expander.js +18 -9
  19. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  20. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5557 -4
  21. package/clinkx-workflows/dist/dashboard/activity-contract.js +55 -3
  22. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -1
  23. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +9 -5
  24. package/clinkx-workflows/dist/dashboard/activity-reader.js +15 -2
  25. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -1
  26. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +4 -0
  27. package/clinkx-workflows/dist/dashboard/api-routes.js +133 -25
  28. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -1
  29. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
  30. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
  31. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
  32. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +36 -2
  33. package/clinkx-workflows/dist/dashboard/context-run-reader.js +142 -19
  34. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -1
  35. package/clinkx-workflows/dist/dashboard/contracts.d.ts +20027 -480
  36. package/clinkx-workflows/dist/dashboard/contracts.js +133 -6
  37. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
  38. package/clinkx-workflows/dist/dashboard/index.d.ts +4 -0
  39. package/clinkx-workflows/dist/dashboard/index.js +4 -0
  40. package/clinkx-workflows/dist/dashboard/index.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
  42. package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
  43. package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +14 -1
  45. package/clinkx-workflows/dist/dashboard/lazy-content.js +53 -2
  46. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -1
  47. package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
  48. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
  49. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
  51. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
  52. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +147 -4
  54. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +11 -11
  55. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -1
  56. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +4 -56
  57. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -1
  58. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +2 -1
  59. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +6 -5
  60. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -1
  61. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
  62. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
  63. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
  64. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
  65. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
  66. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
  67. package/clinkx-workflows/dist/dashboard/request-admission.js +4 -0
  68. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -1
  69. package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
  70. package/clinkx-workflows/dist/dashboard/scanner.js +52 -8
  71. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
  72. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +14 -1
  73. package/clinkx-workflows/dist/dashboard/server-factory.js +73 -21
  74. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -1
  75. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +223 -56
  76. package/clinkx-workflows/dist/dashboard/session-log-contract.js +26 -0
  77. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -1
  78. package/clinkx-workflows/dist/dashboard/session-log-journal.js +10 -3
  79. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -1
  80. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +9 -1
  81. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +793 -59
  82. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
  83. package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
  84. package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
  85. package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
  86. package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
  87. package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
  88. package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
  89. package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
  90. package/clinkx-workflows/dist/delegate-signature.js +14 -0
  91. package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
  92. package/clinkx-workflows/dist/delegate-validate.d.ts +32 -2
  93. package/clinkx-workflows/dist/delegate-validate.js +51 -0
  94. package/clinkx-workflows/dist/delegate-validate.js.map +1 -1
  95. package/clinkx-workflows/dist/engine.d.ts +113 -3
  96. package/clinkx-workflows/dist/engine.js +476 -80
  97. package/clinkx-workflows/dist/engine.js.map +1 -1
  98. package/clinkx-workflows/dist/errors.d.ts +22 -2
  99. package/clinkx-workflows/dist/errors.js +30 -2
  100. package/clinkx-workflows/dist/errors.js.map +1 -1
  101. package/clinkx-workflows/dist/fidelity.d.ts +47 -1
  102. package/clinkx-workflows/dist/fidelity.js +54 -1
  103. package/clinkx-workflows/dist/fidelity.js.map +1 -1
  104. package/clinkx-workflows/dist/fingerprint.d.ts +24 -6
  105. package/clinkx-workflows/dist/fingerprint.js +32 -9
  106. package/clinkx-workflows/dist/fingerprint.js.map +1 -1
  107. package/clinkx-workflows/dist/index.js +53 -1
  108. package/clinkx-workflows/dist/index.js.map +1 -1
  109. package/clinkx-workflows/dist/logger.d.ts +1 -1
  110. package/clinkx-workflows/dist/logger.js +17 -3
  111. package/clinkx-workflows/dist/logger.js.map +1 -1
  112. package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
  113. package/clinkx-workflows/dist/monotonic-clock.js +22 -0
  114. package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
  115. package/clinkx-workflows/dist/plan-review/contract.d.ts +52 -52
  116. package/clinkx-workflows/dist/plan-review/publication.d.ts +4 -4
  117. package/clinkx-workflows/dist/plan-review/run-state.d.ts +16 -16
  118. package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
  119. package/clinkx-workflows/dist/roster-contract.js +180 -0
  120. package/clinkx-workflows/dist/roster-contract.js.map +1 -0
  121. package/clinkx-workflows/dist/rosters.d.ts +14 -0
  122. package/clinkx-workflows/dist/rosters.js +169 -0
  123. package/clinkx-workflows/dist/rosters.js.map +1 -0
  124. package/clinkx-workflows/dist/run-health.d.ts +7 -0
  125. package/clinkx-workflows/dist/run-health.js +25 -0
  126. package/clinkx-workflows/dist/run-health.js.map +1 -0
  127. package/clinkx-workflows/dist/schema.d.ts +64 -0
  128. package/clinkx-workflows/dist/schema.js +24 -0
  129. package/clinkx-workflows/dist/schema.js.map +1 -1
  130. package/clinkx-workflows/dist/server.js +2 -0
  131. package/clinkx-workflows/dist/server.js.map +1 -1
  132. package/clinkx-workflows/dist/shutdown.d.ts +15 -2
  133. package/clinkx-workflows/dist/shutdown.js +60 -16
  134. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  135. package/clinkx-workflows/dist/state-schema.d.ts +23 -0
  136. package/clinkx-workflows/dist/state.d.ts +5 -0
  137. package/clinkx-workflows/dist/state.js +128 -21
  138. package/clinkx-workflows/dist/state.js.map +1 -1
  139. package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
  140. package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
  141. package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
  142. package/clinkx-workflows/dist/types.d.ts +6 -0
  143. package/clinkx-workflows/dist/validators/council.d.ts +50 -50
  144. package/clinkx-workflows/dist/validators/plan-review.d.ts +3 -3
  145. package/clinkx-workflows/dist/workflow-tools.d.ts +70 -0
  146. package/clinkx-workflows/dist/workflow-tools.js +760 -168
  147. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  148. package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
  149. package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
  150. package/clinkx-workflows/templates/council/answer-specialist.txt +3 -0
  151. package/clinkx-workflows/templates/council/chairman.txt +3 -0
  152. package/clinkx-workflows/templates/council/code-review-chairman.txt +3 -0
  153. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +3 -0
  154. package/clinkx-workflows/templates/council/code-review-reviewer.txt +3 -0
  155. package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
  156. package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
  157. package/clinkx-workflows/templates/council/debug-debugger.txt +3 -0
  158. package/clinkx-workflows/templates/council/discover-analyst.txt +3 -0
  159. package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
  160. package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
  161. package/clinkx-workflows/templates/council/member.txt +3 -0
  162. package/clinkx-workflows/templates/council/plan-review-chairman.txt +3 -0
  163. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +3 -0
  164. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +3 -0
  165. package/clinkx-workflows/templates/council/reviewer.txt +3 -0
  166. package/clinkx-workflows/workflows/council-answer.yaml +3 -0
  167. package/clinkx-workflows/workflows/council-code-review.yaml +3 -0
  168. package/clinkx-workflows/workflows/council-debug.yaml +3 -0
  169. package/clinkx-workflows/workflows/council-default.yaml +7 -0
  170. package/clinkx-workflows/workflows/council-discover.yaml +3 -0
  171. package/clinkx-workflows/workflows/council-plan-review.yaml +3 -0
  172. package/conf/adapters/agy.json +9 -2
  173. package/conf/adapters/claude.json +7 -0
  174. package/conf/adapters/codex.json +7 -0
  175. package/conf/adapters/glm.json +19 -3
  176. package/conf/adapters/grok.json +9 -2
  177. package/conf/adapters/hapi/agy.json +9 -2
  178. package/conf/adapters/hapi/claude.json +7 -0
  179. package/conf/adapters/hapi/codex.json +7 -0
  180. package/conf/adapters/hapi/glm.json +20 -4
  181. package/conf/rosters/default-council.json +21 -0
  182. package/dist/activity-journal.d.ts +17 -3
  183. package/dist/activity-journal.js +404 -79
  184. package/dist/activity-journal.js.map +1 -1
  185. package/dist/codex/agent-tools.js +36 -0
  186. package/dist/codex/agent-tools.js.map +1 -1
  187. package/dist/codex/app-server-client.d.ts +3 -0
  188. package/dist/codex/app-server-client.js +11 -0
  189. package/dist/codex/app-server-client.js.map +1 -1
  190. package/dist/codex/app-server-registry.d.ts +18 -0
  191. package/dist/codex/app-server-registry.js +88 -2
  192. package/dist/codex/app-server-registry.js.map +1 -1
  193. package/dist/codex/client-types.d.ts +7 -1
  194. package/dist/codex/protocol.d.ts +4 -4
  195. package/dist/config.d.ts +220 -23
  196. package/dist/config.js +104 -1
  197. package/dist/config.js.map +1 -1
  198. package/dist/context-builder/bundle.d.ts +13 -0
  199. package/dist/context-builder/bundle.js +46 -0
  200. package/dist/context-builder/bundle.js.map +1 -1
  201. package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
  202. package/dist/context-builder/delegate-finalizer-effective.js +82 -0
  203. package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
  204. package/dist/context-builder/delegate-packet.d.ts +18 -0
  205. package/dist/context-builder/delegate-packet.js +73 -9
  206. package/dist/context-builder/delegate-packet.js.map +1 -1
  207. package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
  208. package/dist/context-builder/delegate-reconstruction.js +110 -41
  209. package/dist/context-builder/delegate-reconstruction.js.map +1 -1
  210. package/dist/context-builder/delegate-validation.d.ts +31 -0
  211. package/dist/context-builder/delegate-validation.js +87 -10
  212. package/dist/context-builder/delegate-validation.js.map +1 -1
  213. package/dist/context-builder/delegate.d.ts +9 -0
  214. package/dist/context-builder/delegate.js +210 -59
  215. package/dist/context-builder/delegate.js.map +1 -1
  216. package/dist/context-builder/diagnostics.d.ts +1 -1
  217. package/dist/context-builder/diagnostics.js.map +1 -1
  218. package/dist/context-builder/discovery-progress.d.ts +25 -0
  219. package/dist/context-builder/discovery-progress.js +64 -0
  220. package/dist/context-builder/discovery-progress.js.map +1 -0
  221. package/dist/context-builder/discovery-watchdog.d.ts +15 -0
  222. package/dist/context-builder/discovery-watchdog.js +39 -0
  223. package/dist/context-builder/discovery-watchdog.js.map +1 -0
  224. package/dist/context-builder/discovery.d.ts +7 -0
  225. package/dist/context-builder/discovery.js +119 -7
  226. package/dist/context-builder/discovery.js.map +1 -1
  227. package/dist/context-builder/external-gaps.d.ts +73 -0
  228. package/dist/context-builder/external-gaps.js +248 -0
  229. package/dist/context-builder/external-gaps.js.map +1 -0
  230. package/dist/context-builder/globs.d.ts +1 -0
  231. package/dist/context-builder/globs.js +15 -0
  232. package/dist/context-builder/globs.js.map +1 -1
  233. package/dist/context-builder/handler.d.ts +42 -1
  234. package/dist/context-builder/handler.js +690 -62
  235. package/dist/context-builder/handler.js.map +1 -1
  236. package/dist/context-builder/invocation-sidecar.d.ts +41 -0
  237. package/dist/context-builder/invocation-sidecar.js +189 -0
  238. package/dist/context-builder/invocation-sidecar.js.map +1 -0
  239. package/dist/context-builder/lifecycle-contract.d.ts +51 -51
  240. package/dist/context-builder/mutation-lock.d.ts +32 -0
  241. package/dist/context-builder/mutation-lock.js +321 -0
  242. package/dist/context-builder/mutation-lock.js.map +1 -0
  243. package/dist/context-builder/review-coverage.d.ts +32 -0
  244. package/dist/context-builder/review-coverage.js +130 -23
  245. package/dist/context-builder/review-coverage.js.map +1 -1
  246. package/dist/context-builder/schema.d.ts +76 -1
  247. package/dist/context-builder/schema.js +21 -1
  248. package/dist/context-builder/schema.js.map +1 -1
  249. package/dist/context-builder/secrets.d.ts +31 -0
  250. package/dist/context-builder/secrets.js +871 -84
  251. package/dist/context-builder/secrets.js.map +1 -1
  252. package/dist/context-builder/status.d.ts +1 -1
  253. package/dist/context-builder/status.js +1 -0
  254. package/dist/context-builder/status.js.map +1 -1
  255. package/dist/context-builder/storage.d.ts +29 -5
  256. package/dist/context-builder/storage.js +311 -236
  257. package/dist/context-builder/storage.js.map +1 -1
  258. package/dist/context-builder/versions.d.ts +19 -4
  259. package/dist/context-builder/versions.js +19 -4
  260. package/dist/context-builder/versions.js.map +1 -1
  261. package/dist/context-builder/workflow-runner.d.ts +6 -0
  262. package/dist/context-builder/workflow-runner.js +363 -173
  263. package/dist/context-builder/workflow-runner.js.map +1 -1
  264. package/dist/dashboard-adapter-catalog.js +7 -0
  265. package/dist/dashboard-adapter-catalog.js.map +1 -1
  266. package/dist/dashboard-cli.d.ts +2 -0
  267. package/dist/dashboard-cli.js +40 -2
  268. package/dist/dashboard-cli.js.map +1 -1
  269. package/dist/dashboard-companion/client.d.ts +1 -0
  270. package/dist/dashboard-companion/client.js +22 -3
  271. package/dist/dashboard-companion/client.js.map +1 -1
  272. package/dist/dashboard-companion/protocol.js +4 -1
  273. package/dist/dashboard-companion/protocol.js.map +1 -1
  274. package/dist/dashboard-gateway/index.d.ts +1 -0
  275. package/dist/dashboard-gateway/index.js +33 -2
  276. package/dist/dashboard-gateway/index.js.map +1 -1
  277. package/dist/handler.js +5 -1
  278. package/dist/handler.js.map +1 -1
  279. package/dist/index.d.ts +18 -1
  280. package/dist/index.js +145 -7
  281. package/dist/index.js.map +1 -1
  282. package/dist/logger.d.ts +1 -1
  283. package/dist/logger.js +18 -3
  284. package/dist/logger.js.map +1 -1
  285. package/dist/parsers/agy-stream-json.d.ts +14 -0
  286. package/dist/parsers/agy-stream-json.js +172 -0
  287. package/dist/parsers/agy-stream-json.js.map +1 -0
  288. package/dist/parsers/extract.js +5 -1
  289. package/dist/parsers/extract.js.map +1 -1
  290. package/dist/parsers/observed-invocation.d.ts +20 -0
  291. package/dist/parsers/observed-invocation.js +42 -0
  292. package/dist/parsers/observed-invocation.js.map +1 -0
  293. package/dist/pipeline.d.ts +13 -0
  294. package/dist/pipeline.js +60 -10
  295. package/dist/pipeline.js.map +1 -1
  296. package/dist/progress-report/lock.d.ts +74 -0
  297. package/dist/progress-report/lock.js +343 -0
  298. package/dist/progress-report/lock.js.map +1 -0
  299. package/dist/progress-report/schema.d.ts +808 -0
  300. package/dist/progress-report/schema.js +119 -0
  301. package/dist/progress-report/schema.js.map +1 -0
  302. package/dist/progress-report/store.d.ts +66 -0
  303. package/dist/progress-report/store.js +766 -0
  304. package/dist/progress-report/store.js.map +1 -0
  305. package/dist/progress-report/tools.d.ts +20 -0
  306. package/dist/progress-report/tools.js +141 -0
  307. package/dist/progress-report/tools.js.map +1 -0
  308. package/dist/prompt-mode.d.ts +1 -1
  309. package/dist/prompt-mode.js +7 -2
  310. package/dist/prompt-mode.js.map +1 -1
  311. package/dist/runner.d.ts +7 -0
  312. package/dist/runner.js +10 -3
  313. package/dist/runner.js.map +1 -1
  314. package/dist/safety.d.ts +50 -0
  315. package/dist/safety.js +182 -1
  316. package/dist/safety.js.map +1 -1
  317. package/dist/schema.d.ts +8 -0
  318. package/dist/schema.js +20 -0
  319. package/dist/schema.js.map +1 -1
  320. package/dist/server.d.ts +6 -1
  321. package/dist/server.js +94 -8
  322. package/dist/server.js.map +1 -1
  323. package/dist/session-log-stream.d.ts +1 -1
  324. package/dist/session-log-stream.js +280 -33
  325. package/dist/session-log-stream.js.map +1 -1
  326. package/dist/stdio-activity.d.ts +28 -0
  327. package/dist/stdio-activity.js +72 -0
  328. package/dist/stdio-activity.js.map +1 -0
  329. package/docs/configuration.md +426 -0
  330. package/docs/security.md +293 -0
  331. package/package.json +5 -5
  332. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
  333. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
@@ -22,6 +22,657 @@ const MAX_YAML_SOURCE_CHARS = 5_000_000;
22
22
  const MAX_JS_KEY_LOOKBACK = 32_768;
23
23
  const MAX_TS_TYPE_CHARS = 4_096;
24
24
  const MAX_TS_TYPE_DEPTH = 32;
25
+ /**
26
+ * Fail-closed provenance for a body whose deterministic scan work exceeded
27
+ * its budget. The budget is a backstop, not the performance mechanism: the
28
+ * type-body header scan is a deterministic delimiter-aware linear scan, and
29
+ * the other index-backed detectors are linear and memoized per text, so
30
+ * realistic and generated corpora never trip it. Known residuals: the
31
+ * dense_fields shape stays super-linear but bounded (tracked follow-up), and
32
+ * the value-span/redaction regexes still have catastrophic-backtracking
33
+ * shapes (member-call-chain, backslash runs; see the scan-wedge issue doc)
34
+ * that can wedge inside a single regex match before the budget trips. Work
35
+ * is charged at real interrupt points (per header candidate, mid index
36
+ * build), so exhaustion unwinds and fails closed. Revisit the calibration
37
+ * if a legitimate body ever reports this reason.
38
+ */
39
+ export const SANITIZE_BUDGET_EXHAUSTED_PROVENANCE = "sanitize_budget_exhausted";
40
+ // Calibrated at >= 20x the linearized realistic-shape cost, with headroom
41
+ // for the dense_fields adversarial shape (measured ~4_000 work units per
42
+ // char at 256 KiB; see tests/context-builder/secrets-perf.test.ts). Larger
43
+ // or denser pathological bodies exhaust it by design and fail closed.
44
+ const SECRET_SCAN_WORK_PER_CHAR = 6_000;
45
+ const SECRET_SCAN_WORK_FLOOR = 4_000_000;
46
+ class SecretScanBudgetExhausted extends Error {
47
+ }
48
+ let activeSecretScanSession;
49
+ let secretScanWorkTotal = 0;
50
+ let secretScanBudgetOverride;
51
+ function chargeWork(units) {
52
+ secretScanWorkTotal += units;
53
+ const session = activeSecretScanSession;
54
+ if (session != null) {
55
+ session.work += units;
56
+ if (session.work > session.limit) {
57
+ throw new SecretScanBudgetExhausted();
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Runs one public detector entry point inside a per-body session. Nested
63
+ * entry points (sanitize -> scan/redact/residual) share the outer budget and
64
+ * memo; exhaustion unwinds to the outermost entry, which fails closed.
65
+ */
66
+ function withSecretScanSession(text, work, onExhausted) {
67
+ if (activeSecretScanSession != null) {
68
+ return work();
69
+ }
70
+ activeSecretScanSession = {
71
+ limit: secretScanBudgetOverride ??
72
+ SECRET_SCAN_WORK_FLOOR + SECRET_SCAN_WORK_PER_CHAR * text.length,
73
+ work: 0,
74
+ texts: new Map(),
75
+ };
76
+ try {
77
+ return work();
78
+ }
79
+ catch (error) {
80
+ if (error instanceof SecretScanBudgetExhausted) {
81
+ return onExhausted();
82
+ }
83
+ throw error;
84
+ }
85
+ finally {
86
+ activeSecretScanSession = undefined;
87
+ }
88
+ }
89
+ function analysisFor(text) {
90
+ const session = activeSecretScanSession;
91
+ if (session == null) {
92
+ return undefined;
93
+ }
94
+ let analysis = session.texts.get(text);
95
+ if (analysis == null) {
96
+ analysis = {};
97
+ session.texts.set(text, analysis);
98
+ }
99
+ return analysis;
100
+ }
101
+ /** Test-only: deterministic work units consumed by `work`. */
102
+ export function measureSecretScanWorkForTests(work) {
103
+ const before = secretScanWorkTotal;
104
+ const result = work();
105
+ return { result, work: secretScanWorkTotal - before };
106
+ }
107
+ /** Test-only: run `work` with a fixed per-body work budget. */
108
+ export function withSecretScanBudgetForTests(limit, work) {
109
+ const previous = secretScanBudgetOverride;
110
+ secretScanBudgetOverride = limit;
111
+ try {
112
+ return work();
113
+ }
114
+ finally {
115
+ secretScanBudgetOverride = previous;
116
+ }
117
+ }
118
+ /** Test-only: (start, brace) offset of every type-body header match. */
119
+ export function typeBodyHeaderMatchesForTests(text) {
120
+ const headers = collectTypeBodyHeaders(text);
121
+ const pairs = [];
122
+ for (let slot = 0; slot < headers.starts.length; slot++) {
123
+ const start = headers.starts[slot];
124
+ const brace = headers.braces[slot];
125
+ if (start != null && brace != null) {
126
+ pairs.push({ start, brace });
127
+ }
128
+ }
129
+ return pairs;
130
+ }
131
+ function budgetExhaustedFinding() {
132
+ return contentFinding(SANITIZE_BUDGET_EXHAUSTED_PROVENANCE, 1);
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Linear per-text indexes.
136
+ //
137
+ // The detectors below used to rescan a fresh 32 KiB lookback slice per '='
138
+ // (regex matchAll over the window plus a brace scan, per query). Each index is
139
+ // built once per distinct body-level string per scan session, memoized in the
140
+ // SecretTextAnalysis slots, and queried with binary searches / O(1) lookups.
141
+ // Query semantics replicate the legacy slice behavior exactly, including the
142
+ // slice-relative `^`/`\b` window edge (a header or typed field starting
143
+ // exactly at the window start counts) — see the per-query comments.
144
+ // ---------------------------------------------------------------------------
145
+ const TYPED_FIELD_PATTERN = /(?:^|[;{}\r\n])[ \t]*(?:(?:public|private|protected|static|readonly|declare|abstract|override)[ \t]+)*([A-Za-z_$][A-Za-z0-9_$-]*)(?:[?!])?[ \t]*:[ \t]*/g;
146
+ const TYPED_FIELD_MODIFIERS = [
147
+ "public",
148
+ "private",
149
+ "protected",
150
+ "static",
151
+ "readonly",
152
+ "declare",
153
+ "abstract",
154
+ "override",
155
+ ];
156
+ const MAX_TS_TYPE_ONLY_FIELD_CHARS = 4_096;
157
+ // Same composition as the per-call `new RegExp` the malformed-YAML span walk
158
+ // used to compile for every candidate value start.
159
+ const MALFORMED_YAML_CONTINUES_PATTERN = new RegExp(`^(?:&[A-Za-z0-9_.-]+[ \\t]+|!!?[^ \\t\\r\\n]+[ \\t]+|!<[^>\\r\\n]+>[ \\t]+)*${YAML_BLOCK_HEADER_PATTERN_SOURCE}(?:[ \\t]+#.*)?$`);
160
+ /**
161
+ * (start, brace) offset of every `class`/`interface`/`type` header, from a
162
+ * delimiter-aware linear scan equivalent to matchAll of
163
+ * /\b(?:class|interface|type)\s+[A-Za-z_$][A-Za-z0-9_$]*[^{};]*\{/g (the
164
+ * pattern's greedy `[^{};]*` backtracks one char at a time per candidate, so
165
+ * a header succeeds exactly when the first `{`/`}`/`;` after the ident is
166
+ * `{`; resolving that stopper through an amortized forward scan removed the
167
+ * quadratic blowup on dense failed-header bodies). Deliberately not
168
+ * quote-aware: matches inside
169
+ * strings and comments are golden-pinned semantics. `charge` gates the
170
+ * budget charges: the whole-text index build charges here, while the bounded
171
+ * lookback-slice fallback does not (its caller already charges the slice
172
+ * length, and the slice volume per body is a pre-existing property).
173
+ */
174
+ function collectTypeBodyHeaders(text, charge = true) {
175
+ const length = text.length;
176
+ const starts = [];
177
+ const braces = [];
178
+ if (charge) {
179
+ chargeWork(length);
180
+ }
181
+ // Stopper lookup (first `{`/`}`/`;` at or after a cursor) via one amortized
182
+ // forward scan: region [regionStart, regionStopper) provably holds no
183
+ // stopper, so every candidate cursor inside it reuses regionStopper — a
184
+ // dense run of failed candidates pays one scan, not one per candidate.
185
+ // The absent-stopper sentinel is regionStopper === length, so the
186
+ // text[stopper] === "{" success test fails naturally at EOF. No index
187
+ // array is allocated: the lookback-slice fallback below runs thousands of
188
+ // times per body on the dense shapes, and a per-call Int32Array plus full
189
+ // backward pass dominated the wall clock there.
190
+ let regionStart = 0;
191
+ let regionStopper = -1;
192
+ const stopperAfter = (from) => {
193
+ if (regionStart <= from && from <= regionStopper) {
194
+ return regionStopper;
195
+ }
196
+ let index = from;
197
+ while (index < length) {
198
+ const code = text.charCodeAt(index);
199
+ if (code === 0x7b || code === 0x7d || code === 0x3b) {
200
+ break;
201
+ }
202
+ index++;
203
+ }
204
+ regionStart = from;
205
+ regionStopper = index;
206
+ return index;
207
+ };
208
+ // Fresh locator per invocation: literal alternation plus lookahead, no
209
+ // backtracking structure. The regex \b stays the boundary authority (word
210
+ // chars are [A-Za-z0-9_], so `$class` matches while `xclass` and `Class`
211
+ // do not); lastIndex is managed by hand so a successful header can
212
+ // swallow its span the way matchAll's lastIndex jump did.
213
+ const locator = /\b(?:class|interface|type)(?=\s)/g;
214
+ let keyword;
215
+ while ((keyword = locator.exec(text)) != null) {
216
+ const start = keyword.index;
217
+ let cursor = start + keyword[0].length;
218
+ while (cursor < length && isJsWhitespaceChar(text[cursor] ?? "")) {
219
+ cursor++;
220
+ }
221
+ if (cursor >= length || !isIdentStartCode(text.charCodeAt(cursor))) {
222
+ if (charge) {
223
+ chargeWork(8);
224
+ }
225
+ continue;
226
+ }
227
+ const identStart = cursor;
228
+ while (cursor < length && isIdentCharCode(text.charCodeAt(cursor))) {
229
+ cursor++;
230
+ }
231
+ if (charge) {
232
+ chargeWork(8 + (cursor - identStart));
233
+ }
234
+ const stopper = stopperAfter(cursor);
235
+ // Failed candidates never jump the locator: it is already past the
236
+ // keyword, and the very next keyword may still succeed
237
+ // (`class A; type B {`). Jumping to the failure-stopper + 1 would also
238
+ // be safe; this scan commits to no-jump.
239
+ if (text.charCodeAt(stopper) === 0x7b) {
240
+ starts.push(start);
241
+ braces.push(stopper);
242
+ locator.lastIndex = stopper + 1;
243
+ }
244
+ }
245
+ return { starts, braces };
246
+ }
247
+ function typeBodyIndexFor(text) {
248
+ const analysis = analysisFor(text);
249
+ const cached = analysis?.typeBody;
250
+ if (cached != null) {
251
+ return cached;
252
+ }
253
+ // collectTypeBodyHeaders charges the amortized stopper scan and, per
254
+ // candidate, 8 + identLength — real work with honest budget interrupt
255
+ // points between candidates (plain JS checks, not event-loop yields). This
256
+ // charge covers the stack pass below; mid-enumeration exhaustion unwinds
257
+ // and fails closed instead of leaving a partial index in the session memo.
258
+ const headers = collectTypeBodyHeaders(text);
259
+ chargeWork(text.length);
260
+ const headerStarts = headers.starts;
261
+ const headerBraces = headers.braces;
262
+ const pastEnd = text.length + 1;
263
+ // Per-position close/exceed facts for every `{`, from one stack pass. The
264
+ // stack bases are strictly increasing (unique per open frame), so the frame
265
+ // that newly exceeds MAX_TS_TYPE_DEPTH when depth becomes d is exactly the
266
+ // one with base d - 33, found by binary search.
267
+ const closeAt = new Int32Array(text.length);
268
+ const exceedAt = new Int32Array(text.length);
269
+ closeAt.fill(pastEnd);
270
+ exceedAt.fill(pastEnd);
271
+ const stackPos = [];
272
+ const stackBase = [];
273
+ const stackExceed = [];
274
+ const stackHeaderSlot = [];
275
+ let depth = 0;
276
+ const headerSlots = new Map();
277
+ for (let slot = 0; slot < headerBraces.length; slot++) {
278
+ headerSlots.set(headerBraces[slot] ?? -1, slot);
279
+ }
280
+ for (let index = 0; index < text.length; index++) {
281
+ const code = text.charCodeAt(index);
282
+ if (code === 0x7b) {
283
+ stackPos.push(index);
284
+ stackBase.push(depth);
285
+ stackExceed.push(pastEnd);
286
+ stackHeaderSlot.push(headerSlots.get(index) ?? -1);
287
+ depth++;
288
+ const exceedBase = depth - (MAX_TS_TYPE_DEPTH + 1);
289
+ const frame = binarySearchExact(stackBase, exceedBase);
290
+ if (frame >= 0 && (stackExceed[frame] ?? pastEnd) === pastEnd) {
291
+ stackExceed[frame] = index + 1;
292
+ }
293
+ continue;
294
+ }
295
+ if (code === 0x7d) {
296
+ const pos = stackPos.pop();
297
+ const base = stackBase.pop();
298
+ const exceed = stackExceed.pop();
299
+ const headerSlot = stackHeaderSlot.pop();
300
+ depth--;
301
+ if (pos == null || base == null || exceed == null || headerSlot == null) {
302
+ continue;
303
+ }
304
+ closeAt[pos] = index + 1;
305
+ exceedAt[pos] = exceed;
306
+ continue;
307
+ }
308
+ }
309
+ // Frames never popped: closeAt stays pastEnd; keep any recorded exceed.
310
+ for (let frame = 0; frame < stackPos.length; frame++) {
311
+ const pos = stackPos[frame];
312
+ const exceed = stackExceed[frame];
313
+ if (pos == null || exceed == null) {
314
+ continue;
315
+ }
316
+ exceedAt[pos] = exceed;
317
+ }
318
+ const index = {
319
+ starts: Int32Array.from(headerStarts),
320
+ braces: Int32Array.from(headerBraces),
321
+ closeAt,
322
+ exceedAt,
323
+ pastEnd,
324
+ };
325
+ if (analysis != null) {
326
+ analysis.typeBody = index;
327
+ }
328
+ return index;
329
+ }
330
+ function typedFieldCandidatesFor(text) {
331
+ const analysis = analysisFor(text);
332
+ const cached = analysis?.typedFields;
333
+ if (cached != null) {
334
+ return cached;
335
+ }
336
+ chargeWork(2 * text.length);
337
+ const matchStarts = [];
338
+ const keyStarts = [];
339
+ const keyEnds = [];
340
+ const typeStarts = [];
341
+ for (const match of text.matchAll(TYPED_FIELD_PATTERN)) {
342
+ const rawKey = match[1] ?? "";
343
+ const matchStart = match.index ?? 0;
344
+ const keyStart = matchStart + match[0].lastIndexOf(rawKey);
345
+ matchStarts.push(matchStart);
346
+ keyStarts.push(keyStart);
347
+ keyEnds.push(keyStart + rawKey.length);
348
+ typeStarts.push(matchStart + match[0].length);
349
+ }
350
+ const candidates = {
351
+ matchStarts: Int32Array.from(matchStarts),
352
+ keyStarts: Int32Array.from(keyStarts),
353
+ keyEnds: Int32Array.from(keyEnds),
354
+ typeStarts: Int32Array.from(typeStarts),
355
+ count: matchStarts.length,
356
+ };
357
+ if (analysis != null) {
358
+ analysis.typedFields = candidates;
359
+ }
360
+ return candidates;
361
+ }
362
+ function newlineOffsetsFor(text) {
363
+ const analysis = analysisFor(text);
364
+ const cached = analysis?.newlineOffsets;
365
+ if (cached != null) {
366
+ return cached;
367
+ }
368
+ chargeWork(text.length);
369
+ const offsets = [];
370
+ for (let index = 0; index < text.length; index++) {
371
+ if (text.charCodeAt(index) === 0x0a) {
372
+ offsets.push(index);
373
+ }
374
+ }
375
+ const result = Int32Array.from(offsets);
376
+ if (analysis != null) {
377
+ analysis.newlineOffsets = result;
378
+ }
379
+ return result;
380
+ }
381
+ function nextSemicolonFor(text) {
382
+ const analysis = analysisFor(text);
383
+ const cached = analysis?.nextSemicolon;
384
+ if (cached != null) {
385
+ return cached;
386
+ }
387
+ chargeWork(text.length);
388
+ const result = new Int32Array(text.length + 1);
389
+ result.fill(-1);
390
+ for (let index = text.length - 1; index >= 0; index--) {
391
+ result[index] =
392
+ text.charCodeAt(index) === 0x3b ? index : result[index + 1] ?? -1;
393
+ }
394
+ if (analysis != null) {
395
+ analysis.nextSemicolon = result;
396
+ }
397
+ return result;
398
+ }
399
+ function yamlBlockHeaderIndexFor(text) {
400
+ const analysis = analysisFor(text);
401
+ const cached = analysis?.yamlBlockHeaders;
402
+ if (cached != null) {
403
+ return cached;
404
+ }
405
+ chargeWork(text.length);
406
+ const starts = [0];
407
+ for (let index = 0; index < text.length; index++) {
408
+ if (text.charCodeAt(index) === 0x0a) {
409
+ starts.push(index + 1);
410
+ }
411
+ }
412
+ const result = { lineStarts: Int32Array.from(starts) };
413
+ if (analysis != null) {
414
+ analysis.yamlBlockHeaders = result;
415
+ }
416
+ return result;
417
+ }
418
+ /** Rightmost index with values[index] < target (-1 when none). */
419
+ function binarySearchLastLessThan(values, target) {
420
+ let low = 0;
421
+ let high = values.length - 1;
422
+ let result = -1;
423
+ while (low <= high) {
424
+ const mid = (low + high) >>> 1;
425
+ if ((values[mid] ?? 0) < target) {
426
+ result = mid;
427
+ low = mid + 1;
428
+ }
429
+ else {
430
+ high = mid - 1;
431
+ }
432
+ }
433
+ return result;
434
+ }
435
+ /** Exact-match index in a sorted-ascending array (-1 when absent). */
436
+ function binarySearchExact(values, target) {
437
+ let low = 0;
438
+ let high = values.length - 1;
439
+ while (low <= high) {
440
+ const mid = (low + high) >>> 1;
441
+ const value = values[mid] ?? 0;
442
+ if (value === target) {
443
+ return mid;
444
+ }
445
+ if (value < target) {
446
+ low = mid + 1;
447
+ }
448
+ else {
449
+ high = mid - 1;
450
+ }
451
+ }
452
+ return -1;
453
+ }
454
+ function isJsWordCode(code) {
455
+ return ((code >= 0x30 && code <= 0x39) ||
456
+ (code >= 0x41 && code <= 0x5a) ||
457
+ (code >= 0x61 && code <= 0x7a) ||
458
+ code === 0x5f ||
459
+ code === 0x24);
460
+ }
461
+ function isIdentStartCode(code) {
462
+ return ((code >= 0x41 && code <= 0x5a) ||
463
+ (code >= 0x61 && code <= 0x7a) ||
464
+ code === 0x5f ||
465
+ code === 0x24);
466
+ }
467
+ function isIdentCharCode(code) {
468
+ return isJsWordCode(code);
469
+ }
470
+ function isJsWhitespaceChar(char) {
471
+ return /\s/.test(char);
472
+ }
473
+ function isLineTerminatorCode(code) {
474
+ return (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029);
475
+ }
476
+ /**
477
+ * Brace offset of a `class`/`interface`/`type` header starting exactly at
478
+ * `start` (the slice-relative `\b` window edge: a header beginning exactly at
479
+ * the lookback window start counts even when the preceding character would
480
+ * block the word boundary in the full text). `-1` when no header completes
481
+ * before `limit`.
482
+ */
483
+ function typeBodyHeaderBraceAt(text, start, limit) {
484
+ const keywordLength = text.startsWith("interface", start)
485
+ ? "interface".length
486
+ : text.startsWith("class", start)
487
+ ? "class".length
488
+ : text.startsWith("type", start)
489
+ ? "type".length
490
+ : -1;
491
+ if (keywordLength < 0) {
492
+ return -1;
493
+ }
494
+ let cursor = start + keywordLength;
495
+ if (cursor >= limit || !isJsWhitespaceChar(text[cursor] ?? "")) {
496
+ return -1;
497
+ }
498
+ while (cursor < limit && isJsWhitespaceChar(text[cursor] ?? "")) {
499
+ cursor++;
500
+ }
501
+ if (cursor >= limit || !isIdentStartCode(text.charCodeAt(cursor))) {
502
+ return -1;
503
+ }
504
+ cursor++;
505
+ while (cursor < limit && isIdentCharCode(text.charCodeAt(cursor))) {
506
+ cursor++;
507
+ }
508
+ // `[^{};]*` runs to the first stopper; only `{` can complete the header.
509
+ while (cursor < limit) {
510
+ const code = text.charCodeAt(cursor);
511
+ if (code === 0x7b) {
512
+ return cursor;
513
+ }
514
+ if (code === 0x7d || code === 0x3b) {
515
+ return -1;
516
+ }
517
+ cursor++;
518
+ }
519
+ return -1;
520
+ }
521
+ /**
522
+ * Index-backed equivalent of the legacy isInsideBoundedTypeBody(prefix,
523
+ * offset) where `prefix` was the caller's window slice starting at
524
+ * `windowStart` (0 for full-text callers). Falls back to the exact legacy
525
+ * slice scan when a header match straddles the window start — the one
526
+ * configuration where slice sub-matches exist that a whole-text matchAll
527
+ * never records.
528
+ */
529
+ function isInsideBoundedTypeBodyAt(text, windowStart, offset) {
530
+ const lo = Math.max(0, windowStart, offset - MAX_JS_KEY_LOOKBACK);
531
+ if (lo >= offset) {
532
+ return false;
533
+ }
534
+ const index = typeBodyIndexFor(text);
535
+ chargeWork(8);
536
+ const straddling = binarySearchLastLessThan(index.starts, lo);
537
+ if (straddling >= 0 &&
538
+ (index.braces[straddling] ?? -1) >= lo) {
539
+ chargeWork(offset - lo);
540
+ return isInsideBoundedTypeBodyBefore(text.slice(lo, offset));
541
+ }
542
+ let brace = -1;
543
+ const lastVisible = binarySearchLastLessThan(index.braces, offset);
544
+ if (lastVisible >= 0 &&
545
+ (index.starts[lastVisible] ?? -1) >= lo) {
546
+ brace = index.braces[lastVisible] ?? -1;
547
+ }
548
+ if (lo > 0 && isJsWordCode(text.charCodeAt(lo - 1))) {
549
+ const synthetic = typeBodyHeaderBraceAt(text, lo, offset);
550
+ if (synthetic > brace) {
551
+ brace = synthetic;
552
+ }
553
+ }
554
+ if (brace < 0) {
555
+ return false;
556
+ }
557
+ return ((index.closeAt[brace] ?? index.pastEnd) > offset &&
558
+ (index.exceedAt[brace] ?? index.pastEnd) > offset);
559
+ }
560
+ /** Exact legacy scan over an already-sliced lookback `before`. */
561
+ function isInsideBoundedTypeBodyBefore(before) {
562
+ // Shared header scan on the slice: offset 0 counts as a boundary, exactly
563
+ // the slice-relative \b semantics the slice's own matchAll had. Only the
564
+ // last header's brace feeds the depth walk (legacy last-match semantics).
565
+ // Uncharged: the caller already charges the slice length, and this fallback
566
+ // runs per straddling lookback query (thousands of 32 KiB slices on the
567
+ // dense shapes), so double-charging here would exhaust the budget.
568
+ const classBrace = collectTypeBodyHeaders(before, false).braces.at(-1) ?? -1;
569
+ if (classBrace < 0) {
570
+ return false;
571
+ }
572
+ let depth = 0;
573
+ for (let index = classBrace; index < before.length; index++) {
574
+ if (before[index] === "{") {
575
+ depth++;
576
+ }
577
+ else if (before[index] === "}") {
578
+ depth--;
579
+ if (depth === 0) {
580
+ return false;
581
+ }
582
+ }
583
+ if (depth > MAX_TS_TYPE_DEPTH) {
584
+ return false;
585
+ }
586
+ }
587
+ return depth > 0;
588
+ }
589
+ /**
590
+ * Parses `[ \t]*(modifier[ \t]+)*key[?!]?[ \t]*:[ \t]*` starting exactly at
591
+ * `start` — the typed-field pattern with the boundary alternative elided.
592
+ * This reproduces the regex's greedy modifier consumption with rightmost
593
+ * backtracking (e.g. `readonly : T` parses with key `readonly`), which is
594
+ * what a window slice's slice-relative `^` anchoring would see at position 0.
595
+ */
596
+ function matchTypedFieldContinuationAt(text, start, limit) {
597
+ let cursor = start;
598
+ while (cursor < limit &&
599
+ (text.charCodeAt(cursor) === 0x20 || text.charCodeAt(cursor) === 0x09)) {
600
+ cursor++;
601
+ }
602
+ const modifierStarts = [];
603
+ for (;;) {
604
+ let matched = -1;
605
+ for (const modifier of TYPED_FIELD_MODIFIERS) {
606
+ if (text.startsWith(modifier, cursor)) {
607
+ matched = modifier.length;
608
+ break;
609
+ }
610
+ }
611
+ if (matched < 0 ||
612
+ cursor + matched >= limit ||
613
+ (text.charCodeAt(cursor + matched) !== 0x20 &&
614
+ text.charCodeAt(cursor + matched) !== 0x09)) {
615
+ break;
616
+ }
617
+ modifierStarts.push(cursor);
618
+ cursor += matched;
619
+ while (cursor < limit &&
620
+ (text.charCodeAt(cursor) === 0x20 || text.charCodeAt(cursor) === 0x09)) {
621
+ cursor++;
622
+ }
623
+ }
624
+ for (;;) {
625
+ const keyStart = cursor;
626
+ if (keyStart < limit && isIdentStartCode(text.charCodeAt(keyStart))) {
627
+ let keyEnd = keyStart + 1;
628
+ // The regex key class is [A-Za-z0-9_$-]*: hyphens are legal key
629
+ // continuations (e.g. `api-key: string;`) even though they are not
630
+ // word characters.
631
+ while (keyEnd < limit &&
632
+ (isIdentCharCode(text.charCodeAt(keyEnd)) ||
633
+ text.charCodeAt(keyEnd) === 0x2d)) {
634
+ keyEnd++;
635
+ }
636
+ let afterKey = keyEnd;
637
+ const maybeOptional = text.charCodeAt(afterKey);
638
+ if (maybeOptional === 0x3f || maybeOptional === 0x21) {
639
+ afterKey++;
640
+ }
641
+ while (afterKey < limit &&
642
+ (text.charCodeAt(afterKey) === 0x20 ||
643
+ text.charCodeAt(afterKey) === 0x09)) {
644
+ afterKey++;
645
+ }
646
+ if (afterKey < limit && text.charCodeAt(afterKey) === 0x3a) {
647
+ let typeStart = afterKey + 1;
648
+ while (typeStart < limit &&
649
+ (text.charCodeAt(typeStart) === 0x20 ||
650
+ text.charCodeAt(typeStart) === 0x09)) {
651
+ typeStart++;
652
+ }
653
+ return { keyStart, keyEnd, typeStart };
654
+ }
655
+ }
656
+ if (modifierStarts.length === 0) {
657
+ return undefined;
658
+ }
659
+ cursor = modifierStarts.pop() ?? start;
660
+ }
661
+ }
662
+ /** Memoized whole-text comment mask; only called on body-level strings. */
663
+ function commentMaskedFor(text) {
664
+ const analysis = analysisFor(text);
665
+ const cached = analysis?.commentMasked;
666
+ if (cached != null) {
667
+ return cached;
668
+ }
669
+ chargeWork(2 * text.length);
670
+ const masked = maskJsCommentTrivia(text);
671
+ if (analysis != null) {
672
+ analysis.commentMasked = masked;
673
+ }
674
+ return masked;
675
+ }
25
676
  const PLACEHOLDER_VALUES = new Set([
26
677
  "changeme",
27
678
  "example",
@@ -77,6 +728,9 @@ export function scanSecretPath(rootRelativePath) {
77
728
  return dedupeFindings(findings);
78
729
  }
79
730
  export function scanHighConfidenceSecretContent(text) {
731
+ return withSecretScanSession(text, () => scanHighConfidenceSecretContentInSession(text), () => [budgetExhaustedFinding()]);
732
+ }
733
+ function scanHighConfidenceSecretContentInSession(text) {
80
734
  const findings = [];
81
735
  const privateKeyLine = findPatternLine(text, PRIVATE_KEY_BLOCK_PATTERN);
82
736
  if (privateKeyLine != null) {
@@ -98,7 +752,14 @@ export function scanHighConfidenceSecretContent(text) {
98
752
  export function isBlockingSecretFinding(finding) {
99
753
  return finding.provenance !== "assignment_secret";
100
754
  }
755
+ /**
756
+ * Budget exhaustion here fails closed by removing the whole body: a partial
757
+ * redaction cannot be proven complete.
758
+ */
101
759
  export function redactHighConfidenceAssignmentSecrets(text) {
760
+ return withSecretScanSession(text, () => redactHighConfidenceAssignmentSecretsInSession(text), () => "");
761
+ }
762
+ function redactHighConfidenceAssignmentSecretsInSession(text) {
102
763
  const secretAssignments = [...iterateAssignmentSecrets(text)].filter((assignment) => assignment.confidence === "high").sort((left, right) => left.valueStart - right.valueStart);
103
764
  let redacted = text;
104
765
  for (let index = secretAssignments.length - 1; index >= 0; index--) {
@@ -116,6 +777,9 @@ export function redactHighConfidenceAssignmentSecrets(text) {
116
777
  * content the primary assignment grammar did not classify.
117
778
  */
118
779
  export function sanitizeHighConfidenceSecretContent(text, residualDetector = hasConservativeResidualSecret) {
780
+ return withSecretScanSession(text, () => sanitizeHighConfidenceSecretContentInSession(text, residualDetector), () => ({ status: "blocked", content: "", findings: [budgetExhaustedFinding()] }));
781
+ }
782
+ function sanitizeHighConfidenceSecretContentInSession(text, residualDetector) {
119
783
  const findings = scanHighConfidenceSecretContent(text);
120
784
  if (findings.some(isBlockingSecretFinding)) {
121
785
  return { status: "blocked", content: "", findings };
@@ -146,6 +810,10 @@ export function sanitizeHighConfidenceSecretContent(text, residualDetector = has
146
810
  * YAML block continuations with a separate, intentionally conservative lexer.
147
811
  */
148
812
  export function hasConservativeResidualSecret(text) {
813
+ return withSecretScanSession(text, () => hasConservativeResidualSecretInSession(text), () => true);
814
+ }
815
+ function hasConservativeResidualSecretInSession(text) {
816
+ chargeWork(text.length * (1 + WELL_KNOWN_TOKEN_PATTERNS.length));
149
817
  if (PRIVATE_KEY_BLOCK_PATTERN.test(text)) {
150
818
  return true;
151
819
  }
@@ -176,7 +844,7 @@ export function hasDisallowedSecretMaterial(text, allowSecrets) {
176
844
  if (allowSecrets === true) {
177
845
  return false;
178
846
  }
179
- return hasHighConfidenceSecret(text) || hasConservativeResidualSecret(text);
847
+ return withSecretScanSession(text, () => hasHighConfidenceSecret(text) || hasConservativeResidualSecret(text), () => true);
180
848
  }
181
849
  export function assertNoHighConfidenceSecretsInTask(task) {
182
850
  if (!hasDisallowedSecretMaterial(task, false)) {
@@ -244,6 +912,7 @@ function isSecretDirectorySegment(segment) {
244
912
  segment === "credentials");
245
913
  }
246
914
  function findPatternLine(text, pattern) {
915
+ chargeWork(2 * text.length);
247
916
  const lines = text.split(/\r?\n/);
248
917
  for (let index = 0; index < lines.length; index++) {
249
918
  if (pattern.test(lines[index] ?? "")) {
@@ -371,6 +1040,19 @@ function isRedactionMarkerValue(value) {
371
1040
  .trim().length === 0));
372
1041
  }
373
1042
  function analyzeYamlStructuralSecrets(text) {
1043
+ const analysis = analysisFor(text);
1044
+ const cached = analysis?.yaml;
1045
+ if (cached != null) {
1046
+ return cached;
1047
+ }
1048
+ const result = analyzeYamlStructuralSecretsUncached(text);
1049
+ if (analysis != null) {
1050
+ analysis.yaml = result;
1051
+ }
1052
+ return result;
1053
+ }
1054
+ function analyzeYamlStructuralSecretsUncached(text) {
1055
+ chargeWork(text.length);
374
1056
  if (text.length > MAX_YAML_SOURCE_CHARS) {
375
1057
  return { ...emptyYamlStructuralAnalysis(), recognized: true, exhausted: true };
376
1058
  }
@@ -415,6 +1097,7 @@ function analyzeYamlStructuralSecrets(text) {
415
1097
  let current = node;
416
1098
  const active = new Set();
417
1099
  for (let depth = 0; depth <= MAX_YAML_DEPTH; depth++) {
1100
+ chargeWork(1);
418
1101
  if (current == null || ++nodesSeen > MAX_YAML_NODES) {
419
1102
  analysis.exhausted = true;
420
1103
  return undefined;
@@ -436,6 +1119,7 @@ function analyzeYamlStructuralSecrets(text) {
436
1119
  if (node == null || analysis.exhausted) {
437
1120
  return;
438
1121
  }
1122
+ chargeWork(1);
439
1123
  nodesSeen++;
440
1124
  if (nodesSeen > MAX_YAML_NODES || depth > MAX_YAML_DEPTH) {
441
1125
  analysis.exhausted = true;
@@ -644,6 +1328,12 @@ function emptyYamlStructuralAnalysis() {
644
1328
  };
645
1329
  }
646
1330
  function hasMalformedYamlSensitiveSecret(text) {
1331
+ const analysis = analysisFor(text);
1332
+ const cached = analysis?.malformedYamlSecret;
1333
+ if (cached != null) {
1334
+ return cached;
1335
+ }
1336
+ let result = false;
647
1337
  // Valid TypeScript class fields are not YAML merely because a nested type
648
1338
  // property has a sensitive name. Remove only declarations whose bounded RHS
649
1339
  // is an unambiguous reference; literal/object RHS values remain visible to
@@ -656,37 +1346,48 @@ function hasMalformedYamlSensitiveSecret(text) {
656
1346
  continue;
657
1347
  }
658
1348
  if (spanContainsHighEntropyLiteral(span)) {
659
- return true;
1349
+ result = true;
1350
+ break;
660
1351
  }
661
1352
  }
662
- return false;
1353
+ if (analysis != null) {
1354
+ analysis.malformedYamlSecret = result;
1355
+ }
1356
+ return result;
663
1357
  }
664
1358
  function malformedYamlValueSpan(text, valueStart) {
665
- const firstLineEnd = lineEndIndex(text, valueStart);
1359
+ const { lineStarts } = yamlBlockHeaderIndexFor(text);
1360
+ const lineSlot = binarySearchLastLessThan(lineStarts, valueStart + 1);
1361
+ let lineIndex = lineSlot < 0 ? 0 : lineSlot;
1362
+ const firstLineEnd = lineEndViaStarts(text, lineStarts, lineIndex);
666
1363
  const firstLine = text.slice(valueStart, firstLineEnd);
667
1364
  const firstValue = firstLine.replace(/^[ \t]*/, "");
668
1365
  const continues = firstValue.length === 0 ||
669
1366
  firstValue.startsWith("#") ||
670
- new RegExp(`^(?:&[A-Za-z0-9_.-]+[ \\t]+|!!?[^ \\t\\r\\n]+[ \\t]+|!<[^>\\r\\n]+>[ \\t]+)*${YAML_BLOCK_HEADER_PATTERN_SOURCE}(?:[ \\t]+#.*)?$`).test(firstValue);
1367
+ MALFORMED_YAML_CONTINUES_PATTERN.test(firstValue);
671
1368
  if (!continues) {
672
1369
  return firstLine;
673
1370
  }
674
- const keyLineStart = text.lastIndexOf("\n", valueStart - 1) + 1;
1371
+ const keyLineStart = lineStarts[lineIndex] ?? 0;
675
1372
  const keyIndent = leadingIndentWidth(text.slice(keyLineStart, valueStart));
676
1373
  let cursor = firstLineEnd;
677
1374
  let end = firstLineEnd;
678
1375
  let lines = 0;
679
1376
  while (cursor < text.length && lines <= 512 && cursor - valueStart < 32_768) {
680
- const newlineLength = text.startsWith("\r\n", cursor)
681
- ? 2
682
- : text[cursor] === "\n"
683
- ? 1
684
- : 0;
685
- if (newlineLength === 0) {
1377
+ chargeWork(1);
1378
+ if (lineIndex + 1 >= lineStarts.length) {
686
1379
  break;
687
1380
  }
688
- const lineStart = cursor + newlineLength;
689
- const lineEnd = lineEndIndex(text, lineStart);
1381
+ const lineStart = lineStarts[lineIndex + 1] ?? text.length;
1382
+ if (text.charCodeAt(cursor) === 0x0d) {
1383
+ if (text.charCodeAt(cursor + 1) !== 0x0a) {
1384
+ break;
1385
+ }
1386
+ }
1387
+ else if (text.charCodeAt(cursor) !== 0x0a) {
1388
+ break;
1389
+ }
1390
+ const lineEnd = lineEndViaStarts(text, lineStarts, lineIndex + 1);
690
1391
  const line = text.slice(lineStart, lineEnd);
691
1392
  if (line.trim().length > 0 && leadingIndentWidth(line) <= keyIndent) {
692
1393
  break;
@@ -694,25 +1395,89 @@ function malformedYamlValueSpan(text, valueStart) {
694
1395
  end = lineEnd;
695
1396
  cursor = lineEnd;
696
1397
  lines++;
1398
+ lineIndex++;
697
1399
  }
698
1400
  return text.slice(valueStart, end);
699
1401
  }
1402
+ /** End offset (exclusive of the line terminator) of line `lineIndex`. */
1403
+ function lineEndViaStarts(text, lineStarts, lineIndex) {
1404
+ if (lineIndex + 1 >= lineStarts.length) {
1405
+ return text.length;
1406
+ }
1407
+ const nextStart = lineStarts[lineIndex + 1] ?? text.length;
1408
+ const newline = nextStart - 1;
1409
+ return newline > 0 && text.charCodeAt(newline - 1) === 0x0d
1410
+ ? newline - 1
1411
+ : newline;
1412
+ }
700
1413
  function maskConfidentlySafeTypeOnlyFields(text) {
701
- const typeOnlyField = /(^|[;{}\r\n])([ \t]*(?:(?:public|private|protected|static|readonly|declare|abstract|override)[ \t]+)*[A-Za-z_$][A-Za-z0-9_$-]*(?:[?!])?[ \t]*:[ \t]*)([\s\S]{1,4096}?);/gm;
702
- return text.replace(typeOnlyField, (whole, boundary, _prefix, type, offset) => {
703
- if (!isInsideBoundedTypeBody(text, offset + boundary.length) ||
704
- !looksLikeTsTypeExpression(type, true)) {
705
- return whole;
1414
+ // Manual linear scan reproducing
1415
+ // /(^|[;{}\r\n])(prefix)([\s\S]{1,4096}?);/gm.replace semantics exactly:
1416
+ // `^` only at line starts, `;`-terminated lazy type bodies resolved through
1417
+ // the next-semicolon index, and the scan resuming after each match's `;`
1418
+ // whether or not the match was rewritten.
1419
+ const nextSemicolon = nextSemicolonFor(text);
1420
+ const isTypeBodyBoundaryChar = (code) => code === 0x3b ||
1421
+ code === 0x7b ||
1422
+ code === 0x7d ||
1423
+ code === 0x0d ||
1424
+ code === 0x0a;
1425
+ let output = "";
1426
+ let copied = 0;
1427
+ let position = 0;
1428
+ while (position < text.length) {
1429
+ chargeWork(1);
1430
+ const atLineStart = position === 0 || isLineTerminatorCode(text.charCodeAt(position - 1));
1431
+ let boundaryLength = -1;
1432
+ let continuation;
1433
+ if (atLineStart) {
1434
+ continuation = matchTypedFieldContinuationAt(text, position, text.length);
1435
+ if (continuation != null) {
1436
+ boundaryLength = 0;
1437
+ }
706
1438
  }
707
- return `${boundary}${whole.slice(boundary.length).replace(/[^\r\n]/g, " ")}`;
708
- });
1439
+ if (boundaryLength < 0 && isTypeBodyBoundaryChar(text.charCodeAt(position))) {
1440
+ const afterBoundary = matchTypedFieldContinuationAt(text, position + 1, text.length);
1441
+ if (afterBoundary != null) {
1442
+ boundaryLength = 1;
1443
+ continuation = afterBoundary;
1444
+ }
1445
+ }
1446
+ if (boundaryLength < 0 || continuation == null) {
1447
+ position++;
1448
+ continue;
1449
+ }
1450
+ const typeStart = continuation.typeStart;
1451
+ const terminator = typeStart + 1 <= text.length ? nextSemicolon[typeStart + 1] ?? -1 : -1;
1452
+ if (terminator < 0 || terminator - typeStart > MAX_TS_TYPE_ONLY_FIELD_CHARS) {
1453
+ // The lazy group cannot reach a `;` in range. Prefix backtracking never
1454
+ // rescues the match: a shorter modifier run leaves a keyword followed by
1455
+ // whitespace, which the key production cannot re-parse.
1456
+ position++;
1457
+ continue;
1458
+ }
1459
+ const type = text.slice(typeStart, terminator);
1460
+ if (isInsideBoundedTypeBodyAt(text, 0, position + boundaryLength) &&
1461
+ looksLikeTsTypeExpression(type, true)) {
1462
+ output += text.slice(copied, position);
1463
+ output += text.slice(position, position + boundaryLength);
1464
+ for (let index = position + boundaryLength; index <= terminator; index++) {
1465
+ const code = text.charCodeAt(index);
1466
+ output += code === 0x0a || code === 0x0d ? text[index] ?? "" : " ";
1467
+ }
1468
+ copied = terminator + 1;
1469
+ }
1470
+ position = terminator + 1;
1471
+ }
1472
+ return output + text.slice(copied);
709
1473
  }
710
1474
  function maskConfidentlySafeTypedFields(text) {
711
- const masked = maskJsCommentTrivia(text);
1475
+ const masked = commentMaskedFor(text);
712
1476
  const safeSpans = [];
713
1477
  let quote;
714
1478
  let escaped = false;
715
1479
  for (let equalsAt = 0; equalsAt < masked.length; equalsAt++) {
1480
+ chargeWork(1);
716
1481
  const character = masked[equalsAt] ?? "";
717
1482
  if (quote != null) {
718
1483
  if (!escaped && character === quote) {
@@ -731,8 +1496,9 @@ function maskConfidentlySafeTypedFields(text) {
731
1496
  /[=>]/.test(masked[equalsAt + 1] ?? "")) {
732
1497
  continue;
733
1498
  }
1499
+ chargeWork(32);
734
1500
  const windowStart = Math.max(0, equalsAt - MAX_JS_KEY_LOOKBACK);
735
- const field = findBoundedTypedField(masked.slice(windowStart, equalsAt), windowStart);
1501
+ const field = findBoundedTypedField(masked, windowStart, equalsAt);
736
1502
  if (field == null || field === "exhausted") {
737
1503
  continue;
738
1504
  }
@@ -773,6 +1539,7 @@ function isSafeTypedFieldReference(source, value) {
773
1539
  /^keyof\s+typeof\s+[A-Za-z_$][A-Za-z0-9_$]*(?:(?:\.|::)[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(expression))));
774
1540
  }
775
1541
  function malformedYamlSensitiveValueStarts(text) {
1542
+ chargeWork(4 * text.length);
776
1543
  const starts = [];
777
1544
  const keyToken = String.raw `[A-Za-z_$][A-Za-z0-9_$-]*(?:\.[A-Za-z_$][A-Za-z0-9_$-]*)*`;
778
1545
  for (const match of text.matchAll(/^[ \t]*(["'])((?:\\.|(?!\1)[^\r\n])*)\1[ \t]*:/gm)) {
@@ -827,22 +1594,31 @@ function redactYamlScalarSource(source) {
827
1594
  return redactYamlBlockPayload(source);
828
1595
  }
829
1596
  function lineNumberAtOffset(text, offset) {
830
- let line = 1;
831
- for (let index = 0; index < offset; index++) {
832
- if (text[index] === "\n") {
833
- line++;
834
- }
835
- }
836
- return line;
1597
+ const newlineOffsets = newlineOffsetsFor(text);
1598
+ return 1 + binarySearchLastLessThan(newlineOffsets, offset) + 1;
837
1599
  }
838
1600
  function analyzeJsStructuralSecrets(text, primaryRedaction = true) {
839
- const masked = maskJsCommentTrivia(text);
1601
+ const analysis = analysisFor(text);
1602
+ const slot = primaryRedaction ? "jsPrimary" : "jsResidual";
1603
+ const cached = analysis?.[slot];
1604
+ if (cached != null) {
1605
+ return cached;
1606
+ }
1607
+ const result = analyzeJsStructuralSecretsUncached(text, primaryRedaction);
1608
+ if (analysis != null) {
1609
+ analysis[slot] = result;
1610
+ }
1611
+ return result;
1612
+ }
1613
+ function analyzeJsStructuralSecretsUncached(text, primaryRedaction) {
1614
+ const masked = commentMaskedFor(text);
840
1615
  const matches = [];
841
1616
  let unsafeSensitiveContext = false;
842
1617
  let quote;
843
1618
  let escaped = false;
844
1619
  let scanned = 0;
845
1620
  for (let cursor = 0; cursor < masked.length; cursor++) {
1621
+ chargeWork(1);
846
1622
  if (++scanned > 1_000_000) {
847
1623
  return { exhausted: true, unsafeSensitiveContext, matches };
848
1624
  }
@@ -864,6 +1640,7 @@ function analyzeJsStructuralSecrets(text, primaryRedaction = true) {
864
1640
  /[=>]/.test(masked[cursor + 1] ?? "")) {
865
1641
  continue;
866
1642
  }
1643
+ chargeWork(32);
867
1644
  const key = sensitiveJsKeyBefore(text, masked, cursor);
868
1645
  if (key === "exhausted") {
869
1646
  const span = residualJsValueSpan(text, masked, cursor + 1);
@@ -899,7 +1676,7 @@ function analyzeJsStructuralSecrets(text, primaryRedaction = true) {
899
1676
  continue;
900
1677
  }
901
1678
  const typedFieldWindowStart = Math.max(0, cursor - MAX_JS_KEY_LOOKBACK);
902
- const typedField = findBoundedTypedField(masked.slice(typedFieldWindowStart, cursor), typedFieldWindowStart);
1679
+ const typedField = findBoundedTypedField(masked, typedFieldWindowStart, cursor);
903
1680
  const safeTypedReference = typedField != null &&
904
1681
  typedField !== "exhausted" &&
905
1682
  reconstructed != null &&
@@ -993,11 +1770,11 @@ function spanContainsHighEntropyLiteral(span) {
993
1770
  }
994
1771
  function sensitiveJsKeyBefore(text, masked, equalsAt) {
995
1772
  const windowStart = Math.max(0, equalsAt - MAX_JS_KEY_LOOKBACK);
996
- const prefix = masked.slice(windowStart, equalsAt);
997
- const typedField = findSensitiveTypedField(prefix, windowStart);
1773
+ const typedField = findSensitiveTypedField(masked, windowStart, equalsAt);
998
1774
  if (typedField != null) {
999
1775
  return typedField;
1000
1776
  }
1777
+ const prefix = masked.slice(windowStart, equalsAt);
1001
1778
  const bracket = /\[\s*(["'])((?:\\.|(?!\1)[^\r\n])*)\1\s*\]\s*$/.exec(prefix);
1002
1779
  if (bracket != null) {
1003
1780
  const encoded = bracket[2] ?? "";
@@ -1079,8 +1856,8 @@ function classifyTsTypeAnnotation(value) {
1079
1856
  }
1080
1857
  return stack.length === 0 ? "valid" : "unclassifiable";
1081
1858
  }
1082
- function findSensitiveTypedField(prefix, absoluteStart) {
1083
- const field = findBoundedTypedField(prefix, absoluteStart);
1859
+ function findSensitiveTypedField(source, windowStart, windowEnd) {
1860
+ const field = findBoundedTypedField(source, windowStart, windowEnd);
1084
1861
  if (field === "exhausted" || field == null || field.sensitive) {
1085
1862
  return field === "exhausted" || field == null
1086
1863
  ? field
@@ -1088,23 +1865,30 @@ function findSensitiveTypedField(prefix, absoluteStart) {
1088
1865
  }
1089
1866
  return undefined;
1090
1867
  }
1091
- function findBoundedTypedField(prefix, absoluteStart) {
1092
- const pattern = /(?:^|[;{}\r\n])[ \t]*(?:(?:public|private|protected|static|readonly|declare|abstract|override)[ \t]+)*([A-Za-z_$][A-Za-z0-9_$-]*)(?:[?!])?[ \t]*:[ \t]*/g;
1093
- for (const match of prefix.matchAll(pattern)) {
1094
- const rawKey = match[1] ?? "";
1095
- const keyOffset = match[0].lastIndexOf(rawKey);
1096
- const relativeKeyStart = (match.index ?? 0) + keyOffset;
1097
- if (!isInsideBoundedTypeBody(prefix, relativeKeyStart)) {
1098
- continue;
1099
- }
1100
- const typeStart = (match.index ?? 0) + match[0].length;
1101
- const type = prefix.slice(typeStart).trim();
1868
+ function findBoundedTypedField(source, windowStart, windowEnd) {
1869
+ // Index-backed enumeration. A window slice's matchAll could additionally
1870
+ // match at position 0 through the `^` alternative (no boundary character
1871
+ // consumed) — including continuations of a declaration whose boundary sits
1872
+ // just before the window and mid-identifier splits. That candidate comes
1873
+ // first in slice scan order; precomputed matches starting inside it would
1874
+ // have been consumed by the slice scan, so iteration resumes from its end.
1875
+ const candidates = typedFieldCandidatesFor(source);
1876
+ chargeWork(8);
1877
+ const synthetic = matchTypedFieldContinuationAt(source, windowStart, windowEnd);
1878
+ const evaluate = (keyStart, keyEnd, typeStart) => {
1879
+ const rawKey = source.slice(keyStart, keyEnd);
1880
+ if (!isInsideBoundedTypeBodyAt(source, windowStart, keyStart)) {
1881
+ return "continue";
1882
+ }
1883
+ // The legacy window ended at windowEnd, so the type annotation under
1884
+ // examination is bounded by it exactly as the slice was.
1885
+ const type = source.slice(typeStart, windowEnd).trim();
1102
1886
  const classification = classifyTsTypeAnnotation(type);
1103
1887
  if (classification === "member_boundary") {
1104
- continue;
1888
+ return "continue";
1105
1889
  }
1106
1890
  if (classification === "nested_candidate") {
1107
- continue;
1891
+ return "continue";
1108
1892
  }
1109
1893
  if (classification === "unclassifiable") {
1110
1894
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(rawKey) &&
@@ -1114,41 +1898,34 @@ function findBoundedTypedField(prefix, absoluteStart) {
1114
1898
  }
1115
1899
  // The first candidate whose entire suffix is a balanced type annotation is
1116
1900
  // the outer field. Nested object-type properties leave unmatched closers.
1117
- const start = absoluteStart + relativeKeyStart;
1118
1901
  return {
1119
- start,
1120
- end: start + rawKey.length,
1902
+ start: keyStart,
1903
+ end: keyEnd,
1121
1904
  sensitive: isSensitiveDecodedKey(rawKey),
1122
1905
  };
1123
- }
1124
- return undefined;
1125
- }
1126
- function isInsideBoundedTypeBody(prefix, offset) {
1127
- const start = Math.max(0, offset - MAX_JS_KEY_LOOKBACK);
1128
- const before = prefix.slice(start, offset);
1129
- let classBrace = -1;
1130
- for (const match of before.matchAll(/\b(?:class|interface|type)\s+[A-Za-z_$][A-Za-z0-9_$]*[^{};]*\{/g)) {
1131
- classBrace = (match.index ?? 0) + match[0].length - 1;
1132
- }
1133
- if (classBrace < 0) {
1134
- return false;
1135
- }
1136
- let depth = 0;
1137
- for (let index = classBrace; index < before.length; index++) {
1138
- if (before[index] === "{") {
1139
- depth++;
1140
- }
1141
- else if (before[index] === "}") {
1142
- depth--;
1143
- if (depth === 0) {
1144
- return false;
1145
- }
1906
+ };
1907
+ if (synthetic != null) {
1908
+ chargeWork(4);
1909
+ const outcome = evaluate(synthetic.keyStart, synthetic.keyEnd, synthetic.typeStart);
1910
+ if (outcome !== "continue") {
1911
+ return outcome;
1912
+ }
1913
+ }
1914
+ const resumeFrom = synthetic != null ? synthetic.typeStart : windowStart;
1915
+ let slot = binarySearchLastLessThan(candidates.matchStarts, resumeFrom) + 1;
1916
+ while (slot < candidates.count) {
1917
+ chargeWork(4);
1918
+ const typeStart = candidates.typeStarts[slot] ?? 0;
1919
+ if (typeStart > windowEnd) {
1920
+ break;
1146
1921
  }
1147
- if (depth > MAX_TS_TYPE_DEPTH) {
1148
- return false;
1922
+ const outcome = evaluate(candidates.keyStarts[slot] ?? typeStart, candidates.keyEnds[slot] ?? typeStart, typeStart);
1923
+ if (outcome !== "continue") {
1924
+ return outcome;
1149
1925
  }
1926
+ slot++;
1150
1927
  }
1151
- return depth > 0;
1928
+ return undefined;
1152
1929
  }
1153
1930
  function findTsFieldAssignmentEquals(text, start, end) {
1154
1931
  for (let index = start; index < end; index++) {
@@ -1253,12 +2030,15 @@ function* iterateAssignmentSecrets(text) {
1253
2030
  if (!js.exhausted) {
1254
2031
  yield* js.matches;
1255
2032
  }
1256
- const commentMasked = maskJsCommentTrivia(text);
2033
+ const commentMasked = commentMaskedFor(text);
2034
+ const nextSemicolon = nextSemicolonFor(commentMasked);
2035
+ chargeWork(2 * text.length);
1257
2036
  let line = 1;
1258
2037
  let lineCursor = 0;
1259
2038
  const keyPattern = new RegExp(ASSIGNMENT_KEY_PATTERN.source, ASSIGNMENT_KEY_PATTERN.flags);
1260
2039
  let match;
1261
2040
  while ((match = keyPattern.exec(commentMasked)) != null) {
2041
+ chargeWork(16);
1262
2042
  const matchStart = match.index;
1263
2043
  for (let index = lineCursor; index < matchStart; index++) {
1264
2044
  if (text[index] === "\n") {
@@ -1293,19 +2073,21 @@ function* iterateAssignmentSecrets(text) {
1293
2073
  if (delimiter === ":") {
1294
2074
  const declarationStart = text.lastIndexOf("\n", matchStart - 1) + 1;
1295
2075
  const declarationEnd = lineEndIndex(text, cursor);
1296
- const typeTerminator = commentMasked.indexOf(";", cursor + 1);
2076
+ const typeTerminator = cursor + 1 <= commentMasked.length
2077
+ ? nextSemicolon[cursor + 1] ?? -1
2078
+ : -1;
1297
2079
  const typeOnlyDeclaration = typeTerminator >= 0 &&
1298
2080
  typeTerminator <= declarationEnd
1299
2081
  ? commentMasked.slice(cursor + 1, typeTerminator).trim()
1300
2082
  : "";
1301
2083
  if (typeOnlyDeclaration.length > 0 &&
1302
- isInsideBoundedTypeBody(commentMasked, keyStart) &&
2084
+ isInsideBoundedTypeBodyAt(commentMasked, 0, keyStart) &&
1303
2085
  looksLikeTsTypeExpression(typeOnlyDeclaration, true)) {
1304
2086
  continue;
1305
2087
  }
1306
2088
  const assignmentEquals = findTsFieldAssignmentEquals(commentMasked, cursor + 1, declarationEnd);
1307
2089
  if (assignmentEquals != null) {
1308
- const typedField = findBoundedTypedField(commentMasked.slice(declarationStart, assignmentEquals), declarationStart);
2090
+ const typedField = findBoundedTypedField(commentMasked, declarationStart, assignmentEquals);
1309
2091
  if (typedField != null) {
1310
2092
  continue;
1311
2093
  }
@@ -1373,7 +2155,7 @@ function* iterateAssignmentSecrets(text) {
1373
2155
  const tailStart = skipHorizontalWhitespace(text, valueEnd + 1);
1374
2156
  if (text[tailStart] === "+") {
1375
2157
  valueStart = cursor;
1376
- const logicalSpan = residualJsValueSpan(text, maskJsCommentTrivia(text), cursor);
2158
+ const logicalSpan = residualJsValueSpan(text, commentMaskedFor(text), cursor);
1377
2159
  valueEnd = cursor + logicalSpan.content.length;
1378
2160
  }
1379
2161
  }
@@ -1577,8 +2359,11 @@ function residualJsValueSpan(text, commentMasked, valueStart) {
1577
2359
  (cursor - valueStart >= 32_768 || lineCount >= 64),
1578
2360
  };
1579
2361
  }
2362
+ // Returns `text` itself when nothing is masked: callers key memo Maps by the
2363
+ // result, and Node 24's V8 compares a distinct-but-equal string per lookup.
1580
2364
  function maskJsCommentTrivia(text) {
1581
2365
  const output = text.split("");
2366
+ let changed = false;
1582
2367
  let quote;
1583
2368
  let escaped = false;
1584
2369
  for (let index = 0; index < text.length; index++) {
@@ -1595,6 +2380,7 @@ function maskJsCommentTrivia(text) {
1595
2380
  continue;
1596
2381
  }
1597
2382
  if (character === "/" && text[index + 1] === "*") {
2383
+ changed = true;
1598
2384
  output[index] = " ";
1599
2385
  output[index + 1] = " ";
1600
2386
  index += 2;
@@ -1613,6 +2399,7 @@ function maskJsCommentTrivia(text) {
1613
2399
  continue;
1614
2400
  }
1615
2401
  if (character === "/" && text[index + 1] === "/") {
2402
+ changed = true;
1616
2403
  output[index] = " ";
1617
2404
  output[index + 1] = " ";
1618
2405
  index += 2;
@@ -1623,7 +2410,7 @@ function maskJsCommentTrivia(text) {
1623
2410
  index--;
1624
2411
  }
1625
2412
  }
1626
- return output.join("");
2413
+ return changed ? output.join("") : text;
1627
2414
  }
1628
2415
  function shannonEntropy(value) {
1629
2416
  const counts = new Map();