Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://just-buildit.github.io/standard.mk> 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-<tool>` 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-<tool>`; the Makefile invokes `$(DEV_RUN) <tool>`; `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 `<noun>-<qualifier>`**, making the noun a namespace:
`test-python`, `test-rust`, `version-check`. `make test-<TAB>` 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
<https://just-buildit.github.io/standard.mk>. 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-<tool>`, `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.<manager>]` and consumed by `install-deps`. (`jb-deps.toml` is the standalone alternative for repos that prefer it.) |
| `.github/workflows/*.yml` | Calls `make <target>`. 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 <target>` 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 <standard target>` 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-<tool>`
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
Expand Down
Loading