@forkpoint/agent-lighthouse-core 0.3.0 → 0.4.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/dist/index.d.mts CHANGED
@@ -198,8 +198,89 @@ interface ReadinessVitals {
198
198
  technical: number;
199
199
  }
200
200
 
201
- type ProgressCallback = (pct: number, phase: string) => void | Promise<void>;
202
- declare function runScan(url: string, onProgress?: ProgressCallback, pageOverrides?: PageOverride[] | null, signal?: AbortSignal): Promise<ScanReport>;
201
+ type PhaseId = 'fetch-root' | 'fetch-pages' | 'analyze' | 'audits' | 'report';
202
+ type ScanEvent = {
203
+ type: 'scan:start';
204
+ url: string;
205
+ fraction: number;
206
+ elapsedMs: number;
207
+ } | {
208
+ type: 'phase:start';
209
+ phase: PhaseId;
210
+ totalUnits: number;
211
+ fraction: number;
212
+ elapsedMs: number;
213
+ } | {
214
+ type: 'unit:done';
215
+ phase: PhaseId;
216
+ completed: number;
217
+ total: number;
218
+ label?: string;
219
+ fraction: number;
220
+ elapsedMs: number;
221
+ } | {
222
+ type: 'unit:fail';
223
+ phase: PhaseId;
224
+ label: string;
225
+ error: string;
226
+ fraction: number;
227
+ elapsedMs: number;
228
+ } | {
229
+ type: 'phase:done';
230
+ phase: PhaseId;
231
+ durationMs: number;
232
+ fraction: number;
233
+ elapsedMs: number;
234
+ } | {
235
+ type: 'scan:done';
236
+ durationMs: number;
237
+ score: number;
238
+ fraction: number;
239
+ elapsedMs: number;
240
+ };
241
+ /**
242
+ * How much of the overall scan fraction each phase owns. Sums to 1 so the
243
+ * fraction of a finished scan is exactly 1.
244
+ */
245
+ declare const PHASE_WEIGHTS: Record<PhaseId, number>;
246
+ /**
247
+ * Turns scan milestones into {@link ScanEvent}s. Owns the fraction math
248
+ * (`Σ weights of done phases + current phase weight × completed/total`),
249
+ * clamps it to be monotonic non-decreasing, and stamps `fraction`/`elapsedMs`
250
+ * onto every event. Emission is synchronous — throttling is a consumer concern.
251
+ */
252
+ declare class ProgressTracker {
253
+ private readonly onEvent;
254
+ private readonly startMs;
255
+ private doneWeight;
256
+ private phase;
257
+ private phaseStartMs;
258
+ private totalUnits;
259
+ private completedUnits;
260
+ private lastFraction;
261
+ constructor(onEvent: (event: ScanEvent) => void);
262
+ /** Fraction of the whole scan that is complete, in [0, 1]. Never decreases. */
263
+ get fraction(): number;
264
+ /** Stamp an event with the current fraction/elapsed and advance the floor. */
265
+ private stamp;
266
+ private elapsedMs;
267
+ scanStart(url: string): void;
268
+ phaseStart(phase: PhaseId, totalUnits: number): void;
269
+ /** Correct the current phase's unit total (e.g. discovery finds pages mid-phase). */
270
+ setPhaseTotal(totalUnits: number): void;
271
+ unitDone(label?: string): void;
272
+ /** A failed unit still counts as settled work so the phase can complete. */
273
+ unitFail(label: string, error: string): void;
274
+ phaseDone(): void;
275
+ scanDone(score: number): void;
276
+ }
277
+
278
+ interface ScanOptions {
279
+ onEvent?: (event: ScanEvent) => void;
280
+ pages?: PageOverride[] | null;
281
+ signal?: AbortSignal;
282
+ }
283
+ declare function runScan(url: string, options?: ScanOptions): Promise<ScanReport>;
203
284
 
204
285
  interface A11yNodeFinding {
205
286
  target: string;
@@ -310,12 +391,37 @@ interface AuditRunResult {
310
391
  categories: CategoryResult[];
311
392
  overallScore: number;
312
393
  }
313
- type ProgressFn = (completed: number, total: number) => void;
394
+ /**
395
+ * Progress emitted per settled audit. The orchestrator forwards these into the
396
+ * ProgressTracker, which owns counting and fraction/elapsed stamping.
397
+ */
398
+ type AuditProgressEvent = {
399
+ type: 'unit:done';
400
+ label: string;
401
+ } | {
402
+ type: 'unit:fail';
403
+ label: string;
404
+ error: string;
405
+ };
406
+ interface AuditPlan {
407
+ runnable: Array<{
408
+ reg: AuditRegistration;
409
+ categoryId: string;
410
+ }>;
411
+ skipped: CheckResult[];
412
+ }
413
+ /**
414
+ * Split a scan config into the audits that will actually execute and the
415
+ * page-type-skipped `na` stubs. Exported so the orchestrator can size the
416
+ * audits progress phase before running it.
417
+ */
418
+ declare function planAudits(ctx: CheckContext, config: ScanConfig): AuditPlan;
314
419
  /**
315
420
  * Execute all audits registered in the config against the scan context.
316
421
  * Returns check results grouped into categories with weighted scores.
422
+ * Pass a precomputed `plan` (from {@link planAudits}) to avoid recomputing it.
317
423
  */
318
- declare function runAudits(ctx: CheckContext, config: ScanConfig, onProgress?: ProgressFn): Promise<AuditRunResult>;
424
+ declare function runAudits(ctx: CheckContext, config: ScanConfig, onEvent?: (event: AuditProgressEvent) => void, plan?: AuditPlan): Promise<AuditRunResult>;
319
425
 
320
426
  declare function parseHtml(html: string): CheerioAPI;
321
427
  declare function extractJsonLd($: CheerioAPI): object[];
@@ -711,4 +817,4 @@ declare class Logger {
711
817
  }
712
818
  declare const logger: Logger;
713
819
 
714
- export { type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, CATEGORY_NAMES, CATEGORY_WEIGHTS, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, DEFAULT_SCAN_LIMIT, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PRESETS, type PageContext, type PageOverride, type PageType, type PresetName, type PresetOptions, type ProductFieldVerification, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanReport, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, TAG_SCAN_ERROR, TAG_SKIPPED_PAGE_TYPE, type WafProtection, buildCategoryResult, calculateCategoryScore, calculateOverallScore, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, flattenJsonLd, getMainContentText, getPreset, getScoreTier, getTierColor, getTierLabel, getWordCount, isPrivateIp, isSafeUrl, joinUrl, loadConfigFile, logger, normalizeUrl, parseHtml, runAudits, runScan };
820
+ export { type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditPlan, type AuditProgressEvent, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, CATEGORY_NAMES, CATEGORY_WEIGHTS, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, DEFAULT_SCAN_LIMIT, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PHASE_WEIGHTS, PRESETS, type PageContext, type PageOverride, type PageType, type PhaseId, type PresetName, type PresetOptions, type ProductFieldVerification, ProgressTracker, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanEvent, type ScanOptions, type ScanReport, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, TAG_SCAN_ERROR, TAG_SKIPPED_PAGE_TYPE, type WafProtection, buildCategoryResult, calculateCategoryScore, calculateOverallScore, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, flattenJsonLd, getMainContentText, getPreset, getScoreTier, getTierColor, getTierLabel, getWordCount, isPrivateIp, isSafeUrl, joinUrl, loadConfigFile, logger, normalizeUrl, parseHtml, planAudits, runAudits, runScan };
package/dist/index.d.ts CHANGED
@@ -198,8 +198,89 @@ interface ReadinessVitals {
198
198
  technical: number;
199
199
  }
200
200
 
201
- type ProgressCallback = (pct: number, phase: string) => void | Promise<void>;
202
- declare function runScan(url: string, onProgress?: ProgressCallback, pageOverrides?: PageOverride[] | null, signal?: AbortSignal): Promise<ScanReport>;
201
+ type PhaseId = 'fetch-root' | 'fetch-pages' | 'analyze' | 'audits' | 'report';
202
+ type ScanEvent = {
203
+ type: 'scan:start';
204
+ url: string;
205
+ fraction: number;
206
+ elapsedMs: number;
207
+ } | {
208
+ type: 'phase:start';
209
+ phase: PhaseId;
210
+ totalUnits: number;
211
+ fraction: number;
212
+ elapsedMs: number;
213
+ } | {
214
+ type: 'unit:done';
215
+ phase: PhaseId;
216
+ completed: number;
217
+ total: number;
218
+ label?: string;
219
+ fraction: number;
220
+ elapsedMs: number;
221
+ } | {
222
+ type: 'unit:fail';
223
+ phase: PhaseId;
224
+ label: string;
225
+ error: string;
226
+ fraction: number;
227
+ elapsedMs: number;
228
+ } | {
229
+ type: 'phase:done';
230
+ phase: PhaseId;
231
+ durationMs: number;
232
+ fraction: number;
233
+ elapsedMs: number;
234
+ } | {
235
+ type: 'scan:done';
236
+ durationMs: number;
237
+ score: number;
238
+ fraction: number;
239
+ elapsedMs: number;
240
+ };
241
+ /**
242
+ * How much of the overall scan fraction each phase owns. Sums to 1 so the
243
+ * fraction of a finished scan is exactly 1.
244
+ */
245
+ declare const PHASE_WEIGHTS: Record<PhaseId, number>;
246
+ /**
247
+ * Turns scan milestones into {@link ScanEvent}s. Owns the fraction math
248
+ * (`Σ weights of done phases + current phase weight × completed/total`),
249
+ * clamps it to be monotonic non-decreasing, and stamps `fraction`/`elapsedMs`
250
+ * onto every event. Emission is synchronous — throttling is a consumer concern.
251
+ */
252
+ declare class ProgressTracker {
253
+ private readonly onEvent;
254
+ private readonly startMs;
255
+ private doneWeight;
256
+ private phase;
257
+ private phaseStartMs;
258
+ private totalUnits;
259
+ private completedUnits;
260
+ private lastFraction;
261
+ constructor(onEvent: (event: ScanEvent) => void);
262
+ /** Fraction of the whole scan that is complete, in [0, 1]. Never decreases. */
263
+ get fraction(): number;
264
+ /** Stamp an event with the current fraction/elapsed and advance the floor. */
265
+ private stamp;
266
+ private elapsedMs;
267
+ scanStart(url: string): void;
268
+ phaseStart(phase: PhaseId, totalUnits: number): void;
269
+ /** Correct the current phase's unit total (e.g. discovery finds pages mid-phase). */
270
+ setPhaseTotal(totalUnits: number): void;
271
+ unitDone(label?: string): void;
272
+ /** A failed unit still counts as settled work so the phase can complete. */
273
+ unitFail(label: string, error: string): void;
274
+ phaseDone(): void;
275
+ scanDone(score: number): void;
276
+ }
277
+
278
+ interface ScanOptions {
279
+ onEvent?: (event: ScanEvent) => void;
280
+ pages?: PageOverride[] | null;
281
+ signal?: AbortSignal;
282
+ }
283
+ declare function runScan(url: string, options?: ScanOptions): Promise<ScanReport>;
203
284
 
204
285
  interface A11yNodeFinding {
205
286
  target: string;
@@ -310,12 +391,37 @@ interface AuditRunResult {
310
391
  categories: CategoryResult[];
311
392
  overallScore: number;
312
393
  }
313
- type ProgressFn = (completed: number, total: number) => void;
394
+ /**
395
+ * Progress emitted per settled audit. The orchestrator forwards these into the
396
+ * ProgressTracker, which owns counting and fraction/elapsed stamping.
397
+ */
398
+ type AuditProgressEvent = {
399
+ type: 'unit:done';
400
+ label: string;
401
+ } | {
402
+ type: 'unit:fail';
403
+ label: string;
404
+ error: string;
405
+ };
406
+ interface AuditPlan {
407
+ runnable: Array<{
408
+ reg: AuditRegistration;
409
+ categoryId: string;
410
+ }>;
411
+ skipped: CheckResult[];
412
+ }
413
+ /**
414
+ * Split a scan config into the audits that will actually execute and the
415
+ * page-type-skipped `na` stubs. Exported so the orchestrator can size the
416
+ * audits progress phase before running it.
417
+ */
418
+ declare function planAudits(ctx: CheckContext, config: ScanConfig): AuditPlan;
314
419
  /**
315
420
  * Execute all audits registered in the config against the scan context.
316
421
  * Returns check results grouped into categories with weighted scores.
422
+ * Pass a precomputed `plan` (from {@link planAudits}) to avoid recomputing it.
317
423
  */
318
- declare function runAudits(ctx: CheckContext, config: ScanConfig, onProgress?: ProgressFn): Promise<AuditRunResult>;
424
+ declare function runAudits(ctx: CheckContext, config: ScanConfig, onEvent?: (event: AuditProgressEvent) => void, plan?: AuditPlan): Promise<AuditRunResult>;
319
425
 
320
426
  declare function parseHtml(html: string): CheerioAPI;
321
427
  declare function extractJsonLd($: CheerioAPI): object[];
@@ -711,4 +817,4 @@ declare class Logger {
711
817
  }
712
818
  declare const logger: Logger;
713
819
 
714
- export { type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, CATEGORY_NAMES, CATEGORY_WEIGHTS, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, DEFAULT_SCAN_LIMIT, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PRESETS, type PageContext, type PageOverride, type PageType, type PresetName, type PresetOptions, type ProductFieldVerification, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanReport, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, TAG_SCAN_ERROR, TAG_SKIPPED_PAGE_TYPE, type WafProtection, buildCategoryResult, calculateCategoryScore, calculateOverallScore, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, flattenJsonLd, getMainContentText, getPreset, getScoreTier, getTierColor, getTierLabel, getWordCount, isPrivateIp, isSafeUrl, joinUrl, loadConfigFile, logger, normalizeUrl, parseHtml, runAudits, runScan };
820
+ export { type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditPlan, type AuditProgressEvent, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, CATEGORY_NAMES, CATEGORY_WEIGHTS, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, DEFAULT_SCAN_LIMIT, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PHASE_WEIGHTS, PRESETS, type PageContext, type PageOverride, type PageType, type PhaseId, type PresetName, type PresetOptions, type ProductFieldVerification, ProgressTracker, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanEvent, type ScanOptions, type ScanReport, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, TAG_SCAN_ERROR, TAG_SKIPPED_PAGE_TYPE, type WafProtection, buildCategoryResult, calculateCategoryScore, calculateOverallScore, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, flattenJsonLd, getMainContentText, getPreset, getScoreTier, getTierColor, getTierLabel, getWordCount, isPrivateIp, isSafeUrl, joinUrl, loadConfigFile, logger, normalizeUrl, parseHtml, planAudits, runAudits, runScan };
package/dist/index.js CHANGED
@@ -45,7 +45,9 @@ __export(index_exports, {
45
45
  MAX_PAGES_PER_SCAN: () => MAX_PAGES_PER_SCAN,
46
46
  MAX_RESPONSE_BODY_BYTES: () => MAX_RESPONSE_BODY_BYTES,
47
47
  PAGE_TYPE_LABELS: () => PAGE_TYPE_LABELS,
48
+ PHASE_WEIGHTS: () => PHASE_WEIGHTS,
48
49
  PRESETS: () => PRESETS,
50
+ ProgressTracker: () => ProgressTracker,
49
51
  READINESS_WEIGHTS: () => READINESS_WEIGHTS,
50
52
  REQUEST_TIMEOUT_MS: () => REQUEST_TIMEOUT_MS,
51
53
  SCANNER_USER_AGENT: () => SCANNER_USER_AGENT,
@@ -89,6 +91,7 @@ __export(index_exports, {
89
91
  logger: () => logger,
90
92
  normalizeUrl: () => normalizeUrl,
91
93
  parseHtml: () => parseHtml,
94
+ planAudits: () => planAudits,
92
95
  runAudits: () => runAudits,
93
96
  runScan: () => runScan
94
97
  });
@@ -17492,17 +17495,17 @@ function stubCheck(meta, tag, explanation) {
17492
17495
  tags: [tag]
17493
17496
  };
17494
17497
  }
17495
- async function runAudits(ctx, config, onProgress) {
17498
+ function planAudits(ctx, config) {
17496
17499
  const scannedPageTypes = new Set(ctx.pages.map((p) => p.pageType));
17497
- const all = [];
17498
- const allChecks = [];
17500
+ const runnable = [];
17501
+ const skipped = [];
17499
17502
  for (const cat of config.categories) {
17500
17503
  const regs = config.audits[cat.id] ?? [];
17501
17504
  for (const reg2 of regs) {
17502
17505
  const applicable = reg2.meta.applicablePageTypes;
17503
17506
  if (applicable && applicable.length > 0) {
17504
17507
  if (!applicable.some((pt) => scannedPageTypes.has(pt))) {
17505
- allChecks.push(
17508
+ skipped.push(
17506
17509
  stubCheck(
17507
17510
  reg2.meta,
17508
17511
  TAG_SKIPPED_PAGE_TYPE,
@@ -17512,30 +17515,35 @@ async function runAudits(ctx, config, onProgress) {
17512
17515
  continue;
17513
17516
  }
17514
17517
  }
17515
- all.push({ reg: reg2, categoryId: cat.id });
17518
+ runnable.push({ reg: reg2, categoryId: cat.id });
17516
17519
  }
17517
17520
  }
17518
- const totalAudits = all.length;
17519
- let completed = 0;
17521
+ return { runnable, skipped };
17522
+ }
17523
+ async function runAudits(ctx, config, onEvent, plan) {
17524
+ const { runnable, skipped } = plan ?? planAudits(ctx, config);
17525
+ const allChecks = [...skipped];
17520
17526
  const batchSize = 20;
17521
- for (let i = 0; i < totalAudits; i += batchSize) {
17522
- const batch = all.slice(i, i + batchSize);
17527
+ for (let i = 0; i < runnable.length; i += batchSize) {
17528
+ const batch = runnable.slice(i, i + batchSize);
17523
17529
  const batchResults = await Promise.all(
17524
17530
  batch.map(async ({ reg: reg2 }) => {
17531
+ const label2 = `${reg2.meta.id} ${reg2.meta.title}`;
17525
17532
  try {
17526
17533
  const instance = reg2.create();
17527
17534
  const result = await instance.audit(ctx);
17528
- return instance.toCheckResult(result);
17535
+ const check = instance.toCheckResult(result);
17536
+ onEvent?.({ type: "unit:done", label: label2 });
17537
+ return check;
17529
17538
  } catch (err) {
17530
17539
  logger.error({ err, auditId: reg2.meta.id }, "[scanner] Audit error");
17531
17540
  const message = err instanceof Error ? err.message : String(err);
17541
+ onEvent?.({ type: "unit:fail", label: label2, error: message });
17532
17542
  return stubCheck(reg2.meta, TAG_SCAN_ERROR, `Audit failed to run: ${message}`);
17533
17543
  }
17534
17544
  })
17535
17545
  );
17536
17546
  allChecks.push(...batchResults);
17537
- completed += batchResults.length;
17538
- onProgress?.(completed, totalAudits);
17539
17547
  }
17540
17548
  const categories = config.categories.map((cat) => {
17541
17549
  const catChecks = allChecks.filter((c) => c.category === cat.id);
@@ -17583,6 +17591,110 @@ function buildWeightedCategoryResult(cat, checks2, registrations) {
17583
17591
  };
17584
17592
  }
17585
17593
 
17594
+ // src/progress.ts
17595
+ var PHASE_WEIGHTS = {
17596
+ "fetch-root": 0.35,
17597
+ "fetch-pages": 0.2,
17598
+ analyze: 0.1,
17599
+ audits: 0.3,
17600
+ report: 0.05
17601
+ };
17602
+ var ProgressTracker = class {
17603
+ onEvent;
17604
+ startMs;
17605
+ doneWeight = 0;
17606
+ phase = null;
17607
+ phaseStartMs = 0;
17608
+ totalUnits = 0;
17609
+ completedUnits = 0;
17610
+ lastFraction = 0;
17611
+ constructor(onEvent) {
17612
+ this.onEvent = onEvent;
17613
+ this.startMs = performance.now();
17614
+ }
17615
+ /** Fraction of the whole scan that is complete, in [0, 1]. Never decreases. */
17616
+ get fraction() {
17617
+ let f = this.doneWeight;
17618
+ if (this.phase !== null) {
17619
+ const ratio = this.totalUnits > 0 ? Math.min(1, this.completedUnits / this.totalUnits) : 0;
17620
+ f += PHASE_WEIGHTS[this.phase] * ratio;
17621
+ }
17622
+ return Math.min(1, Math.max(f, this.lastFraction));
17623
+ }
17624
+ /** Stamp an event with the current fraction/elapsed and advance the floor. */
17625
+ stamp() {
17626
+ const fraction = this.fraction;
17627
+ this.lastFraction = Math.max(this.lastFraction, fraction);
17628
+ return { fraction, elapsedMs: this.elapsedMs() };
17629
+ }
17630
+ elapsedMs() {
17631
+ return Math.max(0, Math.round(performance.now() - this.startMs));
17632
+ }
17633
+ scanStart(url) {
17634
+ this.onEvent({ type: "scan:start", url, ...this.stamp() });
17635
+ }
17636
+ phaseStart(phase, totalUnits) {
17637
+ this.phase = phase;
17638
+ this.phaseStartMs = performance.now();
17639
+ this.totalUnits = Math.max(0, totalUnits);
17640
+ this.completedUnits = 0;
17641
+ this.onEvent({
17642
+ type: "phase:start",
17643
+ phase,
17644
+ totalUnits: this.totalUnits,
17645
+ ...this.stamp()
17646
+ });
17647
+ }
17648
+ /** Correct the current phase's unit total (e.g. discovery finds pages mid-phase). */
17649
+ setPhaseTotal(totalUnits) {
17650
+ this.totalUnits = Math.max(this.completedUnits, totalUnits);
17651
+ }
17652
+ unitDone(label2) {
17653
+ const phase = this.phase;
17654
+ if (phase === null) return;
17655
+ this.completedUnits += 1;
17656
+ this.onEvent({
17657
+ type: "unit:done",
17658
+ phase,
17659
+ completed: this.completedUnits,
17660
+ total: this.totalUnits,
17661
+ label: label2,
17662
+ ...this.stamp()
17663
+ });
17664
+ }
17665
+ /** A failed unit still counts as settled work so the phase can complete. */
17666
+ unitFail(label2, error) {
17667
+ const phase = this.phase;
17668
+ if (phase === null) return;
17669
+ this.completedUnits += 1;
17670
+ this.onEvent({
17671
+ type: "unit:fail",
17672
+ phase,
17673
+ label: label2,
17674
+ error,
17675
+ ...this.stamp()
17676
+ });
17677
+ }
17678
+ phaseDone() {
17679
+ const phase = this.phase;
17680
+ if (phase === null) return;
17681
+ this.phase = null;
17682
+ this.doneWeight += PHASE_WEIGHTS[phase];
17683
+ const durationMs = Math.max(0, Math.round(performance.now() - this.phaseStartMs));
17684
+ this.totalUnits = 0;
17685
+ this.completedUnits = 0;
17686
+ this.onEvent({
17687
+ type: "phase:done",
17688
+ phase,
17689
+ durationMs,
17690
+ ...this.stamp()
17691
+ });
17692
+ }
17693
+ scanDone(score) {
17694
+ this.onEvent({ type: "scan:done", durationMs: this.elapsedMs(), score, ...this.stamp() });
17695
+ }
17696
+ };
17697
+
17586
17698
  // src/audits/accessibility/runner.ts
17587
17699
  var import_jsdom = require("jsdom");
17588
17700
 
@@ -23094,9 +23206,11 @@ function discoverPages(homepageUrl, domain, rootFiles, homepage$, exclude, maxAd
23094
23206
  }
23095
23207
  return selected;
23096
23208
  }
23097
- async function runScan(url, onProgress, pageOverrides, signal) {
23098
- const progress = onProgress ?? (() => {
23099
- });
23209
+ async function runScan(url, options) {
23210
+ const onEvent = options?.onEvent;
23211
+ const pageOverrides = options?.pages;
23212
+ const signal = options?.signal;
23213
+ const tracker = new ProgressTracker((event) => onEvent?.(event));
23100
23214
  const start = performance.now();
23101
23215
  const fetcher = createFetcher();
23102
23216
  const baseUrl = new URL(url).origin;
@@ -23119,7 +23233,7 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23119
23233
  }
23120
23234
  logger.debug({ url, domain }, "[orchestrator] Starting runScan");
23121
23235
  signal?.throwIfAborted();
23122
- await progress(5, "Fetching root files");
23236
+ tracker.scanStart(displayUrl);
23123
23237
  const rootFilePaths = [
23124
23238
  "/robots.txt",
23125
23239
  "/llms.txt",
@@ -23157,19 +23271,26 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23157
23271
  "/our-story"
23158
23272
  ];
23159
23273
  logger.debug({ count: rootFilePaths.length }, "[orchestrator] Phase 1: Fetching root files");
23274
+ tracker.phaseStart("fetch-root", rootFilePaths.length);
23160
23275
  const rootResults = await Promise.all(
23161
- rootFilePaths.map((path) => fetcher.fetch({ url: `${baseUrl}${path}`, signal }))
23276
+ rootFilePaths.map(
23277
+ (path) => fetcher.fetch({ url: `${baseUrl}${path}`, signal }).then((result) => {
23278
+ tracker.unitDone(path);
23279
+ return result;
23280
+ })
23281
+ )
23162
23282
  );
23163
23283
  const rootFiles = {};
23164
23284
  rootFilePaths.forEach((path, i) => {
23165
23285
  rootFiles[path] = rootResults[i];
23166
23286
  });
23167
- await progress(25, "Root files fetched");
23287
+ tracker.phaseDone();
23168
23288
  logger.debug("[orchestrator] Phase 1 complete: Root files fetched");
23169
23289
  signal?.throwIfAborted();
23170
- await progress(30, "Fetching pages");
23171
23290
  logger.debug("[orchestrator] Phase 2: Fetching pages");
23291
+ tracker.phaseStart("fetch-pages", 1);
23172
23292
  const homepageResult = await fetcher.fetch({ url, signal });
23293
+ tracker.unitDone(displayUrl);
23173
23294
  const homepage$ = homepageResult.status === 200 && homepageResult.body ? parseHtml(homepageResult.body) : null;
23174
23295
  const discoverLimit = Math.max(0, MAX_PAGES_PER_SCAN - 1 - overrideUrls.length);
23175
23296
  const discoveredUrls = homepage$ ? discoverPages(url, domain, rootFiles, homepage$, new Set(overrideTypeByKey.keys()), discoverLimit) : [];
@@ -23178,12 +23299,22 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23178
23299
  "[orchestrator] Page set: overrides + discovered URLs"
23179
23300
  );
23180
23301
  const extraUrls = [...overrideUrls, ...discoveredUrls];
23181
- await progress(40, `Analyzing ${1 + extraUrls.length} pages`);
23302
+ tracker.setPhaseTotal(1 + extraUrls.length);
23182
23303
  const extraResults = await Promise.all(
23183
- extraUrls.map((pageUrl) => fetcher.fetch({ url: pageUrl, signal }))
23304
+ extraUrls.map(
23305
+ (pageUrl) => fetcher.fetch({ url: pageUrl, signal }).then((result) => {
23306
+ tracker.unitDone(pageUrl);
23307
+ return result;
23308
+ })
23309
+ )
23184
23310
  );
23311
+ tracker.phaseDone();
23185
23312
  const allPageResults = [homepageResult, ...extraResults];
23186
23313
  const allPageUrls = [displayUrl, ...extraUrls];
23314
+ tracker.phaseStart(
23315
+ "analyze",
23316
+ allPageResults.filter((r) => r.status === 200 && r.body).length
23317
+ );
23187
23318
  const pages = allPageResults.map((r, i) => ({ result: r, url: allPageUrls[i], index: i })).filter((p) => p.result.status === 200 && p.result.body).map((p) => {
23188
23319
  const $ = parseHtml(p.result.body);
23189
23320
  const jsonLd = extractJsonLd($);
@@ -23191,6 +23322,7 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23191
23322
  const meta = extractMetaTags($);
23192
23323
  const isFirstPage = p.index === 0;
23193
23324
  const forcedType = overrideTypeByKey.get(p.url.replace(/\/$/, ""));
23325
+ tracker.unitDone(p.url);
23194
23326
  return {
23195
23327
  url: p.url,
23196
23328
  pageType: forcedType ?? detectPageType(p.url, $, structuredData, meta, isFirstPage),
@@ -23208,13 +23340,12 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23208
23340
  p.a11yResults = await runA11yForHtml(p.fetchResult.body, p.url, A11Y_RULES);
23209
23341
  })
23210
23342
  );
23211
- await progress(55, "Page analysis complete");
23343
+ tracker.phaseDone();
23212
23344
  logger.debug(
23213
23345
  { pagesAnalyzed: pages.length },
23214
23346
  "[orchestrator] Phase 2 complete: Page analysis complete"
23215
23347
  );
23216
23348
  signal?.throwIfAborted();
23217
- await progress(60, "Running audits");
23218
23349
  logger.debug("[orchestrator] Phase 3: Running audits");
23219
23350
  const wafProtection = detectWafProtection(url, homepageResult, rootFiles, pages.length);
23220
23351
  const ctx = {
@@ -23222,19 +23353,27 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23222
23353
  pages,
23223
23354
  domain,
23224
23355
  baseUrl,
23225
- fetch: (options) => fetcher.fetch({ ...options, signal }),
23356
+ fetch: (options2) => fetcher.fetch({ ...options2, signal }),
23226
23357
  wafProtection: wafProtection ?? void 0
23227
23358
  };
23359
+ const auditPlan = planAudits(ctx, defaultConfig);
23360
+ tracker.phaseStart("audits", auditPlan.runnable.length);
23228
23361
  const {
23229
23362
  checks: allChecks,
23230
23363
  categories,
23231
23364
  overallScore
23232
- } = await runAudits(ctx, defaultConfig, (completed, total) => {
23233
- const checkProgress = 60 + Math.round(completed / total * 30);
23234
- void progress(checkProgress, `Running audits (${completed}/${total})`);
23235
- });
23365
+ } = await runAudits(
23366
+ ctx,
23367
+ defaultConfig,
23368
+ (event) => {
23369
+ if (event.type === "unit:done") tracker.unitDone(event.label);
23370
+ else tracker.unitFail(event.label, event.error);
23371
+ },
23372
+ auditPlan
23373
+ );
23374
+ tracker.phaseDone();
23236
23375
  logger.debug("[orchestrator] Phase 3 complete: Audits finished");
23237
- await progress(97, "Building report");
23376
+ tracker.phaseStart("report", 1);
23238
23377
  logger.debug("[orchestrator] Phase 4: Building final report");
23239
23378
  const durationMs = Math.round(performance.now() - start);
23240
23379
  const recommendations = allChecks.filter((c) => c.status !== "pass").slice().sort((a, b) => {
@@ -23251,7 +23390,6 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23251
23390
  const topPasses = allChecks.filter((c) => c.status === "pass").slice().sort(
23252
23391
  (a, b) => (weightMap.get(b.id) ?? 1) - (weightMap.get(a.id) ?? 1)
23253
23392
  ).slice(0, 10);
23254
- await progress(100, "Complete");
23255
23393
  const readinessVitals = calculateReadinessVitals(allChecks);
23256
23394
  const readinessScore = Math.round(
23257
23395
  readinessVitals.commerce * READINESS_WEIGHTS.commerce + readinessVitals.content * READINESS_WEIGHTS.content + readinessVitals.botAccessibility * READINESS_WEIGHTS.botAccessibility + readinessVitals.technical * READINESS_WEIGHTS.technical
@@ -23281,6 +23419,9 @@ async function runScan(url, onProgress, pageOverrides, signal) {
23281
23419
  productFields: [...overrideTypeByKey.values()].includes("product") ? extractProductFieldVerification(pages) : void 0
23282
23420
  };
23283
23421
  report.summary = generateScanSummary(report);
23422
+ tracker.unitDone();
23423
+ tracker.phaseDone();
23424
+ tracker.scanDone(overallScore);
23284
23425
  logger.debug({ durationMs, score: overallScore }, "[orchestrator] runScan complete");
23285
23426
  return report;
23286
23427
  }
@@ -23466,7 +23607,9 @@ function loadConfigFile(customPath) {
23466
23607
  MAX_PAGES_PER_SCAN,
23467
23608
  MAX_RESPONSE_BODY_BYTES,
23468
23609
  PAGE_TYPE_LABELS,
23610
+ PHASE_WEIGHTS,
23469
23611
  PRESETS,
23612
+ ProgressTracker,
23470
23613
  READINESS_WEIGHTS,
23471
23614
  REQUEST_TIMEOUT_MS,
23472
23615
  SCANNER_USER_AGENT,
@@ -23510,6 +23653,7 @@ function loadConfigFile(customPath) {
23510
23653
  logger,
23511
23654
  normalizeUrl,
23512
23655
  parseHtml,
23656
+ planAudits,
23513
23657
  runAudits,
23514
23658
  runScan
23515
23659
  });