explainmyrepo 0.1.1

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.
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ // make-dropin.mjs — build the v1.3.1 single-384 drop-in zip for ONE target.
3
+ //
4
+ // Layout (mirrors the proven #1 metaharness-dropin.zip, single-variant):
5
+ // for-ai/ <slug>-kb.rvf (+ .idmap.json) + <slug>-kb.passages.jsonl + <slug>-kb.ids.json
6
+ // + ask-kb.mjs + kb-mcp-server.mjs + kb.config.mjs + resolve-deps.mjs + package.json
7
+ // for-humans/ <slug>-primer.md + studio/{audio + report + prompt} ← STUDIO IS IN THE ZIP (D13/V)
8
+ // README.md manifest.json
9
+ //
10
+ // Usage: node kb/make-dropin.mjs ruvn ../ruv-explainer-ruvn/downloads/ruvn-dropin.zip
11
+ //
12
+ // Self-contained: the studio media (NotebookLM audio overview + report + prompt) is bundled INSIDE
13
+ // the zip's for-humans/studio/ — verify with `unzip -l`. Uses system `zip`.
14
+
15
+ import { execFileSync } from 'node:child_process';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import os from 'node:os';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const KB_DIR = path.dirname(fileURLToPath(import.meta.url)); // kb/
22
+ const ROOT = path.resolve(KB_DIR, '..');
23
+
24
+ const slug = process.argv[2];
25
+ const outArg = process.argv[3];
26
+ if (!slug || !outArg) {
27
+ console.error('usage: node kb/make-dropin.mjs <slug> <out.zip>');
28
+ process.exit(2);
29
+ }
30
+ const OUT = path.isAbsolute(outArg) ? outArg : path.resolve(process.cwd(), outArg);
31
+ const storeDir = path.join(KB_DIR, 'stores', slug);
32
+ const studioDir = path.join(storeDir, 'studio', 'for-humans');
33
+
34
+ // Read provenance from kb.config (sha/date) + the gate score we just proved.
35
+ function readLastBuilt() {
36
+ const p = path.join(KB_DIR, '.last-built.json');
37
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
38
+ }
39
+
40
+ function must(p) { if (!fs.existsSync(p)) throw new Error(`missing: ${p}`); return p; }
41
+
42
+ const FOR_AI = [
43
+ [`${slug}-kb.rvf`, `for-ai/${slug}-kb.rvf`],
44
+ [`${slug}-kb.rvf.idmap.json`, `for-ai/${slug}-kb.rvf.idmap.json`],
45
+ [`${slug}-kb.rvf.embed.json`, `for-ai/${slug}-kb.rvf.embed.json`],
46
+ [`${slug}-kb.passages.jsonl`, `for-ai/${slug}-kb.passages.jsonl`],
47
+ [`${slug}-kb.ids.json`, `for-ai/${slug}-kb.ids.json`],
48
+ ].map(([f, dst]) => [path.join(storeDir, f), dst]);
49
+
50
+ // Structured enrichment artifacts (AI-comprehension recipe v1.3.2): exact symbol/dep/entrypoint
51
+ // lookups the AI uses alongside semantic search. Generated by kb/extract-symbols|dep-graph|
52
+ // entrypoints.mjs. Shipped if present; a build that skips them still produces a valid drop-in.
53
+ const FOR_AI_STRUCTURED = [
54
+ [`${slug}-symbols.json`, `for-ai/${slug}-symbols.json`],
55
+ [`${slug}-dep-graph.json`, `for-ai/${slug}-dep-graph.json`],
56
+ [`${slug}-entrypoints.json`, `for-ai/${slug}-entrypoints.json`],
57
+ ].map(([f, dst]) => [path.join(storeDir, f), dst]).filter(([src]) => fs.existsSync(src));
58
+
59
+ const FOR_AI_TOOLS = [
60
+ ['ask-kb.mjs', 'for-ai/ask-kb.mjs'],
61
+ ['kb-mcp-server.mjs', 'for-ai/kb-mcp-server.mjs'],
62
+ ['kb.config.mjs', 'for-ai/kb.config.mjs'],
63
+ ['resolve-deps.mjs', 'for-ai/resolve-deps.mjs'],
64
+ ].map(([f, dst]) => [path.join(KB_DIR, f), dst]);
65
+
66
+ // for-ai/package.json (drop-in runnable)
67
+ const forAiPkg = {
68
+ name: `${slug}-dropin-kb`,
69
+ version: '0.1.0',
70
+ private: true,
71
+ type: 'module',
72
+ description: `${slug} drop-in knowledge pack — single 384-dim RVF knowledge base + structured symbol/dep/entrypoint index + MCP server (search_kb · lookup_symbol · get_entrypoints · get_dep_graph). Run \`npm i\` then \`node ask-kb.mjs ${slug} "your question"\` or \`node ask-kb.mjs ${slug} --symbol <name>\`.`,
73
+ scripts: { ask: `node ask-kb.mjs ${slug}`, mcp: 'node kb-mcp-server.mjs' },
74
+ dependencies: { '@ruvector/rvf': '^0.2.2', '@xenova/transformers': '^2.17.2' },
75
+ engines: { node: '>=18' },
76
+ };
77
+
78
+ // for-humans/ primer + studio (studio MUST be present — D13/V; fail RED otherwise)
79
+ const primerSrc = must(path.join(storeDir, `${slug}-primer.md`));
80
+ // `studio-links.json` is metadata (notebook/video URLs) read below — NOT a file to ship in the zip.
81
+ const studioFiles = fs.existsSync(studioDir)
82
+ ? fs.readdirSync(studioDir)
83
+ .filter((f) => !f.startsWith('.') && f !== 'studio-links.json')
84
+ .map((f) => [path.join(studioDir, f), `for-humans/studio/${f}`])
85
+ : [];
86
+ const hasAudio = studioFiles.some(([, d]) => /\.(m4a|mp3|wav)$/i.test(d));
87
+ const hasReport = studioFiles.some(([, d]) => /report\.md$/i.test(d));
88
+ if (!studioFiles.length || !hasAudio || !hasReport) {
89
+ throw new Error(`STUDIO-IN-ZIP guard (D13/V): for-humans/studio must contain the audio overview AND report. ` +
90
+ `Found under ${studioDir}: ${studioFiles.map(([, d]) => path.basename(d)).join(', ') || '(none)'}. ` +
91
+ `Refusing to build a studio-less drop-in.`);
92
+ }
93
+
94
+ // Optional EXTRA studio artifacts that ride in the zip (infographic PNG, slide-deck PDF/PPTX).
95
+ const infographicFile = studioFiles.find(([, d]) => /infographic.*\.png$/i.test(d));
96
+ const slidesFile = studioFiles.find(([, d]) => /slides?.*\.(pdf|pptx)$/i.test(d));
97
+ const infographicName = infographicFile ? path.basename(infographicFile[1]) : null;
98
+ const slidesName = slidesFile ? path.basename(slidesFile[1]) : null;
99
+
100
+ // Optional links to the (large) video + the public NotebookLM notebook, kept OUT of the zip to stay light.
101
+ // Read from studio/for-humans/studio-links.json: { "notebookUrl": "...", "videoUrl": "...", "videoNote": "..." }
102
+ let studioLinks = {};
103
+ try { studioLinks = JSON.parse(fs.readFileSync(path.join(studioDir, 'studio-links.json'), 'utf8')); } catch { /* optional */ }
104
+
105
+ const lb = readLastBuilt();
106
+ const manifest = {
107
+ name: `${slug}-dropin`,
108
+ version: lb.version || 'v0.1.1',
109
+ builtAt: lb.builtAt || new Date().toISOString().slice(0, 10),
110
+ sha: lb.targetRepoSha || lb.sha || null,
111
+ embedder: 'Xenova/bge-small-en-v1.5 (384-dim, single variant, recipe v1.3.1)',
112
+ gateA: lb.gateA || { passed: true },
113
+ contents: {
114
+ 'for-ai': [...FOR_AI, ...FOR_AI_STRUCTURED, ...FOR_AI_TOOLS].map(([, d]) => d.replace('for-ai/', '')).concat(['package.json']),
115
+ 'for-humans': [`${slug}-primer.md`, ...studioFiles.map(([, d]) => d.replace('for-humans/', ''))],
116
+ },
117
+ structured: {
118
+ symbols: FOR_AI_STRUCTURED.some(([, d]) => d.endsWith('-symbols.json')) ? `${slug}-symbols.json` : null,
119
+ depGraph: FOR_AI_STRUCTURED.some(([, d]) => d.endsWith('-dep-graph.json')) ? `${slug}-dep-graph.json` : null,
120
+ entrypoints: FOR_AI_STRUCTURED.some(([, d]) => d.endsWith('-entrypoints.json')) ? `${slug}-entrypoints.json` : null,
121
+ note: 'Exact symbol/dependency/entrypoint lookups (AI-comprehension recipe). Use alongside ask-kb semantic search.',
122
+ },
123
+ studio: {
124
+ audio: studioFiles.find(([, d]) => /\.(m4a|mp3|wav)$/i.test(d))?.[1] || null,
125
+ report: studioFiles.find(([, d]) => /report\.md$/i.test(d))?.[1] || null,
126
+ prompt: studioFiles.find(([, d]) => /prompt\.md$/i.test(d))?.[1] || null,
127
+ infographic: infographicFile?.[1] || null,
128
+ slides: slidesFile?.[1] || null,
129
+ notebookUrl: studioLinks.notebookUrl || null,
130
+ videoUrl: studioLinks.videoUrl || null,
131
+ note: 'NotebookLM media pack — play the audio first, then skim the report; infographic + slides ride inside; the video + live notebook are linked (see README).',
132
+ },
133
+ description: `${slug} drop-in knowledge pack. Unzip, npm i in for-ai/, point .mcp.json at kb-mcp-server.mjs, add CLAUDE.md gate. The for-humans/studio/ media (audio, report, infographic, slides) ride inside this zip; the video + public notebook are linked from the README. See README.md.`,
134
+ };
135
+
136
+ const audioName = path.basename(manifest.studio.audio || `${slug}-audio-overview.m4a`);
137
+ const reportName = path.basename(manifest.studio.report || `${slug}-report.md`);
138
+
139
+ const extraTreeLines = [
140
+ infographicName ? ` 🖼 ${infographicName} — the whole story on one sheet (NotebookLM infographic)` : null,
141
+ slidesName ? ` 📊 ${slidesName} — a detailed slide deck you can flip through` : null,
142
+ ].filter(Boolean).join('\n');
143
+
144
+ const linkLines = [
145
+ studioLinks.notebookUrl ? `- **Open the full NotebookLM studio (public):** ${studioLinks.notebookUrl}` : null,
146
+ studioLinks.videoUrl
147
+ ? `- **Watch the video overview:** ${studioLinks.videoUrl}${studioLinks.videoNote ? ` — ${studioLinks.videoNote}` : ''}`
148
+ : null,
149
+ ].filter(Boolean).join('\n');
150
+
151
+ const readme = `# ${slug} Drop-in
152
+
153
+ One zip. Two halves. The NotebookLM studio media rides **inside** this zip.
154
+
155
+ ## What's inside
156
+
157
+ \`\`\`
158
+ ${slug}-dropin/
159
+ for-humans/ — read & listen first
160
+ ${slug}-primer.md
161
+ studio/
162
+ 🎧 ${audioName} — NotebookLM AUDIO OVERVIEW (play this first)
163
+ 📄 ${reportName} — the written deep-dive briefing
164
+ ${extraTreeLines ? extraTreeLines + '\n' : ''} audio-overview-prompt.md — the prompt that generated the audio
165
+ for-ai/ — the searchable knowledge pack (wire this into your agent)
166
+ ${slug}-kb.rvf — single 384-dim bge-small vector DB (the "brain")
167
+ ${slug}-kb.passages.jsonl — full passage text (tagged source_type: src|test|example|doc|config)
168
+ ${slug}-kb.ids.json — id → passage index
169
+ ${slug}-symbols.json — exact public API: every symbol + signature + doc + location
170
+ ${slug}-dep-graph.json — component dependency graph (what depends on what)
171
+ ${slug}-entrypoints.json — build/test/run/install commands + binaries
172
+ ask-kb.mjs · kb-mcp-server.mjs · kb.config.mjs · resolve-deps.mjs · package.json
173
+ README.md — this file
174
+ manifest.json — build metadata + Gate A score
175
+ \`\`\`
176
+
177
+ ## Studio (NotebookLM)
178
+ Start with the audio: play \`for-humans/studio/${audioName}\` — a friendly two-host overview of ${slug}.
179
+ Prefer reading? Open \`for-humans/studio/${reportName}\`.${infographicName ? `
180
+ Want it on one sheet? Open \`for-humans/studio/${infographicName}\`.` : ''}${slidesName ? `
181
+ Prefer slides? Open \`for-humans/studio/${slidesName}\`.` : ''}
182
+ ${linkLines ? `
183
+ The full media pack also includes a **video overview**, linked below to keep this download light:
184
+
185
+ ${linkLines}
186
+ ` : ''}
187
+
188
+ ## Three steps to wire the AI half in
189
+ \`\`\`bash
190
+ # 1 — unzip + install (two deps, Node 18+)
191
+ unzip ${slug}-dropin.zip && cd ${slug}-dropin/for-ai && npm install
192
+
193
+ # 2 — ask the brain a question straight away
194
+ node ask-kb.mjs ${slug} "what does ${slug} actually do"
195
+
196
+ # 3 — point your AI host at it with a .mcp.json in your project root:
197
+ # { "mcpServers": { "${slug}-kb": { "command": "node", "args": ["for-ai/kb-mcp-server.mjs"] } } }
198
+ \`\`\`
199
+
200
+ Built with the Repo-Primer recipe (ADR-0001 v1.3.1). Embedder: ${manifest.embedder}.
201
+ Source: github.com/ruvnet/${slug} @ ${manifest.sha || '(see manifest)'}.
202
+ `;
203
+
204
+ // ---- stage + zip ----
205
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), `dropin-${slug}-`));
206
+ function copy(src, dst) {
207
+ const d = path.join(stage, dst);
208
+ fs.mkdirSync(path.dirname(d), { recursive: true });
209
+ fs.copyFileSync(src, d);
210
+ }
211
+ for (const [src, dst] of [...FOR_AI, ...FOR_AI_STRUCTURED, ...FOR_AI_TOOLS]) copy(must(src), dst);
212
+ copy(primerSrc, `for-humans/${slug}-primer.md`);
213
+ for (const [src, dst] of studioFiles) copy(src, dst);
214
+ fs.writeFileSync(path.join(stage, 'for-ai/package.json'), JSON.stringify(forAiPkg, null, 2) + '\n');
215
+ fs.writeFileSync(path.join(stage, 'README.md'), readme);
216
+ fs.writeFileSync(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
217
+
218
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
219
+ fs.rmSync(OUT, { force: true });
220
+ execFileSync('zip', ['-r', '-X', OUT, '.'], { cwd: stage, stdio: 'inherit' });
221
+ fs.rmSync(stage, { recursive: true, force: true });
222
+ const size = fs.statSync(OUT).size;
223
+ console.log(`\nbuilt ${path.relative(ROOT, OUT)} (${(size / 1e6).toFixed(1)} MB)`);
224
+ console.log(`studio in zip: 🎧 ${audioName} 📄 ${reportName}`);
@@ -0,0 +1,126 @@
1
+ // resolve-deps.mjs — portable resolver for the Cognitum KB scripts.
2
+ //
3
+ // Resolves the two runtime deps the KB needs in a machine-independent order:
4
+ //
5
+ // @ruvector/rvf -> RvfDatabase (the .rvf vector store)
6
+ // @xenova/transformers -> the MiniLM embedder
7
+ //
8
+ // Resolution order (first hit wins) for EACH dep:
9
+ // 1. The project's own node_modules (so `cd kb && npm i` or a root install just works).
10
+ // 2. An explicit env override (RVF_MODULE_PATH / XENOVA_PATH).
11
+ // 3. The author's Mac npm-global / AppealArmor paths (LAST resort, so local dev still runs).
12
+ //
13
+ // This file ships INSIDE the zips, so it must not assume anything beyond Node 18+ and the
14
+ // two npm deps being installable via `npm i @ruvector/rvf @xenova/transformers`.
15
+
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const KB_DIR = path.dirname(__filename);
23
+
24
+ // require() rooted at THIS file — node walks up node_modules from kb/ to the project root,
25
+ // so it finds deps installed either in kb/node_modules or the project root node_modules.
26
+ const localRequire = createRequire(__filename);
27
+
28
+ // @ruvector/rvf and @xenova/transformers are declared dependencies, so they resolve from
29
+ // node_modules (step 1) on any machine after `npm install`. The env overrides remain for
30
+ // non-standard layouts. (No machine-specific fallback paths — those don't belong in a
31
+ // published package.)
32
+
33
+ function existsModuleDir(p) {
34
+ // p may be a package root dir OR a file path; treat presence as resolvable.
35
+ try { return fs.existsSync(p); } catch { return false; }
36
+ }
37
+
38
+ /**
39
+ * Resolve and return the @ruvector/rvf module ({ RvfDatabase, ... }).
40
+ * Order: project node_modules -> RVF_MODULE_PATH env -> Mac npm-global.
41
+ */
42
+ export function loadRvf() {
43
+ // 1. project node_modules (kb/ or root)
44
+ try {
45
+ return { mod: localRequire('@ruvector/rvf'), via: 'project node_modules' };
46
+ } catch { /* fall through */ }
47
+
48
+ // 2. explicit env override (dir of the package, or a path createRequire can resolve from)
49
+ const envPath = process.env.RVF_MODULE_PATH;
50
+ if (envPath && existsModuleDir(envPath)) {
51
+ // If env points at a node_modules root, require @ruvector/rvf from there; else require it directly.
52
+ const base = envPath.endsWith('@ruvector/rvf') || envPath.endsWith('@ruvector/rvf/')
53
+ ? envPath
54
+ : path.join(envPath, '@ruvector/rvf');
55
+ try {
56
+ const req = createRequire(path.join(envPath, 'noop.js'));
57
+ return { mod: req('@ruvector/rvf'), via: `RVF_MODULE_PATH (${envPath})` };
58
+ } catch {
59
+ try { return { mod: localRequire(base), via: `RVF_MODULE_PATH dir (${base})` }; } catch { /* fall through */ }
60
+ }
61
+ }
62
+
63
+ throw new Error(
64
+ "Cannot resolve '@ruvector/rvf'. Run `npm i` (it is a declared dependency), "
65
+ + 'or set RVF_MODULE_PATH to a node_modules dir that contains it.'
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Resolve and dynamically import @xenova/transformers.
71
+ * Order: project node_modules -> XENOVA_PATH env -> Mac AppealArmor build.
72
+ * Returns { T, modelCache, via } where T is the imported module namespace.
73
+ */
74
+ export async function loadTransformers() {
75
+ // 1. project node_modules — resolve the package entry, import via file:// URL.
76
+ try {
77
+ const resolved = localRequire.resolve('@xenova/transformers');
78
+ const T = await import('file://' + resolved);
79
+ return { T, modelCache: chooseModelCache(), via: 'project node_modules' };
80
+ } catch { /* fall through */ }
81
+
82
+ // 2. explicit env override — may be a transformers.js file path or a package dir.
83
+ const envPath = process.env.XENOVA_PATH;
84
+ if (envPath) {
85
+ const url = envPath.startsWith('file://') ? envPath
86
+ : envPath.endsWith('.js') ? 'file://' + path.resolve(envPath)
87
+ : 'file://' + path.join(path.resolve(envPath), 'src/transformers.js');
88
+ try {
89
+ const T = await import(url);
90
+ return { T, modelCache: chooseModelCache(), via: `XENOVA_PATH (${envPath})` };
91
+ } catch { /* fall through */ }
92
+ }
93
+
94
+ throw new Error(
95
+ "Cannot resolve '@xenova/transformers'. Run `npm i` (it is a declared dependency), "
96
+ + 'or set XENOVA_PATH to the transformers package dir / src/transformers.js.'
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Pick a model cache directory. KB_MODEL_CACHE wins; otherwise a kb-local `models-cache`
102
+ * if it already has the model; otherwise the Mac cache if present; otherwise a kb-local
103
+ * dir (created lazily) into which a remote download will be cached.
104
+ * `modelName` lets the big (bge) variant resolve its cache by its own model, not just MiniLM;
105
+ * defaults to MiniLM-384 so existing small-build callers are unchanged.
106
+ */
107
+ export function chooseModelCache(modelName = 'Xenova/all-MiniLM-L6-v2') {
108
+ if (process.env.KB_MODEL_CACHE) return process.env.KB_MODEL_CACHE;
109
+ const kbLocal = path.join(KB_DIR, 'models-cache');
110
+ if (fs.existsSync(path.join(kbLocal, modelName))) return kbLocal;
111
+ return kbLocal; // remote download lands here on first run
112
+ }
113
+
114
+ /**
115
+ * Configure a transformers namespace `T` for a given model: point at the cache, and allow
116
+ * remote download ONLY when THAT model isn't already cached (offline-first).
117
+ * `modelName` defaults to the MiniLM-384 embedder so existing callers keep working; the big
118
+ * variant passes `'Xenova/bge-base-en-v1.5'` so its own cache check is correct.
119
+ * Returns { modelCache, haveLocalModel }.
120
+ */
121
+ export function configureModel(T, modelCache, modelName = 'Xenova/all-MiniLM-L6-v2') {
122
+ const haveLocalModel = fs.existsSync(path.join(modelCache, modelName));
123
+ T.env.localModelPath = modelCache;
124
+ T.env.allowRemoteModels = !haveLocalModel; // fresh machine -> download from HuggingFace
125
+ return { modelCache, haveLocalModel };
126
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "explainmyrepo",
3
+ "version": "0.1.1",
4
+ "description": "Turn any GitHub repo into a bespoke, art-directed explainer site — for humans and their AI — in one command.",
5
+ "type": "module",
6
+ "bin": {
7
+ "explainmyrepo": "bin/explainmyrepo.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "tools/",
13
+ "kb/*.mjs",
14
+ "kb/CONTRACT.md",
15
+ "assets/"
16
+ ],
17
+ "keywords": [
18
+ "explainer",
19
+ "documentation",
20
+ "github",
21
+ "repo",
22
+ "ai",
23
+ "knowledge-base",
24
+ "static-site",
25
+ "npx"
26
+ ],
27
+ "homepage": "https://github.com/stuinfla/Repo-Explainer#readme",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/stuinfla/Repo-Explainer.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/stuinfla/Repo-Explainer/issues"
34
+ },
35
+ "author": "ISOvision",
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@ruvector/rvf": "^0.2.2",
45
+ "@xenova/transformers": "^2.17.2",
46
+ "playwright": "^1.61.1"
47
+ },
48
+ "scripts": {
49
+ "test": "node --test tests/*.test.mjs",
50
+ "start": "node bin/explainmyrepo.mjs"
51
+ }
52
+ }