@wenathlan/devthink 2.0.14 → 2.0.19

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 (444) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/data.ts DELETED
@@ -1,943 +0,0 @@
1
- /** The data module of the 1.1.90 consolidation: every correlated variation of the dataset records, the streaming pipelines and the chunked parsing logic interned in this one file, so the module family carries one surface without duplicate variations. */
2
-
3
- /* ── Merged from datacommand.ts: the 1.1.90 consolidation interns the correlated datacommand logic here, so no variation of the same file lives beside another. ── */
4
- import type {
5
- artifactrecord,
6
- chunkextractcursor,
7
- chunkextractwindowresult,
8
- columnspec,
9
- dataset,
10
- datasetrow,
11
- dedupekey,
12
- exportedartifact,
13
- extractbatch,
14
- extractpipeline,
15
- extractrow,
16
- extractsession,
17
- gridcolumn,
18
- provlogentry,
19
- provenancerecord,
20
- samplepolicy,
21
- sourcestamprecord,
22
- streamcursor,
23
- streamstate,
24
- streamparsechunk,
25
- streamparsetoken,
26
- toolstep,
27
- transformrule,
28
- } from "./types.js";
29
- import { tocsv, toexcel, tojson } from "./page.js";
30
-
31
- /**
32
- * Dataset command logics for the background executors.
33
- * Every correlated rule for dataset records, artifact exports with checksums, chunked streaming with backpressure, extraction cursors, loop row variables, provenance records and artifact retention lives in this file.
34
- */
35
-
36
- /** Builds one dataset record from a scraped grid result. */
37
- export function builddataset(
38
- id: string,
39
- name: string,
40
- grid: {
41
- columns: columnspec[];
42
- rows: datasetrow[];
43
- children?: Array<{ parentrow: number; selector: string; columns: columnspec[]; rows: datasetrow[] }>;
44
- },
45
- at: number,
46
- ): dataset {
47
- return { id, name: name || id, columns: grid.columns, rows: grid.rows, sources: [], at };
48
- }
49
-
50
- /** Computes the deterministic checksum of an exported artifact's content. */
51
- export function checksum(value: string): string {
52
- let hash = 5381;
53
- for (let index = 0; index < value.length; index += 1) hash = ((hash * 33) ^ value.charCodeAt(index)) >>> 0;
54
- return `fnv1a-${hash.toString(16)}`;
55
- }
56
-
57
- /** Serializes one dataset into the reviewed export format. */
58
- export function exportcontent(datasetvalue: dataset, format: "csv" | "json" | "excel", delimiter = ","): string {
59
- if (format === "json") return tojson(datasetvalue.columns, datasetvalue.rows);
60
- if (format === "excel") return toexcel(datasetvalue.columns, datasetvalue.rows, datasetvalue.name);
61
- return tocsv(datasetvalue.columns, datasetvalue.rows, delimiter);
62
- }
63
-
64
- /** Builds one exported artifact record with its content and checksum for the task artifact store. */
65
- export function exportartifact(
66
- id: string,
67
- datasetvalue: dataset,
68
- format: "csv" | "json" | "excel",
69
- stepid: string,
70
- content: string,
71
- at: number,
72
- ): exportedartifact {
73
- const extension = format === "excel" ? "xml" : format;
74
- return {
75
- id,
76
- kind: format,
77
- name: `${datasetvalue.name || datasetvalue.id}.${extension}`,
78
- stepid,
79
- rowcount: datasetvalue.rows.length,
80
- content,
81
- checksum: checksum(content),
82
- at,
83
- };
84
- }
85
-
86
- /** Converts one exported artifact into the artifact record shape the run store keeps. */
87
- export function artifactrecordof(artifact: exportedartifact): artifactrecord {
88
- return { id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, at: artifact.at };
89
- }
90
-
91
- /** Plans the chunk boundaries of a streaming export from a user configured chunk size with no code ceiling. */
92
- export function chunkplan(rows: number, chunk: number): Array<{ index: number; from: number; to: number }> {
93
- const size = Math.max(1, Math.floor(chunk));
94
- const chunks: Array<{ index: number; from: number; to: number }> = [];
95
- for (let from = 0; from < rows || chunks.length === 0; from += size) {
96
- const to = Math.min(rows, from + size);
97
- chunks.push({ index: chunks.length, from, to });
98
- if (to >= rows) break;
99
- }
100
- return chunks;
101
- }
102
-
103
- /** True when the stream writer must wait for acknowledgements: pending writes reached the in-flight budget of one. */
104
- export function backpressure(written: number, acknowledged: number): boolean {
105
- return written - acknowledged >= 1;
106
- }
107
-
108
- /** Advances one stream state by one acknowledged chunk of rows. */
109
- export function advancestream(
110
- state: streamstate,
111
- chunk: { index: number; to: number },
112
- at: number,
113
- done: boolean,
114
- ): streamstate {
115
- return {
116
- datasetid: state.datasetid,
117
- name: state.name,
118
- chunk: chunk.index + 1,
119
- chunks: state.chunks,
120
- written: chunk.to,
121
- ...(done ? { done: true } : {}),
122
- at,
123
- };
124
- }
125
-
126
- /** Returns the first unwritten row index of a stream, starting a fresh stream at zero. */
127
- export function streamfrom(state: streamstate | undefined, rows: number): number {
128
- if (!state || state.done) return 0;
129
- return Math.min(state.written, rows);
130
- }
131
-
132
- /** Builds the initial stream state of one dataset. */
133
- export function newstream(datasetvalue: dataset, chunks: number, at: number): streamstate {
134
- return { datasetid: datasetvalue.id, name: datasetvalue.name, chunk: 0, chunks, written: 0, at };
135
- }
136
-
137
- /** Advances one extraction session by one extracted page with its row count. */
138
- export function advancecursor(
139
- sessionvalue: extractsession,
140
- page: string,
141
- rows: number,
142
- at: number,
143
- done: boolean,
144
- ): extractsession {
145
- return {
146
- id: sessionvalue.id,
147
- datasetid: sessionvalue.datasetid,
148
- name: sessionvalue.name,
149
- target: sessionvalue.target,
150
- next: sessionvalue.next,
151
- planned: sessionvalue.planned,
152
- pages: [...sessionvalue.pages, page],
153
- rows: sessionvalue.rows + rows,
154
- cursor: sessionvalue.cursor + 1,
155
- ...(done || sessionvalue.cursor + 1 >= sessionvalue.planned ? { done: true } : {}),
156
- startedat: sessionvalue.startedat,
157
- updatedat: at,
158
- };
159
- }
160
-
161
- /** Builds the initial extraction session of one dataset extraction. */
162
- export function newextractsession(
163
- id: string,
164
- datasetid: string,
165
- name: string,
166
- target: string,
167
- next: string,
168
- planned: number,
169
- at: number,
170
- ): extractsession {
171
- return { id, datasetid, name, target, next, planned, pages: [], rows: 0, cursor: 0, startedat: at, updatedat: at };
172
- }
173
-
174
- /** Returns the pages an interrupted extraction still owes after its stored cursor. */
175
- export function remainingpages(sessionvalue: extractsession, planned: number): number {
176
- if (sessionvalue.done) return 0;
177
- return Math.max(0, Math.max(sessionvalue.planned, planned) - sessionvalue.cursor);
178
- }
179
-
180
- /** Builds one provenance record of an exported artifact with its source url, step ref, row range and checksum. */
181
- export function provenancefor(
182
- artifact: { id: string; name: string; rowcount: number; checksum: string },
183
- url: string,
184
- stepid: string,
185
- at: number,
186
- ): provenancerecord {
187
- return {
188
- artifact: artifact.id,
189
- name: artifact.name,
190
- url,
191
- stepid,
192
- rowstart: artifact.rowcount > 0 ? 1 : 0,
193
- rowend: artifact.rowcount,
194
- checksum: artifact.checksum,
195
- at,
196
- };
197
- }
198
-
199
- /** Applies the user configured artifact retention to exported artifacts; an absent setting keeps everything. */
200
- export function retainedexports<T>(records: T[], retention: number | undefined): T[] {
201
- return retention === undefined ? records : records.slice(0, retention);
202
- }
203
-
204
- /** Interpolates one text through the {{column}} tokens of a dataset row; only the row's own string values answer a token because an inherited property never leaks object internals into a step payload. The token walk is a single linear scan: a brace expression over library input answers attacker paced backtracking, so the scanner resolves each token with plain index arithmetic instead. */
205
- export function interpolate(text: string, row: datasetrow): string {
206
- let out = "";
207
- let cursor = 0;
208
- for (;;) {
209
- const open = text.indexOf("{{", cursor);
210
- if (open < 0) return out + text.slice(cursor);
211
- const close = text.indexOf("}", open + 2);
212
- if (close < 0) return out + text.slice(cursor);
213
- if (close > open + 2 && text.charCodeAt(close + 1) === 125) {
214
- out += text.slice(cursor, open);
215
- const value = row[text.slice(open + 2, close).trim()];
216
- out += typeof value === "string" ? value : "";
217
- cursor = close + 2;
218
- continue;
219
- }
220
- out += text.slice(cursor, open + 2);
221
- cursor = open + 2;
222
- }
223
- }
224
-
225
- /** Substitutes the row variables of one looprows iteration into the target, value and options of the inner step. */
226
- export function loopstep(step: toolstep, row: datasetrow): toolstep {
227
- return {
228
- ...step,
229
- ...(step.target !== undefined ? { target: interpolate(step.target, row) } : {}),
230
- ...(step.value !== undefined ? { value: interpolate(step.value, row) } : {}),
231
- ...(step.options !== undefined ? { options: interpolate(step.options, row) } : {}),
232
- };
233
- }
234
-
235
- /** Exposes one dataset row as the step variables of a looprows iteration. */
236
- export function loopvariables(row: datasetrow): datasetrow {
237
- return { ...row };
238
- }
239
-
240
- /** Builds the grid preview of a dataset with its column order, total rows and sampled rows. */
241
- export function gridpreview(
242
- datasetvalue: dataset,
243
- sample: number,
244
- ): { datasetid: string; columns: string[]; rows: number; sample: datasetrow[] } {
245
- return {
246
- datasetid: datasetvalue.id,
247
- columns: datasetvalue.columns.map((column) => column.key),
248
- rows: datasetvalue.rows.length,
249
- sample: datasetvalue.rows.slice(0, Math.max(0, Math.floor(sample))),
250
- };
251
- }
252
-
253
- /** Sorts dataset rows by one column key in the reviewed direction with a stable fallback for equal values. */
254
- export function sortrows(rows: datasetrow[], key: string, direction: "asc" | "desc"): datasetrow[] {
255
- const sign = direction === "desc" ? -1 : 1;
256
- return [...rows].sort((left, right) => {
257
- const a = left[key] ?? "";
258
- const b = right[key] ?? "";
259
- const numeric = Number(a);
260
- const numericb = Number(b);
261
- if (Number.isFinite(numeric) && Number.isFinite(numericb) && a.trim() !== "" && b.trim() !== "")
262
- return (numeric - numericb) * sign;
263
- return a.localeCompare(b) * sign;
264
- });
265
- }
266
-
267
- /** Builds the sheet push payload of one dataset for a reviewed sheet endpoint. */
268
- export function sheetpayload(
269
- datasetvalue: dataset,
270
- sheet: string,
271
- ): { sheet: string; columns: string[]; rows: datasetrow[] } {
272
- return { sheet, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows };
273
- }
274
-
275
- /** Merges reviewed transform rules and dedupe keys into the task rules record of one task. */
276
- export function mergetaskrules(
277
- existing: { taskid: string; transforms: transformrule[]; dedupekeys: string[] } | undefined,
278
- taskid: string,
279
- transforms: transformrule[],
280
- dedupekeys: string[],
281
- at: number,
282
- ): { taskid: string; transforms: transformrule[]; dedupekeys: string[]; at: number } {
283
- return {
284
- taskid,
285
- transforms: transforms.length > 0 ? transforms : (existing?.transforms ?? []),
286
- dedupekeys: dedupekeys.length > 0 ? dedupekeys : (existing?.dedupekeys ?? []),
287
- at,
288
- };
289
- }
290
-
291
- /* ── Merged from pipelines.ts: the 1.1.90 consolidation interns the correlated pipelines logic here, so no variation of the same file lives beside another. ── */
292
- /**
293
- * Data pipeline logic of the 1.1.75 family.
294
- * Every correlated rule for the streaming of large extracts to disk chunk by chunk with a cursor that checkpoints after every write, the resume of an interrupted extraction from its streamcursor with the already persisted rows skipped by their keys, the transform rules that reshape one value at a time while the raw value stays beside the transformed one, the row deduplication over the configured columns with whitespace and case normalization, the preview sampling of a subset without ever touching the full extract, the source stamps that attach the url, the step and the capture time to every row and every cell, the grid preview that projects rows and columns into a sortable filterable view, the provenance log that records one append only entry per stream, transform, dedupe, sample and resume pass and answers provenance queries by row key lives in this file.
295
- * The pipeline layer never bypasses a review: streamdisk writes through the reviewed sink only, resumeextract continues only the same plan of the same origin, transformvalues refuses the transforms that would drop rows silently, deduperows keeps the first occurrence and reports every dropped row, samplerows never mutates the stored extract, sourcestamp never rewrites a stamped timestamp, gridpreview projects a read only view, and provlog redacts the secret shaped values before any entry lands so the audit trail carries provenance without payloads.
296
- */
297
-
298
- /** Normalizes one key value for the dedupe comparison: whitespace folds its runs and trims the ends, case lowercases, and the configured normalization applies exactly while an unconfigured comparison reads the raw value. */
299
- function normalizekeyvalue(value: string, normalization: dedupekey["normalization"]): string {
300
- let normalized = value;
301
- if (normalization === "whitespace" || normalization === "whitespace and case")
302
- normalized = normalized.replace(/\s+/g, " ").trim();
303
- if (normalization === "case" || normalization === "whitespace and case") normalized = normalized.toLowerCase();
304
- return normalized;
305
- }
306
-
307
- /** Streams the rows of one extract to its sink chunk by chunk: only the active chunk rides the write at a time, every chunk reports the rows and the bytes it persisted, the streamcursor records the last written position after each chunk so the checkpoint call sees every boundary, and an absent chunk size streams the whole extract as one chunk because the chunk bound stays a user choice with no code default. */
308
- export function streamdisk(input: {
309
- pipeline: extractpipeline;
310
- rows: extractrow[];
311
- write: (chunk: extractrow[]) => number;
312
- cursor?: streamcursor;
313
- chunk?: number;
314
- checkpoint?: (cursor: streamcursor) => void;
315
- now: number;
316
- }): { cursor: streamcursor; rowswritten: number; byteswritten: number; chunks: number; reason: string } {
317
- if (input.pipeline.id.trim() === "")
318
- throw new Error("The streamdisk pass needs its pipeline; every streamed extract carries its identity.");
319
- const startoffset =
320
- input.cursor !== undefined && input.cursor.pipelineid === input.pipeline.id ? input.cursor.offset : 0;
321
- const startchunk =
322
- input.cursor !== undefined && input.cursor.pipelineid === input.pipeline.id ? input.cursor.chunk : 0;
323
- if (input.chunk !== undefined && (!Number.isInteger(input.chunk) || input.chunk <= 0))
324
- throw new Error(
325
- "The stream chunk size stays a positive whole number of rows the user chose; the chunk bound never defaults in code.",
326
- );
327
- const remaining = input.rows.slice(startoffset);
328
- const size = input.chunk !== undefined ? input.chunk : remaining.length;
329
- let rowswritten = 0;
330
- let byteswritten = 0;
331
- let chunks = 0;
332
- let cursor: streamcursor = {
333
- pipelineid: input.pipeline.id,
334
- offset: startoffset,
335
- chunk: startchunk,
336
- updatedat: input.now,
337
- };
338
- for (let index = 0; index < remaining.length; index += size) {
339
- const active = remaining.slice(index, index + size);
340
- byteswritten += input.write(active);
341
- rowswritten += active.length;
342
- chunks += 1;
343
- cursor = {
344
- pipelineid: input.pipeline.id,
345
- offset: startoffset + rowswritten,
346
- chunk: startchunk + chunks,
347
- updatedat: input.now,
348
- };
349
- input.checkpoint?.(cursor);
350
- }
351
- return {
352
- cursor,
353
- rowswritten,
354
- byteswritten,
355
- chunks,
356
- reason: `Streamed ${rowswritten} row${rowswritten === 1 ? "" : "s"} of the pipeline ${input.pipeline.name} to the reviewed sink in ${chunks} chunk${chunks === 1 ? "" : "s"} of ${size} row${size === 1 ? "" : "s"} with ${byteswritten} byte${byteswritten === 1 ? "" : "s"} written; only the active chunk rode the write and the cursor checkpoints after every chunk.`,
357
- };
358
- }
359
-
360
- /** Continues one interrupted pipeline from its streamcursor: the rows the cursor offset and the persisted row keys already cover skip so nothing writes twice, the resume marks the pipeline in the provlog, and a pipeline of another plan or another origin refuses loudly because a resume continues exactly the extraction it came from. */
361
- export function resumeextract(input: {
362
- pipeline: extractpipeline;
363
- rows: extractrow[];
364
- cursor?: streamcursor;
365
- persistedkeys?: string[];
366
- logid?: string;
367
- now: number;
368
- }): { pipeline: extractpipeline; rows: extractrow[]; skipped: number; cursor: streamcursor; entry: provlogentry } {
369
- if (input.pipeline.id.trim() === "")
370
- throw new Error("The resume names its pipeline; an interrupted extraction carries its identity.");
371
- if (input.pipeline.state === "finished")
372
- throw new Error(`The pipeline ${input.pipeline.name} already finished; a finished extraction resumes nothing.`);
373
- const cursor = input.cursor ?? { pipelineid: input.pipeline.id, offset: 0, chunk: 0, updatedat: input.now };
374
- if (cursor.pipelineid !== input.pipeline.id)
375
- throw new Error(
376
- `The streamcursor of ${cursor.pipelineid} belongs to another pipeline; the resume continues exactly the extraction it came from.`,
377
- );
378
- const persisted = new Set(input.persistedkeys ?? []);
379
- const remaining: extractrow[] = [];
380
- let skipped = 0;
381
- for (const [index, row] of input.rows.entries()) {
382
- if (index < cursor.offset || persisted.has(row.key)) {
383
- skipped += 1;
384
- continue;
385
- }
386
- remaining.push(row);
387
- }
388
- const entry: provlogentry = {
389
- id: input.logid !== undefined && input.logid.trim() !== "" ? input.logid.trim() : `${input.pipeline.id}:resume`,
390
- pipelineid: input.pipeline.id,
391
- runid: input.pipeline.runid,
392
- operation: "resume",
393
- rowkeys: remaining.map((row) => row.key),
394
- summary: `The pipeline ${input.pipeline.name} resumed from the streamcursor at offset ${cursor.offset} and chunk ${cursor.chunk}: ${skipped} row${skipped === 1 ? "" : "s"} already persisted skipped by their row keys and ${remaining.length} row${remaining.length === 1 ? "" : "s"} continue to the reviewed sink.`,
395
- at: input.now,
396
- };
397
- const pipeline: extractpipeline = { ...input.pipeline, state: "running", updatedat: input.now };
398
- return { pipeline, rows: remaining, skipped, cursor: { ...cursor, updatedat: input.now }, entry };
399
- }
400
-
401
- /** Applies the reviewed transform rules between extraction and export: every rule reshapes exactly its field through its reviewed operation — trim folds the whitespace, case folds the letters, number strips the non numeric shapes and date normalizes to ISO 8601 — the raw value stays beside the transformed value so the audit reads both sides, and a transform never drops a row: an operation that cannot apply keeps the raw value and names its refusal while every row survives the pass. */
402
- export function transformvalues(input: { rows: extractrow[]; rules: transformrule[] }): {
403
- rows: extractrow[];
404
- applied: number;
405
- refusals: string[];
406
- } {
407
- const refusals: string[] = [];
408
- let applied = 0;
409
- const rows = input.rows.map((row) => {
410
- let values = { ...row.values };
411
- let rawvalues = row.rawvalues !== undefined ? { ...row.rawvalues } : {};
412
- for (const rule of input.rules) {
413
- const field = rule.field ?? rule.target;
414
- const original = values[field] ?? "";
415
- if (rule.operation !== undefined && !["trim", "case", "number", "date"].includes(rule.operation)) {
416
- refusals.push(
417
- `${field}: the operation ${rule.operation} is not a reviewed transform; the raw value stays and the pipeline refuses the rule.`,
418
- );
419
- continue;
420
- }
421
- let transformed = original;
422
- if (rule.operation === "trim") transformed = original.replace(/\s+/g, " ").trim();
423
- if (rule.operation === "case") transformed = original.toUpperCase();
424
- if (rule.operation === "number") transformed = original.replace(/[^\d.\-]/g, "");
425
- if (rule.operation === "date") {
426
- const parsed = Date.parse(original.trim());
427
- if (Number.isNaN(parsed)) {
428
- refusals.push(
429
- `${field}: the value ${original} parses as no date; the raw value stays beside the refused transform.`,
430
- );
431
- continue;
432
- }
433
- transformed = new Date(parsed).toISOString();
434
- }
435
- if (rule.operation === undefined) continue;
436
- if (transformed !== original) applied += 1;
437
- rawvalues = { ...rawvalues, [field]: original };
438
- values = { ...values, [field]: transformed };
439
- }
440
- return { ...row, values, ...(Object.keys(rawvalues).length > 0 ? { rawvalues } : {}) };
441
- });
442
- if (rows.length !== input.rows.length)
443
- throw new Error(
444
- "A transform pass never drops a row; every row keeps its place with the raw value beside the transformed one.",
445
- );
446
- return { rows, applied, refusals };
447
- }
448
-
449
- /** Deduplicates the rows of one pipeline by its configured key: the key columns join into one comparison value under the configured normalization — whitespace folding, case folding, both or none — with every column part JSON quoted so a cell value that embeds the join separator never collides two distinct rows, the first occurrence of every key stays, the dropped rows report by count and by key, and an empty key list refuses loudly because a dedupe without its columns would compare whole rows the user never configured. */
450
- export function deduperows(input: { rows: extractrow[]; key: dedupekey }): {
451
- kept: extractrow[];
452
- dropped: number;
453
- droppedkeys: string[];
454
- reason: string;
455
- } {
456
- const columns = input.key.columns.map((column) => column.trim()).filter((column) => column !== "");
457
- if (columns.length === 0)
458
- throw new Error(
459
- "The dedupe key names its columns; an empty key would compare whole rows the user never configured.",
460
- );
461
- const seen = new Set<string>();
462
- const kept: extractrow[] = [];
463
- const droppedkeys: string[] = [];
464
- for (const row of input.rows) {
465
- const joined = columns
466
- .map((column) => JSON.stringify(normalizekeyvalue(row.values[column] ?? "", input.key.normalization)))
467
- .join("|");
468
- if (seen.has(joined)) {
469
- droppedkeys.push(row.key);
470
- continue;
471
- }
472
- seen.add(joined);
473
- kept.push(row);
474
- }
475
- return {
476
- kept,
477
- dropped: droppedkeys.length,
478
- droppedkeys,
479
- reason: `Deduplicated ${input.rows.length} row${input.rows.length === 1 ? "" : "s"} by ${columns.join(", ")} under the ${input.key.normalization} normalization: the first occurrence of every key stays and ${droppedkeys.length} duplicate${droppedkeys.length === 1 ? "" : "s"} dropped with the count reported.`,
480
- };
481
- }
482
-
483
- /** Selects the preview subset of one extract by its sample policy: the first strategy takes the leading rows, the random strategy shuffles deterministically under the seeded generator the policy names, the stratified strategy spreads the pick evenly across the ordered extract, and the full extract never mutates — the subset copies its rows so the stored batch keeps exactly what it held. */
484
- export function samplerows(input: { rows: extractrow[]; policy: samplepolicy; seed?: number }): {
485
- rows: extractrow[];
486
- strategy: samplepolicy["strategy"];
487
- reason: string;
488
- } {
489
- if (!Number.isInteger(input.policy.rows) || input.policy.rows <= 0)
490
- throw new Error(
491
- "The sample policy names its row count as a positive whole number the user chose; the preview bound never defaults in code.",
492
- );
493
- const count = Math.min(input.policy.rows, input.rows.length);
494
- let picked: extractrow[] = [];
495
- if (input.policy.strategy === "first") picked = input.rows.slice(0, count);
496
- else if (input.policy.strategy === "stratified") {
497
- if (count === 0) picked = [];
498
- else {
499
- const stride = input.rows.length / count;
500
- for (let index = 0; index < count; index += 1) {
501
- const row = input.rows[Math.floor(index * stride)];
502
- if (row !== undefined) picked.push(row);
503
- }
504
- }
505
- } else {
506
- let state = (input.seed ?? 1) >>> 0 || 1;
507
- const next = (): number => {
508
- state = (state * 1664525 + 1013904223) >>> 0;
509
- return state / 0x100000000;
510
- };
511
- const ordered = input.rows.map((row, index) => ({ row, order: next() * (input.rows.length + 1) + index }));
512
- picked = ordered
513
- .sort((one, two) => one.order - two.order)
514
- .slice(0, count)
515
- .map((entry) => entry.row);
516
- }
517
- return {
518
- rows: picked.map((row) => ({ ...row, values: { ...row.values } })),
519
- strategy: input.policy.strategy,
520
- reason: `Sampled ${picked.length} of ${input.rows.length} row${input.rows.length === 1 ? "" : "s"} by the ${input.policy.strategy} strategy of the user policy; the full extract stays untouched and the preview reads a copy.`,
521
- };
522
- }
523
-
524
- /** Stamps the rows of one extraction pass with their source: the url, the step id and the capture time build one sourcestamprecord, every row of the pass carries the stamp, every cell links to the stamp id so each value answers which page and step captured it, and a row that already carries the same stamp keeps it once because a stamped timestamp never rewrites. */
525
- export function sourcestamp(input: { rows: extractrow[]; stampid: string; url: string; stepid: string; now: number }): {
526
- rows: extractrow[];
527
- stamp: sourcestamprecord;
528
- } {
529
- if (input.stampid.trim() === "")
530
- throw new Error("The source stamp needs its id; every stamp carries its identity for the cell links.");
531
- if (input.url.trim() === "")
532
- throw new Error("The source stamp names its url; every extracted row answers the page it came from.");
533
- if (input.stepid.trim() === "")
534
- throw new Error("The source stamp names its step; every extracted row answers the reviewed step that captured it.");
535
- const stamp: sourcestamprecord = {
536
- id: input.stampid.trim(),
537
- url: input.url.trim(),
538
- stepid: input.stepid.trim(),
539
- capturedat: input.now,
540
- };
541
- const rows = input.rows.map((row) => {
542
- const stamps = row.stamps.some((existing) => existing.id === stamp.id) ? row.stamps : [...row.stamps, stamp];
543
- const cellstamps: Record<string, string> = { ...(row.cellstamps ?? {}) };
544
- for (const field of Object.keys(row.values)) cellstamps[field] = stamp.id;
545
- return { ...row, stamps, cellstamps };
546
- });
547
- return { rows, stamp };
548
- }
549
-
550
- /** Infers the kind of one grid column from its sampled values: numbers, booleans and ISO dates infer their kind while every other shape stays text; an all empty column stays text because the preview never guesses a kind it did not observe. */
551
- function columnkindof(values: string[]): gridcolumn["kind"] {
552
- const present = values.filter((value) => value.trim() !== "");
553
- if (present.length === 0) return "text";
554
- if (present.every((value) => /^-?\d+(?:\.\d+)?$/.test(value.trim()))) return "number";
555
- if (present.every((value) => value.trim() === "true" || value.trim() === "false")) return "boolean";
556
- if (present.every((value) => !Number.isNaN(Date.parse(value.trim())) && /\d{4}-\d{2}-\d{2}/.test(value.trim())))
557
- return "date";
558
- return "text";
559
- }
560
-
561
- /** Projects the rows of one extract into a read only grid model: the columns build from the observed fields with their kinds inferred from the sampled values and their widths derived from the content, the sort orders by any column without touching the stored extract, the filter keeps the rows whose cells carry the query, and every projected row carries its marks — stamped when its stamps exist, transformed for the fields whose raw values survive beside them, deduplicated for the rows a dedupe pass kept. */
562
- export function pipelinegridpreview(input: {
563
- rows: extractrow[];
564
- sort?: { column: string; direction: "ascending" | "descending" };
565
- filter?: string;
566
- dedupekeys?: string[];
567
- }): {
568
- columns: gridcolumn[];
569
- rows: Array<{
570
- key: string;
571
- values: Record<string, string>;
572
- stamped: boolean;
573
- transformed: string[];
574
- deduplicated: boolean;
575
- }>;
576
- total: number;
577
- } {
578
- const fields = [...new Set(input.rows.flatMap((row) => Object.keys(row.values)))];
579
- const columns: gridcolumn[] = fields.map((field) => {
580
- const values = input.rows.map((row) => row.values[field] ?? "");
581
- const width = Math.max(field.length, ...values.map((value) => value.length));
582
- return { name: field, kind: columnkindof(values), width };
583
- });
584
- const query = (input.filter ?? "").trim().toLowerCase();
585
- const deduped = new Set(input.dedupekeys ?? []);
586
- let rows = input.rows.map((row) => ({
587
- key: row.key,
588
- values: { ...row.values },
589
- stamped: row.stamps.length > 0,
590
- transformed: Object.keys(row.rawvalues ?? {}),
591
- deduplicated: deduped.has(row.key),
592
- }));
593
- if (query !== "")
594
- rows = rows.filter((row) => Object.values(row.values).some((value) => value.toLowerCase().includes(query)));
595
- if (input.sort !== undefined) {
596
- const column = columns.find((candidate) => candidate.name === input.sort?.column);
597
- if (column === undefined)
598
- throw new Error(
599
- `The grid preview knows no ${input.sort.column} column to sort; the projection sorts by the columns it observed.`,
600
- );
601
- const direction = input.sort.direction === "descending" ? -1 : 1;
602
- rows = rows.sort((left, right) => {
603
- const one = left.values[column.name] ?? "";
604
- const two = right.values[column.name] ?? "";
605
- let compared = 0;
606
- if (column.kind === "number") compared = Number(one) - Number(two);
607
- else if (column.kind === "boolean") compared = (one === "true" ? 1 : 0) - (two === "true" ? 1 : 0);
608
- else if (column.kind === "date") compared = Date.parse(one) - Date.parse(two);
609
- else compared = one.localeCompare(two);
610
- return direction * compared;
611
- });
612
- }
613
- return { columns, rows, total: input.rows.length };
614
- }
615
-
616
- /** Redacts the secret shaped values before one provlog entry lands: the assignments of tokens, passwords, keys and bearer credentials mask to their label so the provenance log carries the operation and its row keys without ever carrying a payload. */
617
- function redactprovenance(text: string): string {
618
- return text
619
- .replace(/\b(api[- ]?key|token|secret|password|passwd|bearer|authorization)\b(\s*[:=]\s*)\S+/gi, "$1$2[redacted]")
620
- .replace(/\bsk-[a-z0-9]{16,}\b/gi, "[redacted key]")
621
- .replace(/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, "[redacted token]")
622
- .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[redacted private key]");
623
- }
624
-
625
- /** Appends one provenance log entry for one extraction operation: every stream, transform, dedupe, sample and resume pass lands exactly one entry with the row keys it touched and its redacted summary, the log refuses an entry whose id already sits in it because the provlog stays append only for the audit integrity, and the secret shaped values mask before the entry ever lands. */
626
- export function provlog(input: {
627
- entries: provlogentry[];
628
- id: string;
629
- pipelineid: string;
630
- runid: string;
631
- operation: provlogentry["operation"];
632
- rowkeys: string[];
633
- summary: string;
634
- now: number;
635
- }): { entries: provlogentry[]; entry: provlogentry } {
636
- if (input.id.trim() === "")
637
- throw new Error("The provlog entry needs its id; every logged operation carries its identity.");
638
- if (input.pipelineid.trim() === "")
639
- throw new Error("The provlog entry names its pipeline; the provenance log stays per pipeline.");
640
- if (input.summary.trim() === "")
641
- throw new Error(
642
- "The provlog entry needs its summary in plain language; the audit reads exactly what the operation did.",
643
- );
644
- if (input.entries.some((entry) => entry.id === input.id.trim()))
645
- throw new Error(
646
- `The provlog entry ${input.id.trim()} already sits in the log; the provenance log stays append only and an entry never rewrites.`,
647
- );
648
- const entry: provlogentry = {
649
- id: input.id.trim(),
650
- pipelineid: input.pipelineid.trim(),
651
- runid: input.runid,
652
- operation: input.operation,
653
- rowkeys: [...input.rowkeys],
654
- summary: redactprovenance(input.summary.trim()),
655
- at: input.now,
656
- };
657
- return { entries: [...input.entries, entry], entry };
658
- }
659
-
660
- /** Answers the provenance query of one row key: every provlog entry that touches the row returns in order so the reader walks exactly the operations the row rode — the stream that persisted it, the transforms that reshaped it, the dedupe that kept or dropped it, the sample that previewed it and the resume that continued it. */
661
- export function provlogquery(entries: provlogentry[], rowkey: string): provlogentry[] {
662
- return entries
663
- .filter((entry) => entry.rowkeys.includes(rowkey))
664
- .sort((one, two) => one.at - two.at || (one.id < two.id ? -1 : 1));
665
- }
666
-
667
- /** Tracks the live state of the pipelines of the workspace: the running, paused and finished counts roll per run so the popup and the sidepanel read exactly how many extracts stream, wait and finished, and the counts derive from the stored pipelines without ever writing one. */
668
- export function pipelinestate(input: { pipelines: extractpipeline[] }): {
669
- running: number;
670
- paused: number;
671
- finished: number;
672
- perrun: Array<{ runid: string; running: number; paused: number; finished: number }>;
673
- } {
674
- const runs = [...new Set(input.pipelines.map((pipeline) => pipeline.runid))];
675
- return {
676
- running: input.pipelines.filter((pipeline) => pipeline.state === "running").length,
677
- paused: input.pipelines.filter((pipeline) => pipeline.state === "paused").length,
678
- finished: input.pipelines.filter((pipeline) => pipeline.state === "finished").length,
679
- perrun: runs.map((runid) => ({
680
- runid,
681
- running: input.pipelines.filter((pipeline) => pipeline.runid === runid && pipeline.state === "running").length,
682
- paused: input.pipelines.filter((pipeline) => pipeline.runid === runid && pipeline.state === "paused").length,
683
- finished: input.pipelines.filter((pipeline) => pipeline.runid === runid && pipeline.state === "finished").length,
684
- })),
685
- };
686
- }
687
-
688
- /** Builds one extract batch of a finished pass: the rows and their streamcursor bundle under the pipeline identity so the preview and the resume read exactly the same rows. */
689
- export function extractbatchof(input: {
690
- id: string;
691
- pipeline: extractpipeline;
692
- rows: extractrow[];
693
- cursor: streamcursor;
694
- now: number;
695
- }): extractbatch {
696
- if (input.id.trim() === "")
697
- throw new Error("The extract batch needs its id; every previewed pass carries its identity.");
698
- return { id: input.id.trim(), pipelineid: input.pipeline.id, rows: input.rows, cursor: input.cursor, at: input.now };
699
- }
700
-
701
- /* ── Merged from streamparse.ts: the 1.1.90 consolidation interns the correlated streamparse logic here, so no variation of the same file lives beside another. ── */
702
- /**
703
- * Streamparse logic of the 1.1.68 family.
704
- * Pages too large for one pass parse in chunks inside a worker: the stream tokenizer walks the page text window by window, yields observations progressively as each chunk completes and never holds the full page text in memory, while every chunk passes through the same schemastrict validation the one pass parser runs.
705
- * Big tables extract through chunkextract: the cursor carries the table fingerprint, slices big tables into resumable row windows, emits each window as a partial extraction result, resumes from its cursor after an interruption and merges the windows into the datagrid incrementally.
706
- */
707
-
708
- /** The byte bound of one streamparse chunk the caller supplies: the bound stays the user's choice, and the parser never holds more than the current chunk. */
709
- export type streamparseoptions = { chunkbytes: number };
710
-
711
- /** Splits one large page into chunk windows without holding the full text: the splitter returns the chunk count and the byte range of each chunk while the caller feeds one chunk at a time. */
712
- export function streamparsewindows(input: { bytes: number; chunkbytes: number }): {
713
- chunks: number;
714
- windows: Array<{ index: number; start: number; end: number }>;
715
- } {
716
- if (input.chunkbytes <= 0)
717
- throw new Error(
718
- "The streamparse chunk bound must stay a positive number of bytes; the bound stays the user's choice and never defaults inside the engine.",
719
- );
720
- const chunks = Math.ceil(input.bytes / input.chunkbytes);
721
- const windows = Array.from({ length: chunks }, (_, index) => ({
722
- index,
723
- start: index * input.chunkbytes,
724
- end: Math.min((index + 1) * input.chunkbytes, input.bytes),
725
- }));
726
- return { chunks, windows };
727
- }
728
-
729
- /** Tokenizes one chunk of a large page: the tokenizer walks the chunk text, splits text, element and attribute tokens and keeps the chunk bytes bounded so the full page text never assembles in memory. */
730
- export function streamparsechunkof(input: {
731
- index: number;
732
- text: string;
733
- chunkbytes: number;
734
- last: boolean;
735
- }): streamparsechunk {
736
- if (input.text.length > input.chunkbytes)
737
- throw new Error(
738
- `The streamparse chunk ${input.index} carries ${input.text.length} bytes over the ${input.chunkbytes} byte bound; the parser never holds more than one chunk.`,
739
- );
740
- const tokens: streamparsetoken[] = [];
741
- const pattern = /(<\/?[a-zA-Z][^>]*>)|([^<]+)/g;
742
- for (const match of input.text.matchAll(pattern)) {
743
- const element = match[1];
744
- const text = match[2];
745
- if (element !== undefined) {
746
- tokens.push({ chunk: input.index, text: element, kind: "element" });
747
- const attributes = element.match(/[a-zA-Z-]+="[^"]*"/g) ?? [];
748
- for (const attribute of attributes) tokens.push({ chunk: input.index, text: attribute, kind: "attribute" });
749
- continue;
750
- }
751
- if (text !== undefined && text.trim() !== "") tokens.push({ chunk: input.index, text: text.trim(), kind: "text" });
752
- }
753
- const observations = tokens.filter((token) => token.kind === "text").map((token) => token.text);
754
- return { index: input.index, tokens, observations, bytes: input.text.length, complete: input.last };
755
- }
756
-
757
- /** Validates one streamparse chunk through the same schemastrict validation the one pass parser runs: a chunk outside the shape the parser accepts refuses the whole stream before any observation yields. */
758
- export function validatesetreamparsechunk(chunk: unknown, expectedindex: number): { valid: boolean; reason?: string } {
759
- if (!Boolean(chunk) || typeof chunk !== "object" || Array.isArray(chunk))
760
- return {
761
- valid: false,
762
- reason: "Every streamparse chunk travels as one plain object; schemastrict refuses the chunk carrier.",
763
- };
764
- const candidate = chunk as { index?: unknown; tokens?: unknown; bytes?: unknown; complete?: unknown };
765
- if (typeof candidate.index !== "number" || candidate.index !== expectedindex)
766
- return {
767
- valid: false,
768
- reason: `The streamparse chunk must carry its index ${expectedindex}; a chunk out of order never yields observations.`,
769
- };
770
- if (!Array.isArray(candidate.tokens))
771
- return { valid: false, reason: "The streamparse chunk carries its tokens as an array." };
772
- if (typeof candidate.bytes !== "number")
773
- return { valid: false, reason: "The streamparse chunk carries its byte count as a number." };
774
- if (typeof candidate.complete !== "boolean")
775
- return { valid: false, reason: "The streamparse chunk carries its completion flag as a boolean." };
776
- return { valid: true };
777
- }
778
-
779
- /** Yields the observations of one streamparse chunk progressively: each chunk completes into its own observation batch while the stream never waits for the last chunk before yielding the earlier ones. */
780
- export function streamparseobservations(chunks: streamparsechunk[]): { progressive: string[][]; total: number } {
781
- const progressive: string[][] = [];
782
- let total = 0;
783
- for (const chunk of chunks) {
784
- progressive.push(chunk.observations);
785
- total += chunk.observations.length;
786
- }
787
- return { progressive, total };
788
- }
789
-
790
- /** Builds the memory bound summary of one streamparse stream: the peak bytes stay at one chunk bound because the full page text never assembles in memory. */
791
- export function streamparsememorybound(input: { chunkbytes: number; chunks: number }): {
792
- peak: number;
793
- fullpage: number;
794
- saved: number;
795
- } {
796
- const fullpage = input.chunkbytes * input.chunks;
797
- return { peak: input.chunkbytes, fullpage, saved: Math.max(fullpage - input.chunkbytes, 0) };
798
- }
799
-
800
- /** Opens one chunkextract cursor at the head of a big table: the cursor carries the table fingerprint the resume verifies and the user configured row window. */
801
- export function openchunkextractcursor(input: {
802
- tableid: string;
803
- fingerprint: string;
804
- rows: number;
805
- window?: number;
806
- now?: number;
807
- }): chunkextractcursor {
808
- if (input.tableid.trim() === "") throw new Error("The chunkextract cursor needs its table id.");
809
- if (input.fingerprint.trim() === "")
810
- throw new Error(
811
- "The chunkextract cursor needs its table fingerprint; a resume without its fingerprint never continues.",
812
- );
813
- if (input.window !== undefined && input.window <= 0)
814
- throw new Error("The chunkextract row window stays a positive row count and the user's choice.");
815
- return {
816
- tableid: input.tableid,
817
- fingerprint: input.fingerprint,
818
- rowindex: 0,
819
- window: input.window ?? input.rows,
820
- complete: input.rows === 0,
821
- };
822
- }
823
-
824
- /** Slices one window of rows out of a big table: the cursor advances by the window size, the window emits as one partial extraction result and the cursor completes once the row range ends. */
825
- export function chunkextractwindow(input: {
826
- cursor: chunkextractcursor;
827
- rows: Array<Record<string, string>>;
828
- }): chunkextractwindowresult {
829
- if (input.cursor.rowindex > input.rows.length)
830
- throw new Error("The chunkextract cursor stands past the end of its table; open a fresh cursor for a new table.");
831
- const end = Math.min(input.cursor.rowindex + input.cursor.window, input.rows.length);
832
- const rows = input.rows.slice(input.cursor.rowindex, end);
833
- const cursor: chunkextractcursor = { ...input.cursor, rowindex: end, complete: end >= input.rows.length };
834
- return { cursor, rows, complete: cursor.complete };
835
- }
836
-
837
- /** Resumes one chunkextract cursor after an interruption: the stored fingerprint must match the table the resume addresses or the resume refuses and the caller opens a fresh cursor. */
838
- export function resumechunkextract(input: { cursor: chunkextractcursor; tableid: string; fingerprint: string }): {
839
- resumed: boolean;
840
- reason: string;
841
- } {
842
- if (input.cursor.tableid !== input.tableid)
843
- return {
844
- resumed: false,
845
- reason: `The chunkextract cursor belongs to the table ${input.cursor.tableid} while the resume addresses ${input.tableid}; open a fresh cursor.`,
846
- };
847
- if (input.cursor.fingerprint !== input.fingerprint)
848
- return {
849
- resumed: false,
850
- reason: `The table fingerprint changed since the cursor stopped; the resume refuses so no window mixes two table states.`,
851
- };
852
- if (input.cursor.complete)
853
- return {
854
- resumed: false,
855
- reason: "The chunkextract cursor already completed its table; the merge holds every window.",
856
- };
857
- return { resumed: true, reason: `The cursor resumes at row ${input.cursor.rowindex} of the unchanged table.` };
858
- }
859
-
860
- /** Merges the windows of one chunkextract stream into the datagrid incrementally: every window appends its rows in arrival order while a repeated or out of order window never duplicates a row. */
861
- export function mergechunkwindows(windows: chunkextractwindowresult[]): {
862
- rows: Array<Record<string, string>>;
863
- duplicates: number;
864
- } {
865
- const rows: Array<Record<string, string>> = [];
866
- const seen = new Set<string>();
867
- let duplicates = 0;
868
- for (const window of windows) {
869
- for (const row of window.rows) {
870
- const key = Object.keys(row)
871
- .sort()
872
- .map((column) => `${column}=${row[column] ?? ""}`)
873
- .join("|");
874
- if (seen.has(key)) {
875
- duplicates += 1;
876
- continue;
877
- }
878
- seen.add(key);
879
- rows.push(row);
880
- }
881
- }
882
- return { rows, duplicates };
883
- }
884
-
885
- /** Reads the progress line of one chunkextract stream the datagrid shows: the rows extracted so far beside the total the cursor covers. */
886
- export function chunkextractprogress(
887
- cursor: chunkextractcursor,
888
- totalrows: number,
889
- ): { extracted: number; total: number; share: number; complete: boolean } {
890
- return {
891
- extracted: cursor.rowindex,
892
- total: totalrows,
893
- share: totalrows === 0 ? 1 : cursor.rowindex / totalrows,
894
- complete: cursor.complete,
895
- };
896
- }
897
-
898
- /** Shapes one streamparse stream into the offscreen worker task payload: the chunked parse runs inside the worker pool the 1.1.60 family introduced, beside the chunkextract windows of big tables. */
899
- export function streamparsetaskof(input: { runid: string; stepid: string; chunkbytes: number; chunks: number }): {
900
- task: "streamparse";
901
- runid: string;
902
- stepid: string;
903
- chunkbytes: number;
904
- chunks: number;
905
- } {
906
- if (input.chunkbytes <= 0) throw new Error("The streamparse worker task needs its positive chunk bound.");
907
- return {
908
- task: "streamparse",
909
- runid: input.runid,
910
- stepid: input.stepid,
911
- chunkbytes: input.chunkbytes,
912
- chunks: input.chunks,
913
- };
914
- }
915
-
916
- /** Shapes one chunkextract window task into the offscreen worker payload: the worker slices one row window and returns the advanced cursor with its partial rows. */
917
- export function chunkextracttaskof(input: {
918
- runid: string;
919
- stepid: string;
920
- tableid: string;
921
- fingerprint: string;
922
- rowindex: number;
923
- window: number;
924
- }): {
925
- task: "chunkextract";
926
- runid: string;
927
- stepid: string;
928
- tableid: string;
929
- fingerprint: string;
930
- rowindex: number;
931
- window: number;
932
- } {
933
- if (input.window <= 0) throw new Error("The chunkextract worker task needs its positive row window.");
934
- return {
935
- task: "chunkextract",
936
- runid: input.runid,
937
- stepid: input.stepid,
938
- tableid: input.tableid,
939
- fingerprint: input.fingerprint,
940
- rowindex: input.rowindex,
941
- window: input.window,
942
- };
943
- }