@nitpicker/crawler 0.21.0 → 0.22.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.
Files changed (113) hide show
  1. package/lib/append-retry-failed-common-setup-phases.d.ts +1 -1
  2. package/lib/append-retry-failed-common-setup-phases.js +5 -1
  3. package/lib/append-setup-phases.d.ts +1 -1
  4. package/lib/archive/archive.d.ts +57 -8
  5. package/lib/archive/archive.js +83 -12
  6. package/lib/archive/create-adjunct-tables.d.ts +3 -2
  7. package/lib/archive/create-adjunct-tables.js +12 -9
  8. package/lib/archive/create-entity-tables.js +3 -0
  9. package/lib/archive/database.d.ts +39 -7
  10. package/lib/archive/database.js +49 -9
  11. package/lib/archive/db-ops/_shared/keyset-paginate-by-id.d.ts +49 -0
  12. package/lib/archive/db-ops/_shared/keyset-paginate-by-id.js +69 -0
  13. package/lib/archive/db-ops/_shared/resolve-content-item-id.d.ts +13 -12
  14. package/lib/archive/db-ops/_shared/resolve-content-item-id.js +63 -31
  15. package/lib/archive/db-ops/_shared/types.d.ts +39 -0
  16. package/lib/archive/db-ops/analysis/replace-analysis-violations.js +7 -4
  17. package/lib/archive/db-ops/config/get-created-cwd.d.ts +11 -0
  18. package/lib/archive/db-ops/config/get-created-cwd.js +13 -0
  19. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  20. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-observations.d.ts +57 -0
  21. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-observations.js +87 -0
  22. package/lib/archive/db-ops/errors/insert-page-error.js +3 -1
  23. package/lib/archive/db-ops/lifecycle/init.d.ts +13 -6
  24. package/lib/archive/db-ops/lifecycle/init.js +30 -10
  25. package/lib/archive/db-ops/list-reconcile/record-list-reconcile-run.d.ts +22 -0
  26. package/lib/archive/db-ops/{inventory/record-inventory-run.js → list-reconcile/record-list-reconcile-run.js} +11 -10
  27. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  28. package/lib/archive/db-ops/pages/read/get-crawling-state.d.ts +9 -2
  29. package/lib/archive/db-ops/pages/read/get-crawling-state.js +11 -3
  30. package/lib/archive/db-ops/pages/reset/clear-page-derived-rows.d.ts +20 -0
  31. package/lib/archive/db-ops/pages/reset/clear-page-derived-rows.js +37 -0
  32. package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +3 -2
  33. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +17 -37
  34. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +22 -1
  35. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +79 -33
  36. package/lib/archive/db-ops/pages/reset/reset-pages-by-urls.d.ts +78 -0
  37. package/lib/archive/db-ops/pages/reset/reset-pages-by-urls.js +146 -0
  38. package/lib/archive/db-ops/pages/reset/retryable-image-scan-codes.d.ts +24 -0
  39. package/lib/archive/db-ops/pages/reset/retryable-image-scan-codes.js +24 -0
  40. package/lib/archive/db-ops/pages/write/insert-inventory-content-items.js +2 -1
  41. package/lib/archive/db-ops/pages/write/insert-page.js +4 -2
  42. package/lib/archive/db-ops/pages/write/link-redirect-sources.js +3 -1
  43. package/lib/archive/db-ops/pages/write/record-redirect.js +1 -1
  44. package/lib/archive/db-ops/pages/write/set-skipped-page.js +3 -1
  45. package/lib/archive/db-ops/pages/write/update-page.d.ts +9 -1
  46. package/lib/archive/db-ops/pages/write/update-page.js +28 -6
  47. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +3 -6
  48. package/lib/archive/db-ops/resources/get-resource-url-list.js +10 -35
  49. package/lib/archive/init-schema.d.ts +1 -1
  50. package/lib/archive/init-schema.js +3 -2
  51. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +11 -3
  52. package/lib/archive/meta/compute-main-contents-denormalized.js +14 -2
  53. package/lib/archive/meta/types.d.ts +24 -0
  54. package/lib/archive/migrate-content-items-is-metadata-only.d.ts +44 -0
  55. package/lib/archive/migrate-content-items-is-metadata-only.js +60 -0
  56. package/lib/archive/migrate-info-created-cwd.d.ts +17 -0
  57. package/lib/archive/migrate-info-created-cwd.js +36 -0
  58. package/lib/archive/migrate-inventory-runs-to-list-reconcile-runs.d.ts +34 -0
  59. package/lib/archive/migrate-inventory-runs-to-list-reconcile-runs.js +51 -0
  60. package/lib/archive/{migrate-inventory-runs-exclude-skipped.d.ts → migrate-list-reconcile-runs-exclude-skipped.d.ts} +8 -8
  61. package/lib/archive/{migrate-inventory-runs-exclude-skipped.js → migrate-list-reconcile-runs-exclude-skipped.js} +12 -12
  62. package/lib/archive/migrate-list-reconcile-runs-invalid-skipped.d.ts +16 -0
  63. package/lib/archive/migrate-list-reconcile-runs-invalid-skipped.js +35 -0
  64. package/lib/archive/migrate-page-meta-image-scan.d.ts +27 -0
  65. package/lib/archive/migrate-page-meta-image-scan.js +52 -0
  66. package/lib/archive/page.d.ts +12 -0
  67. package/lib/archive/page.js +16 -0
  68. package/lib/archive/types.d.ts +97 -10
  69. package/lib/compute-auto-retry-backoff-delay.d.ts +30 -0
  70. package/lib/compute-auto-retry-backoff-delay.js +36 -0
  71. package/lib/crawler/apply-crawl-runtime-options-patch.d.ts +32 -0
  72. package/lib/crawler/apply-crawl-runtime-options-patch.js +129 -0
  73. package/lib/crawler/crawler.d.ts +35 -3
  74. package/lib/crawler/crawler.js +486 -338
  75. package/lib/crawler/dedupe/build-dedupe-cap-observation.d.ts +74 -0
  76. package/lib/crawler/dedupe/build-dedupe-cap-observation.js +97 -0
  77. package/lib/crawler/dedupe/compute-meta-signature.d.ts +7 -5
  78. package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
  79. package/lib/crawler/dedupe/compute-shape-key.d.ts +20 -0
  80. package/lib/crawler/dedupe/compute-shape-key.js +73 -1
  81. package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +6 -1
  82. package/lib/crawler/dedupe/dedupe-cap-tracker.js +6 -1
  83. package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +7 -5
  84. package/lib/crawler/dedupe/resolve-og-url-mismatch.js +5 -3
  85. package/lib/crawler/dedupe/types.d.ts +18 -0
  86. package/lib/crawler/fetch-destination.js +1 -0
  87. package/lib/crawler/link-list.d.ts +9 -1
  88. package/lib/crawler/link-list.js +12 -2
  89. package/lib/crawler/link-to-page-data.js +1 -0
  90. package/lib/crawler/resource-to-page-data.js +1 -0
  91. package/lib/crawler/types.d.ts +75 -0
  92. package/lib/crawler-orchestrator.d.ts +171 -8
  93. package/lib/crawler-orchestrator.js +1177 -234
  94. package/lib/crawler.d.ts +3 -0
  95. package/lib/crawler.js +3 -0
  96. package/lib/delay-or-abort.d.ts +20 -0
  97. package/lib/delay-or-abort.js +35 -0
  98. package/lib/pending-urls-remain-error.d.ts +35 -0
  99. package/lib/pending-urls-remain-error.js +43 -0
  100. package/lib/recrawl-setup-phases.d.ts +38 -0
  101. package/lib/recrawl-setup-phases.js +52 -0
  102. package/lib/resume-setup-phases.d.ts +1 -1
  103. package/lib/resume-setup-phases.js +5 -1
  104. package/lib/retry-failed-setup-phases.d.ts +1 -1
  105. package/lib/setup-recovery-phase-labels.d.ts +17 -8
  106. package/lib/setup-recovery-phase-labels.js +17 -8
  107. package/lib/types.d.ts +62 -12
  108. package/lib/utils/compute-file-sha256.d.ts +1 -1
  109. package/lib/utils/compute-file-sha256.js +1 -1
  110. package/package.json +4 -4
  111. package/lib/archive/db-ops/inventory/record-inventory-run.d.ts +0 -21
  112. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +0 -16
  113. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +0 -35
package/lib/crawler.d.ts CHANGED
@@ -40,7 +40,10 @@ export { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
40
40
  export { APPEND_SETUP_PHASES } from './append-setup-phases.js';
41
41
  export { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
42
42
  export { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
43
+ export { RECRAWL_SETUP_PHASES } from './recrawl-setup-phases.js';
43
44
  export { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
45
+ export { PendingUrlsRemainError } from './pending-urls-remain-error.js';
46
+ export { computeAutoRetryBackoffDelayMs } from './compute-auto-retry-backoff-delay.js';
44
47
  export * from './types.js';
45
48
  export * from './crawler/types.js';
46
49
  export { classifyErrorKind } from './classify-error-kind.js';
package/lib/crawler.js CHANGED
@@ -39,7 +39,10 @@ export { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
39
39
  export { APPEND_SETUP_PHASES } from './append-setup-phases.js';
40
40
  export { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
41
41
  export { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
42
+ export { RECRAWL_SETUP_PHASES } from './recrawl-setup-phases.js';
42
43
  export { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
44
+ export { PendingUrlsRemainError } from './pending-urls-remain-error.js';
45
+ export { computeAutoRetryBackoffDelayMs } from './compute-auto-retry-backoff-delay.js';
43
46
  export * from './types.js';
44
47
  export * from './crawler/types.js';
45
48
  export { classifyErrorKind } from './classify-error-kind.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Waits `ms` milliseconds, or returns immediately if `signal` aborts first
3
+ * (issue #350's auto-retry backoff — up to 5 minutes, which a library
4
+ * consumer calling `CrawlerOrchestrator#abort()` must be able to cut short
5
+ * immediately rather than waiting out).
6
+ *
7
+ * WHY NOT `Promise.race([delay(ms), abortPromise])`: `@d-zero/shared`'s
8
+ * `delay()` takes no `AbortSignal` and cannot cancel its own internal
9
+ * `setTimeout` — racing it leaves that timer running (and its executor
10
+ * closure alive) for the full `ms` even after the abort side already won,
11
+ * which is exactly the "loser-side timer never cleared" pattern
12
+ * ARCHITECTURE.md's invariants call out (`raceWithTimeout`'s own docs).
13
+ * This instead owns a single `setTimeout` directly and clears it — via the
14
+ * abort listener when the signal fires first, or by letting it fire
15
+ * naturally and detaching the listener — on whichever path resolves.
16
+ * @param ms - Milliseconds to wait.
17
+ * @param signal - Aborting this resolves the returned promise immediately.
18
+ * @returns A promise that resolves after `ms`, or immediately if already aborted.
19
+ */
20
+ export declare function delayOrAbort(ms: number, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Waits `ms` milliseconds, or returns immediately if `signal` aborts first
3
+ * (issue #350's auto-retry backoff — up to 5 minutes, which a library
4
+ * consumer calling `CrawlerOrchestrator#abort()` must be able to cut short
5
+ * immediately rather than waiting out).
6
+ *
7
+ * WHY NOT `Promise.race([delay(ms), abortPromise])`: `@d-zero/shared`'s
8
+ * `delay()` takes no `AbortSignal` and cannot cancel its own internal
9
+ * `setTimeout` — racing it leaves that timer running (and its executor
10
+ * closure alive) for the full `ms` even after the abort side already won,
11
+ * which is exactly the "loser-side timer never cleared" pattern
12
+ * ARCHITECTURE.md's invariants call out (`raceWithTimeout`'s own docs).
13
+ * This instead owns a single `setTimeout` directly and clears it — via the
14
+ * abort listener when the signal fires first, or by letting it fire
15
+ * naturally and detaching the listener — on whichever path resolves.
16
+ * @param ms - Milliseconds to wait.
17
+ * @param signal - Aborting this resolves the returned promise immediately.
18
+ * @returns A promise that resolves after `ms`, or immediately if already aborted.
19
+ */
20
+ export function delayOrAbort(ms, signal) {
21
+ if (signal.aborted) {
22
+ return Promise.resolve();
23
+ }
24
+ return new Promise((resolve) => {
25
+ const timer = setTimeout(() => {
26
+ signal.removeEventListener('abort', onAbort);
27
+ resolve();
28
+ }, ms);
29
+ const onAbort = () => {
30
+ clearTimeout(timer);
31
+ resolve();
32
+ };
33
+ signal.addEventListener('abort', onAbort, { once: true });
34
+ });
35
+ }
@@ -0,0 +1,35 @@
1
+ import type { PendingUrlsRemainReason } from './types.js';
2
+ /**
3
+ * Thrown by `CrawlerOrchestrator`'s auto-retry loop when a crawl session
4
+ * ends with `content_items` rows still unscraped and no further retry is
5
+ * warranted (issue #350). The archive is deliberately left un-packaged —
6
+ * see the loop's own JSDoc for the `.nitpicker` ⟹ pending = 0 invariant this
7
+ * enforces — so the message points the operator at the stub left behind.
8
+ */
9
+ export declare class PendingUrlsRemainError extends Error {
10
+ /** Attempts actually run before giving up (0 when `--max-auto-retry 0`). */
11
+ readonly attemptsMade: number;
12
+ /** The configured `--max-auto-retry` ceiling. */
13
+ readonly maxAutoRetry: number;
14
+ /** Pending URL count at the moment retrying was abandoned. */
15
+ readonly pendingCount: number;
16
+ /** Why the loop stopped. */
17
+ readonly reason: PendingUrlsRemainReason;
18
+ /** Absolute path of the stub (tmpDir) left behind for `--resume`/`--retry-failed`. */
19
+ readonly stubPath: string;
20
+ /**
21
+ * @param params - See the matching readonly property for each field's meaning.
22
+ * @param params.pendingCount
23
+ * @param params.attemptsMade
24
+ * @param params.maxAutoRetry
25
+ * @param params.reason
26
+ * @param params.stubPath
27
+ */
28
+ constructor(params: {
29
+ pendingCount: number;
30
+ attemptsMade: number;
31
+ maxAutoRetry: number;
32
+ reason: PendingUrlsRemainReason;
33
+ stubPath: string;
34
+ });
35
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Thrown by `CrawlerOrchestrator`'s auto-retry loop when a crawl session
3
+ * ends with `content_items` rows still unscraped and no further retry is
4
+ * warranted (issue #350). The archive is deliberately left un-packaged —
5
+ * see the loop's own JSDoc for the `.nitpicker` ⟹ pending = 0 invariant this
6
+ * enforces — so the message points the operator at the stub left behind.
7
+ */
8
+ export class PendingUrlsRemainError extends Error {
9
+ /** Attempts actually run before giving up (0 when `--max-auto-retry 0`). */
10
+ attemptsMade;
11
+ /** The configured `--max-auto-retry` ceiling. */
12
+ maxAutoRetry;
13
+ /** Pending URL count at the moment retrying was abandoned. */
14
+ pendingCount;
15
+ /** Why the loop stopped. */
16
+ reason;
17
+ /** Absolute path of the stub (tmpDir) left behind for `--resume`/`--retry-failed`. */
18
+ stubPath;
19
+ /**
20
+ * @param params - See the matching readonly property for each field's meaning.
21
+ * @param params.pendingCount
22
+ * @param params.attemptsMade
23
+ * @param params.maxAutoRetry
24
+ * @param params.reason
25
+ * @param params.stubPath
26
+ */
27
+ constructor(params) {
28
+ const { pendingCount, attemptsMade, maxAutoRetry, reason, stubPath } = params;
29
+ const reasonText = reason === 'no-progress'
30
+ ? `an auto-retry attempt made no progress (pending count did not decrease)`
31
+ : `all ${maxAutoRetry} auto-retry attempt(s) were exhausted`;
32
+ super(`${pendingCount} page(s) remain pending after ${attemptsMade} auto-retry attempt(s) — ${reasonText}. ` +
33
+ `The archive was left un-packaged at: ${stubPath}. ` +
34
+ `Run \`crawl ${stubPath} --resume\` (or \`--retry-failed\` once packaged) to continue, ` +
35
+ 'or rerun with a higher --max-auto-retry.');
36
+ this.name = 'PendingUrlsRemainError';
37
+ this.pendingCount = pendingCount;
38
+ this.attemptsMade = attemptsMade;
39
+ this.maxAutoRetry = maxAutoRetry;
40
+ this.reason = reason;
41
+ this.stubPath = stubPath;
42
+ }
43
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels
3
+ * `CrawlerOrchestrator.recrawl` announces, in call order, before its
4
+ * `initializedCallback` fires. Single source of truth for both the `onPhase`
5
+ * call sites in `crawler-orchestrator.ts` and the CLI's setup task list
6
+ * (`create-setup-task-list.ts`), which pre-builds one task-list row per
7
+ * entry — duplicating this sequence by hand at the CLI layer would silently
8
+ * drift the moment a phase is added, renamed, or reordered here.
9
+ *
10
+ * Identical to `INVENTORY_SETUP_PHASES` except for one extra step —
11
+ * `'Resetting matched pages'`, right after `'Backing up archive'` — since
12
+ * `recrawl` un-scrapes URLs that already exist as pages in addition to
13
+ * importing the URLs `inventory` handles (novel URLs the archive does not
14
+ * yet track). This is the **superset** sequence — `recrawl` has three
15
+ * `initializedCallback` call sites, each reached after a different prefix of
16
+ * this list depending on the input:
17
+ * - no matched pages AND no novel URLs (everything already known / out of
18
+ * scope): stops after index 3 (`'Checking for already-known URLs'`), skips
19
+ * the `.bak` backup entirely
20
+ * - novel URLs are all non-HTML and nothing was reset: stops after index 8
21
+ * (`'Recording excluded pages'`) — nothing to render, no crawl state to
22
+ * rebuild
23
+ * - at least one reset page or novel HTML seed: runs the full 13-phase
24
+ * sequence
25
+ *
26
+ * The CLI's setup task list pre-builds all 13 rows and marks whichever
27
+ * suffix never got announced as skipped once `initializedCallback` fires —
28
+ * it does not need to know in advance which of the three prefixes a given
29
+ * run will take. Note `'Loading crawl state'` appears twice (indices 2 and
30
+ * 9): the first read is scoped to the pre-ingestion pending-URL warning, the
31
+ * second re-reads state after the reset pages and new inventory seeds have
32
+ * been written.
33
+ *
34
+ * On failure, a `SETUP_RECOVERY_PHASE_LABELS` entry
35
+ * (`'Restoring archive from backup'` or `'Persisting ingested inventory
36
+ * state'`) is announced instead of continuing this sequence.
37
+ */
38
+ export declare const RECRAWL_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Loading crawl state", "Checking for already-known URLs", "Backing up archive", "Resetting matched pages", "Recording non-HTML resources", "Recording HTML seed pages", "Recording excluded pages", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels
3
+ * `CrawlerOrchestrator.recrawl` announces, in call order, before its
4
+ * `initializedCallback` fires. Single source of truth for both the `onPhase`
5
+ * call sites in `crawler-orchestrator.ts` and the CLI's setup task list
6
+ * (`create-setup-task-list.ts`), which pre-builds one task-list row per
7
+ * entry — duplicating this sequence by hand at the CLI layer would silently
8
+ * drift the moment a phase is added, renamed, or reordered here.
9
+ *
10
+ * Identical to `INVENTORY_SETUP_PHASES` except for one extra step —
11
+ * `'Resetting matched pages'`, right after `'Backing up archive'` — since
12
+ * `recrawl` un-scrapes URLs that already exist as pages in addition to
13
+ * importing the URLs `inventory` handles (novel URLs the archive does not
14
+ * yet track). This is the **superset** sequence — `recrawl` has three
15
+ * `initializedCallback` call sites, each reached after a different prefix of
16
+ * this list depending on the input:
17
+ * - no matched pages AND no novel URLs (everything already known / out of
18
+ * scope): stops after index 3 (`'Checking for already-known URLs'`), skips
19
+ * the `.bak` backup entirely
20
+ * - novel URLs are all non-HTML and nothing was reset: stops after index 8
21
+ * (`'Recording excluded pages'`) — nothing to render, no crawl state to
22
+ * rebuild
23
+ * - at least one reset page or novel HTML seed: runs the full 13-phase
24
+ * sequence
25
+ *
26
+ * The CLI's setup task list pre-builds all 13 rows and marks whichever
27
+ * suffix never got announced as skipped once `initializedCallback` fires —
28
+ * it does not need to know in advance which of the three prefixes a given
29
+ * run will take. Note `'Loading crawl state'` appears twice (indices 2 and
30
+ * 9): the first read is scoped to the pre-ingestion pending-URL warning, the
31
+ * second re-reads state after the reset pages and new inventory seeds have
32
+ * been written.
33
+ *
34
+ * On failure, a `SETUP_RECOVERY_PHASE_LABELS` entry
35
+ * (`'Restoring archive from backup'` or `'Persisting ingested inventory
36
+ * state'`) is announced instead of continuing this sequence.
37
+ */
38
+ export const RECRAWL_SETUP_PHASES = [
39
+ 'Extracting archive',
40
+ 'Loading archive config',
41
+ 'Loading crawl state',
42
+ 'Checking for already-known URLs',
43
+ 'Backing up archive',
44
+ 'Resetting matched pages',
45
+ 'Recording non-HTML resources',
46
+ 'Recording HTML seed pages',
47
+ 'Recording excluded pages',
48
+ 'Loading crawl state',
49
+ 'Loading resource list',
50
+ 'Loading scraped page count',
51
+ 'Restoring crawl state',
52
+ ];
@@ -13,4 +13,4 @@
13
13
  * recovery phase and no branching: every `resume` call announces exactly
14
14
  * these seven phases in this order.
15
15
  */
16
- export declare const RESUME_SETUP_PHASES: readonly ["Reconnecting to archive", "Loading archive config", "Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
16
+ export declare const RESUME_SETUP_PHASES: readonly ["Reconnecting to archive", "Loading archive config", "Loading dedupe-cap state", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -16,7 +16,11 @@
16
16
  export const RESUME_SETUP_PHASES = [
17
17
  'Reconnecting to archive',
18
18
  'Loading archive config',
19
- 'Loading dedupe-cap shape keys',
19
+ // Renamed from 'Loading dedupe-cap shape keys' — this phase now covers
20
+ // both the confirmed-capped shape-key read AND the not-yet-capped shape
21
+ // observation replay (`archive.listDedupeCapObservations` /
22
+ // `#preloadDedupeCapObservations`), not just the former.
23
+ 'Loading dedupe-cap state',
20
24
  'Loading crawl state',
21
25
  'Loading resource list',
22
26
  'Loading scraped page count',
@@ -16,4 +16,4 @@
16
16
  * (`'Restoring archive from backup'`) is announced instead of continuing
17
17
  * this sequence.
18
18
  */
19
- export declare const RETRY_FAILED_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Backing up archive", "Resetting failed pages", "Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
19
+ export declare const RETRY_FAILED_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Backing up archive", "Resetting failed pages", "Loading dedupe-cap state", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -10,12 +10,21 @@
10
10
  * stays visible, rather than trying to guess in advance where in the
11
11
  * success-path sequence a failure might interrupt it.
12
12
  *
13
- * - `'Restoring archive from backup'` — `append` / `inventory` /
14
- * `retryFailed` all take this path when anything from the post-backup
15
- * setup step through the crawl itself throws.
16
- * - `'Persisting ingested inventory state'` — `inventory`-only: once its
17
- * pre-insert + audit-row write has committed, a later failure (state
18
- * rebuild or the crawl itself) must persist the ingested rows to disk
19
- * instead of restoring the pre-inventory `.bak`, or they would be lost.
13
+ * - `'Restoring archive from backup'` — `append` / `inventory` / `recrawl` /
14
+ * `retryFailed` take this path when a failure that is NOT
15
+ * `PendingUrlsRemainError` occurs from the post-backup setup step through
16
+ * the crawl itself.
17
+ * - `'Leaving crawl state for --resume'` all four of the above instead
18
+ * take this path once the failure is specifically `PendingUrlsRemainError`
19
+ * (issue #350's auto-retry loop giving up with pages still pending): the
20
+ * `.bak` is deleted (nothing to restore — `write()` never ran, so the
21
+ * original `.nitpicker` was never touched) and the stub tmpDir is left
22
+ * intact instead, so the operator recovers via `crawl --resume` /
23
+ * `--retry-failed`. `inventory` / `recrawl` additionally reach this same
24
+ * label for any OTHER scrape-phase failure once their pre-insert + audit
25
+ * row has committed (`ingestionComplete`) — restoring the pre-inventory
26
+ * `.bak` at that point would silently wipe the already-committed seeds.
27
+ * The archive is deliberately NOT packaged in either case (see
28
+ * `CrawlerOrchestrator`'s `.nitpicker` ⟹ pending = 0 invariant).
20
29
  */
21
- export declare const SETUP_RECOVERY_PHASE_LABELS: readonly ["Restoring archive from backup", "Persisting ingested inventory state"];
30
+ export declare const SETUP_RECOVERY_PHASE_LABELS: readonly ["Restoring archive from backup", "Leaving crawl state for --resume"];
@@ -10,15 +10,24 @@
10
10
  * stays visible, rather than trying to guess in advance where in the
11
11
  * success-path sequence a failure might interrupt it.
12
12
  *
13
- * - `'Restoring archive from backup'` — `append` / `inventory` /
14
- * `retryFailed` all take this path when anything from the post-backup
15
- * setup step through the crawl itself throws.
16
- * - `'Persisting ingested inventory state'` — `inventory`-only: once its
17
- * pre-insert + audit-row write has committed, a later failure (state
18
- * rebuild or the crawl itself) must persist the ingested rows to disk
19
- * instead of restoring the pre-inventory `.bak`, or they would be lost.
13
+ * - `'Restoring archive from backup'` — `append` / `inventory` / `recrawl` /
14
+ * `retryFailed` take this path when a failure that is NOT
15
+ * `PendingUrlsRemainError` occurs from the post-backup setup step through
16
+ * the crawl itself.
17
+ * - `'Leaving crawl state for --resume'` all four of the above instead
18
+ * take this path once the failure is specifically `PendingUrlsRemainError`
19
+ * (issue #350's auto-retry loop giving up with pages still pending): the
20
+ * `.bak` is deleted (nothing to restore — `write()` never ran, so the
21
+ * original `.nitpicker` was never touched) and the stub tmpDir is left
22
+ * intact instead, so the operator recovers via `crawl --resume` /
23
+ * `--retry-failed`. `inventory` / `recrawl` additionally reach this same
24
+ * label for any OTHER scrape-phase failure once their pre-insert + audit
25
+ * row has committed (`ingestionComplete`) — restoring the pre-inventory
26
+ * `.bak` at that point would silently wipe the already-committed seeds.
27
+ * The archive is deliberately NOT packaged in either case (see
28
+ * `CrawlerOrchestrator`'s `.nitpicker` ⟹ pending = 0 invariant).
20
29
  */
21
30
  export const SETUP_RECOVERY_PHASE_LABELS = [
22
31
  'Restoring archive from backup',
23
- 'Persisting ingested inventory state',
32
+ 'Leaving crawl state for --resume',
24
33
  ];
package/lib/types.d.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  import type { APPEND_SETUP_PHASES } from './append-setup-phases.js';
2
2
  import type { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
3
+ import type { RECRAWL_SETUP_PHASES } from './recrawl-setup-phases.js';
3
4
  import type { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
4
5
  import type { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
5
6
  import type { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
6
7
  import type { CrawlerError, PageData } from './utils/types/types.js';
7
8
  /**
8
9
  * Every label `SetupProgressCallbacks.onPhase` can be called with, across
9
- * all four `CrawlerOrchestrator` setup sequences plus the failure-only
10
+ * all five `CrawlerOrchestrator` setup sequences plus the failure-only
10
11
  * recovery phases. See `RESUME_SETUP_PHASES` / `APPEND_SETUP_PHASES` /
11
12
  * `RETRY_FAILED_SETUP_PHASES` / `INVENTORY_SETUP_PHASES` /
12
- * `SETUP_RECOVERY_PHASE_LABELS` for what each label means and when it fires.
13
+ * `RECRAWL_SETUP_PHASES` / `SETUP_RECOVERY_PHASE_LABELS` for what each label
14
+ * means and when it fires.
13
15
  */
14
- export type SetupPhaseLabel = (typeof RESUME_SETUP_PHASES)[number] | (typeof APPEND_SETUP_PHASES)[number] | (typeof RETRY_FAILED_SETUP_PHASES)[number] | (typeof INVENTORY_SETUP_PHASES)[number] | (typeof SETUP_RECOVERY_PHASE_LABELS)[number];
16
+ export type SetupPhaseLabel = (typeof RESUME_SETUP_PHASES)[number] | (typeof APPEND_SETUP_PHASES)[number] | (typeof RETRY_FAILED_SETUP_PHASES)[number] | (typeof INVENTORY_SETUP_PHASES)[number] | (typeof RECRAWL_SETUP_PHASES)[number] | (typeof SETUP_RECOVERY_PHASE_LABELS)[number];
15
17
  /**
16
18
  * Progress callbacks for the setup phase of `CrawlerOrchestrator.append` /
17
19
  * `inventory` / `retryFailed` / `resume` (issue #294) — everything from
@@ -71,17 +73,17 @@ export interface SetupProgressCallbacks {
71
73
  onLog?: (message: string) => void;
72
74
  }
73
75
  /**
74
- * Aggregate counts captured during a `--inventory` invocation, forwarded to
75
- * `#writeInventoryRunRow` so the audit log row is consistent between the
76
- * HTML-seed branch and the non-HTML-only branch of
77
- * `CrawlerOrchestrator.inventory`.
76
+ * Aggregate counts captured during a `--inventory` or `--recrawl`
77
+ * invocation, forwarded to `#writeListReconcileRunRow` so the audit log row
78
+ * is consistent across every branch of `CrawlerOrchestrator.inventory` and
79
+ * `CrawlerOrchestrator.recrawl` that reaches ingestion.
78
80
  *
79
81
  * Spelled out here (not inlined at the call site) so a new field added to
80
82
  * the audit row has a single edit point and so each field's semantics are
81
- * documented per-property rather than scattered across the two emit sites.
83
+ * documented per-property rather than scattered across the emit sites.
82
84
  */
83
- export interface InventoryRunAggregates {
84
- /** `inventoryUrls.length` as received by `CrawlerOrchestrator.inventory` — the CLI (`inventoryCrawl`) has already warned-and-dropped unparseable-URL lines before this point, so this counts valid URLs, not raw source-file lines. Stored verbatim as `inventory_runs.total_lines`. */
85
+ export interface ListReconcileRunAggregates {
86
+ /** `inventoryUrls.length` as received by `CrawlerOrchestrator.inventory` — the CLI (`inventoryCrawl`) has already warned-and-dropped unparseable-URL lines before this point, so this counts valid URLs, not raw source-file lines. Stored verbatim as `list_reconcile_runs.total_lines`. */
85
87
  inventoryUrlsCount: number;
86
88
  /** Number of novel URLs classified as HTML and queued for render. Stored as `new_pages` (excludes anchor-discovered descendants — those add later via the crawler graph and are NOT counted here). */
87
89
  htmlSeedsCount: number;
@@ -94,7 +96,7 @@ export interface InventoryRunAggregates {
94
96
  /**
95
97
  * SHA-256 hex digest of the source `.txt`, **pre-computed by the caller**
96
98
  * (typically the CLI's `inventoryCrawl`). Stored verbatim as
97
- * `inventory_runs.source_file_sha256`.
99
+ * `list_reconcile_runs.source_file_sha256`.
98
100
  *
99
101
  * Pre-computation lifts the absolute path off the orchestrator
100
102
  * boundary entirely — the path is privacy-sensitive (leaks
@@ -108,11 +110,28 @@ export interface InventoryRunAggregates {
108
110
  /**
109
111
  * Number of source-file lines the CLI warned-and-dropped for failing
110
112
  * URL validation, before `inventoryUrlsCount` was ever counted. Stored
111
- * verbatim as `inventory_runs.invalid_skipped`. `null` for programmatic
113
+ * verbatim as `list_reconcile_runs.invalid_skipped`. `null` for programmatic
112
114
  * callers that built `inventoryUrls` in-memory — there is no source
113
115
  * file, so no line was ever dropped as invalid.
114
116
  */
115
117
  invalidSkipped: number | null;
118
+ /**
119
+ * Overrides the audit row's `list_label` prefix (before the `-${ranAt}`
120
+ * timestamp suffix `#writeListReconcileRunRow` always appends). Omit for
121
+ * the default `'inventory'`; `CrawlerOrchestrator.recrawl` passes
122
+ * `'recrawl'` so the two invocation kinds stay distinguishable in
123
+ * `query list-reconcile-runs` output despite sharing one audit table.
124
+ */
125
+ listLabelPrefix?: string;
126
+ /**
127
+ * Free-form text stored verbatim as `list_reconcile_runs.notes`. `recrawl`
128
+ * uses this to record how many existing pages it reset back to
129
+ * pending — a fact `--inventory` never produces and that therefore has
130
+ * no dedicated column (adding one would leave it `NULL` on every
131
+ * `--inventory` row forever). `null`/omitted leaves the column `NULL`,
132
+ * matching `--inventory`'s existing rows.
133
+ */
134
+ notes?: string | null;
116
135
  }
117
136
  /**
118
137
  * Coarse cause of a crawl/scrape failure.
@@ -165,6 +184,18 @@ export interface InventoryRunAggregates {
165
184
  * PUPPETEER_FALLBACK entry).
166
185
  */
167
186
  export type ErrorKind = 'dns' | 'dns-transient' | 'connection-refused' | 'connection-reset' | 'connection-timeout' | 'tls' | 'local-network' | 'parse-error' | 'client-blocked' | 'redirect-loop' | 'timeout' | 'protocol' | 'unknown';
187
+ /**
188
+ * Why an auto-retry loop stopped without reaching `pending === 0` (issue
189
+ * #350):
190
+ *
191
+ * - `'exhausted'` — every configured `--max-auto-retry` attempt ran and
192
+ * pending URLs still remain.
193
+ * - `'no-progress'` — a single attempt made no dent in the pending count
194
+ * (it stayed the same or grew), so the remaining budget is abandoned
195
+ * early instead of burning every attempt against a cause retrying will
196
+ * not fix (e.g. a wholesale host outage).
197
+ */
198
+ export type PendingUrlsRemainReason = 'exhausted' | 'no-progress';
168
199
  /**
169
200
  * Event map for the `CrawlerOrchestrator` class.
170
201
  *
@@ -260,6 +291,25 @@ export interface CrawlEvent {
260
291
  /** The formatted, ready-to-display notice text. */
261
292
  message: string;
262
293
  };
294
+ /**
295
+ * Emitted once before each auto-retry attempt's backoff wait begins
296
+ * (issue #350's `#crawlUntilPendingClears`) — see that method's JSDoc for
297
+ * the loop it belongs to. Purely observational: the CLI does not
298
+ * subscribe to this for display (the orchestrator prints its own
299
+ * `console.error` line for the same reason `networkOutageConfirmed`
300
+ * does), but tests and other programmatic consumers can use it to
301
+ * observe retry progress without capturing stdout.
302
+ */
303
+ autoRetryWaiting: {
304
+ /** The 1-indexed attempt about to run after this wait. */
305
+ attempt: number;
306
+ /** The configured `--max-auto-retry` ceiling. */
307
+ maxAttempts: number;
308
+ /** Pending URL count that triggered this retry. */
309
+ pendingCount: number;
310
+ /** Backoff delay before the attempt starts, in milliseconds. */
311
+ delayMs: number;
312
+ };
263
313
  /**
264
314
  * Emitted when an error occurs during crawling or archiving.
265
315
  */
@@ -2,7 +2,7 @@
2
2
  * Compute the SHA-256 digest of an in-memory byte buffer.
3
3
  *
4
4
  * Used by `CrawlerOrchestrator.inventory` to fingerprint the source URL
5
- * list — the digest is both recorded on the `inventory_runs` audit row
5
+ * list — the digest is both recorded on the `list_reconcile_runs` audit row
6
6
  * (the content-identity key for "was this exact list applied before") and
7
7
  * used as the file name under which the raw list is archived
8
8
  * (`Archive.saveInventorySourceList`).
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  * Compute the SHA-256 digest of an in-memory byte buffer.
4
4
  *
5
5
  * Used by `CrawlerOrchestrator.inventory` to fingerprint the source URL
6
- * list — the digest is both recorded on the `inventory_runs` audit row
6
+ * list — the digest is both recorded on the `list_reconcile_runs` audit row
7
7
  * (the content-identity key for "was this exact list applied before") and
8
8
  * used as the file name under which the raw list is archived
9
9
  * (`Archive.saveInventorySourceList`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitpicker/crawler",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Web crawler engine with headless browser rendering and archive storage",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
@@ -30,8 +30,8 @@
30
30
  "clean": "tsc --build --clean"
31
31
  },
32
32
  "dependencies": {
33
- "@d-zero/beholder": "4.2.3",
34
- "@d-zero/dealer": "1.13.1",
33
+ "@d-zero/beholder": "5.0.0",
34
+ "@d-zero/dealer": "1.14.0",
35
35
  "@d-zero/fs": "0.2.7",
36
36
  "@d-zero/shared": "0.23.0",
37
37
  "ansi-colors": "4.1.3",
@@ -51,5 +51,5 @@
51
51
  "@types/tar": "7.0.87",
52
52
  "@types/unzipper": "0.10.11"
53
53
  },
54
- "gitHead": "233d081009aeeeb2d2622536ca29b22693ba65ca"
54
+ "gitHead": "934b1714f4394effb3865d84bf8348ed74f45840"
55
55
  }
@@ -1,21 +0,0 @@
1
- import type { InventoryRunMeta } from '../../types.js';
2
- import type { Knex } from 'knex';
3
- /**
4
- * Appends one row to the `inventory_runs` audit log.
5
- *
6
- * Called by `CrawlerOrchestrator.inventory` on every successful
7
- * `--inventory <list>` invocation so the archive carries a durable
8
- * record of which deploy list was applied when and at what scale —
9
- * the operational question "did we apply last month's list" the
10
- * archive itself can answer without consulting external bookkeeping.
11
- *
12
- * Append-only. There is intentionally no UPDATE path and no UNIQUE
13
- * constraint on `source_file_sha256`; two applies of the same list
14
- * each get their own row. Duplicate detection is left to readers —
15
- * the hash is recorded as the content-identity key they would use.
16
- * Field-level NULL semantics live on {@link InventoryRunMeta}.
17
- * @param knex - Knex query builder connected to the archive DB.
18
- * @param meta - The run metadata to record. Only `ran_at` is required.
19
- * @returns The autoincremented `id` of the newly-inserted row.
20
- */
21
- export declare function recordInventoryRun(knex: Knex, meta: InventoryRunMeta): Promise<number>;
@@ -1,16 +0,0 @@
1
- import type { Knex } from 'knex';
2
- /**
3
- * Adds the `inventory_runs.invalid_skipped` column to archives created
4
- * before it existed. `CREATE TABLE IF NOT EXISTS` (used for `inventory_runs`
5
- * itself) cannot retrofit a new column onto an already-existing table, so
6
- * this lightweight, `hasColumn`-guarded `ALTER TABLE` runs on every
7
- * `initSchema` call — idempotent, and self-healing for archives whose
8
- * provisioning crashed partway through.
9
- * @param instance - The Knex query builder instance connected to the database.
10
- * @param onLog - Called instead of `console.error` when this migration
11
- * actually applies (issue #294: a bare `console.error` here can fire
12
- * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
13
- * `Archive.open`, corrupting its cursor tracking). Falls back to
14
- * `console.error` when omitted (direct/test callers).
15
- */
16
- export declare function migrateInventoryRunsInvalidSkipped(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -1,35 +0,0 @@
1
- /**
2
- * Adds the `inventory_runs.invalid_skipped` column to archives created
3
- * before it existed. `CREATE TABLE IF NOT EXISTS` (used for `inventory_runs`
4
- * itself) cannot retrofit a new column onto an already-existing table, so
5
- * this lightweight, `hasColumn`-guarded `ALTER TABLE` runs on every
6
- * `initSchema` call — idempotent, and self-healing for archives whose
7
- * provisioning crashed partway through.
8
- * @param instance - The Knex query builder instance connected to the database.
9
- * @param onLog - Called instead of `console.error` when this migration
10
- * actually applies (issue #294: a bare `console.error` here can fire
11
- * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
12
- * `Archive.open`, corrupting its cursor tracking). Falls back to
13
- * `console.error` when omitted (direct/test callers).
14
- */
15
- export async function migrateInventoryRunsInvalidSkipped(instance, onLog) {
16
- const hasTable = await instance.schema.hasTable('inventory_runs');
17
- if (!hasTable) {
18
- return;
19
- }
20
- const hasColumn = await instance.schema.hasColumn('inventory_runs', 'invalid_skipped');
21
- if (hasColumn) {
22
- return;
23
- }
24
- await instance.schema.table('inventory_runs', (t) => {
25
- t.integer('invalid_skipped');
26
- });
27
- const message = '[migrate] inventory_runs.invalid_skipped column added';
28
- if (onLog) {
29
- onLog(message);
30
- }
31
- else {
32
- // eslint-disable-next-line no-console
33
- console.error(message);
34
- }
35
- }