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.
- package/bin/explainmyrepo.mjs +4 -1
- package/package.json +1 -1
- package/src/orchestrator.mjs +25 -20
- package/tools/quality-grade.mjs +24 -0
package/bin/explainmyrepo.mjs
CHANGED
|
@@ -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
package/src/orchestrator.mjs
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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}
|
|
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
|
-
//
|
|
398
|
+
// Below the world-class bar. DEFAULT: hold — never 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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
log(`\n${C.
|
|
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
|
-
|
|
414
|
-
|
|
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);
|
package/tools/quality-grade.mjs
CHANGED
|
@@ -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 });
|