@ultimat3/scraping 25.2.0 → 26.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +5 -5
- package/src/index.ts +1 -1
- package/src/recover.ts +7 -25
- package/src/scrape.ts +9 -2
package/README.md
CHANGED
|
@@ -499,4 +499,4 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
499
499
|
|
|
500
500
|
## Boundary
|
|
501
501
|
|
|
502
|
-
Tier
|
|
502
|
+
Tier 4. May import tiers 0-3 only — enforced by `bun run scripts/boundaries.ts`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/scraping",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.1.0",
|
|
4
4
|
"description": "Browser automation as a job: scrape() returns a JobHandle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
35
|
-
"@ultimat3/jobs": "
|
|
36
|
-
"@ultimat3/schema": "
|
|
37
|
-
"@ultimat3/storage": "
|
|
34
|
+
"@ultimat3/core": "26.1.0",
|
|
35
|
+
"@ultimat3/jobs": "26.1.0",
|
|
36
|
+
"@ultimat3/schema": "26.1.0",
|
|
37
|
+
"@ultimat3/storage": "26.1.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -154,7 +154,7 @@ export {
|
|
|
154
154
|
parseHttpRecording,
|
|
155
155
|
parseRecording,
|
|
156
156
|
} from './recording';
|
|
157
|
-
export type {
|
|
157
|
+
export type { RecoveryAttempt, RecoveryHook } from './recover';
|
|
158
158
|
export { runRecovery } from './recover';
|
|
159
159
|
export type {
|
|
160
160
|
ConsoleLine,
|
package/src/recover.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
// The recovery seam: what happens when a selector has moved and the run failed on it.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `X_NOT_IMPLEMENTED` today: correct types
|
|
7
|
-
// so an app can be written against it, one labelled throw, no silent no-op.
|
|
3
|
+
// One shape: a FUNCTION. An app writes its own fallback, gets the page and the failure, and
|
|
4
|
+
// answers whether the attempt should be retried. A model that re-derives the selector is the app's
|
|
5
|
+
// own hook wrapping `llm()` (axiom 8), never a string the framework interprets.
|
|
8
6
|
|
|
9
|
-
import { NotImplementedError } from '@ultimat3/core';
|
|
10
7
|
import { recoverRefused } from './error-throws';
|
|
11
8
|
import type { ScrapePage } from './page';
|
|
12
9
|
|
|
@@ -25,25 +22,10 @@ export interface RecoveryAttempt {
|
|
|
25
22
|
*/
|
|
26
23
|
export type RecoveryHook = (attempt: RecoveryAttempt) => Promise<boolean> | boolean;
|
|
27
24
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readonly kind: 'agent';
|
|
33
|
-
/** The model call. Deliberately not typed against `@ultimat3/ai` yet — see the throw below. */
|
|
34
|
-
readonly instruction?: string | undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const AGENT_FIX =
|
|
38
|
-
"replace recover: 'agent' with a function — recover: ({ page }) => page.waitFor('<the moved selector>').then(() => true) — which is complete today";
|
|
39
|
-
|
|
40
|
-
export async function runRecovery(recovery: Recovery, attempt: RecoveryAttempt): Promise<boolean> {
|
|
41
|
-
if (recovery === 'agent') {
|
|
42
|
-
// The agent path lands with `@ultimat3/ai`'s `llm()` behind it: page HTML in, a selector out,
|
|
43
|
-
// re-run once. Until it does, this throws rather than answering `false` — a recovery that
|
|
44
|
-
// silently declines is indistinguishable from one that was never configured.
|
|
45
|
-
throw new NotImplementedError({ cause: "recover: 'agent' is not implemented", fix: AGENT_FIX });
|
|
46
|
-
}
|
|
25
|
+
export async function runRecovery(
|
|
26
|
+
recovery: RecoveryHook,
|
|
27
|
+
attempt: RecoveryAttempt,
|
|
28
|
+
): Promise<boolean> {
|
|
47
29
|
const verdict: unknown = await recovery(attempt);
|
|
48
30
|
if (typeof verdict !== 'boolean') {
|
|
49
31
|
throw recoverRefused(attempt.scrape, 'the hook answered something other than true or false');
|
package/src/scrape.ts
CHANGED
|
@@ -33,7 +33,7 @@ import type { YieldExpectation, YieldHistory } from './expect';
|
|
|
33
33
|
import { maxDropFraction } from './expect';
|
|
34
34
|
import type { ScrapeHttp } from './http';
|
|
35
35
|
import type { ScrapePage } from './page';
|
|
36
|
-
import type {
|
|
36
|
+
import type { RecoveryHook } from './recover';
|
|
37
37
|
import type { ResourceType } from './rings';
|
|
38
38
|
import type { RobotsPolicy } from './robots';
|
|
39
39
|
import { runScrape } from './scrape-run';
|
|
@@ -103,7 +103,7 @@ export interface ScrapeDefinition<I, Row> {
|
|
|
103
103
|
readonly artifacts?: ScrapeArtifacts | undefined;
|
|
104
104
|
/** NAMES, never values. */
|
|
105
105
|
readonly secrets?: readonly string[] | undefined;
|
|
106
|
-
readonly recover?:
|
|
106
|
+
readonly recover?: RecoveryHook | undefined;
|
|
107
107
|
/**
|
|
108
108
|
* Session lifecycle: acquire, persist, reuse, validate, burn. Authenticated scraping is the
|
|
109
109
|
* primary case, so this is declared rather than hand-rolled per app. See `auth.ts`.
|
|
@@ -195,6 +195,13 @@ export function scrape<I, Row>(definition: ScrapeDefinition<I, Row>): JobHandle<
|
|
|
195
195
|
`scrape "${definition.name}" declares rate: ${String(definition.rate)} — a rate is navigations per second, greater than zero`,
|
|
196
196
|
`set rate: 1 on scrape("${definition.name}"), or leave it out — to go faster raise the number, there is no unpaced mode`,
|
|
197
197
|
);
|
|
198
|
+
// The type says function; this catches plain JS and the removed `recover: 'agent'` (26.0.0),
|
|
199
|
+
// which would otherwise surface as a TypeError on the first moved selector, mid-session.
|
|
200
|
+
assert(
|
|
201
|
+
definition.recover === undefined || typeof definition.recover === 'function',
|
|
202
|
+
`scrape "${definition.name}" declares recover: ${JSON.stringify(definition.recover)} — a recover hook is a function`,
|
|
203
|
+
`on scrape("${definition.name}") write recover: ({ page }) => page.waitFor('<the moved selector>').then(() => true) — to ask a model, call your own llm() action inside that function`,
|
|
204
|
+
);
|
|
198
205
|
// Two halves that must be set together. `maxDrop` is a fraction of a trailing median and only
|
|
199
206
|
// `history:` can supply one, so declaring it alone is an alarm that cannot fire — refused here,
|
|
200
207
|
// where it is written, rather than discovered as a scrape that never once went red.
|