@gscdump/cli 3.8.0 → 4.0.1

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 (73) hide show
  1. package/README.md +115 -30
  2. package/bin/gscdump.mjs +21 -1
  3. package/dist/analysis-local.mjs +172 -53
  4. package/dist/auth-state.mjs +21 -6
  5. package/dist/auth.mjs +27 -18
  6. package/dist/bing-data.mjs +6 -2
  7. package/dist/bing-hosted.mjs +1 -1
  8. package/dist/cli-args.mjs +8 -0
  9. package/dist/cli.d.mts +5 -1
  10. package/dist/cli.mjs +41 -10
  11. package/dist/cloud-google.mjs +8 -1
  12. package/dist/command-meta.mjs +4 -4
  13. package/dist/command-registry.mjs +86 -10
  14. package/dist/commands/analyze.mjs +84 -43
  15. package/dist/commands/auth.mjs +46 -37
  16. package/dist/commands/bing.mjs +3 -3
  17. package/dist/commands/compact.mjs +12 -8
  18. package/dist/commands/config.mjs +14 -14
  19. package/dist/commands/doctor.mjs +26 -26
  20. package/dist/commands/dump.mjs +203 -105
  21. package/dist/commands/entities.mjs +10 -113
  22. package/dist/commands/gc.mjs +4 -3
  23. package/dist/commands/indexing-urls.mjs +185 -0
  24. package/dist/commands/indexing.mjs +12 -8
  25. package/dist/commands/init.mjs +77 -18
  26. package/dist/commands/inspect.mjs +97 -110
  27. package/dist/commands/mcp.mjs +37 -57
  28. package/dist/commands/papercut.mjs +1 -1
  29. package/dist/commands/profile.mjs +9 -17
  30. package/dist/commands/query.mjs +219 -210
  31. package/dist/commands/report.mjs +68 -85
  32. package/dist/commands/rollups.mjs +9 -6
  33. package/dist/commands/sitemaps.mjs +51 -104
  34. package/dist/commands/sites.mjs +40 -24
  35. package/dist/commands/stats.mjs +14 -17
  36. package/dist/commands/store-purge.mjs +9 -5
  37. package/dist/commands/store.mjs +0 -12
  38. package/dist/commands/sync.mjs +774 -296
  39. package/dist/config.mjs +7 -4
  40. package/dist/context.mjs +51 -21
  41. package/dist/coverage.mjs +251 -0
  42. package/dist/dump-bing.mjs +71 -0
  43. package/dist/dump-writers.mjs +246 -0
  44. package/dist/error-handler.mjs +108 -38
  45. package/dist/filters.mjs +125 -0
  46. package/dist/hosted-site.mjs +91 -0
  47. package/dist/inspect-urls.mjs +76 -0
  48. package/dist/inspection-record.mjs +93 -0
  49. package/dist/local-entities.mjs +658 -0
  50. package/dist/mcp/errors.mjs +71 -18
  51. package/dist/mcp/handlers/diagnostics.mjs +15 -12
  52. package/dist/mcp/handlers/reports.mjs +33 -42
  53. package/dist/mcp/server/index.mjs +80 -38
  54. package/dist/mcp/types.mjs +7 -7
  55. package/dist/package.mjs +1 -1
  56. package/dist/quota-ledger.mjs +370 -0
  57. package/dist/render/analysis.mjs +8 -1
  58. package/dist/render/report.mjs +8 -1
  59. package/dist/request-pacer.mjs +29 -0
  60. package/dist/route.mjs +301 -0
  61. package/dist/sitemap.mjs +4 -0
  62. package/dist/sql-views.mjs +117 -0
  63. package/dist/store-sites.mjs +134 -0
  64. package/dist/sync-plan.mjs +129 -0
  65. package/dist/sync-run.mjs +96 -0
  66. package/dist/table-sources.mjs +57 -0
  67. package/dist/token-info.mjs +54 -0
  68. package/dist/utils.mjs +28 -19
  69. package/dist/window.mjs +159 -0
  70. package/package.json +17 -14
  71. package/skills/gscdump/SKILL.md +172 -37
  72. package/dist/commands/export.mjs +0 -76
  73. package/dist/native-duckdb.mjs +0 -46
@@ -0,0 +1,658 @@
1
+ import { blockUntil, googleErrorMessage, parseQuotaRefusal } from "./quota-ledger.mjs";
2
+ import { runWithConcurrency } from "./utils.mjs";
3
+ import { latestByUrl, planInspections, recentMonths, scheduleOf, toInspectionRecord } from "./inspection-record.mjs";
4
+ import { createIndexingMetadataStore, createInspectionStore, createSitemapListStore, createSitemapReadStore, createSitemapStore, hashUrl, parseSitemapFeedIdentity } from "@gscdump/engine/entities";
5
+ async function loadInspectionHistory(dataSource, ctx) {
6
+ const inspector = createInspectionStore({ dataSource });
7
+ const months = await inspector.listHistoryMonths(ctx);
8
+ const records = [];
9
+ for (const month of months) {
10
+ const shard = await inspector.loadHistory(ctx, month);
11
+ if (shard) records.push(...shard.records);
12
+ }
13
+ return records;
14
+ }
15
+ async function loadInspectionState(dataSource, ctx, now) {
16
+ const inspector = createInspectionStore({ dataSource });
17
+ const indexed = await inspector.loadMaterialized(ctx);
18
+ if (!indexed) {
19
+ const history = await loadInspectionHistory(dataSource, ctx);
20
+ return {
21
+ latest: latestByUrl(history),
22
+ recent: history
23
+ };
24
+ }
25
+ const recent = [];
26
+ for (const month of recentMonths(now)) {
27
+ const shard = await inspector.loadHistory(ctx, month);
28
+ if (shard) recent.push(...shard.records);
29
+ }
30
+ return {
31
+ latest: latestByUrl([...indexed.map(fromParquetRow), ...recent]),
32
+ recent
33
+ };
34
+ }
35
+ function fromParquetRow(row) {
36
+ const record = {
37
+ url: row.url,
38
+ inspectedAt: row.inspectedAt
39
+ };
40
+ for (const field of [
41
+ "indexStatus",
42
+ "lastCrawlTime",
43
+ "googleCanonical",
44
+ "userCanonical",
45
+ "coverageState",
46
+ "robotsTxtState",
47
+ "indexingState",
48
+ "pageFetchState",
49
+ "mobileUsabilityVerdict",
50
+ "richResultsVerdict"
51
+ ]) {
52
+ const value = row[field];
53
+ if (value != null) record[field] = value;
54
+ }
55
+ if (row.scheduleNextAt != null && row.scheduleConsecutiveUnchanged != null && row.schedulePolicyVersion != null) record.raw = { schedule: {
56
+ nextAt: row.scheduleNextAt,
57
+ consecutiveUnchanged: row.scheduleConsecutiveUnchanged,
58
+ policyVersion: row.schedulePolicyVersion
59
+ } };
60
+ return record;
61
+ }
62
+ function toParquetRow(record) {
63
+ const schedule = scheduleOf(record);
64
+ return {
65
+ urlHash: hashUrl(record.url),
66
+ url: record.url,
67
+ inspectedAt: record.inspectedAt,
68
+ indexStatus: record.indexStatus ?? null,
69
+ lastCrawlTime: record.lastCrawlTime ?? null,
70
+ googleCanonical: record.googleCanonical ?? null,
71
+ userCanonical: record.userCanonical ?? null,
72
+ coverageState: record.coverageState ?? null,
73
+ robotsTxtState: record.robotsTxtState ?? null,
74
+ indexingState: record.indexingState ?? null,
75
+ pageFetchState: record.pageFetchState ?? null,
76
+ mobileUsabilityVerdict: record.mobileUsabilityVerdict ?? null,
77
+ richResultsVerdict: record.richResultsVerdict ?? null,
78
+ scheduleNextAt: schedule.nextAt,
79
+ scheduleConsecutiveUnchanged: schedule.consecutiveUnchanged,
80
+ schedulePolicyVersion: schedule.policyVersion
81
+ };
82
+ }
83
+ async function appendInspections(dataSource, ctx, records) {
84
+ if (records.length > 0) await createInspectionStore({ dataSource }).appendHistory(ctx, records);
85
+ }
86
+ async function materializeInspectionIndex(dataSource, ctx, latest, records) {
87
+ const merged = latestByUrl([...latest.values(), ...records]);
88
+ const result = await createInspectionStore({ dataSource }).materialize(ctx, [...merged.values()].map(toParquetRow));
89
+ return {
90
+ indexKey: result.key,
91
+ indexRows: result.rowCount
92
+ };
93
+ }
94
+ async function recordInspections(dataSource, ctx, latest, records) {
95
+ if (records.length === 0) return void 0;
96
+ await appendInspections(dataSource, ctx, records);
97
+ return materializeInspectionIndex(dataSource, ctx, latest, records);
98
+ }
99
+ function urlInProperty(siteUrl, url) {
100
+ let parsed;
101
+ try {
102
+ parsed = new URL(url);
103
+ } catch {
104
+ return false;
105
+ }
106
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
107
+ if (siteUrl.startsWith("sc-domain:")) {
108
+ const domain = siteUrl.slice(10).toLowerCase();
109
+ const host = parsed.hostname.toLowerCase();
110
+ return host === domain || host.endsWith(`.${domain}`);
111
+ }
112
+ return url.startsWith(siteUrl);
113
+ }
114
+ function resolvePagePaths(paths, siteUrl, knownUrls) {
115
+ let origin;
116
+ if (siteUrl.startsWith("sc-domain:")) {
117
+ const counts = /* @__PURE__ */ new Map();
118
+ for (const url of knownUrls) {
119
+ if (!urlInProperty(siteUrl, url)) continue;
120
+ const o = new URL(url).origin;
121
+ counts.set(o, (counts.get(o) ?? 0) + 1);
122
+ }
123
+ origin = [...counts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? `https://${siteUrl.slice(10)}`;
124
+ }
125
+ const out = [];
126
+ for (const value of paths) try {
127
+ out.push(new URL(value, origin ?? siteUrl).toString());
128
+ } catch {}
129
+ return out;
130
+ }
131
+ const INSPECT_PAGE_CANDIDATES = 5e3;
132
+ async function topPagePaths(store, siteUrl) {
133
+ if ((await store.engine.listLive({
134
+ userId: store.userId,
135
+ siteId: store.siteIdFor(siteUrl),
136
+ table: "pages"
137
+ })).length === 0) return [];
138
+ const { rows } = await store.runRawSql({
139
+ sql: `SELECT url, SUM(impressions) AS impressions FROM read_parquet({{FILES}}, union_by_name = true) GROUP BY url ORDER BY impressions DESC LIMIT ${INSPECT_PAGE_CANDIDATES}`,
140
+ siteUrl,
141
+ table: "pages"
142
+ });
143
+ return rows.map((row) => String(row.url));
144
+ }
145
+ async function inspectionCandidates(store, siteUrl) {
146
+ const ctx = {
147
+ userId: store.userId,
148
+ siteId: store.siteIdFor(siteUrl)
149
+ };
150
+ const sitemapUrls = await loadSitemapGenerationUrls(store.dataSource, ctx);
151
+ const trafficUrls = resolvePagePaths(await topPagePaths(store, siteUrl), siteUrl, sitemapUrls);
152
+ const rank = new Map(trafficUrls.map((url, i) => [url, i]));
153
+ const sitemapFirst = [...sitemapUrls].sort((a, b) => (rank.get(a) ?? Infinity) - (rank.get(b) ?? Infinity));
154
+ const listed = new Set(sitemapUrls);
155
+ return [.../* @__PURE__ */ new Set([...sitemapFirst, ...trafficUrls.filter((url) => !listed.has(url))])].filter((url) => urlInProperty(siteUrl, url));
156
+ }
157
+ const INSPECTION_CHUNK = 20;
158
+ async function syncInspections(deps) {
159
+ const state = await loadInspectionState(deps.dataSource, deps.ctx, deps.now());
160
+ let latest = state.latest;
161
+ const candidates = deps.candidates.filter((url) => urlInProperty(deps.siteUrl, url));
162
+ const plan = planInspections({
163
+ candidates,
164
+ latest,
165
+ recent: state.recent,
166
+ now: deps.now(),
167
+ limit: deps.limit
168
+ });
169
+ if (plan.urls.length === 0) return {
170
+ _tag: "nothing_due",
171
+ candidates: candidates.length,
172
+ quotaLeft: plan.quotaLeft
173
+ };
174
+ const failures = [];
175
+ let inspected = 0;
176
+ let done = 0;
177
+ let stopped;
178
+ const queue = [...plan.urls];
179
+ while (queue.length > 0 && !stopped) {
180
+ const decision = deps.ledger.reserve("urlInspection", deps.siteUrl, Math.min(INSPECTION_CHUNK, queue.length));
181
+ if (decision.kind === "exhausted") {
182
+ stopped = {
183
+ reason: decision.reason,
184
+ resetsAt: decision.resetsAt
185
+ };
186
+ break;
187
+ }
188
+ const chunk = queue.splice(0, decision.n);
189
+ const records = [];
190
+ let unused = 0;
191
+ await runWithConcurrency(chunk, deps.concurrency, async (url) => {
192
+ if (stopped) {
193
+ unused++;
194
+ return;
195
+ }
196
+ const response = await deps.client.inspect(deps.siteUrl, url).catch((error) => ({ error }));
197
+ if ("error" in response) {
198
+ const refusal = parseQuotaRefusal(response.error);
199
+ if (refusal) {
200
+ deps.ledger.record("urlInspection", deps.siteUrl, {
201
+ kind: "refused",
202
+ reason: refusal
203
+ });
204
+ stopped ??= {
205
+ reason: refusal,
206
+ resetsAt: blockUntil("urlInspection", refusal, deps.now())
207
+ };
208
+ unused++;
209
+ return;
210
+ }
211
+ failures.push({
212
+ url,
213
+ error: googleErrorMessage(response.error)
214
+ });
215
+ } else records.push(toInspectionRecord({
216
+ url,
217
+ result: response.inspectionResult,
218
+ inspectedAt: deps.now(),
219
+ previous: latest.get(url)
220
+ }));
221
+ done++;
222
+ deps.onProgress?.(done, plan.urls.length);
223
+ });
224
+ if (unused > 0) deps.ledger.record("urlInspection", deps.siteUrl, {
225
+ kind: "unused",
226
+ n: unused
227
+ });
228
+ await recordInspections(deps.dataSource, deps.ctx, latest, records);
229
+ latest = latestByUrl([...latest.values(), ...records]);
230
+ inspected += records.length;
231
+ }
232
+ const notRun = plan.urls.length - done;
233
+ if (done === 0 && stopped) return {
234
+ _tag: "quota_exhausted",
235
+ reason: stopped.reason,
236
+ resetsAt: stopped.resetsAt,
237
+ deferred: plan.deferred + notRun
238
+ };
239
+ return {
240
+ _tag: "inspected",
241
+ inspected,
242
+ failed: failures.length,
243
+ failures,
244
+ deferred: plan.deferred + notRun,
245
+ quotaLeft: Math.max(0, plan.quotaLeft - done),
246
+ ...stopped ? { stopped } : {}
247
+ };
248
+ }
249
+ function toCount(value) {
250
+ const n = Number(value);
251
+ return Number.isFinite(n) ? n : 0;
252
+ }
253
+ function toSitemapListEntry(sitemap) {
254
+ if (!sitemap.path) return void 0;
255
+ return {
256
+ path: sitemap.path,
257
+ type: sitemap.type ?? null,
258
+ isPending: sitemap.isPending ?? false,
259
+ isSitemapsIndex: sitemap.isSitemapsIndex ?? false,
260
+ lastSubmitted: sitemap.lastSubmitted ?? null,
261
+ lastDownloaded: sitemap.lastDownloaded ?? null,
262
+ warnings: toCount(sitemap.warnings),
263
+ errors: toCount(sitemap.errors),
264
+ contents: (sitemap.contents ?? []).map((content) => ({
265
+ type: content.type ?? "unknown",
266
+ submitted: toCount(content.submitted),
267
+ indexed: content.indexed == null ? null : toCount(content.indexed)
268
+ }))
269
+ };
270
+ }
271
+ function errorMessage(error) {
272
+ return error instanceof Error ? error.message : String(error);
273
+ }
274
+ async function syncSitemaps(deps) {
275
+ const listed = await deps.client.sitemaps.list(deps.siteUrl).catch((error) => new Error(errorMessage(error)));
276
+ if (listed instanceof Error) return {
277
+ _tag: "failed",
278
+ reason: `Search Console sitemap list failed: ${listed.message}`
279
+ };
280
+ const fetchedAt = deps.now();
281
+ const sitemaps = listed.flatMap((sitemap) => toSitemapListEntry(sitemap) ?? []);
282
+ const doc = {
283
+ version: 1,
284
+ fetchedAt: fetchedAt.toISOString(),
285
+ sitemaps
286
+ };
287
+ await createSitemapListStore({ dataSource: deps.dataSource }).save(deps.ctx, doc);
288
+ const store = createSitemapStore({
289
+ dataSource: deps.dataSource,
290
+ withMutation: (_ctx, effect) => effect(),
291
+ now: () => deps.now().getTime()
292
+ });
293
+ const current = await store.getSitemapGeneration(deps.ctx);
294
+ const published = new Set(current._tag === "available" ? Object.values(current.manifest.feeds).map((feed) => feed.feedpath) : []);
295
+ if (sitemaps.length === 0 && published.size === 0) return {
296
+ _tag: "saved",
297
+ sitemaps: 0,
298
+ urls: 0,
299
+ feeds: [],
300
+ generation: "unchanged"
301
+ };
302
+ const generation = {
303
+ _tag: "complete",
304
+ id: deps.generationId(),
305
+ observedAt: fetchedAt.getTime()
306
+ };
307
+ const feeds = [];
308
+ const staged = [];
309
+ let urls = 0;
310
+ let keepReason;
311
+ for (const sitemap of sitemaps) {
312
+ const loaded = await deps.loadFeed(sitemap.path);
313
+ if (loaded._tag === "error") {
314
+ feeds.push({
315
+ _tag: "unreachable",
316
+ path: sitemap.path,
317
+ message: loaded.message
318
+ });
319
+ const identity = parseSitemapFeedIdentity(sitemap.path);
320
+ if (identity._tag === "ok" && published.has(identity.url)) {
321
+ keepReason = `${sitemap.path} could not be read: ${loaded.message}`;
322
+ break;
323
+ }
324
+ continue;
325
+ }
326
+ const result = await store.stageSitemapGenerationFeed(deps.ctx, generation, sitemap.path, loaded.entries);
327
+ if (result._tag !== "staged") {
328
+ feeds.push({
329
+ _tag: "unreachable",
330
+ path: sitemap.path,
331
+ message: `not stored (${result._tag}: ${result.reason})`
332
+ });
333
+ continue;
334
+ }
335
+ staged.push(sitemap.path);
336
+ urls += loaded.entries.length;
337
+ feeds.push({
338
+ _tag: "read",
339
+ path: sitemap.path,
340
+ urls: loaded.entries.length,
341
+ complete: loaded.complete
342
+ });
343
+ }
344
+ if (keepReason) {
345
+ await store.abortSitemapGeneration(deps.ctx, generation);
346
+ return {
347
+ _tag: "list_only",
348
+ sitemaps: sitemaps.length,
349
+ feeds,
350
+ reason: keepReason
351
+ };
352
+ }
353
+ const finalized = await store.finalizeSitemapGeneration(deps.ctx, generation, {
354
+ _tag: "complete",
355
+ expectedFeedpaths: staged
356
+ });
357
+ if (finalized._tag === "published" || finalized._tag === "unchanged") return {
358
+ _tag: "saved",
359
+ sitemaps: sitemaps.length,
360
+ urls,
361
+ feeds,
362
+ generation: finalized._tag
363
+ };
364
+ await store.abortSitemapGeneration(deps.ctx, generation);
365
+ return {
366
+ _tag: "list_only",
367
+ sitemaps: sitemaps.length,
368
+ feeds,
369
+ reason: `sitemap URLs not stored: ${finalized._tag}`
370
+ };
371
+ }
372
+ async function loadSitemapGenerationUrls(dataSource, ctx) {
373
+ const result = await createSitemapReadStore({ dataSource }).iterateSitemapGenerationUrls(ctx);
374
+ if (result._tag !== "iterator") return [];
375
+ const urls = [];
376
+ for await (const record of result.items) urls.push(record.loc);
377
+ return urls;
378
+ }
379
+ const ENTITY_DATASETS = [
380
+ "inspections",
381
+ "inspection_history",
382
+ "sitemaps",
383
+ "sitemap_urls",
384
+ "indexing_metadata"
385
+ ];
386
+ const INSPECTION_COLUMNS = [
387
+ {
388
+ name: "url",
389
+ type: "VARCHAR",
390
+ nullable: false
391
+ },
392
+ {
393
+ name: "inspected_at",
394
+ type: "VARCHAR",
395
+ nullable: false
396
+ },
397
+ {
398
+ name: "index_status",
399
+ type: "VARCHAR",
400
+ nullable: true
401
+ },
402
+ {
403
+ name: "coverage_state",
404
+ type: "VARCHAR",
405
+ nullable: true
406
+ },
407
+ {
408
+ name: "indexing_state",
409
+ type: "VARCHAR",
410
+ nullable: true
411
+ },
412
+ {
413
+ name: "page_fetch_state",
414
+ type: "VARCHAR",
415
+ nullable: true
416
+ },
417
+ {
418
+ name: "robots_txt_state",
419
+ type: "VARCHAR",
420
+ nullable: true
421
+ },
422
+ {
423
+ name: "last_crawl_time",
424
+ type: "VARCHAR",
425
+ nullable: true
426
+ },
427
+ {
428
+ name: "google_canonical",
429
+ type: "VARCHAR",
430
+ nullable: true
431
+ },
432
+ {
433
+ name: "user_canonical",
434
+ type: "VARCHAR",
435
+ nullable: true
436
+ },
437
+ {
438
+ name: "mobile_usability_verdict",
439
+ type: "VARCHAR",
440
+ nullable: true
441
+ },
442
+ {
443
+ name: "rich_results_verdict",
444
+ type: "VARCHAR",
445
+ nullable: true
446
+ },
447
+ {
448
+ name: "next_check_at",
449
+ type: "VARCHAR",
450
+ nullable: true
451
+ },
452
+ {
453
+ name: "raw",
454
+ type: "VARCHAR",
455
+ nullable: true
456
+ }
457
+ ];
458
+ const SITEMAP_COLUMNS = [
459
+ {
460
+ name: "path",
461
+ type: "VARCHAR",
462
+ nullable: false
463
+ },
464
+ {
465
+ name: "type",
466
+ type: "VARCHAR",
467
+ nullable: true
468
+ },
469
+ {
470
+ name: "is_pending",
471
+ type: "INTEGER",
472
+ nullable: false
473
+ },
474
+ {
475
+ name: "is_sitemaps_index",
476
+ type: "INTEGER",
477
+ nullable: false
478
+ },
479
+ {
480
+ name: "last_submitted",
481
+ type: "VARCHAR",
482
+ nullable: true
483
+ },
484
+ {
485
+ name: "last_downloaded",
486
+ type: "VARCHAR",
487
+ nullable: true
488
+ },
489
+ {
490
+ name: "warnings",
491
+ type: "BIGINT",
492
+ nullable: false
493
+ },
494
+ {
495
+ name: "errors",
496
+ type: "BIGINT",
497
+ nullable: false
498
+ },
499
+ {
500
+ name: "submitted",
501
+ type: "BIGINT",
502
+ nullable: false
503
+ },
504
+ {
505
+ name: "contents",
506
+ type: "VARCHAR",
507
+ nullable: false
508
+ },
509
+ {
510
+ name: "fetched_at",
511
+ type: "VARCHAR",
512
+ nullable: false
513
+ }
514
+ ];
515
+ const SITEMAP_URL_COLUMNS = [
516
+ {
517
+ name: "sitemap_path",
518
+ type: "VARCHAR",
519
+ nullable: false
520
+ },
521
+ {
522
+ name: "url",
523
+ type: "VARCHAR",
524
+ nullable: false
525
+ },
526
+ {
527
+ name: "lastmod",
528
+ type: "VARCHAR",
529
+ nullable: true
530
+ },
531
+ {
532
+ name: "first_seen_at",
533
+ type: "VARCHAR",
534
+ nullable: false
535
+ },
536
+ {
537
+ name: "last_seen_at",
538
+ type: "VARCHAR",
539
+ nullable: false
540
+ }
541
+ ];
542
+ const INDEXING_METADATA_COLUMNS = [
543
+ {
544
+ name: "url",
545
+ type: "VARCHAR",
546
+ nullable: false
547
+ },
548
+ {
549
+ name: "captured_at",
550
+ type: "VARCHAR",
551
+ nullable: false
552
+ },
553
+ {
554
+ name: "latest_update_at",
555
+ type: "VARCHAR",
556
+ nullable: true
557
+ },
558
+ {
559
+ name: "latest_remove_at",
560
+ type: "VARCHAR",
561
+ nullable: true
562
+ }
563
+ ];
564
+ function inspectionRow(record) {
565
+ const nextAt = scheduleOf(record).nextAt;
566
+ return {
567
+ url: record.url,
568
+ inspected_at: record.inspectedAt,
569
+ index_status: record.indexStatus ?? null,
570
+ coverage_state: record.coverageState ?? null,
571
+ indexing_state: record.indexingState ?? null,
572
+ page_fetch_state: record.pageFetchState ?? null,
573
+ robots_txt_state: record.robotsTxtState ?? null,
574
+ last_crawl_time: record.lastCrawlTime ?? null,
575
+ google_canonical: record.googleCanonical ?? null,
576
+ user_canonical: record.userCanonical ?? null,
577
+ mobile_usability_verdict: record.mobileUsabilityVerdict ?? null,
578
+ rich_results_verdict: record.richResultsVerdict ?? null,
579
+ next_check_at: Number.isFinite(nextAt) && nextAt > 0 ? new Date(nextAt).toISOString() : null,
580
+ raw: record.raw ? JSON.stringify(record.raw) : null
581
+ };
582
+ }
583
+ function sitemapRow(entry, fetchedAt) {
584
+ return {
585
+ path: entry.path,
586
+ type: entry.type,
587
+ is_pending: entry.isPending,
588
+ is_sitemaps_index: entry.isSitemapsIndex,
589
+ last_submitted: entry.lastSubmitted,
590
+ last_downloaded: entry.lastDownloaded,
591
+ warnings: entry.warnings,
592
+ errors: entry.errors,
593
+ submitted: entry.contents.reduce((sum, content) => sum + content.submitted, 0),
594
+ contents: JSON.stringify(entry.contents),
595
+ fetched_at: fetchedAt
596
+ };
597
+ }
598
+ async function sitemapUrlRows(dataSource, ctx) {
599
+ const result = await createSitemapReadStore({ dataSource }).iterateSitemapGenerationUrls(ctx);
600
+ if (result._tag !== "iterator") return [];
601
+ const rows = [];
602
+ for await (const record of result.items) rows.push({
603
+ sitemap_path: record.feedpath,
604
+ url: record.loc,
605
+ lastmod: record.lastmod ?? null,
606
+ first_seen_at: new Date(record.firstSeenAt).toISOString(),
607
+ last_seen_at: new Date(record.lastSeenAt).toISOString()
608
+ });
609
+ return rows;
610
+ }
611
+ async function readEntityDatasets(dataSource, ctx, wanted) {
612
+ const want = new Set(wanted);
613
+ const out = [];
614
+ if (want.has("inspections") || want.has("inspection_history")) {
615
+ const history = (await loadInspectionHistory(dataSource, ctx)).sort((a, b) => a.url.localeCompare(b.url) || a.inspectedAt.localeCompare(b.inspectedAt));
616
+ if (want.has("inspections")) {
617
+ const latest = [...latestByUrl(history).values()].sort((a, b) => a.url.localeCompare(b.url));
618
+ out.push({
619
+ dataset: "inspections",
620
+ columns: INSPECTION_COLUMNS,
621
+ rows: latest.map(inspectionRow)
622
+ });
623
+ }
624
+ if (want.has("inspection_history")) out.push({
625
+ dataset: "inspection_history",
626
+ columns: INSPECTION_COLUMNS,
627
+ rows: history.map(inspectionRow)
628
+ });
629
+ }
630
+ if (want.has("sitemaps")) {
631
+ const doc = await createSitemapListStore({ dataSource }).load(ctx);
632
+ out.push({
633
+ dataset: "sitemaps",
634
+ columns: SITEMAP_COLUMNS,
635
+ rows: doc ? doc.sitemaps.map((entry) => sitemapRow(entry, doc.fetchedAt)) : []
636
+ });
637
+ }
638
+ if (want.has("sitemap_urls")) out.push({
639
+ dataset: "sitemap_urls",
640
+ columns: SITEMAP_URL_COLUMNS,
641
+ rows: await sitemapUrlRows(dataSource, ctx)
642
+ });
643
+ if (want.has("indexing_metadata")) {
644
+ const index = await createIndexingMetadataStore({ dataSource }).loadIndex(ctx);
645
+ out.push({
646
+ dataset: "indexing_metadata",
647
+ columns: INDEXING_METADATA_COLUMNS,
648
+ rows: Object.values(index.records).sort((a, b) => a.url.localeCompare(b.url)).map((record) => ({
649
+ url: record.url,
650
+ captured_at: record.capturedAt,
651
+ latest_update_at: record.latestUpdateAt ?? null,
652
+ latest_remove_at: record.latestRemoveAt ?? null
653
+ }))
654
+ });
655
+ }
656
+ return out;
657
+ }
658
+ export { ENTITY_DATASETS, appendInspections, inspectionCandidates, loadInspectionHistory, loadInspectionState, loadSitemapGenerationUrls, materializeInspectionIndex, readEntityDatasets, recordInspections, resolvePagePaths, syncInspections, syncSitemaps, toSitemapListEntry, urlInProperty };