@ultimat3/scraping 25.2.0 → 26.0.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 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 5. May import tiers 0-4 only — enforced by `bun run scripts/boundaries.ts`.
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": "25.2.0",
3
+ "version": "26.0.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": "25.2.0",
35
- "@ultimat3/jobs": "25.2.0",
36
- "@ultimat3/schema": "25.2.0",
37
- "@ultimat3/storage": "25.2.0"
34
+ "@ultimat3/core": "26.0.0",
35
+ "@ultimat3/jobs": "26.0.0",
36
+ "@ultimat3/schema": "26.0.0",
37
+ "@ultimat3/storage": "26.0.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 { AgentRecovery, Recovery, RecoveryAttempt, RecoveryHook } from './recover';
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
- // Two shapes. A FUNCTION is complete and shipped — an app writes its own fallback, gets the page
4
- // and the failure, and answers whether the attempt should be retried. `'agent'` is the seam for
5
- // letting a model re-derive the selector from the page, and it is an honest
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 type Recovery = RecoveryHook | 'agent';
29
-
30
- /** What the agent recovery will need when it lands. Declared now so the seam is typed, not free. */
31
- export interface AgentRecovery {
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 { Recovery } from './recover';
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?: Recovery | undefined;
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.