From db2f83f4d91184f286341022e8ddad2d50600d98 Mon Sep 17 00:00:00 2001 From: hunterdsp Date: Thu, 30 Jul 2026 19:28:23 -0400 Subject: [PATCH 1/6] docs: add the cross-org Makefile standard plan below the fold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One `standard.mk` every repo includes, per-repo variation as configuration. Design RFC is doppler-dsp/doppler#555; this records the plan, the measured baseline, and the success criteria in the org's existing planning surface rather than starting a second one. The baseline is what makes the criteria checkable later: 50 vs 27 targets with 18 shared, `make help` covering 60% / 81% of them, and 12/83 vs 4/71 CI steps routing through make. Three of the nine criteria (help completeness, no ghost targets, drift) are enforced by gates rather than by review — none of the problems this fixes were decided, they accumulated, so review is demonstrably not the control. Placed above the Decision log so that section stays last, per the file's existing shape. --- README.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/README.md b/README.md index eb987b4..38fe0f2 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,101 @@ get-jb = "https://just-buildit.github.io/get-jb.sh" - [ ] `jb-deps.toml` / `jb.toml` schemas — JSON Schema for editor completion - [ ] CHANGELOG hygiene across repos is uneven +______________________________________________________________________ + +## Makefile standard — cross-org plan + +One `standard.mk` every repo includes, with per-repo variation expressed as +configuration rather than as a fork. Design RFC and full rationale: +[doppler-dsp/doppler#555](https://github.com/doppler-dsp/doppler/issues/555). + +**Scope:** all repos in `just-buildit` and `doppler-dsp`. **Canonical home:** +this org (`just-buildit`) — it is already a dependency of every repo's CI +bootstrap, so hosting the standard here adds no new relationship. + +### Problem + +A written convention exists (`skills://makefile-convention`) and every repo +hand-implements it, so they drift. Measured 2026-07-30: + +| | doppler | just-makeit | +| ------------------------------- | ------- | ----------- | +| targets defined | 50 | 27 | +| targets shared between the two | 18 | 18 | +| listed by `make help` | 30 | 22 | +| CI `run:` steps invoking `make` | 12 / 83 | 4 / 71 | + +Concrete consequences, all live: doppler has no `format` target; the same +benchmark concepts are named `bench-baseline`/`bench-check` in one repo and +`bench-save`/`bench-compare` in the other; `zensical build --strict` is +implemented in three places that disagree, and every doppler PR builds the docs +site twice; `make wheel` is in `.PHONY` and in `help` with no rule, so it exits +0 having done nothing. + +### The standard + +Universal (8): `all help setup clean test test-fast lint format`, plus one +`lint-` dispatch target per configured tool. Feature groups defined only +when flagged — `HAS_DOCS`, `HAS_C`, `HAS_DOXYGEN`, `HAS_PYTHON`, `HAS_RUST`, +`HAS_BENCH`, `HAS_COVERAGE`, `HAS_RELEASE` — plus the `test-all` / `gates` +aggregates. **Cap: 36 targets with every flag on.** + +- **Dispatch is required, not optional.** `.pre-commit-config.yaml` calls + `make -s lint-`; the Makefile invokes `$(DEV_RUN) `; `uv.lock` + pins the version. This is what makes local and CI resolve identically, so a + hand-pinned `additional_dependencies` list becomes unnecessary. +- **`help` is generated** from `##` comments, never hand-maintained. +- **`release` is reserved** for the C build type (`clean` + `build BUILD_TYPE=Release`). The release *workflow* is `ship` / `tag-release`. +- **Naming is `-`**, making the noun a namespace: + `test-python`, `test-rust`, `version-check`. `make test-` then completes + the whole family. +- **`local.mk`** is included if present and may only *add* targets, never + redefine a standard one — otherwise it becomes the fork this prevents. + +### Success criteria + +Measured against the 2026-07-30 baseline above: + +| # | criterion | today | target | +| --- | -------------------------------------------------------------- | ------------ | ----------- | +| 1 | repo Makefile holds only config + genuinely local targets | 50 / 27 | ≤14 / ≤4 | +| 2 | `make help` lists every target, and every listed target exists | 60% / 81% | 100% / 100% | +| 3 | ghost targets (`.PHONY` with no rule) | 1 / 0 | 0 / 0 | +| 4 | CI `run:` steps are `make ` or environment plumbing | 12/83 / 4/71 | 100% / 100% | +| 5 | `zensical build --strict` implementations | 3 | 1 | +| 6 | docs site builds per doppler PR | 2 | 1 | +| 7 | hand-pinned `additional_dependencies` for lock-managed tools | yes | none | +| 8 | editing vendored `standard.mk` fails `make lint` | n/a | both repos | +| 9 | `make ` behaves identically across repos | no | yes | + +Criteria 2, 3 and 8 are enforced by gates rather than by review, so they cannot +regress silently — which is the point, since none of the problems above were +decided, they accumulated. + +### Phases + +- [ ] **P0 — prototype** `standard.mk` in just-makeit; vendored, drift gate + inert until P1 publishes canonical *(just-makeit)* +- [ ] **P1 — publish** canonical `standard.mk` in this org; wire the drift gate + live *(just-buildit)* +- [ ] **P2 — doppler port**: `docs-check` first (deletes the three-way + divergence and the double site build in one commit), then `lint-` + dispatch, then ghost/backfill/renames *(doppler)* +- [ ] **P3 — convention doc** updated to match, landing *with* P2 — until + `standard.mk` exists, the doc describing the old names is still accurate + *(doppler)* +- [ ] **P4 — CI port** per repo: call standard targets, delete each inline + duplicate in the same commit *(per repo)* + +### Non-goals + +- Not a rewrite of target semantics — the convention already defines them; this + implements them once instead of N times. +- Not removing repo-specific targets. doppler keeps `specan`, `gallery`, + `record-demo`, `blazing` and its bench scripts. +- Not a general fix for environment drift. Dispatch closes it for lock-managed + Python tools; anything resolved outside the lock is still on its own. + ## Decision log - **`jb-deps.toml` beats stdin** when both are present. TTY detection From 520d05880876b56e814ac2102cb4eaf9c497dc23 Mon Sep 17 00:00:00 2001 From: hunterdsp Date: Thu, 30 Jul 2026 19:35:29 -0400 Subject: [PATCH 2/6] docs(review): correct criterion 1, close two coverage gaps, add criterion 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review recomputed criterion 1 from the two Makefiles and found ≤14 unreachable. Reproduced independently: doppler has 50 targets of which 30 map onto the standard, leaving **20** local — not 14. My number came from 50 − 36, which assumed doppler already had all 36 standard targets; it has 30. Arithmetic from memory rather than from the data, which is the same error that produced the other two misses below. Two coverage gaps of the `release-branch` class, both closed: - `test-examples` is in BOTH repos and was in no feature group — now `HAS_EXAMPLES`. - No system-dependency target existed at all. `setup` is project deps in both repos; system deps are `jbx install-deps`. That gap was pointed, because this plan's own hosting rationale is that just-buildit is already in every repo's CI bootstrap — which is true precisely because of the target the standard did not define. Now universal, a no-op where a repo declares none. With both closed and `install` collapsed into `setup` (a strict subset of it in the same file), the derived numbers are: standard 38, doppler 18 local, just-makeit 1 local, and **no target shared by both repos left outside the standard**. That last line is now criterion 10, checked by script rather than reasoned about — three misses of the same class is enough evidence that the list must be derived from the measured union, not assembled from memory. Also records the reviewer's evidence for gates-over-review: `make wheel` was exiting 0 with no rule in a repo that already had a lint gate, CI on every PR, and a help entry advertising it. --- README.md | 50 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 38fe0f2..0faf42d 100644 --- a/README.md +++ b/README.md @@ -204,8 +204,14 @@ site twice; `make wheel` is in `.PHONY` and in `help` with no rule, so it exits Universal (8): `all help setup clean test test-fast lint format`, plus one `lint-` dispatch target per configured tool. Feature groups defined only when flagged — `HAS_DOCS`, `HAS_C`, `HAS_DOXYGEN`, `HAS_PYTHON`, `HAS_RUST`, -`HAS_BENCH`, `HAS_COVERAGE`, `HAS_RELEASE` — plus the `test-all` / `gates` -aggregates. **Cap: 36 targets with every flag on.** +`HAS_BENCH`, `HAS_COVERAGE`, `HAS_RELEASE`, `HAS_EXAMPLES` — plus `test-all` / `gates` +aggregates. **Cap: 38 targets with every flag on.** + +`install-deps` is universal (system packages via `jbx install-deps`; a no-op +where a repo declares none) and is distinct from `setup`, which installs +*project* deps. `test-examples` sits in `HAS_EXAMPLES`. Both were added after +the list was checked against the measured union rather than assembled from +memory — see criterion 10. - **Dispatch is required, not optional.** `.pre-commit-config.yaml` calls `make -s lint-`; the Makefile invokes `$(DEV_RUN) `; `uv.lock` @@ -223,26 +229,40 @@ aggregates. **Cap: 36 targets with every flag on.** Measured against the 2026-07-30 baseline above: -| # | criterion | today | target | -| --- | -------------------------------------------------------------- | ------------ | ----------- | -| 1 | repo Makefile holds only config + genuinely local targets | 50 / 27 | ≤14 / ≤4 | -| 2 | `make help` lists every target, and every listed target exists | 60% / 81% | 100% / 100% | -| 3 | ghost targets (`.PHONY` with no rule) | 1 / 0 | 0 / 0 | -| 4 | CI `run:` steps are `make ` or environment plumbing | 12/83 / 4/71 | 100% / 100% | -| 5 | `zensical build --strict` implementations | 3 | 1 | -| 6 | docs site builds per doppler PR | 2 | 1 | -| 7 | hand-pinned `additional_dependencies` for lock-managed tools | yes | none | -| 8 | editing vendored `standard.mk` fails `make lint` | n/a | both repos | -| 9 | `make ` behaves identically across repos | no | yes | +| # | criterion | today | target | +| --- | ---------------------------------------------------------------------------- | ------------ | ----------- | +| 1 | repo Makefile holds only config + genuinely local targets | 50 / 27 | ≤18 / ≤1 | +| 2 | `make help` lists every target, and every listed target exists | 60% / 81% | 100% / 100% | +| 3 | ghost targets (`.PHONY` with no rule) | 1 / 0 | 0 / 0 | +| 4 | CI `run:` steps are `make ` or environment plumbing | 12/83 / 4/71 | 100% / 100% | +| 5 | `zensical build --strict` implementations | 3 | 1 | +| 6 | docs site builds per doppler PR | 2 | 1 | +| 7 | hand-pinned `additional_dependencies` for lock-managed tools | yes | none | +| 8 | editing vendored `standard.mk` fails `make lint` | n/a | both repos | +| 9 | `make ` behaves identically across repos | no | yes | +| 10 | targets shared by two or more adopting repos that sit *outside* the standard | 3 | 0 | Criteria 2, 3 and 8 are enforced by gates rather than by review, so they cannot regress silently — which is the point, since none of the problems above were -decided, they accumulated. +decided, they accumulated. `make wheel` had been exiting 0 with no rule behind +it in a repo that already had a `make lint` gate, CI on every PR, and a `help` +entry advertising it: every human control was in place, and none of them caught +it. + +Criterion 10 applies the same lesson to the standard's own scope. Three targets +shared by both repos — `release-branch`, `test-examples`, `install-deps` — were +each missed while the list was written from memory, and each was found by +recomputing the union from the two Makefiles. The list is therefore **derived by +script from the measured union of adopting repos**, and the invariant "no target +shared by two or more repos sits outside the standard" is checked rather than +reasoned about. ### Phases - [ ] **P0 — prototype** `standard.mk` in just-makeit; vendored, drift gate - inert until P1 publishes canonical *(just-makeit)* + inert until P1 publishes canonical. Also collapses just-makeit's `install` + (`uv sync --group dev`) into `setup`, of which it is a strict subset, so a + fourth deps-ish name never reaches the standard *(just-makeit)* - [ ] **P1 — publish** canonical `standard.mk` in this org; wire the drift gate live *(just-buildit)* - [ ] **P2 — doppler port**: `docs-check` first (deletes the three-way From 938208f64c74e0d555646395f64221331447c948 Mon Sep 17 00:00:00 2001 From: hunterdsp Date: Thu, 30 Jul 2026 19:43:55 -0400 Subject: [PATCH 3/6] docs: add the required-files table to the Makefile standard plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the convention's "each file owns one concern" model to the files the standard actually introduces, so an adopting repo can see the whole surface in one place rather than inferring it from prose. Checked against both repos rather than listing what a standard "should" need: `Makefile`, `pyproject.toml`, `uv.lock`, `.pre-commit-config.yaml` and `jb.toml` all exist in each today, so **adoption adds exactly one file** (`standard.mk`); `local.mk` is optional and so far unneeded by either. Two accuracy notes from that check. System packages are folded into `jb.toml` under `[dev.]` in both repos — neither carries a standalone `jb-deps.toml` — so the table names `jb.toml` as the real location and mentions `jb-deps.toml` as the alternative. And `uv.lock`'s row states why it is on the list at all: it is what makes local and CI resolve identically, which is the mechanism that lets dispatch close the environment-drift class rather than merely narrow it. --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 0faf42d..056c93e 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,25 @@ memory — see criterion 10. - **`local.mk`** is included if present and may only *add* targets, never redefine a standard one — otherwise it becomes the fork this prevents. +### Required files + +Each file owns exactly one concern; nothing states a tool's invocation twice. + +| File | Purpose | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Makefile` | Configuration only — feature flags, path and tool overrides, `include standard.mk`, and repo-local targets. Nothing shared lives here. | +| `standard.mk` | The shared targets, vendored verbatim. Never edited in-repo: the drift gate fails `make lint` on any difference from canonical. | +| `local.mk` | Optional. Included if present; may only *add* targets, never redefine a standard one — otherwise it becomes the fork this prevents. | +| `pyproject.toml` | **Which** tools, at **what** versions (the `dev` group). | +| `uv.lock` | Pins those versions, committed. This is what makes local and CI resolve identically, and so what lets dispatch close the environment-drift class. | +| `.pre-commit-config.yaml` | **When** a check fires. Dispatches inward (`entry: make -s lint-`, `language: system`) and resolves no tool versions itself. | +| `jb.toml` | Tool manifest, with system packages folded in under `[dev.]` and consumed by `install-deps`. (`jb-deps.toml` is the standalone alternative for repos that prefer it.) | +| `.github/workflows/*.yml` | Calls `make `. Anything else must be provably environment plumbing — runner setup, artifact transport, release packaging. | + +**Adoption adds exactly one file.** Both repos already carry `Makefile`, +`pyproject.toml`, `uv.lock`, `.pre-commit-config.yaml` and `jb.toml` today; +only `standard.mk` is new, and `local.mk` is optional and so far unneeded. + ### Success criteria Measured against the 2026-07-30 baseline above: From 465052dab2dcd3ac5f4c2a8a2200674cf8b61a1b Mon Sep 17 00:00:00 2001 From: hunterdsp Date: Thu, 30 Jul 2026 19:49:45 -0400 Subject: [PATCH 4/6] docs: pin the canonical home and the drift gate cache semantics Two decisions the plan had left implicit. **Canonical home is just-buildit.github.io**, served at https://just-buildit.github.io/standard.mk. The RFC had resolved this only as far as "the org", and the obvious candidate turns out to be disqualified by our own rule: `just-buildit/just-buildit` has a Makefile, so it consumes the standard exactly as doppler and just-makeit do. The org-pages root is a non-consumer whose charter is already "small static resources the toolchain depends on", and serving over the CDN keeps the gate to one curl -- no clone, no auth, and no raw.githubusercontent rate limit, which is the documented reason the jbs/ libs were moved there. **The gate compares against a cache**, refreshed when missing or stale, falling back to the cached copy when the fetch fails. Offline is therefore not a broken make lint, while a real difference still fails. Fresh clone with neither cache nor network reports that it could not run; CI always has both, so the gate is authoritative where it is enforced. --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 056c93e..784b1f3 100644 --- a/README.md +++ b/README.md @@ -176,9 +176,17 @@ One `standard.mk` every repo includes, with per-repo variation expressed as configuration rather than as a fork. Design RFC and full rationale: [doppler-dsp/doppler#555](https://github.com/doppler-dsp/doppler/issues/555). -**Scope:** all repos in `just-buildit` and `doppler-dsp`. **Canonical home:** -this org (`just-buildit`) — it is already a dependency of every repo's CI -bootstrap, so hosting the standard here adds no new relationship. +**Scope:** all repos in `just-buildit` and `doppler-dsp`. + +**Canonical home:** `just-buildit.github.io`, served at + and hand-edited beside +`aliases.toml`. It must not live in a repo that *consumes* the standard, which +rules out doppler, just-makeit **and `just-buildit/just-buildit`** — the last +of those has a `Makefile` of its own, so it is an adopter like any other. The +org-pages root is a non-consumer whose stated charter is already "the small +static resources the toolchain depends on", and serving from the CDN keeps the +drift gate to a single `curl` with no clone, no auth, and no raw.githubusercontent +rate limit — the same reason the `jbs/` libs were moved there. ### Problem @@ -224,6 +232,14 @@ memory — see criterion 10. the whole family. - **`local.mk`** is included if present and may only *add* targets, never redefine a standard one — otherwise it becomes the fork this prevents. +- **The drift gate compares against a cache, so offline works.** `make lint` + checks the vendored copy against a cached canonical at + `${XDG_CACHE_HOME:-~/.cache}/just-buildit/standard.mk`, refreshing it when + missing or stale. A failed fetch falls back to the existing cache rather + than failing the gate, so no network is not a broken `make lint`; a genuine + difference still fails. On a fresh clone with no cache *and* no network the + check reports that it could not run — CI always has both, so the gate is + authoritative exactly where it is enforced. ### Required files From cfcd5b07fec3f52ef72664ba1e03148121286398 Mon Sep 17 00:00:00 2001 From: "Matthew T. Hunter" Date: Thu, 30 Jul 2026 19:59:17 -0400 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20drift=20gate=20fetches=20canonical?= =?UTF-8?q?=20every=20time=20=E2=80=94=20no=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cache fallback made a hard gate soft. It was reasoned from the failure mode "no network", but the likelier one is the fetch failing while the network is fine — a CDN outage, a bad deploy to the pages repo, a 404 after a rename. Under the cached design that degrades silently into "compared against something older", and on a CI runner (a fresh clone, so no cache) it lands on the "could not run" path. One bad deploy would disable the drift gate across every adopting repo at once, with nothing going red — which is the warning-nobody-reads failure this plan already rejects, arriving through the distribution mechanism instead of the gate's wording. No cache: `make lint` fetches https://just-buildit.github.io/standard.mk every time and fails if it cannot. A gate that cannot reach its reference has not passed — it has not run, and it should say so. Same reasoning as the gate failing rather than warning. Offline `make lint` is the cost, and it is the right one to pay: the gate is enforced in CI, which has the network, and a developer who cannot reach the CDN has not been told their vendored copy is clean — only that it could not be checked. --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 784b1f3..c014b7b 100644 --- a/README.md +++ b/README.md @@ -232,14 +232,15 @@ memory — see criterion 10. the whole family. - **`local.mk`** is included if present and may only *add* targets, never redefine a standard one — otherwise it becomes the fork this prevents. -- **The drift gate compares against a cache, so offline works.** `make lint` - checks the vendored copy against a cached canonical at - `${XDG_CACHE_HOME:-~/.cache}/just-buildit/standard.mk`, refreshing it when - missing or stale. A failed fetch falls back to the existing cache rather - than failing the gate, so no network is not a broken `make lint`; a genuine - difference still fails. On a fresh clone with no cache *and* no network the - check reports that it could not run — CI always has both, so the gate is - authoritative exactly where it is enforced. +- **The drift gate fetches canonical every time, and a failed fetch fails the + gate.** `make lint` compares the vendored copy against + . There is no cache: a cache + would mean the most likely failure — the fetch failing while the network is + fine (CDN outage, a bad deploy, a 404 after a rename) — silently degrades + into "compared against something older", and one bad deploy would disable + the drift gate across every repo at once with nothing going red. A gate that + cannot reach its reference has not passed; it has not run, and it says so by + failing. That is the same reason the gate fails rather than warns. ### Required files From b6d8b1eadc53d0bd74fda8b7f1272d670bac8198 Mon Sep 17 00:00:00 2001 From: "Matthew T. Hunter" Date: Thu, 30 Jul 2026 20:06:45 -0400 Subject: [PATCH 6/6] docs: correct the universal count and scope the pre-commit row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review nits, both cases of a summary line disagreeing with the detail it summarises. `Universal (8)` listed eight names while `install-deps` was declared universal in the very next paragraph — and the stated cap of 38 only reconciles with nine (9 universal + 3 lint- + 2 aggregates + 24 across the feature groups). The list and the count now agree, and both agree with the cap. The `.pre-commit-config.yaml` row claimed it "resolves no tool versions itself". That is not reachable and was never intended: clang-format and cmake-format are not Python dev-group tools, so pre-commit must resolve them, and both repos pin them with a `rev:` today. Criterion 7 and the Non-goals were already correctly scoped to lock-managed tools; only this row stated the absolute — and it is the summary table someone would most likely write the gate from, where a gate built on it would fail on clang-format in both repos permanently. --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c014b7b..ba81829 100644 --- a/README.md +++ b/README.md @@ -209,9 +209,9 @@ site twice; `make wheel` is in `.PHONY` and in `help` with no rule, so it exits ### The standard -Universal (8): `all help setup clean test test-fast lint format`, plus one -`lint-` dispatch target per configured tool. Feature groups defined only -when flagged — `HAS_DOCS`, `HAS_C`, `HAS_DOXYGEN`, `HAS_PYTHON`, `HAS_RUST`, +Universal (9): `all help setup clean test test-fast lint format install-deps`, +plus one `lint-` dispatch target per configured tool. Feature groups +defined only when flagged — `HAS_DOCS`, `HAS_C`, `HAS_DOXYGEN`, `HAS_PYTHON`, `HAS_RUST`, `HAS_BENCH`, `HAS_COVERAGE`, `HAS_RELEASE`, `HAS_EXAMPLES` — plus `test-all` / `gates` aggregates. **Cap: 38 targets with every flag on.** @@ -246,16 +246,16 @@ memory — see criterion 10. Each file owns exactly one concern; nothing states a tool's invocation twice. -| File | Purpose | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `Makefile` | Configuration only — feature flags, path and tool overrides, `include standard.mk`, and repo-local targets. Nothing shared lives here. | -| `standard.mk` | The shared targets, vendored verbatim. Never edited in-repo: the drift gate fails `make lint` on any difference from canonical. | -| `local.mk` | Optional. Included if present; may only *add* targets, never redefine a standard one — otherwise it becomes the fork this prevents. | -| `pyproject.toml` | **Which** tools, at **what** versions (the `dev` group). | -| `uv.lock` | Pins those versions, committed. This is what makes local and CI resolve identically, and so what lets dispatch close the environment-drift class. | -| `.pre-commit-config.yaml` | **When** a check fires. Dispatches inward (`entry: make -s lint-`, `language: system`) and resolves no tool versions itself. | -| `jb.toml` | Tool manifest, with system packages folded in under `[dev.]` and consumed by `install-deps`. (`jb-deps.toml` is the standalone alternative for repos that prefer it.) | -| `.github/workflows/*.yml` | Calls `make `. Anything else must be provably environment plumbing — runner setup, artifact transport, release packaging. | +| File | Purpose | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Makefile` | Configuration only — feature flags, path and tool overrides, `include standard.mk`, and repo-local targets. Nothing shared lives here. | +| `standard.mk` | The shared targets, vendored verbatim. Never edited in-repo: the drift gate fails `make lint` on any difference from canonical. | +| `local.mk` | Optional. Included if present; may only *add* targets, never redefine a standard one — otherwise it becomes the fork this prevents. | +| `pyproject.toml` | **Which** tools, at **what** versions (the `dev` group). | +| `uv.lock` | Pins those versions, committed. This is what makes local and CI resolve identically, and so what lets dispatch close the environment-drift class. | +| `.pre-commit-config.yaml` | **When** a check fires. Dispatches inward (`entry: make -s lint-`, `language: system`) and resolves no versions for lock-managed tools. Non-Python tools that cannot come from `uv.lock` — `clang-format`, `cmake-format` — keep their pinned `rev:` here. | +| `jb.toml` | Tool manifest, with system packages folded in under `[dev.]` and consumed by `install-deps`. (`jb-deps.toml` is the standalone alternative for repos that prefer it.) | +| `.github/workflows/*.yml` | Calls `make `. Anything else must be provably environment plumbing — runner setup, artifact transport, release packaging. | **Adoption adds exactly one file.** Both repos already carry `Makefile`, `pyproject.toml`, `uv.lock`, `.pre-commit-config.yaml` and `jb.toml` today;