@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.
- package/CHANGELOG.md +60 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /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
|
-
}
|