@nitpicker/core 0.14.0 → 0.15.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.
@@ -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', path.join(os.tmpdir(), 'nitpicker/cache/table'));
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 templateKeys = await classifyPageTemplates({
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 (templateKeys.size > 0) {
489
- await this.archive.replacePageTemplates(templateKeys);
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 (${templateKeys.size} pages)`));
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 { ProgressEvent } from '@d-zero/page-cluster/resolve-page-cluster-keys';
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 Map from page URL (`page.url.href`) to its template key. Only
55
- * internal HTML pages with retrievable HTML have an entry.
40
+ * @returns See {@link PageTemplateClassification}.
56
41
  * @example
57
42
  * ```ts
58
- * const templateKeys = await classifyPageTemplates({ archive, pages });
59
- * for (const [url, templateKey] of templateKeys) {
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<Map<string, string>>;
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 Map from page URL (`page.url.href`) to its template key. Only
41
- * internal HTML pages with retrievable HTML have an entry.
66
+ * @returns See {@link PageTemplateClassification}.
42
67
  * @example
43
68
  * ```ts
44
- * const templateKeys = await classifyPageTemplates({ archive, pages });
45
- * for (const [url, templateKey] of templateKeys) {
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 new Map(Object.entries(cached));
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 clusterKeys = await resolvePageClusterKeys(factory, onProgress ? { onProgress } : undefined);
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 result = new Map();
110
+ const templateKeysByUrl = new Map();
74
111
  for (const [index, url] of yieldedUrls.entries()) {
75
- result.set(url, clusterKeys[index]);
112
+ templateKeysByUrl.set(url, clusterKeys[index]);
76
113
  }
77
114
  if (cacheKey) {
78
- await cache.store(cacheKey, Object.fromEntries(result));
115
+ await cache.store(cacheKey, {
116
+ templateKeys: Object.fromEntries(templateKeysByUrl),
117
+ clusterReasons: Object.fromEntries(clusterReasonsByTemplateKey),
118
+ });
79
119
  }
80
- return result;
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.14.0",
3
+ "version": "0.15.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.1",
31
+ "@d-zero/page-cluster": "0.5.3",
32
32
  "@d-zero/shared": "0.22.2",
33
- "@nitpicker/crawler": "0.14.0",
34
- "@nitpicker/types": "0.14.0",
33
+ "@nitpicker/crawler": "0.15.0",
34
+ "@nitpicker/types": "0.15.0",
35
35
  "ansi-colors": "4.1.3",
36
36
  "cosmiconfig": "9.0.2",
37
37
  "jsdom": "29.1.1"
38
38
  },
39
- "gitHead": "d6e1894a29aa29ca94785d9db0acfc3acc74764b"
39
+ "gitHead": "d1485df6e43375c6a44edfe07a0293fd39d7810d"
40
40
  }