explainmyrepo 0.1.1 → 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.1",
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": {
@@ -85,14 +85,10 @@ async function kbRegisterStation({ buildDir, env, model, apiKey, opts }) {
85
85
  fs.writeFileSync(genPath, `// GENERATED kb.config target entry for "${slug}" — paste into kb/kb.config.mjs targets{}.\nexport default ${JSON.stringify({ [slug]: entry }, null, 2)};\n`);
86
86
  log(`${C.dim}wrote generated entry → ${path.relative(process.cwd(), genPath)}${C.reset}`);
87
87
 
88
- if (!opts.registerKb) {
89
- throw new Error(
90
- `kb:register "${slug}" must be a registered kb.config target before build-kb can index it.\n` +
91
- ` A generated entry was written to ${genPath}.\n` +
92
- ` • Re-run with --register-kb to inject it into kb/kb.config.mjs automatically (the one step that edits the shared registry), or paste it in by hand.`,
93
- );
94
- }
95
- // --register-kb (opt-in, CLEARLY the single step that mutates kb/kb.config.mjs) ----------------
88
+ // Auto-register (no flag needed) so ANY repo just works. Inject the AI-authored entry into THIS
89
+ // build's kb.config copy. In real use that copy is ephemeral — a fresh CI checkout on the hosted
90
+ // path, or the throwaway npx install locally so this never mutates a shared/committed registry.
91
+ // (--register-kb is now a no-op alias, kept for backward compatibility.)
96
92
  let src = fs.readFileSync(cfgPath, 'utf8');
97
93
  const anchor = 'export const targets = {';
98
94
  const at = src.indexOf(anchor);
@@ -100,7 +96,7 @@ async function kbRegisterStation({ buildDir, env, model, apiKey, opts }) {
100
96
  const inject = `\n ${JSON.stringify(slug)}: ${JSON.stringify(entry, null, 2).replace(/\n/g, '\n ')},`;
101
97
  src = src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length);
102
98
  fs.writeFileSync(cfgPath, src);
103
- log(`${C.yellow}injected "${slug}" into kb/kb.config.mjs (--register-kb).${C.reset}`);
99
+ log(`${C.yellow}auto-registered "${slug}" into this build's kb.config.${C.reset}`);
104
100
  const m2 = await importCfg();
105
101
  try { m2.getTarget(slug); } catch (e) { throw new Error(`kb:register — injection did not take: ${e.message}`); }
106
102
  return { ok: true, registered: true };
@@ -399,19 +395,28 @@ export async function run(repoUrl, opts = {}) {
399
395
  }
400
396
 
401
397
  if (!(quality && quality.passed)) {
402
- // 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.
403
402
  reportQualityGap(quality);
404
- log(`\n${C.yellow}${C.bold}Held at the quality gate did NOT deploy or publish a below-bar page.${C.reset}`);
405
- 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}`);
406
- return { ok: false, gated: true, quality, outDir, results: preR.results };
407
- }
408
-
409
- const meanPair = quality.scorecard.map((c) => `${String(c.device).replace(/\(.*/, '')} ${c.meanScore}`).join(' / ');
410
- if (quality.exemplary) {
411
- 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}`);
412
412
  } else {
413
- log(`\n${C.green}${C.bold}Quality gate PASSED ship-worthy (mean ${meanPair}).${C.reset} Shipping.`);
414
- 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
+ }
415
420
  }
416
421
  const postR = post.length ? await runStations(post, baseArgs, total, qIdx + 1) : { ok: true, results: [] };
417
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 });