From bbd64307aba949bb0735c352e7ff9f7e941312ab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 20 Aug 2026 02:32:21 +0200 Subject: [PATCH] =?UTF-8?q?docs(governance):=20D-263=20=E2=80=94=20one=20C?= =?UTF-8?q?LI=20named=20reach,=20and=20Q-124=20answered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q-124 asked whether the 123-file Python tooling should be retooled into a Rust CLI. The answer is no, and it is a costing rather than a preference. All three frictions it names — per-script permission prompts, the venv/PATH split between interactive and non-interactive shells, and interpreter startup paid four times per push — are packaging problems, and one bare command on PATH with lazy subcommand loading fixes all three. Rust would additionally owe a numerical-equivalence proof on the planet-gen path, whose heightmaps are committed build artefacts with goldens standing on them: a large one-time cost to avoid a small recurring one, paid in the currency the project can least afford to spend. D-263 fixes the shape. tooling/ becomes an installable package behind the `reach` command: a routing-only main.py, every domain under domains// split router/service/schemas/helpers, a core/ bounded on day one to what has no domain, logging and error handling attached as decorators rather than call-site discipline, and pydantic confined to domain schemas — measured at 87 ms against a whole gate check of 20-46 ms, which is why it must never reach the push path. Failures carry the command that fixes them and keep their exit code; a tool that explains itself and exits 0 silently disables its own gate. R-014 records the Rust option as costed down, not argued down, with the condition under which it is worth reopening. T-1247 files the work as eight dependency-ordered epics; only the skeleton is unblocked. Co-Authored-By: Claude Opus 5 (1M context) --- .pql/changelog/ticket_deps/2026-08.sql | 9 +++ .pql/changelog/ticket_history/2026-08.sql | 11 +++ .pql/changelog/ticket_idmap/2026-08.sql | 11 +++ .pql/changelog/tickets/2026-08.sql | 22 ++++++ governance/README.md | 2 + governance/decisions/architecture.md | 89 ++++++++++++++++++++++- governance/questions/architecture.md | 4 +- governance/rejected/architecture.md | 5 ++ 8 files changed, 150 insertions(+), 3 deletions(-) diff --git a/.pql/changelog/ticket_deps/2026-08.sql b/.pql/changelog/ticket_deps/2026-08.sql index f1b8fe579..593592fc0 100644 --- a/.pql/changelog/ticket_deps/2026-08.sql +++ b/.pql/changelog/ticket_deps/2026-08.sql @@ -1 +1,10 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT0TYBD74TQNVKTJMKA8D9KM', '06FT0TZC9MJV6KZTSRVRYQ327M', '2026-07-26 21:56:27.160', '2026-08-06 15:59:23.500', '2026-08-06 15:59:23.500', 'd5c447e9898bbc6d9ef3556b0ed91213', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S392DEG41MRGEFSYWJWP2W', '06G1S3D0M1TQW0GMFBBPQZG3ZM', '2026-08-20 00:24:26.247', '2026-08-20 00:24:26.247', NULL, '39bf5e421900c58b404ab0e13ec3a271', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S392DEG41MRGEFSYWJWP2W', '06G1S39T92PW9JNBPWD0F4JFR0', '2026-08-20 00:24:26.779', '2026-08-20 00:24:26.779', NULL, 'b9a45c52e1102952f2178eda43926904', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S39T92PW9JNBPWD0F4JFR0', '06G1S3DTRHSBYJS55NDT9YR4R0', '2026-08-20 00:24:29.509', '2026-08-20 00:24:29.509', NULL, '39cb42f26d460fb08103e2e4dbcd31f5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3D0M1TQW0GMFBBPQZG3ZM', '06G1S3DTRHSBYJS55NDT9YR4R0', '2026-08-20 00:24:30.055', '2026-08-20 00:24:30.055', NULL, '35605f0847f8a0c6ac3c70d136aa1e03', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3D0M1TQW0GMFBBPQZG3ZM', '06G1S3F6SKZ6EQQ7TY0RKT2970', '2026-08-20 00:24:32.315', '2026-08-20 00:24:32.315', NULL, 'c360233f027f49cad717466f683a8069', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3DTRHSBYJS55NDT9YR4R0', '06G1S3G1D2828RX2F3ZFNWRJGC', '2026-08-20 00:24:32.847', '2026-08-20 00:24:32.847', NULL, '87e729265fef30c452c56e72f612aa37', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3F6SKZ6EQQ7TY0RKT2970', '06G1S3G1D2828RX2F3ZFNWRJGC', '2026-08-20 00:24:35.166', '2026-08-20 00:24:35.166', NULL, '4139189a17dc9c58d5c4fd3fa96b236d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S39T92PW9JNBPWD0F4JFR0', '06G1S3Z1TJHNK1A3YS4HGMBPE4', '2026-08-20 00:26:47.809', '2026-08-20 00:26:47.809', NULL, 'd84cb85d8ed39b4fb6caf70708368031', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3D0M1TQW0GMFBBPQZG3ZM', '06G1S40293BXWK39D9HAXHM8Q4', '2026-08-20 00:26:48.361', '2026-08-20 00:26:48.361', NULL, 'c23fd8b1b991a81c164aa7408406b9a8', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_deps.updated_at; diff --git a/.pql/changelog/ticket_history/2026-08.sql b/.pql/changelog/ticket_history/2026-08.sql index 37cf76f60..2ccefa742 100644 --- a/.pql/changelog/ticket_history/2026-08.sql +++ b/.pql/changelog/ticket_history/2026-08.sql @@ -1243,3 +1243,14 @@ Start here, not with Rust. Measured state: pyproject.toml already exists and mak The real deliverable is ''one bare command reliably on PATH'', not ''a CLI framework''. A [project.scripts] entrypoint lands in .venv/bin/, which is on PATH only when activated — and agents and git hooks never activate it. That is the same split VENV_PY papers over in the Makefile, and the same failure recorded for tea (absolute path breaks the Bash(tea *) rule and prompts every time). Install via uv tool/pipx into ~/.local/bin, or symlink. A Typer app behind an absolute venv path solves nothing. Watch startup cost: a single entrypoint importing every subcommand eagerly pays all 123 modules on every invocation, four times per push. Lazy subcommand registration is mandatory, not an optimisation — measure before and after.', NULL, '2026-08-20 00:00:11', '2026-08-20 00:00:11.592', '2026-08-20 00:00:11.592', NULL, '45f6dd41a5c643a730209c654bd57c76', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3RFJ739EWN4JDG8ZEX89W', 'parent_id', 'T-1253', 'T-1256', NULL, '2026-08-20 00:26:44', '2026-08-20 00:26:44.635', '2026-08-20 00:26:44.635', NULL, 'f71860962dd3296261dca624ae915282', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3SJQKECYH3CKZ3Z215CMW', 'parent_id', 'T-1253', 'T-1256', NULL, '2026-08-20 00:26:44', '2026-08-20 00:26:44.768', '2026-08-20 00:26:44.768', NULL, '7d3de7291e2e58a06c7c7b199f082df0', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S37Y1ARMV68RJT802Z3VPG', 'description', 'Consolidate the 123-file tooling/ tree into one installable Python package behind a single console command, reach. Resolves Q-124; Rust rejected as R-014 because the friction is packaging, not language. Shape per D-263: a routing-only main.py, every domain under tooling/domains// split router/service/schemas/helpers, a bounded core/ holding only what has no domain, shared logging and error handling attached as decorators, pydantic for data shapes but kept off the push-gate import path, installed via uv tool install so reach is a bare name on PATH. Deliberately parentless — this is phase-independent tooling, not cascade work. Six epics; each unfolds into tickets when refined.', 'Consolidate the 123-file tooling/ tree into one installable Python package behind a single console command, reach. Resolves Q-124; Rust rejected as R-014 because the friction is packaging, not language. Shape per D-263: a routing-only main.py, every domain under tooling/domains// split router/service/schemas/helpers, a bounded core/ holding only what has no domain, shared logging and error handling attached as decorators, pydantic for data shapes but kept off the push-gate import path, installed via uv tool install so reach is a bare name on PATH. Deliberately parentless — this is phase-independent tooling, not cascade work. Six epics; each unfolds into tickets when refined. + +Amended 2026-08-20 during the planning session: EIGHT epics, not six. E7 (Adoption) was split out of E6 because retiring old paths and establishing the new way as the only way are different jobs with different failure modes. E8 (Test surface) was added because make test-tooling runs five suites that all move during E3/E5 and nothing else owned rewiring them.', NULL, '2026-08-20 00:26:54', '2026-08-20 00:26:54.553', '2026-08-20 00:26:54.553', NULL, '487f9e6791c4d32c8f01911ee746ac5d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3SJQKECYH3CKZ3Z215CMW', 'description', 'Add the standing rule to settled-reach/CLAUDE.md that all tooling is developed IN the reach CLI and nowhere else — no new loose scripts under tooling/, no new extensionless executables, no new python3 tooling/