@d-zero/page-cluster 0.4.0 → 0.5.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.
- package/README.md +35 -38
- package/dist/build-cluster-reason.d.ts +119 -0
- package/dist/build-cluster-reason.js +63 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +54 -46
- package/dist/derive-blocking-reason.d.ts +32 -0
- package/dist/derive-blocking-reason.js +1 -0
- package/dist/merge-cross-block-clusters.d.ts +39 -3
- package/dist/merge-cross-block-clusters.js +23 -10
- package/dist/pass0-blocking.d.ts +10 -0
- package/dist/pass0-blocking.js +26 -46
- package/dist/per-page-landmark-signatures.d.ts +4 -5
- package/dist/reassign-orphan-block-keys.d.ts +9 -0
- package/dist/reassign-orphan-block-keys.js +5 -2
- package/dist/resolve-blocking-group-keys.d.ts +10 -0
- package/dist/resolve-blocking-group-keys.js +16 -86
- package/dist/resolve-page-cluster-keys.d.ts +28 -53
- package/dist/resolve-page-cluster-keys.js +183 -70
- package/dist/shell-quorum.d.ts +4 -4
- package/dist/shell-quorum.js +4 -4
- package/package.json +10 -2
- package/dist/build-page-landmark-report.d.ts +0 -50
- package/dist/build-page-landmark-report.js +0 -67
package/dist/pass0-blocking.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlockingReason } from './derive-blocking-reason.js';
|
|
1
2
|
import type { ResolveBlockingGroupKeysOptions } from './resolve-blocking-group-keys.js';
|
|
2
3
|
/**
|
|
3
4
|
* The corpus-wide, HTML-free inputs needed by
|
|
@@ -31,6 +32,12 @@ export type ResolveBlockKeysOptions = ResolveBlockingGroupKeysOptions & {
|
|
|
31
32
|
*/
|
|
32
33
|
readonly restrictStylesheetsToFirstParty?: boolean;
|
|
33
34
|
};
|
|
35
|
+
/** Return shape when `includeReasons: true` is passed to `resolveBlockKeys`. */
|
|
36
|
+
export type BlockKeysWithReasons = {
|
|
37
|
+
readonly blockKeys: string[];
|
|
38
|
+
/** One entry per distinct final block key produced, keyed by that key. */
|
|
39
|
+
readonly reasonsByBlockKey: ReadonlyMap<string, BlockingReason>;
|
|
40
|
+
};
|
|
34
41
|
/**
|
|
35
42
|
* Splits `resolvePageClusterKeys` into a size-flat first pass so the driver
|
|
36
43
|
* can decide per-block memory strategy before loading any page HTML. Runs the
|
|
@@ -71,6 +78,9 @@ export type ResolveBlockKeysOptions = ResolveBlockingGroupKeysOptions & {
|
|
|
71
78
|
* // ['css:<hash>', 'css:<hash>', 'path:about']
|
|
72
79
|
* ```
|
|
73
80
|
*/
|
|
81
|
+
export declare function resolveBlockKeys(pages: readonly Pass0PageSignals[], options: ResolveBlockKeysOptions & {
|
|
82
|
+
includeReasons: true;
|
|
83
|
+
}): BlockKeysWithReasons;
|
|
74
84
|
export declare function resolveBlockKeys(pages: readonly Pass0PageSignals[], options?: ResolveBlockKeysOptions): string[];
|
|
75
85
|
/**
|
|
76
86
|
* Groups pages by their block key while preserving each block's members in
|
package/dist/pass0-blocking.js
CHANGED
|
@@ -1,62 +1,42 @@
|
|
|
1
1
|
import { filterFirstPartyStylesheetHrefs } from './filter-first-party-stylesheet-hrefs.js';
|
|
2
|
-
import { reassignOrphanBlockKeys } from './reassign-orphan-block-keys.js';
|
|
2
|
+
import { REASSIGNED_KEY_PREFIX, reassignOrphanBlockKeys, } from './reassign-orphan-block-keys.js';
|
|
3
3
|
import { resolveBlockingGroupKeys } from './resolve-blocking-group-keys.js';
|
|
4
|
-
/**
|
|
5
|
-
* Splits `resolvePageClusterKeys` into a size-flat first pass so the driver
|
|
6
|
-
* can decide per-block memory strategy before loading any page HTML. Runs the
|
|
7
|
-
* three corpus-wide, HTML-free stages of blocking in the same order the
|
|
8
|
-
* in-memory driver already uses — first-party stylesheet filtering, blocking-
|
|
9
|
-
* key derivation, orphan reassignment — and returns one final block key per
|
|
10
|
-
* input page in input order.
|
|
11
|
-
*
|
|
12
|
-
* ## Why extract this from resolvePageClusterKeys?
|
|
13
|
-
*
|
|
14
|
-
* The in-memory driver holds every page's `html`, `remainderHtml`,
|
|
15
|
-
* `landmarks[]`, and pre-Stage-A prepared HTML at once. At 176k pages × ~57
|
|
16
|
-
* KB average, that alone breaks a 17 GB RAM machine well before Stage A
|
|
17
|
-
* starts (measured: OS SIGKILL at ~15,000 pages, before the resolve phase
|
|
18
|
-
* even began). All three blocking stages, in contrast, depend only on
|
|
19
|
-
* `paths` / `stylesheetHrefs` / `host` — a few hundred bytes per page. Running
|
|
20
|
-
* them first, HTML-free, lets the downstream per-block clustering hold HTML
|
|
21
|
-
* for only one block's pages at a time.
|
|
22
|
-
*
|
|
23
|
-
* ## Preserves in-memory driver semantics exactly
|
|
24
|
-
*
|
|
25
|
-
* The output of this function is byte-identical to the block-key portion of
|
|
26
|
-
* the current `resolvePageClusterKeys` for the same input, because it reuses
|
|
27
|
-
* the same three underlying functions in the same order with the same
|
|
28
|
-
* defaults. That guarantee is load-bearing: the size-threshold-gated Pass 1
|
|
29
|
-
* that follows this function runs the current in-memory implementation
|
|
30
|
-
* unchanged for small blocks, and any drift in block-key computation between
|
|
31
|
-
* Pass 0 and Pass 1 would silently misroute pages between the two paths.
|
|
32
|
-
* @param pages
|
|
33
|
-
* @param options
|
|
34
|
-
* @example
|
|
35
|
-
* ```ts
|
|
36
|
-
* const blockKeys = resolveBlockKeys([
|
|
37
|
-
* { paths: ['news', '1'], stylesheetHrefs: ['/a.css'], host: 'example.com' },
|
|
38
|
-
* { paths: ['news', '2'], stylesheetHrefs: ['/a.css'], host: 'example.com' },
|
|
39
|
-
* { paths: ['about'], stylesheetHrefs: [], host: 'example.com' },
|
|
40
|
-
* ]);
|
|
41
|
-
* // ['css:<hash>', 'css:<hash>', 'path:about']
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
4
|
export function resolveBlockKeys(pages, options) {
|
|
45
5
|
const restrictStylesheetsToFirstParty = options?.restrictStylesheetsToFirstParty ?? true;
|
|
46
6
|
const blockingPages = restrictStylesheetsToFirstParty
|
|
47
7
|
? filterFirstPartyStylesheetHrefs(pages)
|
|
48
8
|
: pages;
|
|
49
|
-
const rawBlockKeys = resolveBlockingGroupKeys(blockingPages, options);
|
|
50
|
-
const reassignOrphans = options?.reassignOrphans ?? true;
|
|
51
|
-
if (!reassignOrphans)
|
|
52
|
-
return rawBlockKeys;
|
|
53
9
|
// Orphan reassignment always uses a numeric `pathDepth`. When the caller
|
|
54
10
|
// asked for `'auto'`, fall back to the historical default 1 here — a
|
|
55
11
|
// future PR that wires the auto-cut depth through can compute it once
|
|
56
12
|
// and pass it as a number to both `resolveBlockingGroupKeys` and this
|
|
57
13
|
// call to keep them consistent.
|
|
58
14
|
const numericPathDepth = typeof options?.pathDepth === 'number' ? options.pathDepth : undefined;
|
|
59
|
-
|
|
15
|
+
const reassignOrphans = options?.reassignOrphans ?? true;
|
|
16
|
+
if (!options?.includeReasons) {
|
|
17
|
+
const rawBlockKeys = resolveBlockingGroupKeys(blockingPages, options);
|
|
18
|
+
if (!reassignOrphans)
|
|
19
|
+
return rawBlockKeys;
|
|
20
|
+
return reassignOrphanBlockKeys(blockingPages, rawBlockKeys, numericPathDepth);
|
|
21
|
+
}
|
|
22
|
+
const { keys: rawBlockKeys, reasonsByKey } = resolveBlockingGroupKeys(blockingPages, {
|
|
23
|
+
...options,
|
|
24
|
+
includeReasons: true,
|
|
25
|
+
});
|
|
26
|
+
if (!reassignOrphans) {
|
|
27
|
+
return { blockKeys: rawBlockKeys, reasonsByBlockKey: reasonsByKey };
|
|
28
|
+
}
|
|
29
|
+
const finalBlockKeys = reassignOrphanBlockKeys(blockingPages, rawBlockKeys, numericPathDepth);
|
|
30
|
+
const reasonsByBlockKey = new Map(reasonsByKey);
|
|
31
|
+
for (const blockKey of finalBlockKeys) {
|
|
32
|
+
if (reasonsByBlockKey.has(blockKey))
|
|
33
|
+
continue;
|
|
34
|
+
reasonsByBlockKey.set(blockKey, {
|
|
35
|
+
kind: 'orphanMerge',
|
|
36
|
+
pathKey: blockKey.slice(REASSIGNED_KEY_PREFIX.length),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { blockKeys: finalBlockKeys, reasonsByBlockKey };
|
|
60
40
|
}
|
|
61
41
|
/**
|
|
62
42
|
* Groups pages by their block key while preserving each block's members in
|
|
@@ -16,11 +16,10 @@ export declare const ALL_LANDMARK_TYPES: readonly LandmarkType[];
|
|
|
16
16
|
*
|
|
17
17
|
* `position` is the instance's location in the page it came from, computed
|
|
18
18
|
* once by {@link ./extract-landmarks.js | extractLandmarks} and carried here
|
|
19
|
-
* unchanged — a back-reference for callers
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the corpus-frequency logic that consumes this type.
|
|
19
|
+
* unchanged — a back-reference for callers that need to report where a
|
|
20
|
+
* chrome-classified instance actually sits, not just that it exists. It
|
|
21
|
+
* plays no part in `tokens`/`signature` computation or in the corpus-
|
|
22
|
+
* frequency logic that consumes this type.
|
|
24
23
|
*/
|
|
25
24
|
export type PerPageLandmarkInstance = {
|
|
26
25
|
readonly type: LandmarkType;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { PageBlockingSignals } from './resolve-blocking-group-keys.js';
|
|
2
|
+
/**
|
|
3
|
+
* Prefix distinguishing a reassigned key from the `css:`/`path:` keys
|
|
4
|
+
* {@link ./resolve-blocking-group-keys.js | resolveBlockingGroupKeys} itself
|
|
5
|
+
* produces, so the two families can never collide. Exported so
|
|
6
|
+
* {@link ./pass0-blocking.js | resolveBlockKeys} can recover the confined
|
|
7
|
+
* path key back out of a reassigned block key when building `BlockingReason`s,
|
|
8
|
+
* without duplicating this literal.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REASSIGNED_KEY_PREFIX = "orphan-merge:";
|
|
2
11
|
/**
|
|
3
12
|
* Rewrites the `path:`-fallback key of an "orphan" page — one with no
|
|
4
13
|
* stylesheet references recorded at all — to match a same-URL-section `css:`
|
|
@@ -2,9 +2,12 @@ import { derivePathGroupKey } from './derive-path-group-key.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Prefix distinguishing a reassigned key from the `css:`/`path:` keys
|
|
4
4
|
* {@link ./resolve-blocking-group-keys.js | resolveBlockingGroupKeys} itself
|
|
5
|
-
* produces, so the two families can never collide.
|
|
5
|
+
* produces, so the two families can never collide. Exported so
|
|
6
|
+
* {@link ./pass0-blocking.js | resolveBlockKeys} can recover the confined
|
|
7
|
+
* path key back out of a reassigned block key when building `BlockingReason`s,
|
|
8
|
+
* without duplicating this literal.
|
|
6
9
|
*/
|
|
7
|
-
const REASSIGNED_KEY_PREFIX = 'orphan-merge:';
|
|
10
|
+
export const REASSIGNED_KEY_PREFIX = 'orphan-merge:';
|
|
8
11
|
/**
|
|
9
12
|
* Reads `values[index]`, throwing instead of returning `undefined`. Every
|
|
10
13
|
* call site here indexes `pages`/`pathKeys`/`blockKeys` with a position
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlockingReason } from './derive-blocking-reason.js';
|
|
1
2
|
/**
|
|
2
3
|
* The two blocking signals {@link ./derive-path-group-key.js | derivePathGroupKey}
|
|
3
4
|
* and {@link ./derive-stylesheet-group-key.js | deriveStylesheetGroupKey} need,
|
|
@@ -36,6 +37,12 @@ export type ResolveBlockingGroupKeysOptions = {
|
|
|
36
37
|
/** Forwarded to `splitTokensByFrequency` as-is. */
|
|
37
38
|
hrefCommonThreshold?: number;
|
|
38
39
|
};
|
|
40
|
+
/** Return shape when `includeReasons: true` is passed to `resolveBlockingGroupKeys`. */
|
|
41
|
+
export type BlockingGroupKeysWithReasons = {
|
|
42
|
+
readonly keys: string[];
|
|
43
|
+
/** One entry per distinct blocking key produced, keyed by that key. */
|
|
44
|
+
readonly reasonsByKey: ReadonlyMap<string, BlockingReason>;
|
|
45
|
+
};
|
|
39
46
|
/**
|
|
40
47
|
* Resolves, per page, which of the two independent blocking signals — the
|
|
41
48
|
* exact stylesheet set or the URL path — to actually use as that page's
|
|
@@ -119,4 +126,7 @@ export type ResolveBlockingGroupKeysOptions = {
|
|
|
119
126
|
* // common.css is loaded by all 3 pages and is filtered out as non-discriminative chrome.
|
|
120
127
|
* ```
|
|
121
128
|
*/
|
|
129
|
+
export declare function resolveBlockingGroupKeys(pages: readonly PageBlockingSignals[], options: ResolveBlockingGroupKeysOptions & {
|
|
130
|
+
includeReasons: true;
|
|
131
|
+
}): BlockingGroupKeysWithReasons;
|
|
122
132
|
export declare function resolveBlockingGroupKeys(pages: readonly PageBlockingSignals[], options?: ResolveBlockingGroupKeysOptions): string[];
|
|
@@ -4,89 +4,6 @@ import { derivePathGroupKey } from './derive-path-group-key.js';
|
|
|
4
4
|
import { deriveStylesheetGroupKey } from './derive-stylesheet-group-key.js';
|
|
5
5
|
import { splitTokensByFrequency } from './split-tokens-by-frequency.js';
|
|
6
6
|
const DEFAULT_MIN_CSS_GROUP_SIZE = 2;
|
|
7
|
-
/**
|
|
8
|
-
* Resolves, per page, which of the two independent blocking signals — the
|
|
9
|
-
* exact stylesheet set or the URL path — to actually use as that page's
|
|
10
|
-
* grouping key. Returns one key per page, in the same order as `pages`.
|
|
11
|
-
*
|
|
12
|
-
* Literature on entity-resolution blocking (Michelson & Knoblock's DNF
|
|
13
|
-
* scheme, canopy clustering, ensemble blocking) combines independent
|
|
14
|
-
* blocking predicates with OR to generate *candidate pairs* for a later
|
|
15
|
-
* similarity/classification pass. This function instead commits each page to
|
|
16
|
-
* exactly one final key: `resolve-page-cluster-keys.js`'s
|
|
17
|
-
* `resolvePageClusterKeys` *does* run a later refinement step
|
|
18
|
-
* (`resolveStructuralClusterKeys`) on top of whichever key a page lands on,
|
|
19
|
-
* but only within that one key's candidate pool — it has no way to pull in
|
|
20
|
-
* a page that this function routed to a different key. So this function's
|
|
21
|
-
* per-page choice is still effectively final for blocking purposes: a page
|
|
22
|
-
* assigned to the wrong key here never re-enters the correct key's pool
|
|
23
|
-
* downstream. A true OR-merge (letting a page carry both the stylesheet and
|
|
24
|
-
* path candidates, deferring to the refinement step to reconcile overlapping
|
|
25
|
-
* results across them) would close that gap, but is deliberately deferred —
|
|
26
|
-
* it needs the same literature-plus-real-data validation cycle this
|
|
27
|
-
* package's linkage-criterion and NN-chain choices already went through, not
|
|
28
|
-
* a change bundled in alongside unrelated fixes. Until then, a
|
|
29
|
-
* priority-with-fallback decision — try the strong signal, fall back to the
|
|
30
|
-
* weak one — is the applicable pattern here, not OR-merge: a union of
|
|
31
|
-
* equivalence relations can only ever coarsen a partition, never split it,
|
|
32
|
-
* but the whole point of preferring the stylesheet signal is that it *splits*
|
|
33
|
-
* pages a URL-path-only grouping would otherwise lump together (confirmed
|
|
34
|
-
* against real crawl data: a single page embedded under an otherwise-uniform
|
|
35
|
-
* URL section, but loading a completely different stylesheet set, is exactly
|
|
36
|
-
* the case a path-only key misses and a stylesheet key catches).
|
|
37
|
-
*
|
|
38
|
-
* Before comparing stylesheet sets, this reuses
|
|
39
|
-
* {@link ./compute-document-frequency.js | computeDocumentFrequency} and
|
|
40
|
-
* {@link ./split-tokens-by-frequency.js | splitTokensByFrequency} — originally
|
|
41
|
-
* built to separate a page's site-wide chrome from its page-specific HTML
|
|
42
|
-
* structure — to strip stylesheet hrefs that recur across most of `pages`
|
|
43
|
-
* (e.g. a shared reset/font stylesheet) before hashing. Without this, two
|
|
44
|
-
* pages from otherwise-unrelated sections that happen to load only that one
|
|
45
|
-
* shared stylesheet would satisfy `minCssGroupSize` and be wrongly treated as
|
|
46
|
-
* the same template family: the problem there isn't too few pages sharing
|
|
47
|
-
* the key (raising `minCssGroupSize` doesn't fix it), it's that the key
|
|
48
|
-
* itself carries no discriminative information. A page whose stylesheet set
|
|
49
|
-
* is empty, or becomes empty after this filtering, always falls back to the
|
|
50
|
-
* path key — loading no distinctive stylesheet is an absence of evidence,
|
|
51
|
-
* not evidence of a shared template, so it must never itself become a
|
|
52
|
-
* matching signal.
|
|
53
|
-
*
|
|
54
|
-
* Document frequency is computed only over pages that load at least one
|
|
55
|
-
* stylesheet: including stylesheet-less pages in the denominator would dilute
|
|
56
|
-
* every href's frequency ratio (e.g. a stylesheet loaded by 100% of the pages
|
|
57
|
-
* that load *any* stylesheet would read as a low, "distinctive" frequency if
|
|
58
|
-
* most pages in the batch load none), letting a genuinely non-discriminative,
|
|
59
|
-
* site-wide stylesheet slip through the common-href filter.
|
|
60
|
-
*
|
|
61
|
-
* Like `computeDocumentFrequency` itself, this expects `pages` to be a
|
|
62
|
-
* roughly homogeneous batch (one site, or one section of a large
|
|
63
|
-
* multi-template site) — see that function's JSDoc for why a federation of
|
|
64
|
-
* independently-templated sub-sections defeats frequency-based filtering.
|
|
65
|
-
* Splitting a heterogeneous crawl into sections before calling this function
|
|
66
|
-
* is the caller's responsibility.
|
|
67
|
-
*
|
|
68
|
-
* This filtering needs enough stylesheet-bearing pages to tell "loaded by
|
|
69
|
-
* every page that has any stylesheet" apart from "coincidentally the only
|
|
70
|
-
* stylesheet two pages happen to load": with only two stylesheet-bearing
|
|
71
|
-
* pages in the whole batch and nothing else to contrast against, any
|
|
72
|
-
* stylesheet they share reads as 100% common and gets filtered out,
|
|
73
|
-
* producing a path-key fallback even when the two pages are a genuine
|
|
74
|
-
* template match. A third, differently-styled page (as in the example below)
|
|
75
|
-
* is what gives the shared stylesheet a frequency below the common-href
|
|
76
|
-
* cutoff.
|
|
77
|
-
* @param pages
|
|
78
|
-
* @param options
|
|
79
|
-
* @example
|
|
80
|
-
* ```ts
|
|
81
|
-
* resolveBlockingGroupKeys([
|
|
82
|
-
* { paths: ['dept-a', 'news', '1'], stylesheetHrefs: ['https://example.com/a.css', 'https://example.com/common.css'] },
|
|
83
|
-
* { paths: ['dept-a', 'news', '2'], stylesheetHrefs: ['https://example.com/a.css', 'https://example.com/common.css'] },
|
|
84
|
-
* { paths: ['dept-b', 'about'], stylesheetHrefs: ['https://example.com/common.css'] },
|
|
85
|
-
* ]);
|
|
86
|
-
* // ['css:<hash of a.css>', 'css:<hash of a.css>', 'path:dept-b']
|
|
87
|
-
* // common.css is loaded by all 3 pages and is filtered out as non-discriminative chrome.
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
7
|
export function resolveBlockingGroupKeys(pages, options) {
|
|
91
8
|
const pathDepthOption = options?.pathDepth;
|
|
92
9
|
const minCssGroupSize = options?.minCssGroupSize ?? DEFAULT_MIN_CSS_GROUP_SIZE;
|
|
@@ -121,14 +38,27 @@ export function resolveBlockingGroupKeys(pages, options) {
|
|
|
121
38
|
cssKeyCounts.set(cssKey, (cssKeyCounts.get(cssKey) ?? 0) + 1);
|
|
122
39
|
}
|
|
123
40
|
}
|
|
124
|
-
|
|
41
|
+
const reasonsByKey = new Map();
|
|
42
|
+
const keys = pages.map((page, index) => {
|
|
125
43
|
const cssKey = cssKeys[index];
|
|
126
44
|
if (cssKey !== undefined && (cssKeyCounts.get(cssKey) ?? 0) >= minCssGroupSize) {
|
|
127
|
-
|
|
45
|
+
const key = `css:${cssKey}`;
|
|
46
|
+
if (!reasonsByKey.has(key)) {
|
|
47
|
+
reasonsByKey.set(key, {
|
|
48
|
+
kind: 'css',
|
|
49
|
+
distinctiveStylesheetHrefs: [...(distinctiveHrefs[index] ?? [])].toSorted(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return key;
|
|
128
53
|
}
|
|
129
54
|
const pathKey = perPagePathKeys === null
|
|
130
55
|
? derivePathGroupKey(page.paths, pathDepthOption)
|
|
131
56
|
: (perPagePathKeys[index] ?? '');
|
|
132
|
-
|
|
57
|
+
const key = `path:${pathKey}`;
|
|
58
|
+
if (!reasonsByKey.has(key)) {
|
|
59
|
+
reasonsByKey.set(key, { kind: 'path', pathKey });
|
|
60
|
+
}
|
|
61
|
+
return key;
|
|
133
62
|
});
|
|
63
|
+
return options?.includeReasons ? { keys, reasonsByKey } : keys;
|
|
134
64
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import type { ClusterReason } from './build-cluster-reason.js';
|
|
1
2
|
import type { ExtractLandmarksResult } from './extract-landmarks.js';
|
|
2
3
|
import type { PerPageLandmarkInstance } from './per-page-landmark-signatures.js';
|
|
3
4
|
import type { ResolveBlockingGroupKeysOptions } from './resolve-blocking-group-keys.js';
|
|
4
5
|
import type { ResolveStructuralClusterKeysOptions } from './resolve-structural-cluster-keys.js';
|
|
5
6
|
import type { TokenizeOptions } from './types.js';
|
|
6
|
-
import { type PageLandmarkReport } from './build-page-landmark-report.js';
|
|
7
7
|
/**
|
|
8
8
|
* Reinjects each page's *local* (non-corpus-wide) landmark-instance tokens
|
|
9
9
|
* into its block token set for Stage A clustering, restoring exactly the
|
|
@@ -165,47 +165,42 @@ export type ResolvePageClusterKeysOptions = TokenizeOptions & ResolveBlockingGro
|
|
|
165
165
|
* sync helper to running a per-block async loop that emits
|
|
166
166
|
* `pass1-block-complete` and `stage-b-start`. Omitting `onProgress`
|
|
167
167
|
* keeps the small-corpus branch on the pre-refactor sync path with
|
|
168
|
-
* zero yield overhead. Ignored when `
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
168
|
+
* zero yield overhead. Ignored when `onClusterReason` is set — that
|
|
169
|
+
* option always routes the small-corpus branch through the sync
|
|
170
|
+
* helper (see `onClusterReason`'s own JSDoc), so no progress events
|
|
171
|
+
* fire in that combination.
|
|
172
172
|
*/
|
|
173
173
|
onProgress?: (event: ProgressEvent) => void;
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
176
|
-
* {@link
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* `
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* unchanged.
|
|
175
|
+
* Optional observability hook invoked once per **final cluster** (not
|
|
176
|
+
* per page) with that cluster's {@link ClusterReason} — the blocking
|
|
177
|
+
* signal that grouped it, its DOM-structural token core, its
|
|
178
|
+
* per-landmark-type commonality, and the sibling cluster keys it was
|
|
179
|
+
* split from within the same Pass-0 block. Unlike `onProgress`, this
|
|
180
|
+
* fires on every path — small-corpus and streaming alike — because a
|
|
181
|
+
* `ClusterReason` is sized by cluster count, not page count, so it
|
|
182
|
+
* carries no streaming-path memory risk the way a per-page report
|
|
183
|
+
* would.
|
|
185
184
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* `RangeError` up front if both apply to the same call, rather than
|
|
192
|
-
* silently degrading semantics.
|
|
185
|
+
* Building the reasons re-uses Stage A/B's own intermediate state (the
|
|
186
|
+
* quorum core, the per-unit landmark instances, the blocking
|
|
187
|
+
* evidence) — it does not re-tokenize pages or re-run corpus-wide
|
|
188
|
+
* discovery. Omitting `onClusterReason` skips that bookkeeping
|
|
189
|
+
* entirely, so existing callers pay nothing for this option.
|
|
193
190
|
*
|
|
194
|
-
* On the async factory-based `resolvePageClusterKeys`, this
|
|
191
|
+
* On the async factory-based `resolvePageClusterKeys`, setting this
|
|
195
192
|
* forces the small-corpus branch through the sync
|
|
196
193
|
* `resolvePageClusterKeysInMemory` helper regardless of `onProgress`
|
|
197
194
|
* — see `onProgress`'s own JSDoc.
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* const reasons = new Map<string, ClusterReason>();
|
|
198
|
+
* const keys = await resolvePageClusterKeys(pages, {
|
|
199
|
+
* onClusterReason: (key, reason) => reasons.set(key, reason),
|
|
200
|
+
* });
|
|
201
|
+
* ```
|
|
198
202
|
*/
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
/**
|
|
202
|
-
* One page's clustering result when `includeLandmarkPositions` is `true`:
|
|
203
|
-
* the same `clusterKey` every caller already gets, plus that page's
|
|
204
|
-
* {@link PageLandmarkReport}.
|
|
205
|
-
*/
|
|
206
|
-
export type PageClusterKeyResult = {
|
|
207
|
-
readonly clusterKey: string;
|
|
208
|
-
readonly landmarks: PageLandmarkReport;
|
|
203
|
+
onClusterReason?: (clusterKey: string, reason: ClusterReason) => void;
|
|
209
204
|
};
|
|
210
205
|
/**
|
|
211
206
|
* Corpus size at or below which the async factory-based
|
|
@@ -229,17 +224,6 @@ export type PageClusterKeyResult = {
|
|
|
229
224
|
* anything above 20,000 is routed to streaming.
|
|
230
225
|
*/
|
|
231
226
|
export declare const CORPUS_INLINE_THRESHOLD = 20000;
|
|
232
|
-
/**
|
|
233
|
-
* Throws when `includeLandmarkPositions` is combined with a corpus over
|
|
234
|
-
* `threshold` pages (the streaming path — see `includeLandmarkPositions`'s
|
|
235
|
-
* own JSDoc for why it has no sample-based equivalent there). Split out from
|
|
236
|
-
* its call site so tests can exercise the boundary with a small injected
|
|
237
|
-
* `threshold` instead of constructing a 20,001-page fixture to cross the
|
|
238
|
-
* real {@link CORPUS_INLINE_THRESHOLD}.
|
|
239
|
-
* @param pageCount
|
|
240
|
-
* @param threshold
|
|
241
|
-
*/
|
|
242
|
-
export declare function assertLandmarkPositionsSupportedForPageCount(pageCount: number, threshold: number): void;
|
|
243
227
|
/**
|
|
244
228
|
* Reservoir-sample size per block on the streaming path. Blocks larger than
|
|
245
229
|
* this have Stage A run on a random sample of `BLOCK_SAMPLE_SIZE` pages,
|
|
@@ -292,9 +276,6 @@ export declare const BLOCK_SAMPLE_SIZE = 100;
|
|
|
292
276
|
* @param pages
|
|
293
277
|
* @param options
|
|
294
278
|
*/
|
|
295
|
-
export declare function resolvePageClusterKeysInMemory(pages: readonly PageClusterSignals[], options: ResolvePageClusterKeysOptions & {
|
|
296
|
-
includeLandmarkPositions: true;
|
|
297
|
-
}): PageClusterKeyResult[];
|
|
298
279
|
export declare function resolvePageClusterKeysInMemory(pages: readonly PageClusterSignals[], options?: ResolvePageClusterKeysOptions): string[];
|
|
299
280
|
/**
|
|
300
281
|
* Factory function returning an iterator over pages. Called once per streaming
|
|
@@ -358,9 +339,6 @@ export type PageFactory = () => Iterable<PageClusterSignals> | AsyncIterable<Pag
|
|
|
358
339
|
* });
|
|
359
340
|
* ```
|
|
360
341
|
*/
|
|
361
|
-
export declare function resolvePageClusterKeys(pages: PageFactory, options: ResolvePageClusterKeysOptions & {
|
|
362
|
-
includeLandmarkPositions: true;
|
|
363
|
-
}): Promise<PageClusterKeyResult[]>;
|
|
364
342
|
export declare function resolvePageClusterKeys(pages: PageFactory, options?: ResolvePageClusterKeysOptions): Promise<string[]>;
|
|
365
343
|
/**
|
|
366
344
|
* Convenience wrapper that runs {@link ./resolve-page-cluster-keys.js | resolvePageClusterKeys}
|
|
@@ -378,7 +356,4 @@ export declare function resolvePageClusterKeys(pages: PageFactory, options?: Res
|
|
|
378
356
|
* ]);
|
|
379
357
|
* ```
|
|
380
358
|
*/
|
|
381
|
-
export declare function resolvePageClusterKeysFromArray(pages: readonly PageClusterSignals[], options: ResolvePageClusterKeysOptions & {
|
|
382
|
-
includeLandmarkPositions: true;
|
|
383
|
-
}): Promise<PageClusterKeyResult[]>;
|
|
384
359
|
export declare function resolvePageClusterKeysFromArray(pages: readonly PageClusterSignals[], options?: ResolvePageClusterKeysOptions): Promise<string[]>;
|