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,301 @@
1
+ # `tools/` CONTRACT — the anti-brittleness anchor
2
+
3
+ **Version:** 0.1.0
4
+ **Created:** 2026-06-28
5
+ **Paired ADR:** `docs/adr/0005-skill-based-explainer-recipe.md` (D4 — strict brain/tools split + one
6
+ `BuildContext`).
7
+ **Paired DDD:** `docs/ddd/explainmyrepo-recipe-domain.md` (§4 Brain/Tools/BuildContext triangle,
8
+ §8.6 BuildContext, INV-09/INV-10).
9
+
10
+ This file is **load-bearing**. Generation 2 of this project died because phases were coupled by
11
+ string-matched HTML markers — any phase that nudged a marker silently broke a later phase. This
12
+ contract is the rule set that makes that failure mode *structurally impossible* again. Every tool
13
+ author and the skill ("the brain") MUST obey it.
14
+
15
+ > **The one-sentence law:** the **brain decides, the tools do.** A tool never thinks, never decides
16
+ > "good enough," never reads another tool's files, and never reads the whole `BuildContext`. It
17
+ > takes a clear input, produces a clear output, and either succeeds or **fails loud**.
18
+
19
+ ---
20
+
21
+ ## (a) The `BuildContext` — ONE in-memory contract (per ADR-0005 D4 / DDD §8.6)
22
+
23
+ There is exactly **one** data contract for an entire build: the `BuildContext`, persisted on disk as
24
+ **`build.json`** inside the build directory. The brain owns it. Each station fills **its own slot**.
25
+ There are **no string-coupled HTML markers** — the page is rendered **once** (INV-10) from typed
26
+ slots, never mutated incrementally.
27
+
28
+ > **One contract, two views.** This is the *same single contract* the ADR (§D4) and the DDD (§8.6)
29
+ > model — here the slots are named for the foundation roster below. Neither view adds nor drops data.
30
+
31
+ ```jsonc
32
+ {
33
+ "buildId": "uuid-v4", // correlation key + idempotency key; set first (clone-repo)
34
+
35
+ "repo": { // ← clone-repo (Station 0–1: validate + clone)
36
+ "url": "https://github.com/owner/name",
37
+ "owner": "owner",
38
+ "name": "name",
39
+ "slug": "name", // KB slug / repo key used by every kb/ script
40
+ "private": false,
41
+ "defaultBranch": "main",
42
+ "clonePath": "<build-dir>/repo", // where the working tree lives
43
+ "reachable": true
44
+ },
45
+
46
+ "understanding": { // ← build-kb (Station 1: the deep read)
47
+ "repoName": "name", // named CORRECTLY (a Station-1 cue)
48
+ "summary": "what this repo is, in one honest paragraph",
49
+ "passageCount": 0 // > 0 or the build hard-fails (no fake KB — INV-06)
50
+ },
51
+
52
+ "kb": { // ← build-kb (Station 1: the real RVF engine)
53
+ "slug": "name",
54
+ "storeDir": "kb/stores/name",
55
+ "rvfPath": "kb/stores/name/name-kb.rvf", // canonical .rvf (embed block set — NOT .small.rvf)
56
+ "passagesPath": "kb/stores/name/name-kb.passages.jsonl",// full text — search returns TEXT, not {id,distance}
57
+ "idsPath": "kb/stores/name/name-kb.ids.json",
58
+ "embedModel": "Xenova/bge-small-en-v1.5", // 384-dim; explainer targets MUST set an embed block
59
+ "primerPath": "kb/stores/name/name-primer.md", // authored for-humans primer (make-dropin line-79 must())
60
+ "symbolsPath": "kb/stores/name/name-symbols.json", // \
61
+ "depGraphPath": "kb/stores/name/name-dep-graph.json", // } pack prerequisites — 3 of 4 MCP tools read these
62
+ "entrypointsPath": "kb/stores/name/name-entrypoints.json" // /
63
+ },
64
+
65
+ "concept": { // ← THE ART-DIRECTION BRIEF (Station 2: pure JUDGMENT, brain-authored)
66
+ "metaphor": "the repo's visual metaphor (prism / dossier / orb …) — specific to THIS repo",
67
+ "palette": { "...": "colours chosen to fit the metaphor — DesignSystem expression knobs" },
68
+ "typePersonality": "display + body + mono pairing that carries the metaphor's voice",
69
+ "layoutRhythm": ["heroArchetype", "problemArchetype", "..."],
70
+ "heroConcept": "the single emotional opening-image idea",
71
+ "copyVoice": "tone / register for all authored text",
72
+ "tagline": "the one line baked into the social card + og:description"
73
+ },
74
+
75
+ "content": { // ← author (Station 3: pure JUDGMENT, brain-authored)
76
+ "arc": [ { "question": "What world am I in?", "section": "hero", "altitude": "high" } ],
77
+ "sections": {
78
+ "hero": {}, "problem": {}, "whatItIs": {}, "insight": {},
79
+ "howItWorks": {}, "useCases": {}, "getStarted": {}, "pack": {}
80
+ },
81
+ "citations": [ { "claim": "…", "passageId": "…" } ] // every claim traceable to a KB passage (INV-06)
82
+ },
83
+
84
+ "visuals": { // ← generate-image (raster) + make-diagrams (SVG) (Station 4)
85
+ "hero": { "role": "metaphor", "prompt": "…", "file": "<build-dir>/assets/hero.png",
86
+ "px": "1536x1024", "engine": "gpt-image-2", "http200": true },
87
+ "sections": [ { "id": "problem", "role": "problem illustration",
88
+ "file": "<build-dir>/assets/problem.png", "px": "1024x1024",
89
+ "engine": "gpt-image-2", "http200": true },
90
+ { "id": "useCase", "role": "scenario", "file": "…", "px": "1024x1024",
91
+ "engine": "gpt-image-2", "http200": true } ],
92
+ "architectureDiagram": { "ascii": "…", "svgPath": "<build-dir>/assets/architecture.svg",
93
+ "altText": "…", "xmllintOK": true }, // from dep-graph + symbols (REAL structure)
94
+ "flowDiagram": { "ascii": "…", "svgPath": "<build-dir>/assets/flow.svg",
95
+ "altText": "…", "xmllintOK": true }, // from entrypoints (REAL runtime flow)
96
+ "bigIdeaDiagram": { "ascii": "…", "svgPath": "<build-dir>/assets/big-idea.svg",
97
+ "altText": "…", "xmllintOK": true },
98
+ "insightDiagram": { "ascii": "…", "svgPath": "<build-dir>/assets/insight.svg",
99
+ "altText": "…", "xmllintOK": true }
100
+ },
101
+
102
+ "brand": { // ← make-favicon + make-social-card (Station 5)
103
+ "favicon": { "set": ["favicon-16.png","favicon-32.png","favicon.ico"],
104
+ "appleTouchIcon": "apple-touch-icon.png", "derivedFromHero": true },
105
+ "socialCard": { "px": "1200x630", "file": "<build-dir>/assets/social-card.png",
106
+ "tagline": "the tagline baked in" }
107
+ },
108
+
109
+ "page": { // ← assemble-page (Station 6: rendered ONCE)
110
+ "dir": "<build-dir>/site",
111
+ "htmlPath": "<build-dir>/site/index.html",
112
+ "cssPath": "<build-dir>/site/styles.css",
113
+ "tokensUsed": ["--accent", "--spectrum", "…"],
114
+ "seo": { "title": "…", "description": "…", "canonical": "https://…",
115
+ "jsonLd": "SoftwareApplication", "sitemap": "sitemap.xml",
116
+ "robots": "robots.txt", "llmsTxt": "llms.txt" },
117
+ "social":{ "og": { "...": "og:title/description/image/url" },
118
+ "twitter": { "card": "summary_large_image" } }
119
+ },
120
+
121
+ "pack": { // ← make-pack (Station 6: studio-less make-dropin)
122
+ "zipPath": "<build-dir>/site/name-knowledge-pack.zip",
123
+ "forAi": ["name-kb.rvf","name-kb.passages.jsonl","name-symbols.json",
124
+ "name-dep-graph.json","name-entrypoints.json","ask-kb.mjs","kb-mcp-server.mjs"],
125
+ "forHumans": ["name-primer.md"],
126
+ "opens": true, // zip opens, KB loads, ask-kb returns TEXT (Station-6 cue)
127
+ "kbLoads": true
128
+ },
129
+
130
+ "quality": { // ← quality-grade (Station 7: the completion criterion)
131
+ "scorecard": [
132
+ { "device": "mobile(390)", "gateA": { "A1":0,"A2":0,"A3":0,"A4":0,"A5":0 },
133
+ "gateB": { "B1":0,"B2":0,"B3":0,"B4":0,"B5":0 },
134
+ "rationales": { "B1": "what the vision model SAW" },
135
+ "headlineScore": 0, // = MIN across all 10 criteria (never the mean)
136
+ "passed": false }, // headlineScore >= 95
137
+ { "device": "desktop(1440)", "gateA": {}, "gateB": {}, "rationales": {},
138
+ "headlineScore": 0, "passed": false }
139
+ ],
140
+ "passed": false, // BOTH devices' headlineScore >= 95
141
+ "iterations": 0 // refine-loop count
142
+ },
143
+
144
+ "publish": { // ← publish-repo + deploy + repo-seo (Station 8)
145
+ "explainerRepoUrl": "https://github.com/owner/name-explainer",
146
+ "liveUrl": "https://name-explainer.netlify.app",
147
+ "http200": true, // returns 200 UNAUTHENTICATED
148
+ "ownerInvited": true, // best-effort collaborator invite
149
+ "repoTopics": ["…"], // RepoSEO on the EXPLAINER repo (via GitHub API)
150
+ "repoDescription": "…",
151
+ "sourceRepoSeoSuggested":{ "topics": ["…"], "description": "…" } // SUGGESTED only (offered, never forced)
152
+ },
153
+
154
+ "readmePr": { // ← readme-enhance (Station 8b: OPTIONAL, off critical path, PR-only)
155
+ "prUrl": "https://github.com/owner/name/pull/N", // or the string "declined"
156
+ "svgsShared": ["architecture.svg","flow.svg"] // the SAME Station-4 SVGs, reused
157
+ },
158
+
159
+ "notify": { // ← notify (Station 9)
160
+ "emailSent": true,
161
+ "smtp250": true, // send confirmed
162
+ "inlineReturned": true
163
+ }
164
+ }
165
+ ```
166
+
167
+ **Slot ownership table** (who writes what — a tool writes **only** its own slot):
168
+
169
+ | Slot | Owning tool(s) | Station |
170
+ |------|----------------|---------|
171
+ | `repo` | `clone-repo` | 0–1 |
172
+ | `understanding`, `kb` | `build-kb` | 1 |
173
+ | `concept` | *(brain — pure judgment, no tool)* | 2 |
174
+ | `content` | *(brain — pure judgment, no tool)* | 3 |
175
+ | `visuals.hero`, `visuals.sections[]` | `generate-image` | 4 |
176
+ | `visuals.architectureDiagram` / `.flowDiagram` / `.bigIdeaDiagram` / `.insightDiagram` | `make-diagrams` | 4 |
177
+ | `brand.favicon` | `make-favicon` | 5 |
178
+ | `brand.socialCard` | `make-social-card` | 5 |
179
+ | `page` | `assemble-page` | 6 |
180
+ | `pack` | `make-pack` | 6 |
181
+ | `quality` | `quality-grade` | 7 |
182
+ | `publish.explainerRepoUrl` / `.ownerInvited` | `publish-repo` | 8 |
183
+ | `publish.liveUrl` / `.http200` | `deploy` | 8 |
184
+ | `publish.repoTopics` / `.repoDescription` / `.sourceRepoSeoSuggested` | `repo-seo` | 8 |
185
+ | `readmePr` (optional) | `readme-enhance` | 8b |
186
+ | `notify` | `notify` | 9 |
187
+
188
+ `concept` and `content` are filled by the **brain directly** (pure judgment, ADR-0005 S2/S3) — there
189
+ is intentionally **no tool** for them. Every other slot is filled by a pure tool below.
190
+
191
+ ---
192
+
193
+ ## (b) The UNIFORM tool invocation convention
194
+
195
+ Every tool is one file, invoked the **same way**, with **one** positional argument — the build
196
+ directory:
197
+
198
+ ```bash
199
+ node tools/<name>.mjs <build-dir>
200
+ ```
201
+
202
+ `<build-dir>` is a self-contained directory for **one** build. It holds:
203
+
204
+ ```
205
+ <build-dir>/
206
+ build.json # the BuildContext (the ONLY cross-tool channel)
207
+ repo/ # the cloned working tree (after clone-repo)
208
+ assets/ # generated images, SVGs, favicons, social card
209
+ site/ # the assembled page + the knowledge-pack zip
210
+ ```
211
+
212
+ **Every tool, without exception, MUST:**
213
+
214
+ 1. **Read** `<build-dir>/build.json` and take **only the slice it declares** (its inputs below).
215
+ It MUST NOT read slots it does not declare, and MUST NOT read another tool's output files.
216
+ 2. **Do its one mechanical job** — embed, call an image API, render a diagram, screenshot, zip,
217
+ deploy, email. It never makes a judgment call and never decides "good enough."
218
+ 3. **Write its outputs into `<build-dir>`** (under `assets/`, `site/`, or the kb store) — never
219
+ outside the build dir, never into another tool's files.
220
+ 4. **Merge ONLY its own slot** back into `build.json` (read-modify-write the single slot it owns;
221
+ leave every other slot byte-for-byte untouched).
222
+ 5. **Print a JSON result object to stdout** and nothing else of substance on stdout:
223
+
224
+ ```jsonc
225
+ { "ok": true, "outputs": { "...": "paths + the slot it merged" }, "error": null }
226
+ // or, on any failure:
227
+ { "ok": false, "outputs": {}, "error": "clear, human-readable reason" }
228
+ ```
229
+
230
+ 6. **Exit code is the source of truth.** `process.exit(0)` **iff** `ok: true`. On **any** failure
231
+ it **exits NON-ZERO** with a clear message (also surfaced in `error`). **Never** exit 0 on a
232
+ partial/failed result. **Never** swallow an error. **Never** write a placeholder, a default
233
+ asset, a stub file, or `"TODO"` to limp past a failure — a missing input is a **loud stop**, not
234
+ a silent substitution (INV-04, Never-Fail-Silently). The brain reads the exit code first, then
235
+ the JSON.
236
+
237
+ **Idempotency.** Re-running a tool on the same `<build-dir>` with the same inputs yields the same
238
+ observable result (INV-12). Re-running overwrites that tool's outputs + slot; it never appends.
239
+
240
+ **Logging.** Diagnostics go to **stderr**. **stdout carries the single JSON result object only**, so
241
+ the brain can `JSON.parse` stdout unconditionally.
242
+
243
+ ---
244
+
245
+ ## (c) Tools are PURE and individually testable
246
+
247
+ - **Pure.** A tool reads **only its declared inputs** (its slice of `build.json` + the named assets
248
+ it owns) and writes **only its declared outputs** (its asset files + its one slot). It **never**
249
+ reaches into another tool's internals, intermediate files, or slots (INV-09). The only channel
250
+ between tools is the typed `build.json` slot — never a shared global, never a file path guessed
251
+ from a sibling tool, never a parsed HTML marker (INV-10).
252
+ - **Individually testable.** Because a tool depends only on its declared slice, you can test it in
253
+ isolation: hand-craft a minimal `build.json` containing just that tool's inputs, run
254
+ `node tools/<name>.mjs <fixture-dir>`, and assert on (1) the exit code, (2) the stdout JSON, and
255
+ (3) the files + slot it wrote. No other tool, no network of phases, no ordering needed. A tool
256
+ that can only be tested "as part of the whole pipeline" is a contract violation.
257
+ - **No hidden ordering.** Ordering is the **brain's** responsibility (it runs the stations in order
258
+ and checks each cue). A tool must not assume "the previous tool already ran" beyond the inputs it
259
+ explicitly declares as present in `build.json`. If a declared input is absent, the tool **fails
260
+ loud** (per (b)·6) — it does not invent it.
261
+
262
+ ---
263
+
264
+ ## (d) The tool roster — job · inputs · outputs
265
+
266
+ The recipe ships **the foundation roster below**. (The task brief enumerates these by name; note the
267
+ brief's count of "13" is a miscount — **14** tools are named, all listed here.) Several wrap the
268
+ real, already-working `kb/` engine (ADR-0005 D3) or a verified Claude Code skill — they do **not**
269
+ re-implement it. Each row: the tool's **one-line job**, the `build.json` slice + files it **reads**,
270
+ and the slot + files it **writes**.
271
+
272
+ | # | Tool (`node tools/<name>.mjs <build-dir>`) | One-line job | Reads (declared inputs) | Writes (outputs) |
273
+ |---|---|---|---|---|
274
+ | 1 | **clone-repo** | Validate the URL is reachable + clone the repo (supports private / owner repos via token). | `repo.url` (+ GitHub token from env) | `repo` slot (`owner/name/slug/private/defaultBranch/reachable`); working tree at `<build-dir>/repo/` |
275
+ | 2 | **build-kb** | Build the **real RVF KB** + structured indexes for the cloned repo — wraps `kb/build-kb.mjs` **and** `kb/extract-symbols.mjs` / `kb/dep-graph.mjs` / `kb/entrypoints.mjs` / `kb/index-primer.mjs`. Hard-fails on a failed RVF build (no JSON fallback — INV-06). | `repo.slug`, `repo.clonePath` (and the authored `kb/stores/<slug>/<slug>-primer.md` for the index-primer step) | `understanding` + `kb` slots; `kb/stores/<slug>/` (`.rvf`, `.rvf.idmap.json`, `.rvf.embed.json`, `.passages.jsonl`, `.ids.json`, `-symbols.json`, `-dep-graph.json`, `-entrypoints.json`) |
276
+ | 3 | **generate-image** | Generate **one** raster rung (hero or a section illustration) — probes + uses `gpt-image-2` (verified primary), falls back loud to `gpt-image-1` only on a build-time probe failure. One pure API call per invocation. | one rung's `{ role, prompt, px }` from `visuals` + `concept` palette (+ OpenAI key from env) | the rung's entry in `visuals.hero` / `visuals.sections[]`; the `.png` under `<build-dir>/assets/` (must be HTTP 200 / valid) |
277
+ | 4 | **make-diagrams** | Author the **structural SVG rungs** — big-idea, insight, architecture, flow — ASCII → crisp accessible SVG via the `ascii-to-svg` skill. Architecture/flow grounded in the REAL `kb/dep-graph` + `kb/entrypoints` (never invented). | the four diagrams' ASCII (brain-authored) + `kb.depGraphPath` / `kb.entrypointsPath` / `kb.symbolsPath` | `visuals.architectureDiagram` / `.flowDiagram` / `.bigIdeaDiagram` / `.insightDiagram`; the `.svg` files under `<build-dir>/assets/` (each xmllint-clean, with ASCII fallback) |
278
+ | 5 | **assemble-page** | Render the page **ONCE** onto the shared design system (`assets/design-system/`) from typed slots + wire the SEO/social `<head>` (title, meta, canonical, JSON-LD, OG/Twitter, favicon links) + emit `sitemap.xml` / `robots.txt` / `llms.txt`. No string markers (INV-10). Includes the required explainer **footer** (credit + "create one" CTA). | `concept`, `content`, `visuals`, `brand`, `kb.primerPath` | `page` slot (+ `page.seo`, `page.social`); `<build-dir>/site/` (`index.html`, `styles.css`, `sitemap.xml`, `robots.txt`, `llms.txt`) |
279
+ | 6 | **make-favicon** | From the hero's visual identity, produce the **full favicon set** (favicon + `apple-touch-icon` + standard sizes). Runs right after the hero. | `visuals.hero.file`, `concept.palette` | `brand.favicon` slot; favicon files under `<build-dir>/assets/` |
280
+ | 7 | **make-social-card** | From the hero identity + the authored tagline, render the designed **1200×630** social card (tagline baked in) for OG / Twitter `summary_large_image`. | `visuals.hero.file`, `concept.palette`, `concept.tagline` | `brand.socialCard` slot; `social-card.png` under `<build-dir>/assets/` |
281
+ | 8 | **make-pack** | Build the downloadable **AI knowledge pack** — wraps `kb/make-dropin.mjs` via its **`--no-studio`** variant (studio-less first; the one acknowledged engine change, ADR-0005 S6). Ships the for-AI half + the for-humans primer. | `kb` slot (store dir + the three structured JSONs + primer), `repo.slug` | `pack` slot; `<build-dir>/site/<slug>-knowledge-pack.zip` (opens, KB loads, `ask-kb` returns TEXT) |
282
+ | 9 | **quality-grade** | Render the **assembled site LOCALLY** in a real browser (Playwright), full-page screenshot at **390px + 1440px**, vision-score against the verbatim **Gate A/B** rubric as a harsh critic, return two scorecards. Malformed/missing per-criterion scores → **loud stop, never a silent pass**. | `page.dir` / `page.htmlPath` | `quality` slot (`scorecard[]` per device, `headlineScore` = MIN, `passed`); both screenshots under `<build-dir>/assets/` |
283
+ | 10 | **deploy** | Deploy the **already-passed** page to its **own per-build URL** (default Netlify, provider-agnostic — Vercel is a one-line swap). The FIRST + only deploy (QUALITY precedes PUBLISH). | `page.dir`, `repo.slug` (+ deploy-provider token from env) | `publish.liveUrl` / `publish.http200` (200 unauthenticated) |
284
+ | 11 | **publish-repo** | Create the dedicated **explainer GitHub repo** (public) and invite the owner as a collaborator (best-effort). | `repo.owner` / `repo.name` / `repo.slug`, `page.dir` (+ GitHub token from env) | `publish.explainerRepoUrl` / `publish.ownerInvited` |
285
+ | 12 | **repo-seo** | Set **GitHub topics + a strong description** on the explainer repo (via API) so it is discoverable; emit **suggested** topic/description improvements for the SOURCE repo (offered, never set directly — INV-16). | `publish.explainerRepoUrl`, `concept`, `understanding.summary` (+ GitHub token) | `publish.repoTopics` / `.repoDescription` / `.sourceRepoSeoSuggested` |
286
+ | 13 | **readme-enhance** | **OPTIONAL (Station 8b, off the critical path).** Offer to enhance the SOURCE repo's README via the `readme-enhance` skill — architectural explanation + the **shared Station-4 SVGs** + an explainer badge — delivered **ONLY as a pull request** (never a direct push — INV-16). Failure is a warning, never sinks the build. | `repo` slot, `publish.liveUrl`, `visuals.architectureDiagram` / `.flowDiagram` (the shared SVGs) | `readmePr` slot (a PR URL or `"declined"`) |
287
+ | 14 | **notify** | Email the owner the **scorecard + both screenshots + links** (live URL, explainer repo, pack, and any optional README-PR / source-repo SEO suggestions); also return inline. Pure SMTP (absorbs the old `phase9-send-email.mjs`). A notify failure degrades to a warning — it never inverts a live, graded, deployed build. | `publish` slot, `quality.scorecard`, the two screenshot paths, `pack.zipPath`, `readmePr` (+ SMTP creds from env) | `notify` slot (`emailSent` / `smtp250` / `inlineReturned`) |
288
+
289
+ ### Secrets
290
+
291
+ Tools read credentials (GitHub token, OpenAI key, deploy-provider token, SMTP creds) from the
292
+ **environment** — never from `build.json`, never hard-coded, never committed. `build.json` carries
293
+ build state only, never secrets.
294
+
295
+ ### The completion bar
296
+
297
+ A build is **done** only when the dual gate is satisfied: every Gate-A and Gate-B criterion **≥ 95
298
+ on BOTH mobile (390) and desktop (1440)**, the two pre-ship eyes (vision model + operator) agree,
299
+ the page is deployed (200 unauthenticated), and the AI pack ships. The mission, not a slogan: *a
300
+ stranger looks at the result and smiles — "that's really cool."* If a build cannot clear that bar,
301
+ **flag it honestly** — never ship slop and call it done (INV-05).