@d-zero/page-cluster 0.5.6 → 0.6.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.
- package/README.md +35 -0
- package/dist/build-mirrored-template-fixture.d.ts +61 -0
- package/dist/build-mirrored-template-fixture.js +127 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -55
- package/dist/compute-cluster-cohesion.d.ts +59 -0
- package/dist/compute-cluster-cohesion.js +107 -0
- package/dist/detect-mirror-axis.d.ts +74 -0
- package/dist/detect-mirror-axis.js +0 -0
- package/dist/find-cross-cluster-duplicates.d.ts +94 -0
- package/dist/find-cross-cluster-duplicates.js +164 -0
- package/dist/merge-cross-block-clusters.d.ts +18 -0
- package/dist/merge-cross-block-clusters.js +256 -9
- package/dist/merge-validated-clusters.d.ts +27 -0
- package/dist/merge-validated-clusters.js +50 -0
- package/dist/normalize-href-by-mirror-axis.d.ts +25 -0
- package/dist/normalize-href-by-mirror-axis.js +30 -0
- package/dist/normalize-path-by-mirror-axis.d.ts +22 -0
- package/dist/normalize-path-by-mirror-axis.js +25 -0
- package/dist/resolve-page-cluster-keys.d.ts +43 -0
- package/dist/resolve-page-cluster-keys.js +99 -3
- package/dist/stage-a-per-block.js +1 -0
- package/dist/validate-cluster-partition.d.ts +70 -0
- package/dist/validate-cluster-partition.js +49 -0
- package/package.json +36 -4
|
@@ -6,11 +6,13 @@ import { extractLandmarks } from './extract-landmarks.js';
|
|
|
6
6
|
import { filterFirstPartyStylesheetHrefs } from './filter-first-party-stylesheet-hrefs.js';
|
|
7
7
|
import { jaccardSimilarity } from './jaccard-similarity.js';
|
|
8
8
|
import { mergeCrossBlockClusters } from './merge-cross-block-clusters.js';
|
|
9
|
+
import { mergeValidatedClusters } from './merge-validated-clusters.js';
|
|
9
10
|
import { groupIndicesByBlockKey, resolveBlockKeys } from './pass0-blocking.js';
|
|
10
11
|
import { computePerPageLandmarkInstances } from './per-page-landmark-signatures.js';
|
|
11
12
|
import { removeContentBlocks } from './remove-content-blocks.js';
|
|
12
13
|
import { stageAPerBlock } from './stage-a-per-block.js';
|
|
13
14
|
import { tokenize } from './tokenize.js';
|
|
15
|
+
import { validateClusterPartition } from './validate-cluster-partition.js';
|
|
14
16
|
/**
|
|
15
17
|
* FNV-1a 32-bit hash of a string, used to seed the per-block PRNG so
|
|
16
18
|
* reservoir sampling on the streaming path is deterministic for a given
|
|
@@ -257,6 +259,92 @@ function resolveBlockKeysForClustering(blockingPages, options, needReasons) {
|
|
|
257
259
|
const result = resolveBlockKeys(blockingPages, { ...options, includeReasons: true });
|
|
258
260
|
return { blockKeys: result.blockKeys, reasonsByBlockKey: result.reasonsByBlockKey };
|
|
259
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Runs {@link ./validate-cluster-partition.js | validateClusterPartition}
|
|
264
|
+
* over Stage B's finished grouping and applies the built-in auto-merge
|
|
265
|
+
* policy (see `onPartitionReport`'s own JSDoc) directly to `finalKeys`,
|
|
266
|
+
* mutating it in place — the same in-place style the driver's own
|
|
267
|
+
* `rootByKey` rewrite loop already uses just before calling this.
|
|
268
|
+
*
|
|
269
|
+
* No-ops (returning `rootByKey`/`finalGroupsByRoot` unchanged) when
|
|
270
|
+
* `onPartitionReport` is `undefined` — the single gate every driver defers
|
|
271
|
+
* to, mirroring `emitClusterReasons`'s own gate for `onClusterReason`.
|
|
272
|
+
*
|
|
273
|
+
* When a merge is applied, `rootByKey` and `finalGroupsByRoot` are folded
|
|
274
|
+
* according to the same rename so `emitClusterReasons` (called with this
|
|
275
|
+
* function's return value, not the pre-merge originals) builds each
|
|
276
|
+
* survivor's `ClusterReason` from its *actual* post-merge membership —
|
|
277
|
+
* without this, a merged-away key's members would report `finalKeys[i]`
|
|
278
|
+
* pointing at the survivor while its own stale `ClusterReason.memberCount`
|
|
279
|
+
* still reflected only its pre-merge share.
|
|
280
|
+
* @param finalKeys Every page's current final cluster key, in input order.
|
|
281
|
+
* Mutated in place when a merge is applied.
|
|
282
|
+
* @param rootByKey Stage B's own result (see
|
|
283
|
+
* {@link ./merge-cross-block-clusters.js | MergeCrossBlockClustersResult}).
|
|
284
|
+
* @param finalGroupsByRoot Stage B's own result.
|
|
285
|
+
* @param getPageSignals Given an original page index, returns that page's
|
|
286
|
+
* `paths`/`stylesheetHrefs` — `undefined` for an index the caller has no
|
|
287
|
+
* record of (defensive; should not occur for indices Stage A/B actually
|
|
288
|
+
* retained).
|
|
289
|
+
* @param onPartitionReport
|
|
290
|
+
*/
|
|
291
|
+
function validateAndMergePartition(finalKeys, rootByKey, finalGroupsByRoot, getPageSignals, onPartitionReport) {
|
|
292
|
+
if (!onPartitionReport)
|
|
293
|
+
return { rootByKey, finalGroupsByRoot };
|
|
294
|
+
const clusteredPages = [];
|
|
295
|
+
for (const [finalKey, group] of finalGroupsByRoot) {
|
|
296
|
+
for (const [i, tokens] of group.tokenSets.entries()) {
|
|
297
|
+
const pageIndex = group.pageIndices[i] ?? -1;
|
|
298
|
+
if (pageIndex === -1)
|
|
299
|
+
continue;
|
|
300
|
+
const signals = getPageSignals(pageIndex);
|
|
301
|
+
if (!signals)
|
|
302
|
+
continue;
|
|
303
|
+
clusteredPages.push({
|
|
304
|
+
clusterKey: finalKey,
|
|
305
|
+
tokens,
|
|
306
|
+
paths: signals.paths,
|
|
307
|
+
stylesheetHrefs: signals.stylesheetHrefs,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const report = validateClusterPartition(clusteredPages);
|
|
312
|
+
onPartitionReport(report);
|
|
313
|
+
// `findCrossClusterDuplicates` only ever returns entries that already
|
|
314
|
+
// satisfy this condition (its exact-match pass sets `similarity: 1`
|
|
315
|
+
// unconditionally; its near-match pass requires `corroboratedByMirrorAxis`
|
|
316
|
+
// to accept a pair at all) — this filter is a defensive invariant check,
|
|
317
|
+
// not something that currently narrows the result, kept so this call site
|
|
318
|
+
// keeps working correctly if that policy ever loosens.
|
|
319
|
+
const safeToMerge = report.crossClusterDuplicates.filter((d) => d.similarity === 1 || d.corroboratedByMirrorAxis);
|
|
320
|
+
if (safeToMerge.length === 0)
|
|
321
|
+
return { rootByKey, finalGroupsByRoot };
|
|
322
|
+
const mergedKeys = mergeValidatedClusters(finalKeys, safeToMerge);
|
|
323
|
+
const renameMap = new Map();
|
|
324
|
+
for (const [i, mergedKey] of mergedKeys.entries()) {
|
|
325
|
+
if (finalKeys[i] !== mergedKey)
|
|
326
|
+
renameMap.set(finalKeys[i], mergedKey);
|
|
327
|
+
finalKeys[i] = mergedKey;
|
|
328
|
+
}
|
|
329
|
+
const mergedFinalGroupsByRoot = new Map();
|
|
330
|
+
for (const [key, group] of finalGroupsByRoot) {
|
|
331
|
+
const target = renameMap.get(key) ?? key;
|
|
332
|
+
const existing = mergedFinalGroupsByRoot.get(target);
|
|
333
|
+
mergedFinalGroupsByRoot.set(target, {
|
|
334
|
+
tokenSets: [...(existing?.tokenSets ?? []), ...group.tokenSets],
|
|
335
|
+
landmarkInstances: [
|
|
336
|
+
...(existing?.landmarkInstances ?? []),
|
|
337
|
+
...group.landmarkInstances,
|
|
338
|
+
],
|
|
339
|
+
pageIndices: [...(existing?.pageIndices ?? []), ...group.pageIndices],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const mergedRootByKey = new Map();
|
|
343
|
+
for (const [unitKey, stageBRoot] of rootByKey) {
|
|
344
|
+
mergedRootByKey.set(unitKey, renameMap.get(stageBRoot) ?? stageBRoot);
|
|
345
|
+
}
|
|
346
|
+
return { rootByKey: mergedRootByKey, finalGroupsByRoot: mergedFinalGroupsByRoot };
|
|
347
|
+
}
|
|
260
348
|
/**
|
|
261
349
|
* Builds and emits one {@link ClusterReason} per final cluster via
|
|
262
350
|
* `onClusterReason`, from data Stage A/B already computed for clustering
|
|
@@ -459,7 +547,8 @@ export function resolvePageClusterKeysInMemory(pages, options) {
|
|
|
459
547
|
finalKeys[i] = rootKey;
|
|
460
548
|
}
|
|
461
549
|
}
|
|
462
|
-
|
|
550
|
+
const validated = validateAndMergePartition(finalKeys, rootByKey, finalGroupsByRoot, (i) => pages[i], options?.onPartitionReport);
|
|
551
|
+
emitClusterReasons(crossBlockUnits, validated.rootByKey, validated.finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
463
552
|
return finalKeys;
|
|
464
553
|
}
|
|
465
554
|
/**
|
|
@@ -557,7 +646,8 @@ async function resolveSmallCorpusWithProgress(pages, onProgress, options) {
|
|
|
557
646
|
finalKeys[i] = rootKey;
|
|
558
647
|
}
|
|
559
648
|
}
|
|
560
|
-
|
|
649
|
+
const validated = validateAndMergePartition(finalKeys, rootByKey, finalGroupsByRoot, (i) => pages[i], options?.onPartitionReport);
|
|
650
|
+
emitClusterReasons(crossBlockUnits, validated.rootByKey, validated.finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
561
651
|
return finalKeys;
|
|
562
652
|
}
|
|
563
653
|
/**
|
|
@@ -844,7 +934,13 @@ export async function resolvePageClusterKeys(pages, options) {
|
|
|
844
934
|
finalKeys[i] = rootKey;
|
|
845
935
|
}
|
|
846
936
|
}
|
|
847
|
-
|
|
937
|
+
// `blockingSignals` (built during Pass 0, held for the whole call) has an
|
|
938
|
+
// entry for every page in the corpus, sampled or not — only the pages
|
|
939
|
+
// `finalGroupsByRoot` actually kept a token set for (reservoir-sampled
|
|
940
|
+
// block representatives) end up in the report, per `onPartitionReport`'s
|
|
941
|
+
// own JSDoc.
|
|
942
|
+
const validated = validateAndMergePartition(finalKeys, rootByKey, finalGroupsByRoot, (i) => blockingSignals[i], options?.onPartitionReport);
|
|
943
|
+
emitClusterReasons(crossBlockUnits, validated.rootByKey, validated.finalGroupsByRoot, reasonsByBlockKey, siblingUnitKeysByBlock, onClusterReason);
|
|
848
944
|
return finalKeys;
|
|
849
945
|
}
|
|
850
946
|
/**
|
|
@@ -172,6 +172,7 @@ export function stageAPerBlock(input, options) {
|
|
|
172
172
|
key: unitKey,
|
|
173
173
|
memberTokenSets: sampledPositions.map((pos) => blockTokenSets[pos]),
|
|
174
174
|
memberLandmarkInstances: sampledPositions.map((pos) => memberLandmarkInstancesByPage[pos]),
|
|
175
|
+
memberPageIndices: sampledPositions.map((pos) => memberIndices[pos]),
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
return { pageKeys, crossBlockUnits };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ClusterCohesion, ClusterCohesionOptions } from './compute-cluster-cohesion.js';
|
|
2
|
+
import type { DetectMirrorAxisOptions, MirrorAxis } from './detect-mirror-axis.js';
|
|
3
|
+
import type { ClusteredPage, CrossClusterDuplicate } from './find-cross-cluster-duplicates.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for {@link validateClusterPartition}, forwarded to the three
|
|
6
|
+
* checks it composes.
|
|
7
|
+
*/
|
|
8
|
+
export type ValidateClusterPartitionOptions = {
|
|
9
|
+
readonly mirrorAxis?: DetectMirrorAxisOptions;
|
|
10
|
+
readonly cohesion?: ClusterCohesionOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Only `corroboratedSimilarityThreshold` — `mirrorAxis` itself is always
|
|
13
|
+
* the axis this function just detected, not independently settable here.
|
|
14
|
+
*/
|
|
15
|
+
readonly crossClusterDuplicates?: {
|
|
16
|
+
readonly corroboratedSimilarityThreshold?: number;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Structured evidence that a clustering run's partition may need
|
|
21
|
+
* correcting: which pages are plausibly the same template despite ending up
|
|
22
|
+
* in different clusters, and which clusters plausibly mix unrelated
|
|
23
|
+
* templates together. Carries no verdict on whether to act — same design as
|
|
24
|
+
* {@link ./build-cluster-reason.js | ClusterReason} — a caller decides what
|
|
25
|
+
* to do with `crossClusterDuplicates` (e.g. via
|
|
26
|
+
* {@link ./merge-validated-clusters.js | mergeValidatedClusters}) and with
|
|
27
|
+
* `cohesion`'s `suspicious` flags.
|
|
28
|
+
*/
|
|
29
|
+
export type ClusterPartitionReport = {
|
|
30
|
+
/**
|
|
31
|
+
* The mirror axis detected across every page's `paths`, or `null` if
|
|
32
|
+
* none was found — see
|
|
33
|
+
* {@link ./detect-mirror-axis.js | detectMirrorAxis}.
|
|
34
|
+
*/
|
|
35
|
+
readonly mirrorAxis: MirrorAxis | null;
|
|
36
|
+
/** One entry per distinct cluster key present in `pages`. */
|
|
37
|
+
readonly cohesion: readonly ClusterCohesion[];
|
|
38
|
+
/** See {@link ./find-cross-cluster-duplicates.js | findCrossClusterDuplicates}. */
|
|
39
|
+
readonly crossClusterDuplicates: readonly CrossClusterDuplicate[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Validates a finished clustering partition by checking it against itself,
|
|
43
|
+
* rather than trying to get the clustering right the first time: whether
|
|
44
|
+
* pages that ended up in different clusters are nonetheless structurally
|
|
45
|
+
* identical or near-identical (a likely over-split, see
|
|
46
|
+
* {@link ./find-cross-cluster-duplicates.js | findCrossClusterDuplicates}),
|
|
47
|
+
* and whether any single cluster's members actually agree with each other
|
|
48
|
+
* (a likely over-merge, see
|
|
49
|
+
* {@link ./compute-cluster-cohesion.js | computeClusterCohesion}). Neither
|
|
50
|
+
* check depends on *how* the partition was produced — this is deliberately
|
|
51
|
+
* decoupled from `resolvePageClusterKeys`'s own Stage A/B internals so it
|
|
52
|
+
* can validate a partition regardless of its origin, including a stored one
|
|
53
|
+
* loaded back from an archive.
|
|
54
|
+
*
|
|
55
|
+
* Takes already-tokenized pages rather than raw HTML — a caller with a
|
|
56
|
+
* `resolvePageClusterKeys` result already has token sets on hand (or can
|
|
57
|
+
* derive them via {@link ./tokenize.js | tokenize}), and re-tokenizing here
|
|
58
|
+
* would cost a second full corpus pass for no benefit.
|
|
59
|
+
* @param pages
|
|
60
|
+
* @param options
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const report = validateClusterPartition(pages);
|
|
64
|
+
* const safeToMerge = report.crossClusterDuplicates.filter(
|
|
65
|
+
* (d) => d.similarity === 1 || d.corroboratedByMirrorAxis,
|
|
66
|
+
* );
|
|
67
|
+
* const mergedKeys = mergeValidatedClusters(clusterKeys, safeToMerge);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateClusterPartition(pages: readonly ClusteredPage[], options?: ValidateClusterPartitionOptions): ClusterPartitionReport;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { computeClusterCohesion } from './compute-cluster-cohesion.js';
|
|
2
|
+
import { detectMirrorAxis } from './detect-mirror-axis.js';
|
|
3
|
+
import { findCrossClusterDuplicates } from './find-cross-cluster-duplicates.js';
|
|
4
|
+
/**
|
|
5
|
+
* Validates a finished clustering partition by checking it against itself,
|
|
6
|
+
* rather than trying to get the clustering right the first time: whether
|
|
7
|
+
* pages that ended up in different clusters are nonetheless structurally
|
|
8
|
+
* identical or near-identical (a likely over-split, see
|
|
9
|
+
* {@link ./find-cross-cluster-duplicates.js | findCrossClusterDuplicates}),
|
|
10
|
+
* and whether any single cluster's members actually agree with each other
|
|
11
|
+
* (a likely over-merge, see
|
|
12
|
+
* {@link ./compute-cluster-cohesion.js | computeClusterCohesion}). Neither
|
|
13
|
+
* check depends on *how* the partition was produced — this is deliberately
|
|
14
|
+
* decoupled from `resolvePageClusterKeys`'s own Stage A/B internals so it
|
|
15
|
+
* can validate a partition regardless of its origin, including a stored one
|
|
16
|
+
* loaded back from an archive.
|
|
17
|
+
*
|
|
18
|
+
* Takes already-tokenized pages rather than raw HTML — a caller with a
|
|
19
|
+
* `resolvePageClusterKeys` result already has token sets on hand (or can
|
|
20
|
+
* derive them via {@link ./tokenize.js | tokenize}), and re-tokenizing here
|
|
21
|
+
* would cost a second full corpus pass for no benefit.
|
|
22
|
+
* @param pages
|
|
23
|
+
* @param options
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const report = validateClusterPartition(pages);
|
|
27
|
+
* const safeToMerge = report.crossClusterDuplicates.filter(
|
|
28
|
+
* (d) => d.similarity === 1 || d.corroboratedByMirrorAxis,
|
|
29
|
+
* );
|
|
30
|
+
* const mergedKeys = mergeValidatedClusters(clusterKeys, safeToMerge);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function validateClusterPartition(pages, options) {
|
|
34
|
+
const mirrorAxis = detectMirrorAxis(pages.map((p) => p.paths), options?.mirrorAxis);
|
|
35
|
+
const membersByKey = new Map();
|
|
36
|
+
for (const page of pages) {
|
|
37
|
+
const members = membersByKey.get(page.clusterKey);
|
|
38
|
+
if (members)
|
|
39
|
+
members.push(page.tokens);
|
|
40
|
+
else
|
|
41
|
+
membersByKey.set(page.clusterKey, [page.tokens]);
|
|
42
|
+
}
|
|
43
|
+
const cohesion = computeClusterCohesion(membersByKey, options?.cohesion);
|
|
44
|
+
const crossClusterDuplicates = findCrossClusterDuplicates(pages, {
|
|
45
|
+
mirrorAxis,
|
|
46
|
+
corroboratedSimilarityThreshold: options?.crossClusterDuplicates?.corroboratedSimilarityThreshold,
|
|
47
|
+
});
|
|
48
|
+
return { mirrorAxis, cohesion, crossClusterDuplicates };
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-zero/page-cluster",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24.11.0"
|
|
12
|
+
},
|
|
10
13
|
"type": "module",
|
|
11
14
|
"exports": {
|
|
12
15
|
".": {
|
|
@@ -36,6 +39,34 @@
|
|
|
36
39
|
"./build-cluster-reason": {
|
|
37
40
|
"import": "./dist/build-cluster-reason.js",
|
|
38
41
|
"types": "./dist/build-cluster-reason.d.ts"
|
|
42
|
+
},
|
|
43
|
+
"./detect-mirror-axis": {
|
|
44
|
+
"import": "./dist/detect-mirror-axis.js",
|
|
45
|
+
"types": "./dist/detect-mirror-axis.d.ts"
|
|
46
|
+
},
|
|
47
|
+
"./normalize-path-by-mirror-axis": {
|
|
48
|
+
"import": "./dist/normalize-path-by-mirror-axis.js",
|
|
49
|
+
"types": "./dist/normalize-path-by-mirror-axis.d.ts"
|
|
50
|
+
},
|
|
51
|
+
"./normalize-href-by-mirror-axis": {
|
|
52
|
+
"import": "./dist/normalize-href-by-mirror-axis.js",
|
|
53
|
+
"types": "./dist/normalize-href-by-mirror-axis.d.ts"
|
|
54
|
+
},
|
|
55
|
+
"./compute-cluster-cohesion": {
|
|
56
|
+
"import": "./dist/compute-cluster-cohesion.js",
|
|
57
|
+
"types": "./dist/compute-cluster-cohesion.d.ts"
|
|
58
|
+
},
|
|
59
|
+
"./find-cross-cluster-duplicates": {
|
|
60
|
+
"import": "./dist/find-cross-cluster-duplicates.js",
|
|
61
|
+
"types": "./dist/find-cross-cluster-duplicates.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./validate-cluster-partition": {
|
|
64
|
+
"import": "./dist/validate-cluster-partition.js",
|
|
65
|
+
"types": "./dist/validate-cluster-partition.d.ts"
|
|
66
|
+
},
|
|
67
|
+
"./merge-validated-clusters": {
|
|
68
|
+
"import": "./dist/merge-validated-clusters.js",
|
|
69
|
+
"types": "./dist/merge-validated-clusters.d.ts"
|
|
39
70
|
}
|
|
40
71
|
},
|
|
41
72
|
"bin": "dist/cli.js",
|
|
@@ -48,8 +79,9 @@
|
|
|
48
79
|
"clean": "tsc --build --clean"
|
|
49
80
|
},
|
|
50
81
|
"dependencies": {
|
|
51
|
-
"@d-zero/
|
|
52
|
-
"@d-zero/
|
|
82
|
+
"@d-zero/cli-core": "1.4.0",
|
|
83
|
+
"@d-zero/dealer": "1.11.0",
|
|
84
|
+
"@d-zero/shared": "0.23.0",
|
|
53
85
|
"htmlparser2": "12.0.0"
|
|
54
86
|
},
|
|
55
87
|
"repository": {
|
|
@@ -57,5 +89,5 @@
|
|
|
57
89
|
"url": "https://github.com/d-zero-dev/tools.git",
|
|
58
90
|
"directory": "packages/@d-zero/page-cluster"
|
|
59
91
|
},
|
|
60
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "d48a3ed9c01d32f0ca1a5b00e4abc36c376ce880"
|
|
61
93
|
}
|