diff --git a/README.md b/README.md index eb987b4..ba81829 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,157 @@ 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:** `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 + +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 (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.** + +`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` + 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. +- **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 + +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 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; +only `standard.mk` is new, and `local.mk` is optional and so far unneeded. + +### Success criteria + +Measured against the 2026-07-30 baseline above: + +| # | 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. `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. 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 + 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