explainmyrepo 0.1.2 → 0.1.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.
@@ -39,6 +39,9 @@ OPTIONS
39
39
  --no-quality skip the local vision quality gate (faster dry iterations)
40
40
  --no-refine grade once but don't auto-iterate the copy to lift weak axes
41
41
  --max-refine <n> max content-refine passes when below the quality bar (default 2)
42
+ --ship-best-effort always deploy the best version (with an honest scorecard) instead of
43
+ holding when the world-class bar isn't reached — guarantees a URL.
44
+ Only truly broken pages (missing/broken diagrams) still hold.
42
45
  --register-kb OPT-IN: if the repo isn't a kb.config target, inject a generated
43
46
  entry into kb/kb.config.mjs (the one step that edits the shared registry)
44
47
  --from <station> resume: start at this station id (needs an existing --out build)
@@ -65,7 +68,7 @@ EXAMPLES
65
68
  npx explainmyrepo owner/cool-lib --from concept --out ./explainer-builds/cool-lib
66
69
  `;
67
70
 
68
- const BOOL_FLAGS = new Set(['--no-deploy', '--no-publish', '--no-notify', '--no-quality', '--no-refine', '--register-kb', '--dry-run', '-h', '--help', '-v', '--version']);
71
+ const BOOL_FLAGS = new Set(['--no-deploy', '--no-publish', '--no-notify', '--no-quality', '--no-refine', '--ship-best-effort', '--register-kb', '--dry-run', '-h', '--help', '-v', '--version']);
69
72
  const VALUE_FLAGS = new Set(['--out', '--model', '--from', '--to', '--only', '--max-refine']);
70
73
 
71
74
  function parseArgs(argv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explainmyrepo",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Turn any GitHub repo into a bespoke, art-directed explainer site — for humans and their AI — in one command.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -395,19 +395,28 @@ export async function run(repoUrl, opts = {}) {
395
395
  }
396
396
 
397
397
  if (!(quality && quality.passed)) {
398
- // NEVER ship a below-bar page silently — the whole point. Stop before deploy/publish, report the gap.
398
+ // Below the world-class bar. DEFAULT: holdnever ship slop (the whole point). With
399
+ // --ship-best-effort: deliver the best version anyway so a stranger ALWAYS gets a URL, with the
400
+ // honest scorecard travelling alongside — UNLESS it's genuinely broken (no device rendered the
401
+ // mandatory diagrams), which always holds.
399
402
  reportQualityGap(quality);
400
- log(`\n${C.yellow}${C.bold}Held at the quality gate did NOT deploy or publish a below-bar page.${C.reset}`);
401
- log(`${C.dim}Best local build: ${outDir}/site . Lift the gaps above (or re-run with a higher --max-refine), then ship the remaining steps with: ${C.cyan}--from ${post[0] ? post[0].id : 'deploy'} --out ${outDir}${C.reset}`);
402
- return { ok: false, gated: true, quality, outDir, results: preR.results };
403
- }
404
-
405
- const meanPair = quality.scorecard.map((c) => `${String(c.device).replace(/\(.*/, '')} ${c.meanScore}`).join(' / ');
406
- if (quality.exemplary) {
407
- log(`\n${C.green}${C.bold}Quality gate PASSED — world-class (mean ${meanPair}).${C.reset} Shipping.`);
403
+ const diagramsOk = !!(quality && Array.isArray(quality.scorecard) && quality.scorecard.some((c) => c.inv18?.passed));
404
+ if (!(opts.shipBestEffort && diagramsOk)) {
405
+ log(`\n${C.yellow}${C.bold}Held at the quality gate did NOT deploy or publish a below-bar page.${C.reset}`);
406
+ log(`${C.dim}Best local build: ${outDir}/site . ${opts.shipBestEffort ? 'Held because the mandatory diagrams did not render — the one thing we will not ship broken. ' : ''}Lift the gaps above (or re-run with a higher --max-refine), then ship with: ${C.cyan}--from ${post[0] ? post[0].id : 'deploy'} --out ${outDir}${C.reset}`);
407
+ return { ok: false, gated: true, quality, outDir, results: preR.results };
408
+ }
409
+ const meanBE = quality.scorecard.map((c) => `${String(c.device).replace(/\(.*/, '')} ${c.meanScore}`).join(' / ');
410
+ log(`\n${C.yellow}${C.bold}Below the world-class bar (mean ${meanBE}) — shipping the best version (--ship-best-effort).${C.reset}`);
411
+ log(`${C.dim}Structurally sound (mandatory diagrams present). The per-axis gaps above are recorded in the scorecard and delivered honestly, not hidden. A stranger always gets a real page.${C.reset}`);
408
412
  } else {
409
- log(`\n${C.green}${C.bold}Quality gate PASSED ship-worthy (mean ${meanPair}).${C.reset} Shipping.`);
410
- log(`${C.dim}Genuinely good + no slop + real legible diagrams (INV-18). The world-class target (mean ≥ 90 / worst axis ≥ 85 / all 5 operators) is not fully reached — the per-axis gap is recorded in build.json refineNotes and travels with the scorecard.${C.reset}`);
413
+ const meanPair = quality.scorecard.map((c) => `${String(c.device).replace(/\(.*/, '')} ${c.meanScore}`).join(' / ');
414
+ if (quality.exemplary) {
415
+ log(`\n${C.green}${C.bold}Quality gate PASSED — world-class (mean ${meanPair}).${C.reset} Shipping.`);
416
+ } else {
417
+ log(`\n${C.green}${C.bold}Quality gate PASSED — ship-worthy (mean ${meanPair}).${C.reset} Shipping.`);
418
+ log(`${C.dim}Genuinely good + no slop + real legible diagrams (INV-18). The world-class target (mean ≥ 90 / worst axis ≥ 85 / all 5 operators) is not fully reached — the per-axis gap is recorded in build.json refineNotes and travels with the scorecard.${C.reset}`);
419
+ }
411
420
  }
412
421
  const postR = post.length ? await runStations(post, baseArgs, total, qIdx + 1) : { ok: true, results: [] };
413
422
  finalSummary(outDir);
@@ -348,6 +348,27 @@ async function scrollToTop(page, loc, offset) {
348
348
  // capture the grading crops — viewport-segment section crops + dedicated element
349
349
  // crops of the two mandatory diagrams. Returns { domInv18, fullPagePath, crops[], pageHeight }.
350
350
  // ----------------------------------------------------------------------------
351
+ // Ensure the Chromium browser binary exists. The `playwright` npm package installs fine, but the
352
+ // actual ~150MB browser is downloaded separately — a fresh machine (or `npx explainmyrepo`) won't
353
+ // have it. Install it once, automatically, so the quality gate "just works" for a stranger.
354
+ async function ensureChromium(chromium) {
355
+ let exePath = null;
356
+ try { exePath = chromium.executablePath(); } catch { /* path unknown */ }
357
+ if (exePath && fs.existsSync(exePath)) return; // already installed (respects PLAYWRIGHT_BROWSERS_PATH)
358
+ console.error('[quality-grade] Chromium browser not found — installing it once (~150MB, one-time)…');
359
+ const { execFileSync } = await import('node:child_process');
360
+ const { createRequire } = await import('node:module');
361
+ const require2 = createRequire(import.meta.url);
362
+ // playwright's `exports` blocks require.resolve('playwright/cli.js'), so find the package root
363
+ // (package.json is exported) and reach cli.js — the file that backs the `playwright` bin — directly.
364
+ let pwRoot;
365
+ try { pwRoot = path.dirname(require2.resolve('playwright/package.json')); }
366
+ catch { pwRoot = path.dirname(require2.resolve('playwright')); }
367
+ const pwCli = path.join(pwRoot, 'cli.js');
368
+ execFileSync(process.execPath, [pwCli, 'install', 'chromium'], { stdio: 'inherit', env: process.env });
369
+ console.error('[quality-grade] Chromium installed.');
370
+ }
371
+
351
372
  async function renderDevice(chromium, url, device, assetsDir) {
352
373
  const browser = await chromium.launch({ headless: true });
353
374
  try {
@@ -660,6 +681,9 @@ async function main() {
660
681
  let chromium;
661
682
  try { ({ chromium } = await import('playwright')); }
662
683
  catch (e) { return emit(false, {}, `playwright is not installed (npm i -D playwright && npx playwright install chromium): ${e?.message || e}`); }
684
+ // Fresh machines have the package but not the browser binary — install it once, automatically.
685
+ try { await ensureChromium(chromium); }
686
+ catch (e) { return emit(false, {}, `could not auto-install the Chromium browser (try 'npx playwright install chromium' manually): ${e?.message || e}`); }
663
687
 
664
688
  const assetsDir = path.join(buildDir, 'assets');
665
689
  fs.mkdirSync(assetsDir, { recursive: true });