@nitpicker/crawler 0.13.0 → 0.14.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 (71) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +11 -0
  4. package/lib/archive/archive.js +13 -0
  5. package/lib/archive/create-adjunct-tables.d.ts +7 -0
  6. package/lib/archive/create-adjunct-tables.js +150 -0
  7. package/lib/archive/create-entity-tables.d.ts +14 -0
  8. package/lib/archive/create-entity-tables.js +32 -1
  9. package/lib/archive/database.d.ts +55 -1
  10. package/lib/archive/database.js +81 -0
  11. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +20 -0
  12. package/lib/archive/db-ops/analysis/replace-page-templates.js +52 -0
  13. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  14. package/lib/archive/db-ops/lifecycle/init.d.ts +16 -10
  15. package/lib/archive/db-ops/lifecycle/init.js +20 -10
  16. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  17. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  18. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  19. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  20. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  21. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  22. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  23. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  24. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  25. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  26. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  27. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  28. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  29. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  30. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  31. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  32. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  33. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  34. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +5 -3
  35. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +13 -3
  36. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  37. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  38. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  39. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  40. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  41. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  42. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  43. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  44. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  45. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  46. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  47. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  48. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  49. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  50. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  51. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  52. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  53. package/lib/archive/db-ops/pages/write/update-page.js +21 -0
  54. package/lib/archive/init-schema.js +2 -1
  55. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  56. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  57. package/lib/archive/meta/types.d.ts +263 -0
  58. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  59. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  60. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  61. package/lib/archive/migrate-main-contents-columns.js +50 -0
  62. package/lib/archive/page.d.ts +144 -2
  63. package/lib/archive/page.js +195 -0
  64. package/lib/archive/types.d.ts +39 -0
  65. package/lib/crawler/crawler.js +4 -2
  66. package/lib/crawler/fetch-destination.js +2 -0
  67. package/lib/crawler/link-to-page-data.js +2 -0
  68. package/lib/crawler/resource-to-page-data.js +2 -0
  69. package/lib/crawler/types.d.ts +5 -0
  70. package/lib/crawler-orchestrator.js +2 -0
  71. package/package.json +3 -3
@@ -68,6 +68,27 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
68
68
  * @returns An array of anchor records found on the page.
69
69
  */
70
70
  getAnchorsOnPage(pageId: number): Promise<any[]>;
71
+ /**
72
+ * Retrieves the audios within the given page's detected main content
73
+ * region, from `page_main_content_audios`.
74
+ * @param pageId - The database id of the page.
75
+ * @returns Ordered audio rows.
76
+ */
77
+ getAudiosOfPage(pageId: number): Promise<import("./meta/types.js").MainContentAudioRow[]>;
78
+ /**
79
+ * Retrieves the button-like elements within the given page's detected
80
+ * main content region, from `page_main_content_buttons`.
81
+ * @param pageId - The database id of the page.
82
+ * @returns Ordered button rows.
83
+ */
84
+ getButtonsOfPage(pageId: number): Promise<import("./meta/types.js").MainContentButtonRow[]>;
85
+ /**
86
+ * Retrieves the canvases within the given page's detected main content
87
+ * region, from `page_main_content_canvases`.
88
+ * @param pageId - The database id of the page.
89
+ * @returns Ordered canvas rows.
90
+ */
91
+ getCanvasesOfPage(pageId: number): Promise<import("./meta/types.js").MainContentCanvasRow[]>;
71
92
  /**
72
93
  * Retrieves the crawl configuration stored in the archive database.
73
94
  * @returns The parsed {@link Config} object.
@@ -87,6 +108,13 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
87
108
  * @returns The file contents as a string.
88
109
  */
89
110
  getData(name: string, format?: 'txt' | 'html'): Promise<string>;
111
+ /**
112
+ * Retrieves the headings within the given page's detected main content
113
+ * region, from `page_main_content_headings`.
114
+ * @param pageId - The database id of the page.
115
+ * @returns Ordered heading rows.
116
+ */
117
+ getHeadingsOfPage(pageId: number): Promise<import("./meta/types.js").MainContentHeadingRow[]>;
90
118
  /**
91
119
  * Reads the HTML snapshot of a page from the archive.
92
120
  *
@@ -117,6 +145,13 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
117
145
  * }
118
146
  */
119
147
  getHtmlOfPage(pageId: number): Promise<string | null>;
148
+ /**
149
+ * Retrieves the iframes within the given page's detected main content
150
+ * region, from `page_main_content_iframes`.
151
+ * @param pageId - The database id of the page.
152
+ * @returns Ordered iframe rows.
153
+ */
154
+ getIframesOfPage(pageId: number): Promise<import("./meta/types.js").MainContentIframeRow[]>;
120
155
  /**
121
156
  * Retrieves the JSON-LD / SpeculationRules entries for the given page,
122
157
  * parsed back from the `page_jsonld` table.
@@ -131,6 +166,20 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
131
166
  * @returns The Knex instance connected to the SQLite database.
132
167
  */
133
168
  getKnex(): import("knex").Knex<any, any[]>;
169
+ /**
170
+ * Retrieves the images within the given page's detected main content
171
+ * region, from `page_main_content_images`.
172
+ * @param pageId - The database id of the page.
173
+ * @returns Ordered image rows.
174
+ */
175
+ getMainContentImagesOfPage(pageId: number): Promise<import("./meta/types.js").MainContentImageRow[]>;
176
+ /**
177
+ * Retrieves the tables within the given page's detected main content
178
+ * region, from `page_main_content_tables`.
179
+ * @param pageId - The database id of the page.
180
+ * @returns Ordered table rows.
181
+ */
182
+ getMainContentTablesOfPage(pageId: number): Promise<import("./meta/types.js").MainContentTableRow[]>;
134
183
  /**
135
184
  * Retrieves all pages from the archive, optionally filtered by type.
136
185
  * Eagerly loads redirect relationships (`redirectFrom`) but does NOT load
@@ -179,6 +228,13 @@ export declare class ArchiveAccessor extends EventEmitter<DatabaseEvent> {
179
228
  * @returns Ordered entries with provider, category, externalId, etc.
180
229
  */
181
230
  getTagsOfPage(pageId: number): Promise<import("./meta/types.js").TagRow[]>;
231
+ /**
232
+ * Retrieves the videos within the given page's detected main content
233
+ * region, from `page_main_content_videos`.
234
+ * @param pageId - The database id of the page.
235
+ * @returns Ordered video rows.
236
+ */
237
+ getVideosOfPage(pageId: number): Promise<import("./meta/types.js").MainContentVideoRow[]>;
182
238
  /**
183
239
  * Stores custom data in the archive under the configured namespace.
184
240
  * Requires a namespace to be set on this accessor; throws if namespace is null.
@@ -128,6 +128,33 @@ export class ArchiveAccessor extends EventEmitter {
128
128
  const refs = await this.#db.getAnchorsOnPage(pageId);
129
129
  return refs;
130
130
  }
131
+ /**
132
+ * Retrieves the audios within the given page's detected main content
133
+ * region, from `page_main_content_audios`.
134
+ * @param pageId - The database id of the page.
135
+ * @returns Ordered audio rows.
136
+ */
137
+ async getAudiosOfPage(pageId) {
138
+ return this.#db.getAudiosOfPage(pageId);
139
+ }
140
+ /**
141
+ * Retrieves the button-like elements within the given page's detected
142
+ * main content region, from `page_main_content_buttons`.
143
+ * @param pageId - The database id of the page.
144
+ * @returns Ordered button rows.
145
+ */
146
+ async getButtonsOfPage(pageId) {
147
+ return this.#db.getButtonsOfPage(pageId);
148
+ }
149
+ /**
150
+ * Retrieves the canvases within the given page's detected main content
151
+ * region, from `page_main_content_canvases`.
152
+ * @param pageId - The database id of the page.
153
+ * @returns Ordered canvas rows.
154
+ */
155
+ async getCanvasesOfPage(pageId) {
156
+ return this.#db.getCanvasesOfPage(pageId);
157
+ }
131
158
  /**
132
159
  * Retrieves the crawl configuration stored in the archive database.
133
160
  * @returns The parsed {@link Config} object.
@@ -143,6 +170,15 @@ export class ArchiveAccessor extends EventEmitter {
143
170
  }
144
171
  return await readText(filePath);
145
172
  }
173
+ /**
174
+ * Retrieves the headings within the given page's detected main content
175
+ * region, from `page_main_content_headings`.
176
+ * @param pageId - The database id of the page.
177
+ * @returns Ordered heading rows.
178
+ */
179
+ async getHeadingsOfPage(pageId) {
180
+ return this.#db.getHeadingsOfPage(pageId);
181
+ }
146
182
  /**
147
183
  * Reads the HTML snapshot of a page from the archive.
148
184
  *
@@ -179,6 +215,15 @@ export class ArchiveAccessor extends EventEmitter {
179
215
  }
180
216
  return html;
181
217
  }
218
+ /**
219
+ * Retrieves the iframes within the given page's detected main content
220
+ * region, from `page_main_content_iframes`.
221
+ * @param pageId - The database id of the page.
222
+ * @returns Ordered iframe rows.
223
+ */
224
+ async getIframesOfPage(pageId) {
225
+ return this.#db.getIframesOfPage(pageId);
226
+ }
182
227
  /**
183
228
  * Retrieves the JSON-LD / SpeculationRules entries for the given page,
184
229
  * parsed back from the `page_jsonld` table.
@@ -197,6 +242,24 @@ export class ArchiveAccessor extends EventEmitter {
197
242
  getKnex() {
198
243
  return this.#db.getKnex();
199
244
  }
245
+ /**
246
+ * Retrieves the images within the given page's detected main content
247
+ * region, from `page_main_content_images`.
248
+ * @param pageId - The database id of the page.
249
+ * @returns Ordered image rows.
250
+ */
251
+ async getMainContentImagesOfPage(pageId) {
252
+ return this.#db.getMainContentImagesOfPage(pageId);
253
+ }
254
+ /**
255
+ * Retrieves the tables within the given page's detected main content
256
+ * region, from `page_main_content_tables`.
257
+ * @param pageId - The database id of the page.
258
+ * @returns Ordered table rows.
259
+ */
260
+ async getMainContentTablesOfPage(pageId) {
261
+ return this.#db.getMainContentTablesOfPage(pageId);
262
+ }
200
263
  /**
201
264
  * Retrieves all pages from the archive, optionally filtered by type.
202
265
  * Eagerly loads redirect relationships (`redirectFrom`) but does NOT load
@@ -289,6 +352,15 @@ export class ArchiveAccessor extends EventEmitter {
289
352
  async getTagsOfPage(pageId) {
290
353
  return this.#db.getTagsOfPage(pageId);
291
354
  }
355
+ /**
356
+ * Retrieves the videos within the given page's detected main content
357
+ * region, from `page_main_content_videos`.
358
+ * @param pageId - The database id of the page.
359
+ * @returns Ordered video rows.
360
+ */
361
+ async getVideosOfPage(pageId) {
362
+ return this.#db.getVideosOfPage(pageId);
363
+ }
292
364
  /**
293
365
  * Stores custom data in the archive under the configured namespace.
294
366
  * Requires a namespace to be set on this accessor; throws if namespace is null.
@@ -206,6 +206,17 @@ export default class Archive extends ArchiveAccessor {
206
206
  line?: number | null;
207
207
  col?: number | null;
208
208
  }[]): Promise<void>;
209
+ /**
210
+ * Replaces the archive's DOM-structure template classification
211
+ * (`--templates`) with a fresh SQL-backed set.
212
+ *
213
+ * Thin facade over {@link Database.replacePageTemplates}; kept on
214
+ * `Archive` so the analyze pipeline can persist template keys without
215
+ * reaching into the low-level database class directly.
216
+ * @param templateKeysByUrl - Page URL → template key, as produced by
217
+ * `@nitpicker/core`'s `classifyPageTemplates`.
218
+ */
219
+ replacePageTemplates(templateKeysByUrl: ReadonlyMap<string, string>): Promise<void>;
209
220
  /**
210
221
  * Promote previously-external pages that now fall under the (possibly extended)
211
222
  * scope back to a pending state so that the crawler re-scrapes them as fully
@@ -286,6 +286,19 @@ export default class Archive extends ArchiveAccessor {
286
286
  async replaceAnalysisViolations(violations) {
287
287
  await this.#db.replaceAnalysisViolations(violations);
288
288
  }
289
+ /**
290
+ * Replaces the archive's DOM-structure template classification
291
+ * (`--templates`) with a fresh SQL-backed set.
292
+ *
293
+ * Thin facade over {@link Database.replacePageTemplates}; kept on
294
+ * `Archive` so the analyze pipeline can persist template keys without
295
+ * reaching into the low-level database class directly.
296
+ * @param templateKeysByUrl - Page URL → template key, as produced by
297
+ * `@nitpicker/core`'s `classifyPageTemplates`.
298
+ */
299
+ async replacePageTemplates(templateKeysByUrl) {
300
+ await this.#db.replacePageTemplates(templateKeysByUrl);
301
+ }
289
302
  /**
290
303
  * Promote previously-external pages that now fall under the (possibly extended)
291
304
  * scope back to a pending state so that the crawler re-scrapes them as fully
@@ -9,9 +9,16 @@ import type { Knex } from 'knex';
9
9
  * an external link that failed DNS, or null for a process-level error)
10
10
  * - `page_tags` — Wappalyzer detections, FK → `content_items(id)`
11
11
  * - `page_jsonld` — JSON-LD / SpeculationRules, FK → `content_items(id)`
12
+ * - `page_main_content_headings` / `_images` / `_tables` / `_buttons` /
13
+ * `_iframes` / `_videos` / `_audios` / `_canvases` — beholder
14
+ * `MainContentsData` sub-entity arrays, one row per DOM element, FK →
15
+ * `content_items(id)`
12
16
  * - `inventory_runs` — `--inventory` audit log (no FK; append-only)
13
17
  * - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
14
18
  * FK → `content_items(id)`
19
+ * - `page_templates` — DOM-structure template classification (`--templates`,
20
+ * `@nitpicker/core`'s `template-classification/`), one row per classified
21
+ * page, FK → `content_items(id)`
15
22
  * - `page_html_blobs` + `page_html_ref` — content-addressable HTML
16
23
  * snapshots, FK → `content_items(id)`
17
24
  *
@@ -8,9 +8,16 @@
8
8
  * an external link that failed DNS, or null for a process-level error)
9
9
  * - `page_tags` — Wappalyzer detections, FK → `content_items(id)`
10
10
  * - `page_jsonld` — JSON-LD / SpeculationRules, FK → `content_items(id)`
11
+ * - `page_main_content_headings` / `_images` / `_tables` / `_buttons` /
12
+ * `_iframes` / `_videos` / `_audios` / `_canvases` — beholder
13
+ * `MainContentsData` sub-entity arrays, one row per DOM element, FK →
14
+ * `content_items(id)`
11
15
  * - `inventory_runs` — `--inventory` audit log (no FK; append-only)
12
16
  * - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
13
17
  * FK → `content_items(id)`
18
+ * - `page_templates` — DOM-structure template classification (`--templates`,
19
+ * `@nitpicker/core`'s `template-classification/`), one row per classified
20
+ * page, FK → `content_items(id)`
14
21
  * - `page_html_blobs` + `page_html_ref` — content-addressable HTML
15
22
  * snapshots, FK → `content_items(id)`
16
23
  *
@@ -129,6 +136,134 @@ export async function createAdjunctTables(instance) {
129
136
  // `list_pages_by_jsonld_type` JOINs.
130
137
  await instance.raw('CREATE INDEX page_jsonld_type_pageId ON page_jsonld(type, pageId)');
131
138
  }
139
+ // Beholder `MainContentsData` sub-entities, one adjunct table per array
140
+ // (headings/images/tables/buttons/iframes/videos/audios/canvases). Same
141
+ // shape as `page_tags` / `page_jsonld`: `pageId` FK → `content_items(id)`
142
+ // ON DELETE CASCADE, individually guarded so any subset can pre-exist.
143
+ // `order` preserves the DOM traversal order beholder returns the array
144
+ // in (0-based); it is not itself an index target since these tables are
145
+ // always read whole-page via `WHERE pageId = ? ORDER BY "order"`.
146
+ if (!(await instance.schema.hasTable('page_main_content_headings'))) {
147
+ await instance.schema.createTable('page_main_content_headings', (t) => {
148
+ t.increments('id');
149
+ t.integer('pageId')
150
+ .notNullable()
151
+ .unsigned()
152
+ .references('content_items.id')
153
+ .onDelete('CASCADE');
154
+ t.integer('order').notNullable();
155
+ t.text('text');
156
+ t.integer('level').notNullable();
157
+ t.index('pageId');
158
+ });
159
+ }
160
+ if (!(await instance.schema.hasTable('page_main_content_images'))) {
161
+ await instance.schema.createTable('page_main_content_images', (t) => {
162
+ t.increments('id');
163
+ t.integer('pageId')
164
+ .notNullable()
165
+ .unsigned()
166
+ .references('content_items.id')
167
+ .onDelete('CASCADE');
168
+ t.integer('order').notNullable();
169
+ t.string('src', 8190).notNullable();
170
+ t.text('alt').notNullable();
171
+ t.index('pageId');
172
+ });
173
+ }
174
+ if (!(await instance.schema.hasTable('page_main_content_tables'))) {
175
+ await instance.schema.createTable('page_main_content_tables', (t) => {
176
+ t.increments('id');
177
+ t.integer('pageId')
178
+ .notNullable()
179
+ .unsigned()
180
+ .references('content_items.id')
181
+ .onDelete('CASCADE');
182
+ t.integer('order').notNullable();
183
+ t.integer('rows').notNullable();
184
+ t.integer('cols').notNullable();
185
+ t.boolean('hasHeader').notNullable();
186
+ t.boolean('hasFooter').notNullable();
187
+ t.boolean('hasMergedCell').notNullable();
188
+ t.index('pageId');
189
+ });
190
+ }
191
+ if (!(await instance.schema.hasTable('page_main_content_buttons'))) {
192
+ await instance.schema.createTable('page_main_content_buttons', (t) => {
193
+ t.increments('id');
194
+ t.integer('pageId')
195
+ .notNullable()
196
+ .unsigned()
197
+ .references('content_items.id')
198
+ .onDelete('CASCADE');
199
+ t.integer('order').notNullable();
200
+ t.string('nodeName').notNullable();
201
+ t.string('role');
202
+ t.string('type');
203
+ t.text('text');
204
+ t.boolean('disabled').notNullable();
205
+ t.index('pageId');
206
+ });
207
+ }
208
+ if (!(await instance.schema.hasTable('page_main_content_iframes'))) {
209
+ await instance.schema.createTable('page_main_content_iframes', (t) => {
210
+ t.increments('id');
211
+ t.integer('pageId')
212
+ .notNullable()
213
+ .unsigned()
214
+ .references('content_items.id')
215
+ .onDelete('CASCADE');
216
+ t.integer('order').notNullable();
217
+ t.string('src', 8190).notNullable();
218
+ t.text('title');
219
+ t.string('width');
220
+ t.string('height');
221
+ t.index('pageId');
222
+ });
223
+ }
224
+ if (!(await instance.schema.hasTable('page_main_content_videos'))) {
225
+ await instance.schema.createTable('page_main_content_videos', (t) => {
226
+ t.increments('id');
227
+ t.integer('pageId')
228
+ .notNullable()
229
+ .unsigned()
230
+ .references('content_items.id')
231
+ .onDelete('CASCADE');
232
+ t.integer('order').notNullable();
233
+ t.string('src', 8190).notNullable();
234
+ t.string('poster', 8190);
235
+ t.integer('width').notNullable();
236
+ t.integer('height').notNullable();
237
+ t.index('pageId');
238
+ });
239
+ }
240
+ if (!(await instance.schema.hasTable('page_main_content_audios'))) {
241
+ await instance.schema.createTable('page_main_content_audios', (t) => {
242
+ t.increments('id');
243
+ t.integer('pageId')
244
+ .notNullable()
245
+ .unsigned()
246
+ .references('content_items.id')
247
+ .onDelete('CASCADE');
248
+ t.integer('order').notNullable();
249
+ t.string('src', 8190).notNullable();
250
+ t.index('pageId');
251
+ });
252
+ }
253
+ if (!(await instance.schema.hasTable('page_main_content_canvases'))) {
254
+ await instance.schema.createTable('page_main_content_canvases', (t) => {
255
+ t.increments('id');
256
+ t.integer('pageId')
257
+ .notNullable()
258
+ .unsigned()
259
+ .references('content_items.id')
260
+ .onDelete('CASCADE');
261
+ t.integer('order').notNullable();
262
+ t.integer('width').notNullable();
263
+ t.integer('height').notNullable();
264
+ t.index('pageId');
265
+ });
266
+ }
132
267
  if (!(await instance.schema.hasTable('inventory_runs'))) {
133
268
  await instance.schema.createTable('inventory_runs', (t) => {
134
269
  // One row per successful `--inventory <list>` invocation. The
@@ -184,6 +319,21 @@ export async function createAdjunctTables(instance) {
184
319
  await instance.raw('CREATE INDEX av_code_order ON analysis_violations(code_sort_key, id)');
185
320
  await instance.raw('CREATE INDEX av_page ON analysis_violations(page_id, id)');
186
321
  }
322
+ // DOM-structure template classification (`--templates`). One row per
323
+ // internal HTML page that was classified; `page_id` is both the PK and
324
+ // the natural key (1:1 with `content_items`), so — unlike
325
+ // `analysis_violations`, which is 1:many and needs a surrogate `id` —
326
+ // there's nothing to index beyond the PK itself. `WITHOUT ROWID` packs
327
+ // rows directly in the PK b-tree, matching `page_html_ref`'s shape
328
+ // (small fixed-width row, PK-only lookups).
329
+ if (!(await instance.schema.hasTable('page_templates'))) {
330
+ await instance.raw(`
331
+ CREATE TABLE page_templates (
332
+ page_id INTEGER PRIMARY KEY REFERENCES content_items(id),
333
+ template_key TEXT NOT NULL
334
+ ) WITHOUT ROWID
335
+ `);
336
+ }
187
337
  // Content-addressable HTML blob storage. Knex's schema builder doesn't
188
338
  // expose a WITHOUT ROWID toggle, so the BLOB tables are created via raw
189
339
  // SQL. WITHOUT ROWID keeps the rows packed inside the b-tree leaves
@@ -136,6 +136,20 @@ import type { Knex } from 'knex';
136
136
  * routing so the two entities never disagree on which values count as
137
137
  * "large data URI".
138
138
  *
139
+ * **`page_meta.main_content_*` / `scroll_height_*` columns.** Denormalised
140
+ * aggregates derived from beholder's `MainContentsData` / `ScrollHeightData`
141
+ * (word/element counts, desktop+mobile scroll height), following the same
142
+ * write-once-at-scrape-time pattern as `tag_count` / `jsonld_count` so list
143
+ * / detail reads never re-derive them from the per-page child tables
144
+ * (`page_main_content_headings` etc., see `create-adjunct-tables.ts`).
145
+ * `main_content_node_name` / `_id` / `_role` / `_selector` / `_class_list`
146
+ * identify the detected main-content element; unlike `title_text_id` /
147
+ * `description_text_id` these are stored as plain `TEXT` rather than routed
148
+ * through `text_refs` — the values are page-specific diagnostics with low
149
+ * cross-page reuse, so the ref-table dedup machinery would add write-path
150
+ * cost without a corresponding storage win. `main_content_class_list` holds
151
+ * a JSON-encoded string array.
152
+ *
139
153
  * ### Index rationale
140
154
  *
141
155
  * Every index below reflects a legacy-baseline single-column index that
@@ -135,6 +135,20 @@
135
135
  * routing so the two entities never disagree on which values count as
136
136
  * "large data URI".
137
137
  *
138
+ * **`page_meta.main_content_*` / `scroll_height_*` columns.** Denormalised
139
+ * aggregates derived from beholder's `MainContentsData` / `ScrollHeightData`
140
+ * (word/element counts, desktop+mobile scroll height), following the same
141
+ * write-once-at-scrape-time pattern as `tag_count` / `jsonld_count` so list
142
+ * / detail reads never re-derive them from the per-page child tables
143
+ * (`page_main_content_headings` etc., see `create-adjunct-tables.ts`).
144
+ * `main_content_node_name` / `_id` / `_role` / `_selector` / `_class_list`
145
+ * identify the detected main-content element; unlike `title_text_id` /
146
+ * `description_text_id` these are stored as plain `TEXT` rather than routed
147
+ * through `text_refs` — the values are page-specific diagnostics with low
148
+ * cross-page reuse, so the ref-table dedup machinery would add write-path
149
+ * cost without a corresponding storage win. `main_content_class_list` holds
150
+ * a JSON-encoded string array.
151
+ *
138
152
  * ### Index rationale
139
153
  *
140
154
  * Every index below reflects a legacy-baseline single-column index that
@@ -248,7 +262,24 @@ export async function createEntityTables(instance) {
248
262
  tag_count INTEGER,
249
263
  jsonld_count INTEGER,
250
264
  tags_providers_csv TEXT,
251
- meta_extras_json_id INTEGER REFERENCES json_refs(id)
265
+ meta_extras_json_id INTEGER REFERENCES json_refs(id),
266
+ main_content_node_name TEXT,
267
+ main_content_id TEXT,
268
+ main_content_role TEXT,
269
+ main_content_selector TEXT,
270
+ main_content_class_list TEXT,
271
+ main_content_word_count INTEGER,
272
+ main_content_body_word_count INTEGER,
273
+ main_content_heading_count INTEGER,
274
+ main_content_image_count INTEGER,
275
+ main_content_table_count INTEGER,
276
+ main_content_button_count INTEGER,
277
+ main_content_iframe_count INTEGER,
278
+ main_content_video_count INTEGER,
279
+ main_content_audio_count INTEGER,
280
+ main_content_canvas_count INTEGER,
281
+ scroll_height_desktop INTEGER,
282
+ scroll_height_mobile INTEGER
252
283
  )
253
284
  `);
254
285
  await instance.raw('CREATE INDEX IF NOT EXISTS idx_page_meta_og_type ON page_meta(og_type)');
@@ -1,4 +1,4 @@
1
- import type { JsonLdRow, TagRow } from './meta/types.js';
1
+ import type { JsonLdRow, MainContentAudioRow, MainContentButtonRow, MainContentCanvasRow, MainContentHeadingRow, MainContentIframeRow, MainContentImageRow, MainContentTableRow, MainContentVideoRow, TagRow } from './meta/types.js';
2
2
  import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, InventoryRunMeta, PageFilter, PageSource } from './types.js';
3
3
  import type { PageData, Resource } from '../utils/types/types.js';
4
4
  import type { ExURL, ParseURLOptions } from '@d-zero/shared/parse-url';
@@ -46,6 +46,12 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
46
46
  * @returns An array of anchor records with resolved URL, title, status, and content type.
47
47
  */
48
48
  getAnchorsOnPage(pageId: number): Promise<any[]>;
49
+ /**
50
+ * Retrieves all `page_main_content_audios` rows for the given page id.
51
+ * Delegates to {@link getAudiosOfPageOp}.
52
+ * @param pageId
53
+ */
54
+ getAudiosOfPage(pageId: number): Promise<MainContentAudioRow[]>;
49
55
  /**
50
56
  * Retrieves the base URL of the crawl session from the `info` table.
51
57
  * Delegates to {@link getBaseUrlOp}.
@@ -53,6 +59,18 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
53
59
  * @throws {Error} If no base URL is found in the database.
54
60
  */
55
61
  getBaseUrl(): Promise<string>;
62
+ /**
63
+ * Retrieves all `page_main_content_buttons` rows for the given page id.
64
+ * Delegates to {@link getButtonsOfPageOp}.
65
+ * @param pageId
66
+ */
67
+ getButtonsOfPage(pageId: number): Promise<MainContentButtonRow[]>;
68
+ /**
69
+ * Retrieves all `page_main_content_canvases` rows for the given page id.
70
+ * Delegates to {@link getCanvasesOfPageOp}.
71
+ * @param pageId
72
+ */
73
+ getCanvasesOfPage(pageId: number): Promise<MainContentCanvasRow[]>;
56
74
  /**
57
75
  * Retrieves the full crawl configuration from the `info` table.
58
76
  * Delegates to {@link getConfigOp}.
@@ -85,6 +103,12 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
85
103
  * @returns URLs found in `resources`.
86
104
  */
87
105
  getExistingResourceUrls(urls: readonly string[]): Promise<string[]>;
106
+ /**
107
+ * Retrieves all `page_main_content_headings` rows for the given page id.
108
+ * Delegates to {@link getHeadingsOfPageOp}.
109
+ * @param pageId
110
+ */
111
+ getHeadingsOfPage(pageId: number): Promise<MainContentHeadingRow[]>;
88
112
  /**
89
113
  * Reads the HTML snapshot stored as a zstd-compressed BLOB for the given page.
90
114
  * Delegates to {@link getHtmlOfPageByIdOp}.
@@ -92,6 +116,12 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
92
116
  * @returns The decompressed HTML string, or `null` if no snapshot is stored.
93
117
  */
94
118
  getHtmlOfPageById(pageId: number): Promise<string | null>;
119
+ /**
120
+ * Retrieves all `page_main_content_iframes` rows for the given page id.
121
+ * Delegates to {@link getIframesOfPageOp}.
122
+ * @param pageId
123
+ */
124
+ getIframesOfPage(pageId: number): Promise<MainContentIframeRow[]>;
95
125
  /**
96
126
  * Retrieves all `page_jsonld` rows for the given page id, parsed back into
97
127
  * {@link JsonLdRow} shape. Delegates to {@link getJsonLdOfPageOp}.
@@ -105,6 +135,18 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
105
135
  * @returns The Knex instance connected to the SQLite database.
106
136
  */
107
137
  getKnex(): Knex;
138
+ /**
139
+ * Retrieves all `page_main_content_images` rows for the given page id.
140
+ * Delegates to {@link getMainContentImagesOfPageOp}.
141
+ * @param pageId
142
+ */
143
+ getMainContentImagesOfPage(pageId: number): Promise<MainContentImageRow[]>;
144
+ /**
145
+ * Retrieves all `page_main_content_tables` rows for the given page id.
146
+ * Delegates to {@link getMainContentTablesOfPageOp}.
147
+ * @param pageId
148
+ */
149
+ getMainContentTablesOfPage(pageId: number): Promise<MainContentTableRow[]>;
108
150
  /**
109
151
  * Retrieves the crawl session name from the `info` table.
110
152
  * Delegates to {@link getNameOp}.
@@ -207,6 +249,12 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
207
249
  * @param pageId
208
250
  */
209
251
  getTagsOfPage(pageId: number): Promise<TagRow[]>;
252
+ /**
253
+ * Retrieves all `page_main_content_videos` rows for the given page id.
254
+ * Delegates to {@link getVideosOfPageOp}.
255
+ * @param pageId
256
+ */
257
+ getVideosOfPage(pageId: number): Promise<MainContentVideoRow[]>;
210
258
  /**
211
259
  * Records a crawler-level (`error` channel) failure into `crawl_errors`.
212
260
  * Delegates to {@link insertCrawlErrorOp}.
@@ -291,6 +339,12 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
291
339
  line?: number | null;
292
340
  col?: number | null;
293
341
  }[]): Promise<void>;
342
+ /**
343
+ * Replaces the stored DOM-structure template classification with a
344
+ * freshly generated set. Delegates to {@link replacePageTemplatesOp}.
345
+ * @param templateKeysByUrl - Page URL → template key.
346
+ */
347
+ replacePageTemplates(templateKeysByUrl: ReadonlyMap<string, string>): Promise<void>;
294
348
  /**
295
349
  * Promote previously-external pages whose URL falls under any of the new
296
350
  * scope entries back to a "needs scraping" state.