@d-zero/page-cluster 0.5.1 → 0.5.3
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.
|
@@ -165,10 +165,9 @@ 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.
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* fire in that combination.
|
|
168
|
+
* zero yield overhead. Independent of `onClusterReason` — both hooks
|
|
169
|
+
* can be set together and each fires on its own schedule (see
|
|
170
|
+
* `onClusterReason`'s own JSDoc).
|
|
172
171
|
*/
|
|
173
172
|
onProgress?: (event: ProgressEvent) => void;
|
|
174
173
|
/**
|
|
@@ -188,10 +187,11 @@ export type ResolvePageClusterKeysOptions = TokenizeOptions & ResolveBlockingGro
|
|
|
188
187
|
* discovery. Omitting `onClusterReason` skips that bookkeeping
|
|
189
188
|
* entirely, so existing callers pay nothing for this option.
|
|
190
189
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
190
|
+
* Independent of `onProgress`: on the async factory-based
|
|
191
|
+
* `resolvePageClusterKeys`, the small-corpus branch still chooses
|
|
192
|
+
* between the sync and progress-emitting helpers based on `onProgress`
|
|
193
|
+
* alone, and both helpers derive `ClusterReason`s the same way when
|
|
194
|
+
* this option is set.
|
|
195
195
|
* @example
|
|
196
196
|
* ```ts
|
|
197
197
|
* const reasons = new Map<string, ClusterReason>();
|
|
@@ -236,6 +236,27 @@ export function computeLocalChromeArtifacts(landmarks, tokenizeOptions) {
|
|
|
236
236
|
export function computeLocalLandmarkTokens(landmarks, tokenizeOptions) {
|
|
237
237
|
return [...computeLocalChromeArtifacts(landmarks, tokenizeOptions).localTokensByPage];
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Resolves block keys for a clustering pass, deriving each block key's
|
|
241
|
+
* `BlockingReason` in the same call (via `resolveBlockKeys`'s
|
|
242
|
+
* `includeReasons: true` overload) whenever a caller wants `ClusterReason`s.
|
|
243
|
+
* All three clustering drivers below (in-memory, small-corpus-with-progress,
|
|
244
|
+
* streaming) share this exact branch so the "does this caller need reasons"
|
|
245
|
+
* decision can't drift between them.
|
|
246
|
+
* @param blockingPages
|
|
247
|
+
* @param options
|
|
248
|
+
* @param needReasons
|
|
249
|
+
*/
|
|
250
|
+
function resolveBlockKeysForClustering(blockingPages, options, needReasons) {
|
|
251
|
+
if (!needReasons) {
|
|
252
|
+
return {
|
|
253
|
+
blockKeys: resolveBlockKeys(blockingPages, options),
|
|
254
|
+
reasonsByBlockKey: undefined,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const result = resolveBlockKeys(blockingPages, { ...options, includeReasons: true });
|
|
258
|
+
return { blockKeys: result.blockKeys, reasonsByBlockKey: result.reasonsByBlockKey };
|
|
259
|
+
}
|
|
239
260
|
/**
|
|
240
261
|
* Builds and emits one {@link ClusterReason} per final cluster via
|
|
241
262
|
* `onClusterReason`, from data Stage A/B already computed for clustering
|
|
@@ -245,6 +266,12 @@ export function computeLocalLandmarkTokens(landmarks, tokenizeOptions) {
|
|
|
245
266
|
* derived, and the per-block sibling-unit-key lists the driver accumulated
|
|
246
267
|
* alongside its Stage A loop. No re-tokenization and no extra corpus pass —
|
|
247
268
|
* this only re-groups references the driver already held.
|
|
269
|
+
*
|
|
270
|
+
* No-ops when any of `reasonsByBlockKey` / `siblingUnitKeysByBlock` /
|
|
271
|
+
* `onClusterReason` is `undefined` — the single gate for "was
|
|
272
|
+
* `onClusterReason` requested" that all three clustering drivers below defer
|
|
273
|
+
* to, instead of each repeating the same three-way null check before calling
|
|
274
|
+
* this function.
|
|
248
275
|
* @param crossBlockUnits
|
|
249
276
|
* @param rootByKey
|
|
250
277
|
* @param finalGroupsByRoot
|
|
@@ -253,6 +280,8 @@ export function computeLocalLandmarkTokens(landmarks, tokenizeOptions) {
|
|
|
253
280
|
* @param onClusterReason
|
|
254
281
|
*/
|
|
255
282
|
function emitClusterReasons(crossBlockUnits, rootByKey, finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason) {
|
|
283
|
+
if (!onClusterReason || !reasonsByBlockKey || !siblingUnitKeysByBlock)
|
|
284
|
+
return;
|
|
256
285
|
const unitKeysByRoot = new Map();
|
|
257
286
|
for (const unit of crossBlockUnits) {
|
|
258
287
|
const root = rootByKey.get(unit.key) ?? unit.key;
|
|
@@ -395,16 +424,7 @@ export function resolvePageClusterKeysInMemory(pages, options) {
|
|
|
395
424
|
// actually asked for `onClusterReason` — see that option's own JSDoc for
|
|
396
425
|
// why this is the only place ClusterReason bookkeeping is opt-in.
|
|
397
426
|
const onClusterReason = options?.onClusterReason;
|
|
398
|
-
|
|
399
|
-
let reasonsByBlockKey;
|
|
400
|
-
if (onClusterReason) {
|
|
401
|
-
const result = resolveBlockKeys(blockingPages, { ...options, includeReasons: true });
|
|
402
|
-
blockKeys = result.blockKeys;
|
|
403
|
-
reasonsByBlockKey = result.reasonsByBlockKey;
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
blockKeys = resolveBlockKeys(blockingPages, options);
|
|
407
|
-
}
|
|
427
|
+
const { blockKeys, reasonsByBlockKey } = resolveBlockKeysForClustering(blockingPages, options, onClusterReason !== undefined);
|
|
408
428
|
const indicesByBlockKey = groupIndicesByBlockKey(blockKeys);
|
|
409
429
|
// Validated here, eagerly, because it's otherwise only reached from
|
|
410
430
|
// inside the per-block loop below — which never runs at all for an empty
|
|
@@ -439,9 +459,7 @@ export function resolvePageClusterKeysInMemory(pages, options) {
|
|
|
439
459
|
finalKeys[i] = rootKey;
|
|
440
460
|
}
|
|
441
461
|
}
|
|
442
|
-
|
|
443
|
-
emitClusterReasons(crossBlockUnits, rootByKey, finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
444
|
-
}
|
|
462
|
+
emitClusterReasons(crossBlockUnits, rootByKey, finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
445
463
|
return finalKeys;
|
|
446
464
|
}
|
|
447
465
|
/**
|
|
@@ -457,7 +475,9 @@ export function resolvePageClusterKeysInMemory(pages, options) {
|
|
|
457
475
|
* un-capped Stage B across the entire crossBlockUnits array. `finalKeys`
|
|
458
476
|
* returned here must be byte-for-byte identical to what the sync path
|
|
459
477
|
* would have produced for the same `pages` input — spec-enforced by
|
|
460
|
-
* `resolve-page-cluster-keys-streaming.spec.ts`.
|
|
478
|
+
* `resolve-page-cluster-keys-streaming.spec.ts`. When `options.onClusterReason`
|
|
479
|
+
* is set, it derives and emits `ClusterReason`s the same way the sync path
|
|
480
|
+
* does, so the two hooks compose freely.
|
|
461
481
|
*
|
|
462
482
|
* The sync `resolvePageClusterKeysInMemory` is deliberately left in place
|
|
463
483
|
* as its own implementation rather than being folded into a shared helper.
|
|
@@ -491,11 +511,17 @@ async function resolveSmallCorpusWithProgress(pages, onProgress, options) {
|
|
|
491
511
|
const blockingPages = restrictStylesheetsToFirstParty
|
|
492
512
|
? filterFirstPartyStylesheetHrefs(pages)
|
|
493
513
|
: pages;
|
|
494
|
-
|
|
514
|
+
// Reasons (blocking evidence) are only worth deriving when a caller
|
|
515
|
+
// actually asked for `onClusterReason` — same gate as the in-memory path.
|
|
516
|
+
const onClusterReason = options?.onClusterReason;
|
|
517
|
+
const { blockKeys, reasonsByBlockKey } = resolveBlockKeysForClustering(blockingPages, options, onClusterReason !== undefined);
|
|
495
518
|
const indicesByBlockKey = groupIndicesByBlockKey(blockKeys);
|
|
496
519
|
validateDetectContentDepthCapOptions(options);
|
|
497
520
|
const finalKeys = Array.from({ length: pages.length });
|
|
498
521
|
const crossBlockUnits = [];
|
|
522
|
+
const siblingUnitKeysByBlock = onClusterReason
|
|
523
|
+
? new Map()
|
|
524
|
+
: undefined;
|
|
499
525
|
const totalBlocks = indicesByBlockKey.size;
|
|
500
526
|
let blocksProcessed = 0;
|
|
501
527
|
for (const [blockKey, indices] of indicesByBlockKey) {
|
|
@@ -510,6 +536,7 @@ async function resolveSmallCorpusWithProgress(pages, onProgress, options) {
|
|
|
510
536
|
finalKeys[pageIndex] = key;
|
|
511
537
|
}
|
|
512
538
|
crossBlockUnits.push(...result.crossBlockUnits);
|
|
539
|
+
siblingUnitKeysByBlock?.set(blockKey, result.crossBlockUnits.map((u) => u.key));
|
|
513
540
|
blocksProcessed++;
|
|
514
541
|
onProgress({
|
|
515
542
|
phase: 'pass1-block-complete',
|
|
@@ -522,7 +549,7 @@ async function resolveSmallCorpusWithProgress(pages, onProgress, options) {
|
|
|
522
549
|
await new Promise((resolve) => setImmediate(resolve));
|
|
523
550
|
}
|
|
524
551
|
onProgress({ phase: 'stage-b-start', unitCount: crossBlockUnits.length });
|
|
525
|
-
const { rootByKey } = mergeCrossBlockClusters(crossBlockUnits, options);
|
|
552
|
+
const { rootByKey, finalGroupsByRoot } = mergeCrossBlockClusters(crossBlockUnits, options);
|
|
526
553
|
for (let i = 0; i < finalKeys.length; i++) {
|
|
527
554
|
const currentKey = finalKeys[i];
|
|
528
555
|
const rootKey = rootByKey.get(currentKey);
|
|
@@ -530,6 +557,7 @@ async function resolveSmallCorpusWithProgress(pages, onProgress, options) {
|
|
|
530
557
|
finalKeys[i] = rootKey;
|
|
531
558
|
}
|
|
532
559
|
}
|
|
560
|
+
emitClusterReasons(crossBlockUnits, rootByKey, finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
533
561
|
return finalKeys;
|
|
534
562
|
}
|
|
535
563
|
/**
|
|
@@ -609,12 +637,11 @@ export async function resolvePageClusterKeys(pages, options) {
|
|
|
609
637
|
// into per-block progress, so delegate to the untouched sync path —
|
|
610
638
|
// keeping behavior byte-for-byte identical (and yield-overhead-free)
|
|
611
639
|
// to how library-only consumers experienced this before the CLI
|
|
612
|
-
// progress work landed. `onClusterReason`
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
if (onProgress === undefined || options?.onClusterReason) {
|
|
640
|
+
// progress work landed. `onClusterReason` is independent of this
|
|
641
|
+
// choice: both the sync and progress-emitting paths derive
|
|
642
|
+
// ClusterReasons the same way, so passing it doesn't change which
|
|
643
|
+
// path handles a small corpus.
|
|
644
|
+
if (onProgress === undefined) {
|
|
618
645
|
return resolvePageClusterKeysInMemory(fullPages, options);
|
|
619
646
|
}
|
|
620
647
|
return resolveSmallCorpusWithProgress(fullPages, onProgress, options);
|
|
@@ -634,19 +661,7 @@ export async function resolvePageClusterKeys(pages, options) {
|
|
|
634
661
|
// keyed by distinct block key, not by page — but are only derived when a
|
|
635
662
|
// caller actually asked for `onClusterReason`.
|
|
636
663
|
const onClusterReason = options?.onClusterReason;
|
|
637
|
-
|
|
638
|
-
let reasonsByBlockKey;
|
|
639
|
-
if (onClusterReason) {
|
|
640
|
-
const result = resolveBlockKeys(blockingPagesForKeys, {
|
|
641
|
-
...options,
|
|
642
|
-
includeReasons: true,
|
|
643
|
-
});
|
|
644
|
-
blockKeys = result.blockKeys;
|
|
645
|
-
reasonsByBlockKey = result.reasonsByBlockKey;
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
blockKeys = resolveBlockKeys(blockingPagesForKeys, options);
|
|
649
|
-
}
|
|
664
|
+
const { blockKeys, reasonsByBlockKey } = resolveBlockKeysForClustering(blockingPagesForKeys, options, onClusterReason !== undefined);
|
|
650
665
|
const indicesByBlockKey = groupIndicesByBlockKey(blockKeys);
|
|
651
666
|
const finalKeys = Array.from({ length: blockingSignals.length });
|
|
652
667
|
const crossBlockUnits = [];
|
|
@@ -829,9 +844,7 @@ export async function resolvePageClusterKeys(pages, options) {
|
|
|
829
844
|
finalKeys[i] = rootKey;
|
|
830
845
|
}
|
|
831
846
|
}
|
|
832
|
-
|
|
833
|
-
emitClusterReasons(crossBlockUnits, rootByKey, finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
834
|
-
}
|
|
847
|
+
emitClusterReasons(crossBlockUnits, rootByKey, finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
835
848
|
return finalKeys;
|
|
836
849
|
}
|
|
837
850
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-zero/page-cluster",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Clusters crawled HTML pages by DOM-structure similarity — assigns the same key to pages sharing a template, ignoring text content. CLI-first, with library APIs.",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"./jaccard-similarity": {
|
|
33
33
|
"import": "./dist/jaccard-similarity.js",
|
|
34
34
|
"types": "./dist/jaccard-similarity.d.ts"
|
|
35
|
+
},
|
|
36
|
+
"./build-cluster-reason": {
|
|
37
|
+
"import": "./dist/build-cluster-reason.js",
|
|
38
|
+
"types": "./dist/build-cluster-reason.d.ts"
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
41
|
"bin": "./dist/cli.js",
|
|
@@ -44,8 +48,8 @@
|
|
|
44
48
|
"clean": "tsc --build --clean"
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
|
-
"@d-zero/dealer": "1.10.
|
|
48
|
-
"@d-zero/shared": "0.22.
|
|
51
|
+
"@d-zero/dealer": "1.10.2",
|
|
52
|
+
"@d-zero/shared": "0.22.3",
|
|
49
53
|
"htmlparser2": "12.0.0"
|
|
50
54
|
},
|
|
51
55
|
"repository": {
|
|
@@ -53,5 +57,5 @@
|
|
|
53
57
|
"url": "https://github.com/d-zero-dev/tools.git",
|
|
54
58
|
"directory": "packages/@d-zero/page-cluster"
|
|
55
59
|
},
|
|
56
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "7be4609bd7a2ca80a34d87cde2ade18191c952f9"
|
|
57
61
|
}
|