@nitpicker/core 0.14.0 → 0.16.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/lib/get-table-cache-root.d.ts +18 -0
- package/lib/get-table-cache-root.js +22 -0
- package/lib/nitpicker.js +9 -5
- package/lib/template-classification/classify-page-templates.d.ts +11 -26
- package/lib/template-classification/classify-page-templates.js +52 -12
- package/lib/template-classification/types.d.ts +44 -0
- package/lib/template-classification/types.js +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the storage location for the analyze command's scratch `table`
|
|
3
|
+
* cache (per-page axe results, cleared at the start of every `analyze` run).
|
|
4
|
+
*
|
|
5
|
+
* Defined as a child of {@link getArchiveCacheRoot} so that
|
|
6
|
+
* `NITPICKER_TAR_CACHE_DIR` — which already redirects the tar-extraction
|
|
7
|
+
* cache — consistently redirects this cache too, instead of the two caches
|
|
8
|
+
* silently splitting across different roots.
|
|
9
|
+
* @returns Absolute path to the `table` cache directory. The `Cache` class
|
|
10
|
+
* creates a further subdirectory under this path named after its `name`
|
|
11
|
+
* argument.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const cache = new Cache('nitpicker-axe', getTableCacheRoot());
|
|
15
|
+
* await cache.clear();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function getTableCacheRoot(): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getArchiveCacheRoot } from '@nitpicker/crawler';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the storage location for the analyze command's scratch `table`
|
|
5
|
+
* cache (per-page axe results, cleared at the start of every `analyze` run).
|
|
6
|
+
*
|
|
7
|
+
* Defined as a child of {@link getArchiveCacheRoot} so that
|
|
8
|
+
* `NITPICKER_TAR_CACHE_DIR` — which already redirects the tar-extraction
|
|
9
|
+
* cache — consistently redirects this cache too, instead of the two caches
|
|
10
|
+
* silently splitting across different roots.
|
|
11
|
+
* @returns Absolute path to the `table` cache directory. The `Cache` class
|
|
12
|
+
* creates a further subdirectory under this path named after its `name`
|
|
13
|
+
* argument.
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const cache = new Cache('nitpicker-axe', getTableCacheRoot());
|
|
17
|
+
* await cache.clear();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function getTableCacheRoot() {
|
|
21
|
+
return path.join(getArchiveCacheRoot(), 'table');
|
|
22
|
+
}
|
package/lib/nitpicker.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Cache } from '@d-zero/shared/cache';
|
|
|
4
4
|
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
5
5
|
import { Archive } from '@nitpicker/crawler';
|
|
6
6
|
import c from 'ansi-colors';
|
|
7
|
+
import { getTableCacheRoot } from './get-table-cache-root.js';
|
|
7
8
|
import { importModules } from './import-modules.js';
|
|
8
9
|
import { loadPluginSettings } from './load-plugin-settings.js';
|
|
9
10
|
import { Table } from './table.js';
|
|
@@ -238,7 +239,7 @@ export class Nitpicker extends EventEmitter {
|
|
|
238
239
|
table.addHeaders(mod.headers);
|
|
239
240
|
}
|
|
240
241
|
const allViolations = [];
|
|
241
|
-
const cache = new Cache('nitpicker-axe',
|
|
242
|
+
const cache = new Cache('nitpicker-axe', getTableCacheRoot());
|
|
242
243
|
await cache.clear();
|
|
243
244
|
// Build plugin metadata: lane IDs and display labels
|
|
244
245
|
const eachPagePlugins = [];
|
|
@@ -471,7 +472,7 @@ export class Nitpicker extends EventEmitter {
|
|
|
471
472
|
// phase's outcome.
|
|
472
473
|
if (templateClassificationLaneId != null) {
|
|
473
474
|
try {
|
|
474
|
-
const
|
|
475
|
+
const classification = await classifyPageTemplates({
|
|
475
476
|
archive: this.archive,
|
|
476
477
|
pages: accumulatedPages,
|
|
477
478
|
// Only worth paying for when there's a lane to update —
|
|
@@ -479,16 +480,19 @@ export class Nitpicker extends EventEmitter {
|
|
|
479
480
|
// demotes `resolvePageClusterKeys` off its byte-identical,
|
|
480
481
|
// yield-overhead-free sync path for corpora at or below its
|
|
481
482
|
// inline threshold (see @d-zero/page-cluster's own docs).
|
|
483
|
+
// `classifyPageTemplates` always requests cluster reasons too,
|
|
484
|
+
// independent of this — see its own JSDoc for why that no
|
|
485
|
+
// longer costs the sync-path demotion this comment warns about.
|
|
482
486
|
onProgress: lanes
|
|
483
487
|
? (event) => {
|
|
484
488
|
lanes.update(templateClassificationLaneId, `Template classification: ${formatTemplateClassificationProgress(event)}%braille%`);
|
|
485
489
|
}
|
|
486
490
|
: undefined,
|
|
487
491
|
});
|
|
488
|
-
if (
|
|
489
|
-
await this.archive.replacePageTemplates(
|
|
492
|
+
if (classification.templateKeysByUrl.size > 0) {
|
|
493
|
+
await this.archive.replacePageTemplates(classification.templateKeysByUrl, classification.clusterReasonsByTemplateKey);
|
|
490
494
|
}
|
|
491
|
-
lanes?.update(templateClassificationLaneId, c.green(`Template classification: Done (${
|
|
495
|
+
lanes?.update(templateClassificationLaneId, c.green(`Template classification: Done (${classification.templateKeysByUrl.size} pages)`));
|
|
492
496
|
}
|
|
493
497
|
catch (error) {
|
|
494
498
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Archive, Page } from '@nitpicker/crawler';
|
|
3
|
-
/**
|
|
4
|
-
* Options for {@link classifyPageTemplates}.
|
|
5
|
-
*/
|
|
6
|
-
export interface ClassifyPageTemplatesOptions {
|
|
7
|
-
/** The archive to read pages/stylesheets from and to derive the cache key from. */
|
|
8
|
-
archive: Archive;
|
|
9
|
-
/**
|
|
10
|
-
* All pages already loaded for this `analyze()` run (see
|
|
11
|
-
* `Nitpicker.analyze`'s accumulation across `getPagesWithRefs` batches —
|
|
12
|
-
* classification must run once, globally, not per batch).
|
|
13
|
-
*/
|
|
14
|
-
pages: readonly Page[];
|
|
15
|
-
/**
|
|
16
|
-
* Progress callback forwarded to `@d-zero/page-cluster`'s
|
|
17
|
-
* `resolvePageClusterKeys`, so long-running classification on large
|
|
18
|
-
* archives isn't silently unresponsive.
|
|
19
|
-
*/
|
|
20
|
-
onProgress?: (event: ProgressEvent) => void;
|
|
21
|
-
}
|
|
1
|
+
import type { ClassifyPageTemplatesOptions, PageTemplateClassification } from './types.js';
|
|
22
2
|
/**
|
|
23
3
|
* Classifies every internal HTML page in the archive into a template group
|
|
24
4
|
* by DOM-structure similarity, using `@d-zero/page-cluster`.
|
|
@@ -50,15 +30,20 @@ export interface ClassifyPageTemplatesOptions {
|
|
|
50
30
|
* cluster" concept (`@nitpicker/query`'s `compute-isolated-clusters.ts`,
|
|
51
31
|
* link-reachability graph components — a completely different kind of
|
|
52
32
|
* grouping from DOM-structure similarity).
|
|
33
|
+
*
|
|
34
|
+
* `onClusterReason` is always passed to `resolvePageClusterKeys` (not made
|
|
35
|
+
* conditional the way `onProgress` is) — as of `@d-zero/page-cluster` 0.5.3,
|
|
36
|
+
* requesting cluster reasons no longer forces small corpora off the
|
|
37
|
+
* progress-emitting path (see that option's own JSDoc), so there is no
|
|
38
|
+
* responsiveness cost to always capturing them.
|
|
53
39
|
* @param options - See {@link ClassifyPageTemplatesOptions}.
|
|
54
|
-
* @returns
|
|
55
|
-
* internal HTML pages with retrievable HTML have an entry.
|
|
40
|
+
* @returns See {@link PageTemplateClassification}.
|
|
56
41
|
* @example
|
|
57
42
|
* ```ts
|
|
58
|
-
* const
|
|
59
|
-
* for (const [url, templateKey] of
|
|
43
|
+
* const { templateKeysByUrl } = await classifyPageTemplates({ archive, pages });
|
|
44
|
+
* for (const [url, templateKey] of templateKeysByUrl) {
|
|
60
45
|
* table.addData({ [url]: { templateKey: { value: templateKey } } });
|
|
61
46
|
* }
|
|
62
47
|
* ```
|
|
63
48
|
*/
|
|
64
|
-
export declare function classifyPageTemplates(options: ClassifyPageTemplatesOptions): Promise<
|
|
49
|
+
export declare function classifyPageTemplates(options: ClassifyPageTemplatesOptions): Promise<PageTemplateClassification>;
|
|
@@ -5,6 +5,26 @@ import { resolvePageClusterKeys } from '@d-zero/page-cluster/resolve-page-cluste
|
|
|
5
5
|
import { Cache } from '@d-zero/shared/cache';
|
|
6
6
|
import { collectPageStylesheetUrls } from './collect-page-stylesheet-urls.js';
|
|
7
7
|
import { createPageClusterFactory } from './create-page-cluster-factory.js';
|
|
8
|
+
/**
|
|
9
|
+
* Narrows a loaded cache value to the current {@link TemplateClassificationCacheEntry}
|
|
10
|
+
* shape, so a pre-existing on-disk cache entry from before cluster-reason
|
|
11
|
+
* capture was added (a bare `Record<string, string>`, no `templateKeys` key)
|
|
12
|
+
* is treated as a miss instead of being misread as `{ templateKeys: undefined,
|
|
13
|
+
* clusterReasons: undefined }`. No version counter is used for this — the
|
|
14
|
+
* structural check is sufficient and avoids a magic number that would need
|
|
15
|
+
* bumping on every future cache-shape change.
|
|
16
|
+
* @param value - The value loaded from the persistent cache.
|
|
17
|
+
*/
|
|
18
|
+
function isTemplateClassificationCacheEntry(value) {
|
|
19
|
+
if (typeof value !== 'object' || value === null) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const candidate = value;
|
|
23
|
+
return (typeof candidate.templateKeys === 'object' &&
|
|
24
|
+
candidate.templateKeys !== null &&
|
|
25
|
+
typeof candidate.clusterReasons === 'object' &&
|
|
26
|
+
candidate.clusterReasons !== null);
|
|
27
|
+
}
|
|
8
28
|
/**
|
|
9
29
|
* Classifies every internal HTML page in the archive into a template group
|
|
10
30
|
* by DOM-structure similarity, using `@d-zero/page-cluster`.
|
|
@@ -36,13 +56,18 @@ import { createPageClusterFactory } from './create-page-cluster-factory.js';
|
|
|
36
56
|
* cluster" concept (`@nitpicker/query`'s `compute-isolated-clusters.ts`,
|
|
37
57
|
* link-reachability graph components — a completely different kind of
|
|
38
58
|
* grouping from DOM-structure similarity).
|
|
59
|
+
*
|
|
60
|
+
* `onClusterReason` is always passed to `resolvePageClusterKeys` (not made
|
|
61
|
+
* conditional the way `onProgress` is) — as of `@d-zero/page-cluster` 0.5.3,
|
|
62
|
+
* requesting cluster reasons no longer forces small corpora off the
|
|
63
|
+
* progress-emitting path (see that option's own JSDoc), so there is no
|
|
64
|
+
* responsiveness cost to always capturing them.
|
|
39
65
|
* @param options - See {@link ClassifyPageTemplatesOptions}.
|
|
40
|
-
* @returns
|
|
41
|
-
* internal HTML pages with retrievable HTML have an entry.
|
|
66
|
+
* @returns See {@link PageTemplateClassification}.
|
|
42
67
|
* @example
|
|
43
68
|
* ```ts
|
|
44
|
-
* const
|
|
45
|
-
* for (const [url, templateKey] of
|
|
69
|
+
* const { templateKeysByUrl } = await classifyPageTemplates({ archive, pages });
|
|
70
|
+
* for (const [url, templateKey] of templateKeysByUrl) {
|
|
46
71
|
* table.addData({ [url]: { templateKey: { value: templateKey } } });
|
|
47
72
|
* }
|
|
48
73
|
* ```
|
|
@@ -53,13 +78,22 @@ export async function classifyPageTemplates(options) {
|
|
|
53
78
|
const cacheKey = await buildArchiveCacheKey(archive.filePath, pages.length);
|
|
54
79
|
if (cacheKey) {
|
|
55
80
|
const cached = await cache.load(cacheKey);
|
|
56
|
-
if (cached) {
|
|
57
|
-
return
|
|
81
|
+
if (cached && isTemplateClassificationCacheEntry(cached)) {
|
|
82
|
+
return {
|
|
83
|
+
templateKeysByUrl: new Map(Object.entries(cached.templateKeys)),
|
|
84
|
+
clusterReasonsByTemplateKey: new Map(Object.entries(cached.clusterReasons)),
|
|
85
|
+
};
|
|
58
86
|
}
|
|
59
87
|
}
|
|
60
88
|
const stylesheetsByUrl = await collectPageStylesheetUrls(archive);
|
|
61
89
|
const { factory, getYieldedUrls } = createPageClusterFactory(pages, stylesheetsByUrl);
|
|
62
|
-
const
|
|
90
|
+
const clusterReasonsByTemplateKey = new Map();
|
|
91
|
+
const clusterKeys = await resolvePageClusterKeys(factory, {
|
|
92
|
+
...(onProgress ? { onProgress } : {}),
|
|
93
|
+
onClusterReason: (clusterKey, reason) => {
|
|
94
|
+
clusterReasonsByTemplateKey.set(clusterKey, reason);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
63
97
|
// Safe only after `resolvePageClusterKeys` has resolved — see
|
|
64
98
|
// `createPageClusterFactory`'s JSDoc for why.
|
|
65
99
|
const yieldedUrls = getYieldedUrls();
|
|
@@ -67,17 +101,23 @@ export async function classifyPageTemplates(options) {
|
|
|
67
101
|
// Would silently mis-map template keys to the wrong URLs if allowed
|
|
68
102
|
// through — see createPageClusterFactory's JSDoc for why this should
|
|
69
103
|
// be structurally impossible, but a loud failure here is far
|
|
70
|
-
// preferable to a quiet mis-assignment.
|
|
104
|
+
// preferable to a quiet mis-assignment. No equivalent check exists for
|
|
105
|
+
// `clusterReasonsByTemplateKey` — see `PageTemplateClassification`'s
|
|
106
|
+
// own JSDoc for why that map is a best-effort side channel, not a
|
|
107
|
+
// per-page guarantee.
|
|
71
108
|
throw new Error(`classifyPageTemplates: resolvePageClusterKeys returned ${clusterKeys.length} keys for ${yieldedUrls.length} yielded pages.`);
|
|
72
109
|
}
|
|
73
|
-
const
|
|
110
|
+
const templateKeysByUrl = new Map();
|
|
74
111
|
for (const [index, url] of yieldedUrls.entries()) {
|
|
75
|
-
|
|
112
|
+
templateKeysByUrl.set(url, clusterKeys[index]);
|
|
76
113
|
}
|
|
77
114
|
if (cacheKey) {
|
|
78
|
-
await cache.store(cacheKey,
|
|
115
|
+
await cache.store(cacheKey, {
|
|
116
|
+
templateKeys: Object.fromEntries(templateKeysByUrl),
|
|
117
|
+
clusterReasons: Object.fromEntries(clusterReasonsByTemplateKey),
|
|
118
|
+
});
|
|
79
119
|
}
|
|
80
|
-
return
|
|
120
|
+
return { templateKeysByUrl, clusterReasonsByTemplateKey };
|
|
81
121
|
}
|
|
82
122
|
/**
|
|
83
123
|
* Lazily constructs the persistent template-classification cache. Kept as a
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ProgressEvent } from '@d-zero/page-cluster/resolve-page-cluster-keys';
|
|
2
|
+
import type { Archive, Page, TemplateClusterReason } from '@nitpicker/crawler';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link import('./classify-page-templates.js').classifyPageTemplates}.
|
|
5
|
+
*/
|
|
6
|
+
export interface ClassifyPageTemplatesOptions {
|
|
7
|
+
/** The archive to read pages/stylesheets from and to derive the cache key from. */
|
|
8
|
+
archive: Archive;
|
|
9
|
+
/**
|
|
10
|
+
* All pages already loaded for this `analyze()` run (see
|
|
11
|
+
* `Nitpicker.analyze`'s accumulation across `getPagesWithRefs` batches —
|
|
12
|
+
* classification must run once, globally, not per batch).
|
|
13
|
+
*/
|
|
14
|
+
pages: readonly Page[];
|
|
15
|
+
/**
|
|
16
|
+
* Progress callback forwarded to `@d-zero/page-cluster`'s
|
|
17
|
+
* `resolvePageClusterKeys`, so long-running classification on large
|
|
18
|
+
* archives isn't silently unresponsive. Independent of cluster-reason
|
|
19
|
+
* capture — `@d-zero/page-cluster` 0.5.3+ composes `onProgress` and
|
|
20
|
+
* `onClusterReason` without either one demoting the corpus off its
|
|
21
|
+
* progress-emitting path (see `classifyPageTemplates`'s own JSDoc).
|
|
22
|
+
*/
|
|
23
|
+
onProgress?: (event: ProgressEvent) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of {@link import('./classify-page-templates.js').classifyPageTemplates}.
|
|
27
|
+
*/
|
|
28
|
+
export interface PageTemplateClassification {
|
|
29
|
+
/**
|
|
30
|
+
* Page URL (`page.url.href`) → template key. Only internal HTML pages
|
|
31
|
+
* with retrievable HTML have an entry.
|
|
32
|
+
*/
|
|
33
|
+
readonly templateKeysByUrl: ReadonlyMap<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Template key → `@d-zero/page-cluster`'s cluster-selection evidence for
|
|
36
|
+
* that key. **Not guaranteed to cover every key in `templateKeysByUrl`**:
|
|
37
|
+
* `@d-zero/page-cluster` only emits a reason for a final cluster it still
|
|
38
|
+
* holds full grouping state for at the moment `onClusterReason` fires,
|
|
39
|
+
* which is a best-effort side channel rather than a per-page guarantee
|
|
40
|
+
* (unlike `templateKeysByUrl`, which is verified 1:1 against the yielded
|
|
41
|
+
* page set — see the hard length check in `classifyPageTemplates`).
|
|
42
|
+
*/
|
|
43
|
+
readonly clusterReasonsByTemplateKey: ReadonlyMap<string, TemplateClusterReason>;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitpicker/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Plugin-based page analysis engine for Nitpicker",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@d-zero/dealer": "1.9.4",
|
|
31
|
-
"@d-zero/page-cluster": "0.3
|
|
31
|
+
"@d-zero/page-cluster": "0.5.3",
|
|
32
32
|
"@d-zero/shared": "0.22.2",
|
|
33
|
-
"@nitpicker/crawler": "0.
|
|
34
|
-
"@nitpicker/types": "0.
|
|
33
|
+
"@nitpicker/crawler": "0.16.0",
|
|
34
|
+
"@nitpicker/types": "0.16.0",
|
|
35
35
|
"ansi-colors": "4.1.3",
|
|
36
36
|
"cosmiconfig": "9.0.2",
|
|
37
37
|
"jsdom": "29.1.1"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "bef8b6d48e3ca5167fee643d6aba8644a065df4a"
|
|
40
40
|
}
|