Skip to content
Merged
Show file tree
Hide file tree
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
28 changes: 19 additions & 9 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,19 +221,29 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-22.04

# Toolchain via three-way-parity install.sh (GOVERNANCE §24): same
# semgrep version that dev laptops + devcontainers get, pinned in
# `.mise.toml` as `pipx:semgrep`. No `actions/setup-python` here
# — the host-portability invariant is to minimise GitHub-specific
# surface so switching CI hosts is cheap, and to keep dev-machine
# and build-machine setup as close as possible. Resolves Scorecard
# PinnedDependenciesID #17 + #18 (pip install --upgrade pip + pip
# install semgrep) by removing the unpinned pip-bootstrap surface
# entirely.
steps:
Comment thread
AceHack marked this conversation as resolved.
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.14'

- name: Install Semgrep
run: |
python -m pip install --upgrade pip
pip install semgrep
- name: Install toolchain via three-way-parity script (GOVERNANCE §24)
# Single source of truth for toolchain state. mise installs
# python + semgrep (the latter via the `pipx:semgrep` pin in
# `.mise.toml`, which mise auto-routes through `uv tool install`
# since uv is in the toolchain — see ADR
# docs/DECISIONS/2026-04-27-uv-canonical-python-tool-manager.md).
# shellenv.sh wires BASH_ENV so subsequent run: steps auto-
# source the managed shellenv. Same pattern as build-and-test
# job above.
run: ./tools/setup/install.sh

- name: Run Semgrep
# --error exits non-zero if any rule matches. The 14 rules in
Expand Down
23 changes: 21 additions & 2 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ bun = "1.3"
# uv pinned (round-34 pull-in from ../scratch). Manages Python
# CLI tooling (ruff etc.) via `uv tool install` with
# reproducible versions across laptops + CI. See BACKLOG
# "Python tool management via uv tool (from ../scratch)".
uv = "0.9"
# "Python tool management via uv tool (from ../scratch)". Also
# powers mise's `pipx:` backend below (mise auto-routes `pipx:`
# tools through `uv tool install` when uv is available — no
# pipx package needed, faster + already in our toolchain).
uv = "0.11.8"
# actionlint: GitHub Actions workflow linter. Declarative install
# (GOVERNANCE §24 three-way-parity) so dev laptops + CI runners
# (including ubuntu-slim which doesn't ship with it) have identical
Expand All @@ -62,6 +65,22 @@ node = "22"
# script in package.json) so dev laptops can also `bun run
# lint:markdown`. Both pins point at the same version.
"npm:markdownlint-cli2" = "0.22.1"
# semgrep: SAST linter (the 14 rules in `.semgrep.yml`). Pinned
# here per the three-way-parity invariant (GOVERNANCE §24) — dev
# laptops + CI runners + devcontainers should all install
# semgrep through the same `tools/setup/install.sh` rather than
# CI fetching its own copy via `actions/setup-python` + pip.
# The `pipx:` backend prefix is mise's naming; the actual
# installer mise uses is `uv tool install` (since `uv` above is
# in this toolchain — mise auto-routes pipx: through uv when
# uv is available; faster than real pipx, no separate package
# needed). See docs/DECISIONS/2026-04-27-uv-canonical-python-
# tool-manager.md for the uv-as-canonical decision and lineage
# from ../scratch. Resolves Scorecard PinnedDependenciesID #17
# + #18 (pip install --upgrade pip + pip install semgrep — the
# unpinned pip-bootstrap surface goes away entirely once mise
# owns this tool).
"pipx:semgrep" = "1.161.0"

[settings]
# `python-build-standalone` (upstream for mise's python plugin)
Expand Down
71 changes: 71 additions & 0 deletions docs/DECISIONS/2026-04-27-uv-canonical-python-tool-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ADR: uv is the canonical Python tool manager for Zeta — pipx-named mise tools route through uv

**Date:** 2026-04-27
**Status:** *Decision: `uv` (Astral) is the canonical Python tool installer / runtime / venv manager for the Zeta factory. Tools that the mise registry exposes via the `pipx:` backend (e.g. `pipx:semgrep`) are pinned in `.mise.toml` with the `pipx:` prefix because that is mise's registry naming; the actual installer mise invokes is `uv tool install` (mise auto-routes `pipx:` through uv when uv is in the toolchain — no separate `pipx` package is required). No `pip install` / `actions/setup-python` paths in CI; no parallel pipx-vs-uv camps in the install scripts.*
**Owner:** architect (synthesis); human maintainer (shaping-decision owner).
**Decision confidence:** *high* — choice is dictated by composing two prior commitments (three-way-parity per GOVERNANCE §24 + ../scratch first-class adoption of uv) plus a verified mise behaviour; no novel tradeoff to weigh.

## Context

1. **../scratch made uv first-class first.** `../scratch` is the maintainer's adjacent project where Python tooling decisions get burned in before this factory absorbs them. uv was made first-class there before any Zeta-side commitment, and the round-34 .mise.toml pull-in here cited *"`../scratch`"* explicitly as the source. The lineage matters: this ADR is documenting an *already-made* decision that was sitting implicit in `.mise.toml` comments rather than a fresh choice.
2. **Three-way parity per GOVERNANCE §24.** Dev laptops + CI runners + devcontainers all bootstrap toolchain via `tools/setup/install.sh` → mise → declarative `.mise.toml`. Any second Python tool installer (pipx, bare pip, conda) creates a fork in the install graph and breaks the parity invariant.
3. **mise routes pipx: through uv automatically.** Per upstream mise docs (`mise.jdx.dev/dev-tools/backends/pipx.html`): *"If you have uv installed, mise will use uv tool install under the hood and you don't need to install pipx to run the commands containing 'pipx:'."* Verified locally on 2026-04-27 with `uv = "0.11.8"` + `pipx:semgrep = "1.161.0"` (no separate pipx in the toolchain) — mise installs semgrep via `uv tool install` and `semgrep --version` reports 1.161.0.
4. **Aaron 2026-04-27 explicit course-correction.** During the Scorecard PinnedDependenciesID fix work, Aaron pushed back on an initial draft that added `pipx = "1.11.1"` alongside `uv`:
> "we have uv do we need pipx, isn't there a uvx this should be much faster"

Plus the prior framing:
> "the fact that uv is our desired python setup should be documented somewehre this project ../scratch made it first class too"

This ADR is the documenting move.

## Decision

1. **`uv` is canonical.** Pinned in `.mise.toml` `[tools]` section. All Python-language CLI tooling routes through it.
2. **`pipx:` prefix in mise registry calls is fine.** It is just mise's naming for the namespace; the actual installer is uv. Do NOT add `pipx = "X.Y.Z"` to `.mise.toml` to "satisfy" the prefix — that would install a redundant package that mise will not use.
3. **No `actions/setup-python` in CI.** CI gets Python from the same `mise install` pass that dev laptops run.
4. **No `pip install ...` lines in CI workflows.** Anything that wants a Python CLI tool gets it via `.mise.toml` pin (`pipx:foo = "X.Y.Z"`) or via `uv tool install` invoked from a script that ran through `install.sh`.

## Consequences

**Positive:**

- **Dev/CI parity.** Same Python toolchain, same versions, same install path, same provenance.
- **Speed.** uv's resolver and installer are materially faster than pip+pipx.
- **GitHub artifact attestation verification** for `uv` itself (mise's aqua: backend handles it). One fewer tool in the toolchain (no pipx) means one fewer attestation surface.
- **Resolves Scorecard PinnedDependenciesID** for any future `pip install foo` patterns by removing the pattern entirely from CI.
- **Single-bump surface.** Bumping a Python CLI tool is a single `.mise.toml` edit. CI inherits.

**Negative:**

- **Naming surprise.** `pipx:semgrep` reads as if pipx is involved; the actual installer is uv. The `.mise.toml` comment block on `uv` and on each `pipx:*` entry must call this out (they do, post this ADR). Mitigation: this ADR is linked from those comments.
- **Tied to mise's auto-route behaviour.** If a future mise release changes the auto-route preference (e.g. requires `uvx:` prefix instead), all `pipx:*` entries need a rename. Mitigation: pinned mise version in `.mise.toml`; bump deliberately, with a smoke test.

**Neutral (deferred):**

- A potential future `uvx:` first-class backend in mise would let us drop the `pipx:` prefix entirely. Not blocking; cosmetic.

## What this ADR does NOT decide

- Does NOT decide whether Zeta itself ships Python code (it does not; F# / C# / TS).
- Does NOT decide build-system choice for any future Python *project* (we do not have one). This ADR is about *tooling* — installing Python-language CLI tools (semgrep, ruff, etc.) — not about authoring Python.
- Does NOT decide between `uv tool install` and `uv venv` — both are valid for their respective use cases (CLI tools vs project venvs); they both flow through uv.

## Lineage

- `../scratch` `.mise.toml` (Aaron's adjacent project) — uv made first-class here first
- Round-34 pull-in — Zeta `.mise.toml` cited `../scratch` as the source of the uv pin
- 2026-04-27 — Aaron's explicit ask to document the decision; this ADR

## Composes with

- **GOVERNANCE.md §24** (three-way parity)
- **`docs/DECISIONS/2026-04-20-tools-scripting-language.md`** (no Python authored in Zeta tooling — this ADR is the orthogonal "but Python *tools* are fine, installed via uv" companion)
- **`memory/feedback_three_way_parity_install_scripts_dev_ci_devcontainer_minimize_github_specific_surface_aaron_2026_04_27.md`** (the substrate memory naming Aaron's host-portability invariant)
- **#653** — the PR landing the first concrete `pipx:semgrep` use, removing pip install + actions/setup-python from gate.yml lint-semgrep

## Forward-action

- Land this ADR alongside the `.mise.toml` change in #653
- Update `.mise.toml` comments on `uv` and on each `pipx:*` entry to point at this ADR
- Future `pipx:*` additions cite this ADR in their comment, not the verbose rationale
- If `actions/setup-python` is found anywhere in `.github/workflows/`, file a follow-up issue to migrate to `install.sh + .mise.toml`
1 change: 1 addition & 0 deletions memory/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

**📌 Fast path: read `CURRENT-aaron.md` and `CURRENT-amara.md` first.** These per-maintainer distillations show what's currently in force. Raw memories below are the history; CURRENT files are the projection. (`CURRENT-aaron.md` refreshed 2026-04-25 with the Otto-281..285 substrate cluster + factory-as-superfluid framing — sections 18-22; prior refresh 2026-04-24 covered sections 13-17.)

- [**Three-way-parity invariant — dev/CI/devcontainer share install scripts; minimize GitHub-specific surface so switching CI hosts is cheap (Aaron 2026-04-27)**](feedback_three_way_parity_install_scripts_dev_ci_devcontainer_minimize_github_specific_surface_aaron_2026_04_27.md) — When fixing CI, default-check `.mise.toml` first; reach for GitHub-specific shapes (custom action / container: block / setup-X) only when no parity-preserving option exists. uv-canonical decision documented in `docs/DECISIONS/2026-04-27-uv-canonical-python-tool-manager.md`.
- [**LFG=master, AceHack=0-divergence fork, double-hop AceHack→LFG (Aaron 2026-04-27 strategic reframe)**](feedback_lfg_master_acehack_zero_divergence_fork_double_hop_aaron_2026_04_27.md) — Aaron 2026-04-27: bidirectional content-sync too hard; LFG is master, AceHack is pure fork (0 ahead AND 0 behind), force-push AceHack to LFG state after every paired sync. Replaces Option C parallel-SHA-history. Done criterion: `git diff acehack/main..origin/main` empty AND `git rev-list --count` returns 0 both directions.
- [**0-diff is "start" line — until then we're hobbling (Aaron 2026-04-27)**](feedback_zero_diff_is_start_line_until_then_hobbling_aaron_2026_04_27.md) — Aaron 2026-04-27 reframe: AceHack-LFG content-divergence (53 files / 6065 lines) isn't polish, it's the gate to factory operational status. #43's diff-minimization invariant DEFINES "started." Reverse-sync work moves to high priority. Distinguish commit-count (76/492, NEVER zero, structural) from content-diff (53 files / 6065 lines, CAN reach 0, the actual metric). Forward-action: Batch 1 workflow drift first (~80 lines, 1-2h) as concrete progress on the gate.
- [Laptop-only-source integration HIGH PRIORITY — `../scratch` = future ACE PACKAGE MANAGER seed (22 files); `../SQLSharp` = pre-DBSP event-stream-processing with LINQ/SQL (14 files, predates Aaron's DBSP discovery, Zeta-progenitor); goal = either ship feature OR write detailed-enough design that we no longer need the reference; Aaron 2026-04-27 clarification: NOT literal copy-paste, self-contained-understanding floor; refined triage per directory identity — `../scratch` references absorb into canonical location or design-doc the Ace-package-manager intent; `../SQLSharp` references map to DBSP-rigorous Zeta equivalents or design-doc the gap; sequenced AFTER PR #26 sync](project_laptop_only_source_integration_scratch_sqlsharp_features_or_designs_high_priority_2026_04_27.md) — 2026-04-27 P1 backlog row; per-reference triage with three outcomes (ship / design-doc / delete-decorative); composes Otto-275 (log-but-don't-implement default to design when uncertain) + Otto-323/346 (NOT external deps, in-repo or eliminate) + Otto-340 (substrate IS identity); done = `git grep ../scratch` and `git grep ../SQLSharp` return zero matches; effort L (3+ days); closes with Aaron's "good job today!!" second positive validation; Aaron's third 2026-04-27 clarification reveals `../SQLSharp` features potentially subsumed by Zeta's DBSP-rigorous form (linq-expert + sql-expert + sql-engine-expert skills already track this class).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: Three-way-parity invariant — dev/CI/devcontainer share install scripts; minimize GitHub-specific surface so switching CI hosts is cheap (Aaron 2026-04-27)
description: Aaron 2026-04-27 explicit course-correction during Scorecard PinnedDependencies fix. Otto's first attempt switched gate.yml lint-semgrep job to a SHA-pinned semgrep/semgrep Docker image. Aaron pushed back: this still uses GitHub-specific stuff (Docker container action) AND breaks the dev/CI parity invariant (dev laptops don't run semgrep via Docker). Right answer: install semgrep through the same `tools/setup/install.sh` that dev laptops + devcontainers use — i.e. add `pipx:semgrep` to `.mise.toml`. Composes GOVERNANCE §24 (already-codified three-way parity) + Otto's authority to push toolchain through install.sh + Aaron's host-portability invariant ("easy to switch hosts"). Triggered by Scorecard PinnedDependenciesID #17/#18 work where the convenience-fix (Docker container) would have introduced a real divergence.
type: feedback
---

# Three-way-parity invariant — dev/CI/devcontainer share install scripts

## Verbatim quote (Aaron 2026-04-27)

> "actions/setup-python we should be using our base python that our install scripts install we are trying to not use github stuff unless we have to so it's easy to switch hosts and our dev macchine and build machine setup is the same, that's one of the invariants we want to try to keep as close as possible dev machine / build machines are same/very similar for setup/share the setup/install scripts and post install scripts. this makes CI more deterministic too. if i'm off base here just let me know. but SHA pinning github actions is a supply change risk mitigation good idea."

## The invariant (codified)

GOVERNANCE.md §24 already names this as the "three-way parity" rule:

- **Dev laptop** runs `tools/setup/install.sh` to bootstrap toolchain
- **CI runner** runs `tools/setup/install.sh` to bootstrap toolchain
- **Devcontainer image** runs `tools/setup/install.sh` to bootstrap toolchain

Same script. Same `.mise.toml`. Same versions. No GitHub-specific actions stand in for what install.sh already does (no `actions/setup-python`, no `actions/setup-node`, no `setup-dotnet`).

## What this BUYS

- **Host portability.** If a future CI host swap happens (GitHub Actions → GitLab CI → self-hosted Forgejo runners → BuildKite → ...), the install path is the same script. Only the YAML wrapper differs.
- **Dev/CI parity for debugging.** A failure on CI reproduces locally because the toolchain is bit-identical (same mise pins, same install.sh path).
- **Determinism.** mise resolves declarative pins atomically; pip's dependency-resolver wandering or `actions/setup-python` cache poisoning don't enter the picture.
- **One bump surface.** Bumping a tool version is a single `.mise.toml` edit; CI inherits.

## What was almost violated (today's failure mode)

Otto's first attempt to fix Scorecard PinnedDependenciesID #17/#18 (pip install in lint-semgrep) was to switch the job to a SHA-pinned `semgrep/semgrep:1.161.0@sha256:...` Docker image at the `container:` job level.

That fix:

- **Did** resolve the Scorecard alerts (image bytes are content-addressed)
- **Did NOT** install semgrep on dev laptops via the install path
- **Did** introduce a GitHub-Actions-specific shape (`container:` block) that isn't portable across CI hosts
- **Did** break the dev/CI parity invariant — a dev laptop running semgrep manually wouldn't be running the same version that CI runs

Aaron caught this immediately. The invariant is more important than the immediate fix.

## The right fix (post-correction)

1. Add `pipx = "1.11.1"` to `.mise.toml` (aqua-backed; same SHA-pinned release path as `actionlint` / `shellcheck` / `uv`)
2. Add `"pipx:semgrep" = "1.161.0"` to `.mise.toml` (mise's pipx: backend installs the named PyPI package at the pinned version)
3. Drop `actions/setup-python` + the two `pip install` steps from gate.yml
4. Replace with `./tools/setup/install.sh` (same step build-and-test already uses)

Now: dev laptops + CI + devcontainers all install semgrep 1.161.0 the same way, through the same tooling, pinned in the same `.mise.toml`.

## Operational rule

When fixing a CI-side problem, before reaching for a GitHub-specific solution (custom action, container: block, runner-side feature), check:

- **Does this tool exist in `.mise.toml` already?** If so, use it via install.sh.
- **Could this tool live in `.mise.toml`?** If yes (mise registry has it via aqua / pipx / npm / cargo / etc.), add it there and route CI through install.sh.
- **Is this genuinely GitHub-specific?** (e.g. workflow triggering, GITHUB_TOKEN scope, code-scanning upload) — only then is a GitHub-specific shape correct.

## What this rule does NOT mean

- Does NOT mean "never use GitHub Actions" — `actions/checkout`, `actions/cache`, `github/codeql-action/*`, `actions/upload-artifact` are all genuinely GitHub-specific and stay
- Does NOT mean "never SHA-pin GitHub actions" — Aaron explicitly affirmed: *"SHA pinning github actions is a supply change risk mitigation good idea"*. SHA-pinning the actions we DO use is correct
- Does NOT mean "rewrite all of CI today" — apply the rule going forward; existing breaches are tech-debt to fix opportunistically when touching the surrounding code

## Composes with

- **GOVERNANCE.md §24** — three-way parity rule (this memory is the lived rationale, not just a re-citation)
- **Otto-247 (version-currency)** — version pins in `.mise.toml` get the same WebSearch-first discipline
- **#71 (Otto owns settings)** — install-path decisions are within Otto's authority; the binding decision is "share via install.sh", not "let CI drift"
- **#652 (block-only-on-Aaron-must-do)** — Aaron's earlier framing today: drive forward with best long-term judgment. The course-correction today refines what "best long-term" looks like for CI design

## Forward-action

- File this memory + MEMORY.md row
- Apply the rule to the in-flight fix: replace Docker-container approach with `pipx:semgrep` in `.mise.toml` + install.sh in gate.yml
- Future-self check: when adding a CI step, default-check `.mise.toml` first; reach for a GitHub-specific shape only when no parity-preserving option exists
Loading
Loading