@goflag/cli 0.2.1 → 0.2.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.
@@ -1925,10 +1925,10 @@ function dedupeBy(items, key) {
1925
1925
  return out;
1926
1926
  }
1927
1927
  function splitRoute(pathname) {
1928
- const segments = pathname.split("/").filter(Boolean);
1929
- if (segments.length > 0 && looksLikeLocaleSegment(segments[0])) {
1930
- const locale = localeIdentity(segments[0]);
1931
- const rest = segments.slice(1).join("/");
1928
+ const segments2 = pathname.split("/").filter(Boolean);
1929
+ if (segments2.length > 0 && looksLikeLocaleSegment(segments2[0])) {
1930
+ const locale = localeIdentity(segments2[0]);
1931
+ const rest = segments2.slice(1).join("/");
1932
1932
  return { route: rest ? `/${rest}` : "/", locale };
1933
1933
  }
1934
1934
  return { route: pathname || "/", locale: "x-default" };
@@ -2289,7 +2289,12 @@ function parseSitemap(body) {
2289
2289
 
2290
2290
  // src/lib/core/sitemap/discover.ts
2291
2291
  var DEFAULTS = {
2292
- timeoutMs: 8e3,
2292
+ // A sitemap is one document and it can be large: this repository's own
2293
+ // openfinanceguide serves 3.5 MB across 4008 URLs, and that took 1.2 s on a
2294
+ // good connection. Eight seconds was tight enough that the fetch failed
2295
+ // intermittently, and every failure silently dropped the crawl to link
2296
+ // following. Twenty is generous for a document fetched once per run.
2297
+ timeoutMs: 2e4,
2293
2298
  maxUrls: 5e3,
2294
2299
  maxSitemaps: 50,
2295
2300
  crawlDepth: 1,
@@ -2332,6 +2337,10 @@ async function discoverSitemap(baseUrl, options = {}) {
2332
2337
  const doc = await fetchDoc(candidate.url, options);
2333
2338
  if (!doc.body) {
2334
2339
  if (diagnostics.status === 0) diagnostics.status = doc.status;
2340
+ if (doc.error) {
2341
+ diagnostics.unreachable ??= `${candidate.url}: ${doc.error}`;
2342
+ diagnostics.warnings.push(`Sitemap unreachable at ${candidate.url} \u2014 ${doc.error}`);
2343
+ }
2335
2344
  continue;
2336
2345
  }
2337
2346
  const parsed = parseSitemap(doc.body);
@@ -2472,8 +2481,9 @@ async function fetchDoc(url, options) {
2472
2481
  }
2473
2482
  }
2474
2483
  return { status: res.status, body: await res.text() };
2475
- } catch {
2476
- return { status: 0 };
2484
+ } catch (err) {
2485
+ const reason = err instanceof Error ? err.message : String(err);
2486
+ return { status: 0, error: reason || "network error" };
2477
2487
  } finally {
2478
2488
  cleanup();
2479
2489
  restoreTls();
@@ -2489,6 +2499,90 @@ function relaxTlsIfRequested2(enabled) {
2489
2499
  };
2490
2500
  }
2491
2501
 
2502
+ // src/lib/core/coverage.ts
2503
+ var DEFAULT_PER_FAMILY = 3;
2504
+ var DEFAULT_THRESHOLD = 8;
2505
+ function segments(url) {
2506
+ try {
2507
+ return new URL(url).pathname.split("/").filter(Boolean);
2508
+ } catch {
2509
+ return [];
2510
+ }
2511
+ }
2512
+ function shapeOf(parts, locales) {
2513
+ return parts.map((part) => locales.has(part.toLowerCase()) ? "{locale}" : part);
2514
+ }
2515
+ function patternOf(parts, varDepths) {
2516
+ const out = [];
2517
+ for (let i = 0; i < parts.length; i += 1) {
2518
+ const prefix = parts.slice(0, i).join("/");
2519
+ out.push(varDepths.has(`${i}:${prefix}`) ? `{${i}}` : parts[i]);
2520
+ }
2521
+ return `/${out.join("/")}`;
2522
+ }
2523
+ function representatives(sorted2, k) {
2524
+ if (sorted2.length <= k) return [...sorted2];
2525
+ if (k <= 1) return [sorted2[0]];
2526
+ const picked = [];
2527
+ for (let i = 0; i < k; i += 1) {
2528
+ picked.push(sorted2[Math.round(i * (sorted2.length - 1) / (k - 1))]);
2529
+ }
2530
+ return [...new Set(picked)];
2531
+ }
2532
+ function selectByStructure(urls, options = {}) {
2533
+ const perFamily = options.perFamily ?? DEFAULT_PER_FAMILY;
2534
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
2535
+ const locales = new Set((options.locales ?? []).map((l) => l.toLowerCase()));
2536
+ const parsed = urls.map((url) => ({ url, parts: shapeOf(segments(url), locales) }));
2537
+ const childrenByPrefix = /* @__PURE__ */ new Map();
2538
+ for (const { parts } of parsed) {
2539
+ for (let i = 0; i < parts.length; i += 1) {
2540
+ const key = `${i}:${parts.slice(0, i).join("/")}`;
2541
+ const set = childrenByPrefix.get(key) ?? /* @__PURE__ */ new Set();
2542
+ set.add(parts[i]);
2543
+ childrenByPrefix.set(key, set);
2544
+ }
2545
+ }
2546
+ const varDepths = /* @__PURE__ */ new Set();
2547
+ for (const [key, children] of childrenByPrefix) {
2548
+ const colon = key.indexOf(":");
2549
+ const depth = Number(key.slice(0, colon));
2550
+ const prefix = key.slice(colon + 1);
2551
+ const isFirstSection = depth === 0 || depth === 1 && prefix === "{locale}";
2552
+ if (isFirstSection) continue;
2553
+ if (children.size >= threshold) varDepths.add(key);
2554
+ }
2555
+ const groups = /* @__PURE__ */ new Map();
2556
+ for (const { url, parts } of parsed) {
2557
+ const pattern = patternOf(parts, varDepths);
2558
+ const localePart = segments(url).find((p) => locales.has(p.toLowerCase())) ?? "";
2559
+ const key = `${localePart}|${pattern}`;
2560
+ const group = groups.get(key) ?? { pattern, urls: [] };
2561
+ group.urls.push(url);
2562
+ groups.set(key, group);
2563
+ }
2564
+ const keep = /* @__PURE__ */ new Set();
2565
+ const families = /* @__PURE__ */ new Map();
2566
+ for (const { pattern, urls: groupUrls } of groups.values()) {
2567
+ const sorted2 = [...groupUrls].sort();
2568
+ const chosen = sorted2.length < threshold ? sorted2 : representatives(sorted2, perFamily);
2569
+ for (const url of chosen) keep.add(url);
2570
+ if (sorted2.length >= threshold) {
2571
+ const seen = families.get(pattern);
2572
+ families.set(pattern, {
2573
+ pattern,
2574
+ size: (seen?.size ?? 0) + sorted2.length,
2575
+ sampled: (seen?.sampled ?? 0) + chosen.length
2576
+ });
2577
+ }
2578
+ }
2579
+ return {
2580
+ urls: urls.filter((url) => keep.has(url)),
2581
+ families: [...families.values()].sort((a, b) => b.size - a.size),
2582
+ total: urls.length
2583
+ };
2584
+ }
2585
+
2492
2586
  // src/lib/rules/advisory.ts
2493
2587
  function resolveEvidencePath(extraction, path) {
2494
2588
  let current = extraction;
@@ -3402,7 +3496,11 @@ async function runAudit(inputUrl, options = {}) {
3402
3496
  allowInsecureTls: options.allowInsecureTls,
3403
3497
  crawlFallback: false
3404
3498
  }).catch(() => void 0);
3405
- const sitemapUrls = (discovery?.urls ?? []).map((u) => u.loc);
3499
+ const allSitemapUrls = (discovery?.urls ?? []).map((u) => u.loc);
3500
+ const coverageMode = options.coverage ?? (allSitemapUrls.length > 0 ? "structural" : "all");
3501
+ const selection = coverageMode === "structural" && allSitemapUrls.length > 0 ? selectByStructure(allSitemapUrls, { locales: options.locales }) : void 0;
3502
+ const sitemapUrls = selection?.urls ?? allSitemapUrls;
3503
+ const effectiveMaxPages = selection ? Math.max(options.maxPages ?? maxPages, selection.urls.length + 5) : maxPages;
3406
3504
  const robots = await probeRobots(origin, {
3407
3505
  signal: options.signal,
3408
3506
  timeoutMs: options.timeoutMs
@@ -3411,7 +3509,7 @@ async function runAudit(inputUrl, options = {}) {
3411
3509
  entryUrl: entry,
3412
3510
  seedUrls: sitemapUrls,
3413
3511
  depth,
3414
- maxPages,
3512
+ maxPages: effectiveMaxPages,
3415
3513
  include: options.include,
3416
3514
  exclude: options.exclude,
3417
3515
  inspectOptions: {
@@ -3621,14 +3719,26 @@ async function runAudit(inputUrl, options = {}) {
3621
3719
  pagesFailed: linkReport.diagnostics.pagesFailed,
3622
3720
  truncated: crawlResult.truncated || linkReport.truncated,
3623
3721
  warnings,
3722
+ // What the run actually looked at. A sampled audit that does not say so
3723
+ // is a partial audit wearing the face of a complete one, which is worse
3724
+ // than the cap it replaces.
3725
+ coverage: {
3726
+ mode: coverageMode,
3727
+ ...selection ? {
3728
+ considered: selection.total,
3729
+ selected: selection.urls.length,
3730
+ families: selection.families
3731
+ } : {}
3732
+ },
3624
3733
  ...ignoredHoles > 0 ? { ignoredHoles } : {},
3625
3734
  ...duplicatePages > 0 ? { duplicatePages } : {},
3626
3735
  ...discovery ? {
3627
3736
  sitemap: {
3628
3737
  found: discovery.diagnostics.found,
3629
3738
  sitemapUrl: discovery.diagnostics.sitemapUrl,
3630
- urlCount: sitemapUrls.length,
3631
- uncrawled: countUncrawled(sitemapUrls, pages)
3739
+ urlCount: allSitemapUrls.length,
3740
+ uncrawled: countUncrawled(sitemapUrls, pages),
3741
+ ...discovery.diagnostics.unreachable ? { unreachable: discovery.diagnostics.unreachable } : {}
3632
3742
  }
3633
3743
  } : {}
3634
3744
  }
@@ -3699,6 +3809,24 @@ function renderTerminal(report, options = {}) {
3699
3809
  (report.profile === DEFAULT_PROFILE ? "" : `, profile ${report.profile}`)
3700
3810
  )
3701
3811
  );
3812
+ const coverage = report.diagnostics.coverage;
3813
+ if (coverage?.families?.length && coverage.considered && coverage.selected) {
3814
+ const biggest = coverage.families[0];
3815
+ const audited = report.diagnostics.pagesCrawled;
3816
+ const capped = audited < coverage.selected ? `, capped by --max-pages` : "";
3817
+ lines.push(
3818
+ c(
3819
+ ANSI.dim,
3820
+ `COVERAGE ${audited} of ${coverage.considered} pages audited${capped} \xB7 ${coverage.families.length} families sampled, largest ${biggest.sampled}/${biggest.size} ${biggest.pattern}`
3821
+ )
3822
+ );
3823
+ lines.push(
3824
+ c(
3825
+ ANSI.dim,
3826
+ ` Template rules are conclusive. Copy rules \u2014 title.length, description.length \u2014 are sampled.`
3827
+ )
3828
+ );
3829
+ }
3702
3830
  lines.push("");
3703
3831
  lines.push(
3704
3832
  [
@@ -4275,4 +4403,4 @@ export {
4275
4403
  summarize,
4276
4404
  Logger
4277
4405
  };
4278
- //# sourceMappingURL=chunk-6WOWKLXY.js.map
4406
+ //# sourceMappingURL=chunk-F74SRZVM.js.map