@nitpicker/core 0.23.1 → 0.25.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 (83) hide show
  1. package/README.md +165 -35
  2. package/lib/assert-u32-id.d.ts +15 -0
  3. package/lib/assert-u32-id.js +22 -0
  4. package/lib/check-native-executor-config.d.ts +21 -0
  5. package/lib/check-native-executor-config.js +24 -0
  6. package/lib/compute-alias-keys.d.ts +28 -0
  7. package/lib/compute-alias-keys.js +30 -0
  8. package/lib/compute-body-hash.d.ts +22 -0
  9. package/lib/compute-body-hash.js +25 -0
  10. package/lib/compute-url-sort-ranks.d.ts +28 -0
  11. package/lib/compute-url-sort-ranks.js +49 -0
  12. package/lib/create-isolated-cluster-run.d.ts +35 -0
  13. package/lib/create-isolated-cluster-run.js +52 -0
  14. package/lib/create-template-classification-run.d.ts +49 -0
  15. package/lib/create-template-classification-run.js +386 -0
  16. package/lib/group-alias-candidates.d.ts +40 -0
  17. package/lib/group-alias-candidates.js +104 -0
  18. package/lib/load-native-binding.d.ts +26 -0
  19. package/lib/load-native-binding.js +152 -0
  20. package/lib/match-js-license-comment-signal.d.ts +19 -0
  21. package/lib/match-js-license-comment-signal.js +21 -0
  22. package/lib/match-selector-html.d.ts +41 -0
  23. package/lib/match-selector-html.js +42 -0
  24. package/lib/match-selector-snapshots.d.ts +40 -0
  25. package/lib/match-selector-snapshots.js +57 -0
  26. package/lib/match-structural-technology-signals.d.ts +23 -0
  27. package/lib/match-structural-technology-signals.js +25 -0
  28. package/lib/max-decoded-snapshot-bytes.d.ts +22 -0
  29. package/lib/max-decoded-snapshot-bytes.js +35 -0
  30. package/lib/native-binding.d.ts +9 -0
  31. package/lib/native-binding.js +9 -0
  32. package/lib/pack-stored-snapshots.d.ts +23 -0
  33. package/lib/pack-stored-snapshots.js +53 -0
  34. package/lib/run-with-signal.d.ts +27 -0
  35. package/lib/run-with-signal.js +54 -0
  36. package/lib/search-html-document.d.ts +32 -0
  37. package/lib/search-html-document.js +31 -0
  38. package/lib/search-html-snapshots.d.ts +46 -0
  39. package/lib/search-html-snapshots.js +54 -0
  40. package/lib/template-block-sample-size.d.ts +13 -0
  41. package/lib/template-block-sample-size.js +14 -0
  42. package/lib/template-exact-page-limit.d.ts +18 -0
  43. package/lib/template-exact-page-limit.js +19 -0
  44. package/lib/template-memory-budget.d.ts +16 -0
  45. package/lib/template-memory-budget.js +17 -0
  46. package/lib/to-native-selector-plan.d.ts +16 -0
  47. package/lib/to-native-selector-plan.js +40 -0
  48. package/lib/to-u32-ids.d.ts +14 -0
  49. package/lib/to-u32-ids.js +23 -0
  50. package/lib/types.d.ts +736 -248
  51. package/lib/validate-search-pattern.d.ts +25 -0
  52. package/lib/validate-search-pattern.js +27 -0
  53. package/package.json +83 -16
  54. package/lib/core.d.ts +0 -4
  55. package/lib/core.js +0 -4
  56. package/lib/discover-analyze-plugins.d.ts +0 -14
  57. package/lib/discover-analyze-plugins.js +0 -32
  58. package/lib/get-table-cache-root.d.ts +0 -18
  59. package/lib/get-table-cache-root.js +0 -22
  60. package/lib/hooks/define-plugin.d.ts +0 -68
  61. package/lib/hooks/define-plugin.js +0 -69
  62. package/lib/import-modules.d.ts +0 -24
  63. package/lib/import-modules.js +0 -38
  64. package/lib/load-plugin-settings.d.ts +0 -44
  65. package/lib/load-plugin-settings.js +0 -125
  66. package/lib/nitpicker.d.ts +0 -150
  67. package/lib/nitpicker.js +0 -437
  68. package/lib/page-analysis-worker.d.ts +0 -58
  69. package/lib/page-analysis-worker.js +0 -68
  70. package/lib/read-plugin-labels.d.ts +0 -15
  71. package/lib/read-plugin-labels.js +0 -30
  72. package/lib/table.d.ts +0 -75
  73. package/lib/table.js +0 -132
  74. package/lib/url-event-bus.d.ts +0 -33
  75. package/lib/url-event-bus.js +0 -21
  76. package/lib/worker/runner.d.ts +0 -25
  77. package/lib/worker/runner.js +0 -31
  78. package/lib/worker/types.d.ts +0 -23
  79. package/lib/worker/types.js +0 -1
  80. package/lib/worker/worker-pool.d.ts +0 -82
  81. package/lib/worker/worker-pool.js +0 -278
  82. package/lib/worker/worker.d.ts +0 -34
  83. package/lib/worker/worker.js +0 -108
package/lib/types.d.ts CHANGED
@@ -1,306 +1,794 @@
1
- import type { Lanes } from '@d-zero/dealer';
2
- import type { ExURL as URL } from '@d-zero/shared/parse-url';
3
- import type { TableValue, Violation } from '@nitpicker/types';
4
- import type { DOMWindow } from 'jsdom';
5
1
  /**
6
- * Represents a single analyze plugin loaded from the user's configuration.
7
- *
8
- * Each plugin corresponds to an npm module that exports a {@link PluginFactory}
9
- * factory function as its default export. The `settings` object is passed
10
- * through to that factory at initialization time.
11
- * @see {@link ./load-plugin-settings.ts} for how plugins are discovered from cosmiconfig
12
- * @see {@link ./import-modules.ts} for how plugins are dynamically imported
2
+ * The functions the native addon (`crates/nitpicker_napi`) exports, as seen
3
+ * from JavaScript. Kept in sync with the `#[napi]` functions by hand — the
4
+ * addon's own generated typings are not used, so every export goes through a
5
+ * documented one-function-per-file wrapper instead.
6
+ */
7
+ export interface NativeBinding {
8
+ /** See `compute-body-hash.ts`. */
9
+ computeBodyHash(html: Uint8Array): Buffer;
10
+ /** See `match-structural-technology-signals.ts`. */
11
+ matchStructuralTechnologySignals(html: Uint8Array): StructuralTechnologySignal[];
12
+ /** See `match-js-license-comment-signal.ts`. */
13
+ matchJsLicenseCommentSignal(script: Uint8Array): StructuralTechnologySignal | null;
14
+ /** See `match-selector-html.ts`. */
15
+ matchSelectorHtml(plan: NativeSelectorPlan, html: Uint8Array): HtmlMatchOutcome;
16
+ /** See `match-selector-snapshots.ts`. */
17
+ matchSelectorSnapshots(plan: NativeSelectorPlan, bodies: Uint8Array, offsets: Uint32Array, codecs: Uint8Array, maxDecodedBytes: number, cancelToken?: NativeCancelToken): Promise<Uint8Array>;
18
+ /** See `validate-search-pattern.ts`. */
19
+ validateSearchPattern(pattern: NativeSearchPattern): boolean;
20
+ /** See `search-html-document.ts`. */
21
+ searchHtmlDocument(pattern: NativeSearchPattern, html: Uint8Array, snippetLength: number): SnapshotSearchOutcome;
22
+ /** See `search-html-snapshots.ts`. */
23
+ searchHtmlSnapshots(pattern: NativeSearchPattern, snippetLength: number, bodies: Uint8Array, offsets: Uint32Array, codecs: Uint8Array, maxDecodedBytes: number, cancelToken?: NativeCancelToken): Promise<{
24
+ matchCounts: number[];
25
+ snippets: string[];
26
+ }>;
27
+ /** See `check-native-executor-config.ts`. */
28
+ checkNativeExecutorConfig(): void;
29
+ /** See `run-with-signal.ts`. */
30
+ NativeCancelToken: new () => NativeCancelToken;
31
+ /** See `compute-url-sort-ranks.ts`. */
32
+ computeUrlSortRanks(urls: Uint8Array, offsets: Uint32Array): Uint32Array;
33
+ /** See `compute-alias-keys.ts`. */
34
+ computeAliasKeys(url: string): {
35
+ tierA: string;
36
+ tierB: string;
37
+ } | null;
38
+ /** See `group-alias-candidates.ts`. */
39
+ groupAliasCandidates(input: NativeAliasCandidates): Uint32Array;
40
+ /** See `create-isolated-cluster-run.ts`. */
41
+ IsolatedClusterRun: new (candidateIds: Uint32Array, redirects: Uint32Array) => NativeIsolatedClusterRun;
42
+ /** See `create-template-classification-run.ts`. */
43
+ TemplateClassificationRun: new (onProgress?: (event: NativeTemplateClassificationProgress) => void, memoryBudget?: number) => NativeTemplateClassificationRun;
44
+ /** See `template-block-sample-size.ts`. */
45
+ TEMPLATE_BLOCK_SAMPLE_SIZE: number;
46
+ /** See `template-memory-budget.ts`. */
47
+ TEMPLATE_MEMORY_BUDGET: number;
48
+ /** See `template-exact-page-limit.ts`. */
49
+ TEMPLATE_EXACT_PAGE_LIMIT: number;
50
+ /** See `max-decoded-snapshot-bytes.ts`. */
51
+ MAX_DECODED_SNAPSHOT_BYTES: number;
52
+ }
53
+ /**
54
+ * Stops the asynchronous native calls it was passed to. Raising it is one-way;
55
+ * a call that sees it raised rejects with code `Cancelled`.
13
56
  */
14
- export interface Plugin {
57
+ export interface NativeCancelToken {
58
+ /** Raises the token. */
59
+ cancel(): void;
60
+ }
61
+ /** How two adjacent compounds of a complex selector are joined. */
62
+ export type SelectorCombinator = 'descendant' | 'child';
63
+ /** The attribute selector operators the matcher evaluates. */
64
+ export type SelectorAttributeOperator = 'exists' | 'equals' | 'includes' | 'dash' | 'prefix' | 'suffix' | 'substring';
65
+ /** One attribute condition of a compound (`[a]`, `[a^=v]`, `.cls`, `#id`, ...). */
66
+ export interface SelectorAttributeTest {
67
+ /** Lower-cased attribute name. */
68
+ readonly name: string;
69
+ readonly operator: SelectorAttributeOperator;
70
+ /** The selector's literal value (empty for `exists`). */
71
+ readonly value: string;
72
+ /** Whether values compare ignoring ASCII case. */
73
+ readonly ignoreCase: boolean;
74
+ }
75
+ /** An `An+B` position test (`:nth-child`, `:nth-of-type`, `:first-*`). */
76
+ export interface SelectorNthTest {
77
+ readonly kind: 'child' | 'of-type';
78
+ readonly a: number;
79
+ readonly b: number;
80
+ }
81
+ /** The compound inside `:not(...)` — a compound without negations of its own. */
82
+ export interface SelectorNegatedCompound {
83
+ /** Lower-cased tag name, or `null` for any element. */
84
+ readonly tag: string | null;
85
+ readonly attributes: readonly SelectorAttributeTest[];
86
+ readonly nth: readonly SelectorNthTest[];
87
+ }
88
+ /** A compound selector: every condition must hold on one element. */
89
+ export interface SelectorCompound extends SelectorNegatedCompound {
90
+ readonly negations: readonly SelectorNegatedCompound[];
91
+ }
92
+ /** One comma alternative of a selector list. */
93
+ export interface SelectorAlternative {
94
+ /** Compounds in source (ancestor-first) order; between 1 and 30. */
95
+ readonly compounds: readonly SelectorCompound[];
96
+ /** `combinators[j - 1]` joins `compounds[j - 1]` and `compounds[j]`. */
97
+ readonly combinators: readonly SelectorCombinator[];
15
98
  /**
16
- * Human-readable plugin name. Currently unused at runtime but kept
17
- * for backward compatibility with older config formats.
18
- * @deprecated Use `module` to identify plugins instead.
99
+ * Lower-cased printable-ASCII literals that must occur in the document in
100
+ * this order for the alternative to match, or `null` when none can be derived.
19
101
  */
20
- name: string;
102
+ readonly prefilterLiterals: readonly string[] | null;
21
103
  /**
22
- * The npm module specifier to `import()` (e.g. `"@nitpicker/analyze-axe"`).
104
+ * Whether one start tag decides this alternative: a single compound with
105
+ * no position test whose attribute values have a single stored form.
23
106
  */
24
- module: string;
107
+ readonly direct: boolean;
108
+ }
109
+ /** A validated selector list as the matcher evaluates it. */
110
+ export interface SelectorPlan {
111
+ readonly alternatives: readonly SelectorAlternative[];
112
+ /** Whether any alternative tests `:nth-child` / `:first-child`. */
113
+ readonly needsChildIndex: boolean;
114
+ /** Whether any alternative tests `:nth-of-type` / `:first-of-type`. */
115
+ readonly needsTypeIndex: boolean;
116
+ }
117
+ /** {@link SelectorNegatedCompound} as the native conversion reads it: no tag is absent, not `null`. */
118
+ export interface NativeSelectorConditions {
119
+ readonly tag?: string;
120
+ readonly attributes: readonly SelectorAttributeTest[];
121
+ readonly nth: readonly SelectorNthTest[];
122
+ }
123
+ /** {@link SelectorPlan} as the native conversion reads it (see `to-native-selector-plan.ts`). */
124
+ export interface NativeSelectorPlan {
125
+ readonly alternatives: readonly {
126
+ readonly compounds: readonly (NativeSelectorConditions & {
127
+ readonly negations: readonly NativeSelectorConditions[];
128
+ })[];
129
+ readonly combinators: readonly SelectorCombinator[];
130
+ readonly prefilterLiterals?: readonly string[];
131
+ readonly direct: boolean;
132
+ }[];
133
+ readonly needsChildIndex: boolean;
134
+ readonly needsTypeIndex: boolean;
135
+ }
136
+ /** The verdict on one document and which stages produced it. */
137
+ export interface HtmlMatchOutcome {
138
+ /** Whether an element of the document matches the selector list. */
139
+ readonly matched: boolean;
140
+ /** The ordered-literal prefilter rejected the document; its markup was never read. */
141
+ readonly prefiltered: boolean;
142
+ /** The open-element stack ran over the document. */
143
+ readonly tokenized: boolean;
144
+ /** Elements the stack tested (zero when it did not run). */
145
+ readonly elementsVisited: number;
146
+ }
147
+ /** One stored HTML snapshot as it sits in `page_html_blobs`. */
148
+ export interface StoredHtmlSnapshot {
149
+ /** The stored bytes. */
150
+ readonly body: Uint8Array;
151
+ /** `page_html_blobs.codec`: `zstd` or `none`; anything else is rejected. */
152
+ readonly codec: string;
153
+ }
154
+ /**
155
+ * What `query search-html` looks for: a literal substring or a regular
156
+ * expression (Rust `regex` syntax) with its flags.
157
+ */
158
+ export interface NativeSearchPattern {
159
+ /** The literal text, or the regular expression's source. */
160
+ readonly source: string;
161
+ /** Match `source` as plain text; the flags are then ignored. */
162
+ readonly literal: boolean;
163
+ /** `i`. */
164
+ readonly ignoreCase: boolean;
165
+ /** `m`: `^` and `$` match at every `\n`. */
166
+ readonly multiLine: boolean;
167
+ /** `s`: `.` matches `\n` too. */
168
+ readonly dotAll: boolean;
169
+ }
170
+ /** What a search found in one document. */
171
+ export interface SnapshotSearchOutcome {
172
+ /** Non-overlapping matches; 0 when the document does not match. */
173
+ readonly matchCount: number;
174
+ /** Whitespace-collapsed window around the first match; empty when there is none. */
175
+ readonly snippet: string;
176
+ }
177
+ /** The verdict on one snapshot of a batch. */
178
+ export interface SnapshotMatchOutcome {
179
+ readonly matched: boolean;
180
+ /** The ordered-literal prefilter rejected the document; its markup was never read. */
181
+ readonly prefiltered: boolean;
182
+ /** The open-element stack ran over the document. */
183
+ readonly tokenized: boolean;
184
+ }
185
+ /**
186
+ * How a structural technology signal was found: a marker element or
187
+ * attribute in the HTML, a framework's URL path fragment, a scoped-style
188
+ * attribute convention, a marker too generic to count for much on its own,
189
+ * or a license banner at the top of a JS file.
190
+ */
191
+ export type StructuralTechnologySignalType = 'html-marker' | 'url-pattern' | 'scoped-attr' | 'weak-marker' | 'js-license-comment';
192
+ /**
193
+ * One structural fingerprint definition (in `crates/nitpicker_tech_detect`)
194
+ * that matched, with the evidence cut from its first match.
195
+ */
196
+ export interface StructuralTechnologySignal {
197
+ /** Normalized technology name (`'Next.js'`, `'Vue'`, ...). */
198
+ readonly technology: string;
199
+ readonly signalType: StructuralTechnologySignalType;
200
+ /** The first match, cut to 200 UTF-16 code units. */
201
+ readonly evidence: string;
202
+ /** The definition's confidence in isolation, 0-100. */
203
+ readonly weight: number;
204
+ /** The category attached to the technology when only this signal fired. */
205
+ readonly category: string;
206
+ }
207
+ /**
208
+ * The addon's run, which `createTemplateClassificationRun` wraps into a
209
+ * {@link TemplateClassificationRun}; use that instead. Each computing call
210
+ * resolves with the number of progress events it sent, which may still be on
211
+ * their way to the callback (the wrapper waits for them). A panic in the
212
+ * addon or a poisoned lock rejects the call with a generic failure; neither
213
+ * can be provoked from JavaScript, so no test exercises them.
214
+ * @example
215
+ * ```ts
216
+ * const native = new nativeBinding.TemplateClassificationRun();
217
+ * native.pushSignals([{ paths: ['a'], stylesheetHrefs: [], stylesheetHosts: [] }]);
218
+ * const sent = await native.block(); // 0
219
+ * ```
220
+ */
221
+ export interface NativeTemplateClassificationRun {
25
222
  /**
26
- * Absolute path to the configuration file where this plugin was declared.
27
- * Passed to the plugin so it can resolve relative paths in its own config.
223
+ * {@link TemplateClassificationRun.pushSignals}, each stylesheet href's
224
+ * `new URL(href).host` (`null` where it throws) given alongside.
28
225
  */
29
- configFilePath: string;
226
+ pushSignals(signals: readonly NativeTemplatePageSignals[]): void;
227
+ /** {@link TemplateClassificationRun.block}, resolving with the events sent. */
228
+ block(): Promise<number>;
229
+ /** {@link TemplateClassificationRun.pushPages}, resolving with the events sent. */
230
+ pushPages(pages: readonly TemplatePageHtml[]): Promise<number>;
231
+ /** {@link TemplateClassificationRun.finishPages}, resolving with the events sent. */
232
+ finishPages(): Promise<number>;
233
+ /** {@link TemplateClassificationRun.pageMask}; throws while a call runs. */
234
+ pageMask(): Uint8Array;
235
+ /** {@link TemplateClassificationRun.needsAssignment}; throws while a call runs. */
236
+ needsAssignment(): boolean;
237
+ /** {@link TemplateClassificationRun.pushAssignmentPages}, resolving with the events sent. */
238
+ pushAssignmentPages(pages: readonly TemplatePageHtml[]): Promise<number>;
239
+ /** {@link TemplateClassificationRun.takeOutput}, with the events sent. */
240
+ takeOutput(): Promise<{
241
+ readonly output: NativeTemplateClassificationOutput;
242
+ readonly sent: number;
243
+ }>;
244
+ /** {@link TemplateClassificationRun.cancel}. */
245
+ cancel(): void;
246
+ }
247
+ /**
248
+ * {@link TemplatePageSignals} as the addon takes it.
249
+ * @example
250
+ * ```ts
251
+ * const signals: NativeTemplatePageSignals = {
252
+ * paths: ['news', '1'],
253
+ * stylesheetHrefs: ['https://example.com/a.css', 'not a url'],
254
+ * stylesheetHosts: ['example.com', null],
255
+ * };
256
+ * ```
257
+ */
258
+ export interface NativeTemplatePageSignals {
259
+ readonly paths: readonly string[];
260
+ readonly stylesheetHrefs: readonly string[];
261
+ /** `new URL(href).host` of each of `stylesheetHrefs`, `null` where it throws. */
262
+ readonly stylesheetHosts: readonly (string | null)[];
263
+ readonly host?: string;
264
+ }
265
+ /**
266
+ * {@link TemplateClassificationProgress} as the addon reports it, every
267
+ * field optional; `createTemplateClassificationRun` narrows it.
268
+ * @example
269
+ * ```ts
270
+ * const event: NativeTemplateClassificationProgress = { phase: 'stage-b-start', unitCount: 42 };
271
+ * ```
272
+ */
273
+ export interface NativeTemplateClassificationProgress {
274
+ readonly phase: string;
275
+ readonly blockKey?: string;
276
+ readonly blocksProcessed?: number;
277
+ readonly totalBlocks?: number;
278
+ readonly pagesAssigned?: number;
279
+ readonly pagesToAssign?: number;
280
+ readonly unitCount?: number;
281
+ readonly pageCount?: number;
282
+ readonly pairBytes?: number;
283
+ readonly budget?: number;
284
+ readonly reason?: string;
285
+ }
286
+ /**
287
+ * {@link TemplateClassificationOutput} as the addon returns it: the reasons
288
+ * flattened, which `createTemplateClassificationRun` turns into
289
+ * {@link TemplateClusterReason}s.
290
+ * @example
291
+ * ```ts
292
+ * const { keys, keyOfPage, reasons } = (await native.takeOutput()).output;
293
+ * ```
294
+ */
295
+ export interface NativeTemplateClassificationOutput {
296
+ readonly keys: string[];
297
+ readonly keyOfPage: Uint32Array;
298
+ readonly reasons: readonly {
299
+ readonly clusterKey: string;
300
+ readonly memberCount: number;
301
+ readonly blocking: readonly {
302
+ readonly blockKey: string;
303
+ readonly kind: string;
304
+ readonly distinctiveStylesheetHrefs?: readonly string[];
305
+ readonly pathKey?: string;
306
+ }[];
307
+ readonly structuralCoreTokens: readonly string[];
308
+ readonly landmarks: readonly ({
309
+ readonly type: string;
310
+ } & TemplateLandmarkProfile)[];
311
+ readonly siblingClusterKeys: readonly string[];
312
+ }[];
313
+ readonly sampledBlocks: readonly {
314
+ readonly blockKey: string;
315
+ readonly pageCount: number;
316
+ readonly pairBytes: number;
317
+ readonly reason: string;
318
+ }[];
319
+ }
320
+ /**
321
+ * The element a crawler reported as a page's main content: the element whose
322
+ * subtree holds the page-specific content and under which the classifier
323
+ * caps the nesting depth it compares. Every given field must match the same
324
+ * element; empty strings and lists count as not given, and a hint with
325
+ * nothing given matches nothing (otherwise the shallowest element of the
326
+ * page would win and the cap would silently apply to the wrong node).
327
+ *
328
+ * A structured object rather than a CSS selector string: nothing has to be
329
+ * parsed (the HTML parser already exposes `id`/`class`/`role`), it maps 1:1
330
+ * onto the columns a crawler records for its detected main-content element,
331
+ * and a crawler's own selector string is diagnostic (tag + id + classes) and
332
+ * not guaranteed to be unique, so it is the wrong thing to re-parse.
333
+ * @example
334
+ * ```ts
335
+ * const hint: ContentRootHint = { tagName: 'DIV', id: 'content', classList: ['l-main'] };
336
+ * ```
337
+ */
338
+ export interface ContentRootHint {
339
+ /** Compared case-insensitively. */
340
+ readonly tagName?: string;
341
+ readonly id?: string;
342
+ readonly role?: string;
343
+ /** Every listed class must be on the element. */
344
+ readonly classList?: readonly string[];
345
+ }
346
+ /**
347
+ * What Pass 0 reads of one page handed to a
348
+ * {@link TemplateClassificationRun}: no HTML, a few hundred bytes.
349
+ * @example
350
+ * ```ts
351
+ * const signals: TemplatePageSignals = {
352
+ * paths: ['news', '1'],
353
+ * stylesheetHrefs: ['https://example.com/a.css'],
354
+ * host: 'example.com',
355
+ * };
356
+ * ```
357
+ */
358
+ export interface TemplatePageSignals {
359
+ /** The page URL's path segments; a directory-style URL ends in `''`. */
360
+ readonly paths: readonly string[];
361
+ /** Absolute hrefs of the page's stylesheets, in document order. */
362
+ readonly stylesheetHrefs: readonly string[];
30
363
  /**
31
- * Plugin-specific settings object parsed from the config file.
32
- * The shape is determined by the plugin itself (e.g. `{ lang: "ja" }` for axe).
364
+ * The page URL's host (`new URL(pageUrl).host`). Stylesheets on other
365
+ * hosts are left out of blocking; a page without it is judged against
366
+ * the host most pages load stylesheets from.
33
367
  */
34
- settings?: unknown;
368
+ readonly host?: string;
35
369
  }
36
370
  /**
37
- * Options for {@link ../nitpicker.ts!Nitpicker.analyze}.
38
- *
39
- * Allows callers to provide an external {@link https://www.npmjs.com/package/@d-zero/dealer | Lanes}
40
- * instance for rich progress display, and a verbose flag for non-TTY environments.
371
+ * One page's HTML handed to a {@link TemplateClassificationRun}.
372
+ * @example
373
+ * ```ts
374
+ * const page: TemplatePageHtml = { html: '<body><main>…</main></body>', contentRoot: { tagName: 'main' } };
375
+ * ```
41
376
  */
42
- export interface AnalyzeOptions {
43
- /** Lanes instance for per-plugin progress display. If omitted, no progress is shown. */
44
- readonly lanes?: Lanes;
45
- /** When `true`, outputs plain-text progress lines instead of animated Lanes. */
46
- readonly verbose?: boolean;
377
+ export interface TemplatePageHtml {
378
+ readonly html: string;
379
+ readonly contentRoot?: ContentRootHint;
47
380
  }
48
381
  /**
49
- * CLI-specified overrides for individual plugin settings.
50
- *
51
- * These values are provided via CLI flags (e.g. `--search-keywords`,
52
- * `--axe-lang`) and take precedence over settings loaded from the
53
- * configuration file. Each key maps to a known `@nitpicker/analyze-*`
54
- * plugin module name, and the value is a partial settings object that
55
- * will be shallow-merged into the plugin's config-file settings.
56
- *
57
- * ## Adding CLI overrides for a new plugin
58
- *
59
- * 1. Add a property to this interface with the plugin module name as key
60
- * 2. Add corresponding CLI flag(s) to `commandDef.flags` in `@nitpicker/cli/src/commands/analyze.ts`
61
- * 3. Add flag-to-override mapping in `@nitpicker/cli/src/analyze/build-plugin-overrides.ts`
62
- * 4. Update `README.md` analyze options table
63
- * @see {@link ./load-plugin-settings.ts!loadPluginSettings} for the merge logic
382
+ * Progress of a {@link TemplateClassificationRun}: a block will be clustered
383
+ * from a sample (over the memory budget, or among the largest when the
384
+ * blocks clustered whole would hold too many pages), a block's clusters are done,
385
+ * another thousand pages outside the samples were assigned, or the merge
386
+ * across blocks began.
387
+ * @example
388
+ * ```ts
389
+ * const onProgress = (event: TemplateClassificationProgress) => {
390
+ * if (event.phase === 'pass1-block-complete') {
391
+ * console.log(`${event.blocksProcessed}/${event.totalBlocks} blocks`);
392
+ * }
393
+ * };
394
+ * ```
64
395
  */
65
- export interface PluginOverrides {
66
- /** Overrides for `@nitpicker/analyze-search` plugin settings. */
67
- '@nitpicker/analyze-search'?: {
68
- /** Keywords to search for in page content. */
69
- keywords?: string[];
70
- /** CSS selector to narrow the search scope. */
71
- scope?: string;
72
- };
73
- /** Overrides for `@nitpicker/analyze-axe` plugin settings. */
74
- '@nitpicker/analyze-axe'?: {
75
- /** BCP 47 language tag for locale. */
76
- lang?: string;
396
+ export type TemplateClassificationProgress = {
397
+ readonly phase: 'block-sampled';
398
+ readonly blockKey: string;
399
+ readonly pageCount: number;
400
+ readonly pairBytes: number;
401
+ readonly budget: number;
402
+ readonly reason: TemplateSampleReason;
403
+ } | {
404
+ readonly phase: 'pass1-block-complete';
405
+ readonly blockKey: string;
406
+ readonly blocksProcessed: number;
407
+ readonly totalBlocks: number;
408
+ } | {
409
+ readonly phase: 'pass1b-assign';
410
+ readonly pagesAssigned: number;
411
+ readonly pagesToAssign: number;
412
+ } | {
413
+ readonly phase: 'stage-b-start';
414
+ readonly unitCount: number;
415
+ };
416
+ /**
417
+ * Why Pass 0 formed a block: its pages share a distinctive set of
418
+ * stylesheets (`css`), or fall back to their URL section (`path`), or are
419
+ * pages without stylesheets gathered into their section's stylesheet block
420
+ * (`orphanMerge`).
421
+ * @example
422
+ * ```ts
423
+ * const reason: TemplateBlockingReason = { kind: 'path', pathKey: 'news' };
424
+ * ```
425
+ */
426
+ export type TemplateBlockingReason = {
427
+ readonly kind: 'css';
428
+ readonly distinctiveStylesheetHrefs: readonly string[];
429
+ } | {
430
+ readonly kind: 'path';
431
+ readonly pathKey: string;
432
+ } | {
433
+ readonly kind: 'orphanMerge';
434
+ readonly pathKey: string;
435
+ };
436
+ /**
437
+ * A page region the classifier treats as chrome: HTML5 sectioning elements
438
+ * and their ARIA landmark roles (`<header>` / `banner`, `<footer>` /
439
+ * `contentinfo`, `<nav>` / `navigation`, `<aside>` / `complementary`,
440
+ * `role="form"`, `<search>` / `search`). `main` is content and never one.
441
+ * @example
442
+ * ```ts
443
+ * const type: TemplateLandmarkType = 'footer';
444
+ * ```
445
+ */
446
+ export type TemplateLandmarkType = 'header' | 'footer' | 'nav' | 'aside' | 'form' | 'search';
447
+ /**
448
+ * What `createTemplateClassificationRun`'s progress callback shares with the
449
+ * run: the delivery counts, the waiting call, the run's failure, and the
450
+ * addon's run only through a `WeakRef`. Kept apart from the run's methods so
451
+ * the callback, which the addon holds while the run lives, does not hold the
452
+ * run strongly.
453
+ * @example
454
+ * ```ts
455
+ * const state: TemplateClassificationProgressState = {
456
+ * delivered: 0,
457
+ * expected: 0,
458
+ * onDelivered: undefined,
459
+ * failure: undefined,
460
+ * nativeRef: undefined,
461
+ * };
462
+ * ```
463
+ */
464
+ export interface TemplateClassificationProgressState {
465
+ /** Events the callback received. */
466
+ delivered: number;
467
+ /** Events the finished calls said they sent. */
468
+ expected: number;
469
+ /** Wakes the call waiting for its events, if one is. */
470
+ onDelivered: (() => void) | undefined;
471
+ /** The error that failed the run, once one did. */
472
+ failure: {
473
+ readonly error: unknown;
474
+ } | undefined;
475
+ /** The addon's run, to cancel when the caller's callback throws. */
476
+ nativeRef: WeakRef<NativeTemplateClassificationRun> | undefined;
477
+ }
478
+ /**
479
+ * How common one landmark type is across a cluster's member pages.
480
+ * @example
481
+ * ```ts
482
+ * const footer: TemplateLandmarkProfile = {
483
+ * presenceRate: 1,
484
+ * chromeRate: 1,
485
+ * shellTokens: ['footer>p'],
486
+ * memberCountWithInstance: 12,
487
+ * };
488
+ * ```
489
+ */
490
+ export interface TemplateLandmarkProfile {
491
+ /** Fraction of the member pages carrying the type. */
492
+ readonly presenceRate: number;
493
+ /** Fraction of the type's instances that match the cluster's shell for it. */
494
+ readonly chromeRate: number;
495
+ /** The cluster's shell tokens for the type, in UTF-16 order. */
496
+ readonly shellTokens: readonly string[];
497
+ /** Member pages carrying the type. */
498
+ readonly memberCountWithInstance: number;
499
+ }
500
+ /**
501
+ * Why a final cluster's pages sit together: evidence, no verdicts. Its
502
+ * shape — key order included — is what `@nitpicker/archive` stores as JSON,
503
+ * so the stored bytes follow from it.
504
+ * @example
505
+ * ```ts
506
+ * const reason: TemplateClusterReason = {
507
+ * memberCount: 2,
508
+ * blocking: [{ blockKey: 'path:news', reason: { kind: 'path', pathKey: 'news' } }],
509
+ * structuralCoreTokens: ['article', 'body'],
510
+ * landmarks: {},
511
+ * siblingClusterKeys: [],
512
+ * };
513
+ * ```
514
+ */
515
+ export interface TemplateClusterReason {
516
+ /** Its member pages (the sampled ones, for a block clustered from a sample). */
517
+ readonly memberCount: number;
518
+ /** Each Pass-0 block it came from, with the reason the block was formed. */
519
+ readonly blocking: readonly {
520
+ readonly blockKey: string;
521
+ readonly reason: TemplateBlockingReason;
522
+ }[];
523
+ /** The structure most of its pages share, in UTF-16 order. */
524
+ readonly structuralCoreTokens: readonly string[];
525
+ /** Per landmark type its members carry; absent types are left out. */
526
+ readonly landmarks: {
527
+ readonly [Type in TemplateLandmarkType]?: TemplateLandmarkProfile;
77
528
  };
529
+ /** The other final clusters of its blocks, in UTF-16 order. */
530
+ readonly siblingClusterKeys: readonly string[];
78
531
  }
79
532
  /**
80
- * Internal configuration model used by {@link ../nitpicker.ts!Nitpicker}.
81
- *
82
- * Built by {@link ./load-plugin-settings.ts!loadPluginSettings} from the
83
- * user's cosmiconfig file (e.g. `.nitpickerrc.json`). The external config
84
- * format (`ConfigJSON` from `@nitpicker/types`) uses a `plugins.analyze`
85
- * object keyed by module name; this internal type normalizes it into an
86
- * ordered array of fully-resolved {@link Plugin} entries.
533
+ * Why a block is clustered from a sample of its pages: its Stage A
534
+ * similarity table would exceed the memory budget (`table-over-budget`), or
535
+ * the blocks clustered whole would hold more than
536
+ * `TEMPLATE_EXACT_PAGE_LIMIT` pages and this one is among the largest
537
+ * (`pages-over-limit`).
538
+ * @example
539
+ * ```ts
540
+ * const reason: TemplateSampleReason = 'pages-over-limit';
541
+ * ```
542
+ */
543
+ export type TemplateSampleReason = 'table-over-budget' | 'pages-over-limit';
544
+ /**
545
+ * A block clustered from a sample of its pages; see
546
+ * {@link TemplateSampleReason} for why.
547
+ * @example
548
+ * ```ts
549
+ * const block: TemplateSampledBlock = {
550
+ * blockKey: 'path:news',
551
+ * pageCount: 30_000,
552
+ * pairBytes: 3_599_880_000,
553
+ * reason: 'table-over-budget',
554
+ * };
555
+ * ```
87
556
  */
88
- export interface Config {
89
- /** Ordered list of analyze plugins to execute. */
90
- analyze: Plugin[];
557
+ export interface TemplateSampledBlock {
558
+ /** The block's key. */
559
+ readonly blockKey: string;
560
+ /** Its pages. */
561
+ readonly pageCount: number;
562
+ /** Bytes its similarity table would have taken. */
563
+ readonly pairBytes: number;
564
+ /** Why it was sampled. */
565
+ readonly reason: TemplateSampleReason;
91
566
  }
92
567
  /**
93
- * The runtime interface that every analyze plugin must satisfy after
94
- * its factory function ({@link PluginFactory}) has been invoked.
95
- *
96
- * A plugin may implement one or both callback methods:
97
- *
98
- * - **`eachPage`** - Runs inside a Worker thread with full JSDOM access.
99
- * Best for DOM-dependent analysis (markup validation, text linting,
100
- * accessibility checks). Each invocation receives a parsed `DOMWindow`,
101
- * so plugins can use standard DOM APIs without additional parsing.
568
+ * What a {@link TemplateClassificationRun} produces.
569
+ * @example
570
+ * ```ts
571
+ * const { keys, keyOfPage, reasons } = await run.takeOutput();
572
+ * const keyOfFirstPage = keys[keyOfPage[0]!]!; // '["path:news","cluster:0"]'
573
+ * reasons.get(keyOfFirstPage)?.memberCount;
574
+ * ```
575
+ */
576
+ export interface TemplateClassificationOutput {
577
+ /** Every final cluster key (`JSON.stringify([blockKey, "cluster:N"])`). */
578
+ readonly keys: string[];
579
+ /** Each page's key, as an index into `keys`. */
580
+ readonly keyOfPage: Uint32Array;
581
+ /** Each final cluster's reason, by key. */
582
+ readonly reasons: ReadonlyMap<string, TemplateClusterReason>;
583
+ /** The blocks clustered from a sample, in block order; empty when none was. */
584
+ readonly sampledBlocks: readonly TemplateSampledBlock[];
585
+ }
586
+ /**
587
+ * One template classification run, executing on the addon's thread pool.
588
+ * The caller reads the corpus: every page's signals (`pushSignals`), then
589
+ * `block()`, then every page's HTML in the same order (`pushPages`,
590
+ * `finishPages`), and, when `needsAssignment()`, once more
591
+ * (`pushAssignmentPages`); `takeOutput()` merges and returns the result.
102
592
  *
103
- * - **`eachUrl`** - Runs in the main thread, receives only the URL and
104
- * external/internal flag. Suited for lightweight, network-based checks
105
- * (e.g. link validation, SEO URL pattern checks).
106
- * @template T - String literal union of the column keys this plugin
107
- * contributes to the report table (e.g. `'title' | 'description'`).
108
- * @see {@link ./page-analysis-worker.ts} for how `eachPage` is called inside the worker
109
- * @see {@link ./nitpicker.ts} for how `eachUrl` is called from the main thread
593
+ * Calls must not overlap: await each before the next (an overlapping call
594
+ * rejects and leaves the running one alone). A promise resolves only after
595
+ * every progress event of its call was delivered. A call that rejects —
596
+ * cancelled, a protocol error, or `onProgress` threw (which also cancels the
597
+ * run) — fails the run: every later call rejects with the same error, and no
598
+ * further progress is reported.
599
+ * @example
600
+ * ```ts
601
+ * const run = createTemplateClassificationRun();
602
+ * run.pushSignals([{ paths: ['a'], stylesheetHrefs: [] }]);
603
+ * await run.block();
604
+ * await run.pushPages([{ html: '<body></body>' }]);
605
+ * await run.finishPages();
606
+ * const output = await run.takeOutput();
607
+ * ```
110
608
  */
111
- export interface AnalyzePlugin<T extends string = string> {
112
- /**
113
- * Human-readable display label for interactive prompts.
114
- * Shown instead of the raw package name (e.g. `"axe: アクセシビリティチェック"`).
115
- */
116
- label?: string;
609
+ export interface TemplateClassificationRun {
610
+ /** Adds the next pages' signals, in corpus order. */
611
+ pushSignals(signals: readonly TemplatePageSignals[]): void;
612
+ /** Splits the pages into blocks (Pass 0); the HTML reads follow. */
613
+ block(): Promise<void>;
614
+ /** Reads the next pages' HTML, in corpus order. */
615
+ pushPages(pages: readonly TemplatePageHtml[]): Promise<void>;
616
+ /** Ends the first read; the blocks clustered whole are clustered here. */
617
+ finishPages(): Promise<void>;
117
618
  /**
118
- * Maximum number of pages this plugin processes in parallel.
119
- *
120
- * Each plugin is allocated its own {@link ../worker/worker-pool.ts!WorkerPool}
121
- * sized to this value. Heavy plugins (e.g. one that spawns a
122
- * full Chrome instance per page) should set a small number; lightweight
123
- * DOM-only plugins can leave it unset.
124
- *
125
- * When omitted, the orchestrator falls back to `os.cpus().length`.
619
+ * Which pages the current read uses (`1`): on the first read every page of
620
+ * a block clustered whole and the sampled pages of a sampled block, on the
621
+ * assignment read the other pages of the sampled blocks. Every page must
622
+ * still be pushed, in order; the others may carry empty HTML.
126
623
  */
127
- concurrency?: number;
624
+ pageMask(): Uint8Array;
128
625
  /**
129
- * Column header definitions contributed by this plugin.
130
- * Keys are column identifiers (`T`), values are human-readable labels
131
- * shown in the report header row.
626
+ * Whether the run expects the assignment read: true once the first
627
+ * read has ended with pages outside the samples of a sampled block, until
628
+ * the assignment read has ended; false otherwise.
132
629
  */
133
- headers?: TableHeaders<T>;
630
+ needsAssignment(): boolean;
631
+ /** Assigns the next pages of the second read, in corpus order. */
632
+ pushAssignmentPages(pages: readonly TemplatePageHtml[]): Promise<void>;
633
+ /** Merges the clusters across blocks; the run cannot be used afterwards. */
634
+ takeOutput(): Promise<TemplateClassificationOutput>;
134
635
  /**
135
- * Per-page analysis callback executed in a Worker thread.
136
- * @param page - Context for the current page, including the raw HTML,
137
- * a live JSDOM window, the page URL, and progress counters.
138
- * @param page.url - Parsed URL of the page being analyzed.
139
- * @param page.html - Raw HTML string of the page.
140
- * @param page.window - JSDOM window with the page's DOM tree. Closed after the callback returns.
141
- * @param page.num - Zero-based index of the current page in the batch.
142
- * @param page.total - Total number of pages in the batch.
143
- * @returns Report data for this page, or `null` to skip.
636
+ * Makes the running call, and every later one, reject at its next page or
637
+ * block. A call already finished but waiting on its progress events
638
+ * resolves without them.
144
639
  */
145
- eachPage?(page: {
146
- /** Parsed URL of the page being analyzed. */
147
- url: URL;
148
- /** Raw HTML string of the page. */
149
- html: string;
150
- /** JSDOM window with the page's DOM tree. Closed after the callback returns. */
151
- window: DOMWindow;
152
- /** Zero-based index of the current page in the batch. */
153
- num: number;
154
- /** Total number of pages in the batch. */
155
- total: number;
156
- }): Promise<ReportPage<T> | null> | ReportPage<T> | null;
640
+ cancel(): void;
641
+ }
642
+ /**
643
+ * Inputs to `createTemplateClassificationRun`.
644
+ * @example
645
+ * ```ts
646
+ * const options: CreateTemplateClassificationRunOptions = {
647
+ * onProgress: (event) => console.log(event.phase),
648
+ * };
649
+ * ```
650
+ */
651
+ export interface CreateTemplateClassificationRunOptions {
157
652
  /**
158
- * Per-URL analysis callback executed in the main thread.
159
- *
160
- * Unlike `eachPage`, this callback does **not** receive HTML or a DOM
161
- * window. It is designed for checks that depend only on URL metadata
162
- * (e.g. checking URL patterns, external link policies).
163
- * @param page - URL context including external/internal classification.
164
- * @param page.url - Parsed URL being analyzed.
165
- * @param page.isExternal - Whether this URL is external to the crawled site.
166
- * @returns Report data for this URL, or `null` to skip.
653
+ * Bytes a block's Stage A similarity table (`n * (n - 1) / 2 * 8` for `n`
654
+ * pages) may take before the block is clustered from a sample instead.
655
+ * Defaults to `TEMPLATE_MEMORY_BUDGET`; `0` samples every block.
167
656
  */
168
- eachUrl?(page: {
169
- /** Parsed URL being analyzed. */
170
- url: URL;
171
- /** Whether this URL is external to the crawled site. */
172
- isExternal: boolean;
173
- }): Promise<ReportPage<T> | null> | ReportPage<T> | null;
657
+ readonly memoryBudget?: number;
658
+ /** Receives progress events. */
659
+ readonly onProgress?: (event: TemplateClassificationProgress) => void;
174
660
  }
175
661
  /**
176
- * The return value of a single {@link AnalyzePlugin.eachPage} or {@link AnalyzePlugin.eachUrl}
177
- * invocation for one page/URL.
178
- *
179
- * A plugin can contribute tabular data (displayed in spreadsheet columns)
180
- * and/or violation records (displayed in a dedicated violations sheet).
181
- * @template T - Column key union matching the plugin's `headers`.
662
+ * The addon's isolated-cluster run, which `createIsolatedClusterRun` wraps
663
+ * into an {@link IsolatedClusterRun}; use that instead.
664
+ * @example
665
+ * ```ts
666
+ * const native = new nativeBinding.IsolatedClusterRun(Uint32Array.of(1, 2), new Uint32Array());
667
+ * native.unionEdges(Uint32Array.of(1, 2));
668
+ * native.labels(); // Uint32Array [0, 0]
669
+ * ```
182
670
  */
183
- export interface ReportPage<T extends string> {
184
- /** Column data for this page. Keys must be a subset of `T`. */
185
- page?: TableData<T>;
186
- /** Violations detected on this page (e.g. a11y issues, lint errors). */
187
- violations?: Violation[];
671
+ export interface NativeIsolatedClusterRun {
672
+ /** {@link IsolatedClusterRun.unionEdges}, the pairs flattened. */
673
+ unionEdges(edges: Uint32Array): void;
674
+ /** {@link IsolatedClusterRun.labels}. */
675
+ labels(): Uint32Array;
188
676
  }
189
677
  /**
190
- * Aggregated report data from a Worker thread, keyed by page URL.
191
- *
192
- * This is the message payload returned from the Worker to the main thread
193
- * via the `'finish'` message. It aggregates results from all plugins that
194
- * ran `eachPage` for a single page.
195
- * @template T - Column key union.
196
- * @see {@link ./page-analysis-worker.ts} for the Worker entry point that produces this
197
- * @see {@link ./worker/worker-pool.ts!WorkerPool} for the main-thread consumer
678
+ * The candidates {@link groupAliasCandidates} hands the addon, column by
679
+ * column.
680
+ * @example
681
+ * ```ts
682
+ * const columns: NativeAliasCandidates = {
683
+ * urls: Buffer.from('https://example.com/'),
684
+ * urlOffsets: Uint32Array.of(0, 20),
685
+ * urlIds: Uint32Array.of(1),
686
+ * titleTextIds: Uint32Array.of(1),
687
+ * canonicalUrlIds: Uint32Array.of(4_294_967_295),
688
+ * bodyHashes: new Uint8Array(),
689
+ * hashOffsets: Uint32Array.of(0, 0),
690
+ * hashPresent: Uint8Array.of(0),
691
+ * };
692
+ * ```
198
693
  */
199
- export interface ReportPages<T extends string> {
200
- /** Per-URL table data from all plugins. */
201
- pages?: TablePages<T>;
202
- /** Combined violations from all plugins. */
203
- violations?: Violation[];
694
+ export interface NativeAliasCandidates {
695
+ /** The URLs back to back as UTF-8. */
696
+ readonly urls: Uint8Array;
697
+ /** `n + 1` positions cutting `urls` into the `n` URLs. */
698
+ readonly urlOffsets: Uint32Array;
699
+ readonly urlIds: Uint32Array;
700
+ readonly titleTextIds: Uint32Array;
701
+ /** `4294967295` for a page without a canonical. */
702
+ readonly canonicalUrlIds: Uint32Array;
703
+ /** The body hashes back to back. */
704
+ readonly bodyHashes: Uint8Array;
705
+ /** `n + 1` positions cutting `bodyHashes` into `n` pieces. */
706
+ readonly hashOffsets: Uint32Array;
707
+ /** `0` for a page without a body hash (its piece is empty). */
708
+ readonly hashPresent: Uint8Array;
204
709
  }
205
710
  /**
206
- * Factory function signature that every analyze plugin module must
207
- * export as its default export.
208
- *
209
- * The factory receives the user's settings object (`O`) and returns
210
- * an {@link AnalyzePlugin} instance (or a Promise thereof). This two-phase
211
- * pattern allows plugins to perform async initialization (e.g.
212
- * loading locale files, compiling lint configs) once, then reuse
213
- * the resulting plugin for every page.
214
- *
215
- * Use {@link ./hooks/define-plugin.ts!definePlugin} to define a
216
- * plugin with full type inference.
217
- * @template O - Shape of the plugin's settings from the config file.
218
- * @template T - String literal union of column keys the plugin contributes.
711
+ * The two keys under which pages are grouped as the same page under another
712
+ * URL; see `computeAliasKeys`.
219
713
  * @example
220
714
  * ```ts
221
- * // In @nitpicker/analyze-search/src/index.ts
222
- * import { definePlugin } from '@nitpicker/core';
223
- *
224
- * type Options = { keywords: string[] };
225
- *
226
- * export default definePlugin(async (options: Options) => {
227
- * return {
228
- * headers: { found: 'Keywords Found' },
229
- * async eachPage({ html }) {
230
- * const count = options.keywords.filter(k => html.includes(k)).length;
231
- * return { page: { found: { value: count } } };
232
- * },
233
- * };
234
- * });
715
+ * const keys: AliasKeys = { tierA: 'example.com/about/', tierB: 'example.com/about' };
235
716
  * ```
236
- * @see {@link AnalyzePlugin} for the runtime interface
237
- * @see {@link ./hooks/define-plugin.ts!definePlugin} for the type-safe wrapper
238
717
  */
239
- export type PluginFactory<O, T extends string = string> = (options: O, configFilePath: string) => Promise<AnalyzePlugin<T>> | AnalyzePlugin<T>;
718
+ export interface AliasKeys {
719
+ /** Folds the scheme, the host's case and a trailing `/index.{ext}`. */
720
+ readonly tierA: string;
721
+ /** Tier A with one trailing `/` of the path dropped. */
722
+ readonly tierB: string;
723
+ }
240
724
  /**
241
- * Column header definitions: maps column key to display label.
725
+ * One page that may be the same page as another under a different URL.
242
726
  * @example
243
727
  * ```ts
244
- * const headers: TableHeaders<'title' | 'desc'> = {
245
- * title: 'Page Title',
246
- * desc: 'Meta Description',
728
+ * const page: AliasCandidate = {
729
+ * url: 'https://example.com/about/',
730
+ * urlId: 7,
731
+ * titleTextId: 3,
732
+ * canonicalUrlId: null,
733
+ * bodyHash: null,
247
734
  * };
248
735
  * ```
249
736
  */
250
- export type TableHeaders<K extends string> = Record<K, string>;
251
- /** Internal Map representation of {@link TableHeaders}, used by {@link ../table.ts!Table}. */
252
- export type TableHeaderMap<K extends string> = Map<K, string>;
253
- /** A single row of cell values keyed by column identifier. */
254
- export type TableData<K extends string> = Record<K, TableValue>;
255
- /**
256
- * Multiple rows of table data keyed by page URL.
257
- * This is the serialized form used in Worker message payloads and JSON output.
258
- */
259
- export type TablePages<K extends string> = Record<string, TableData<K>>;
260
- /**
261
- * Internal Map representation of page-keyed table data.
262
- * Used by {@link ../table.ts!Table} for efficient merge operations.
263
- */
264
- export type TableRow<K extends string> = Map<string, TableData<K>>;
265
- /**
266
- * Payload for starting an analyze action.
267
- * @internal
268
- */
269
- export interface PluginExecutionContext {
270
- /** Human-readable name of the plugin module. */
271
- pluginModuleName: string;
272
- /** Resolved file system path to the plugin module. */
273
- pluginModulePath: string;
274
- /** Plugin-specific settings to pass to the hook factory. */
275
- settings: unknown;
276
- /** Path to the config file where this action was declared. */
277
- configFilePath: string;
278
- /** Temporary directory where the archive is extracted. */
279
- archiveTempDir: string;
280
- /** Full resolved configuration. */
281
- config: Config;
737
+ export interface AliasCandidate {
738
+ /** The page's URL. */
739
+ readonly url: string;
740
+ /** `url_refs.id` of that URL. */
741
+ readonly urlId: number;
742
+ /** The id of the page's title text; pages with different titles never group. */
743
+ readonly titleTextId: number;
744
+ /** `url_refs.id` of the URL the page's `<link rel="canonical">` points at. */
745
+ readonly canonicalUrlId: number | null;
746
+ /** The page's body hash; without one the page groups by URL form only. */
747
+ readonly bodyHash: Uint8Array | null;
282
748
  }
283
749
  /**
284
- * Event map for the {@link ../nitpicker.ts!Nitpicker} event emitter.
285
- *
286
- * Consumers can listen to these events via `nitpicker.on('writeFile', ...)`.
287
- * @see {@link ../nitpicker.ts!Nitpicker} which extends `TypedAwaitEventEmitter<NitpickerEvent>`
750
+ * The connected components of the isolated pages, built from the pages' link
751
+ * edges as they are read. Created by `createIsolatedClusterRun`.
752
+ * @example
753
+ * ```ts
754
+ * const run = createIsolatedClusterRun([10, 20, 30], []);
755
+ * run.unionEdges([10, 20]);
756
+ * run.labels(); // Uint32Array [0, 0, 1]
757
+ * ```
288
758
  */
289
- export interface NitpickerEvent {
759
+ export interface IsolatedClusterRun {
290
760
  /**
291
- * Emitted after the archive file has been successfully written to disk.
761
+ * Joins the pages of a batch of link edges, `[page, href, page, href, ...]`
762
+ * by page id, in any order and batch size. An edge counts when `page` is a
763
+ * candidate and `href`, once redirects and aliases are followed, is a
764
+ * different one.
292
765
  */
293
- writeFile: {
294
- /** Absolute path to the written `.nitpicker` archive file. */
295
- filePath: string;
296
- };
766
+ unionEdges(edges: ArrayLike<number>): void;
297
767
  /**
298
- * Emitted when a non-fatal error occurs during analysis.
768
+ * For each candidate, in the order given, the number of its component,
769
+ * counted in order of first appearance.
299
770
  */
300
- error: {
301
- /** Human-readable error description. */
302
- message: string;
303
- /** Original Error object, or `null` if unavailable. */
304
- error: Error | null;
305
- };
771
+ labels(): Uint32Array;
772
+ }
773
+ /**
774
+ * Inputs to `loadNativeBinding`. Every field defaults to the running
775
+ * process; tests override them to exercise other platforms.
776
+ */
777
+ export interface LoadNativeBindingOptions {
778
+ /** `process.platform` of the host. */
779
+ readonly platform: NodeJS.Platform;
780
+ /** `process.arch` of the host. */
781
+ readonly arch: NodeJS.Architecture;
782
+ /**
783
+ * glibc version of the running process (e.g. `"2.35"`), or `null` when the
784
+ * C library is not glibc (musl). Consulted only when `platform` is
785
+ * `linux`; when omitted there, it is read from Node's diagnostic report.
786
+ */
787
+ readonly glibcVersion: string | null;
788
+ /** Absolute path of the `@nitpicker/core` package directory. */
789
+ readonly packageDir: string;
790
+ /** Whether a file exists (`fs.existsSync`). */
791
+ readonly fileExists: (filePath: string) => boolean;
792
+ /** Loads a `.node` file or a package by specifier (`require`). */
793
+ readonly requireModule: (specifier: string) => unknown;
306
794
  }