A dissolved verb run outside any project writes a runs/ store into cwd #218

Closed
opened 2026-07-07 10:45:38 +02:00 by Brummel · 4 comments
Owner

Surfaced by the milestone "Verb dissolution" (#210) close fieldtest (2026-07-07).

Auto-registration (generated campaign + process documents) is not gated on project presence, so a one-off aura generalize --real … (or any dissolved verb) run from a non-project directory silently creates a content-addressed runs/ store in that directory. The fieldtest's Scenario 1 created a runs/ store with 34 families + 5 campaigns + 4 processes from a handful of commands.

The cycle-0110 fieldtest already flagged "refusals leave no store litter", but a successful no-project run still does.

Resolution (one of): gate store creation on a project (Aura.toml present), or document that verbs record to ./runs when run outside a project.

Surfaced by the milestone "Verb dissolution" (#210) close fieldtest (2026-07-07). Auto-registration (generated campaign + process documents) is not gated on project presence, so a one-off `aura generalize --real …` (or any dissolved verb) run from a non-project directory silently creates a content-addressed `runs/` store in that directory. The fieldtest's Scenario 1 created a `runs/` store with 34 families + 5 campaigns + 4 processes from a handful of commands. The cycle-0110 fieldtest already flagged "refusals leave no store litter", but a *successful* no-project run still does. Resolution (one of): gate store creation on a project (`Aura.toml` present), or document that verbs record to `./runs` when run outside a project.
Brummel added this to the Triage harvest 2026-07 — surface honesty & authoring ergonomics milestone 2026-07-09 17:31:15 +02:00
Brummel added the bug label 2026-07-09 17:31:22 +02:00
Author
Owner

Triage 2026-07-09 (full open-backlog review, tree at 68317ec) — confirmed as a live defect, and an inconsistency rather than a doc gap:

  • Env::runs_root() falls back to the relative PathBuf::from("runs") when no project is discovered (crates/aura-cli/src/project.rs:130-138; the doc comment names this as today's behaviour).
  • The explicit path gates: run_campaign refuses without a project ("campaign run needs a project…", crates/aura-cli/src/campaign_run.rs:322-333, pinned at crates/aura-cli/tests/research_docs.rs:958).
  • The dissolved-verb sugar path does not: the four run_*_sugar functions call env.registry() + register_generated* (store writes) with no provenance check (crates/aura-cli/src/verb_sugar.rs:295/337/453/581, writes at :163-166/:236-239), then reach the campaign runner through run_campaign_by_id, which has no gate either (campaign_run.rs:379/396).

So verbs that are pure sugar over the one campaign path skip the very project gate that path enforces. Fix shape: the same provenance gate ahead of env.registry() in the four sugar entry points, plus a test pinning that a dissolved verb run outside a project refuses with the existing "needs a project" message and leaves no runs/ store behind. Label adjusted to bug accordingly.

Triage 2026-07-09 (full open-backlog review, tree at 68317ec) — confirmed as a live defect, and an inconsistency rather than a doc gap: - `Env::runs_root()` falls back to the relative `PathBuf::from("runs")` when no project is discovered (crates/aura-cli/src/project.rs:130-138; the doc comment names this as today's behaviour). - The explicit path gates: `run_campaign` refuses without a project ("campaign run needs a project…", crates/aura-cli/src/campaign_run.rs:322-333, pinned at crates/aura-cli/tests/research_docs.rs:958). - The dissolved-verb sugar path does not: the four `run_*_sugar` functions call `env.registry()` + `register_generated*` (store writes) with no provenance check (crates/aura-cli/src/verb_sugar.rs:295/337/453/581, writes at :163-166/:236-239), then reach the campaign runner through `run_campaign_by_id`, which has no gate either (campaign_run.rs:379/396). So verbs that are pure sugar over the one campaign path skip the very project gate that path enforces. Fix shape: the same provenance gate ahead of `env.registry()` in the four sugar entry points, plus a test pinning that a dissolved verb run outside a project refuses with the existing "needs a project" message and leaves no `runs/` store behind. Label adjusted to `bug` accordingly.
Author
Owner

Design + approach (specify entry, /boss)

The triage located the fix as a project gate in the four run_*_sugar entry
points. Implementing that surfaced a load-bearing refinement and one large
collateral consequence; both are resolved here before spec production.

Refinement — gate placement. The first store touch on the dissolved-verb
path is not in run_*_sugar but earlier, in validate_and_register_axes
(put_blueprint, main.rs:2529). That function validates the --axis names
first (main.rs:2515-2522, returning UnknownAxis → exit 2) and only then writes
the blueprint. Registry::open is lazy (registry lib.rs:59-61 — it stores a
path, no I/O until a write) and blueprint_axis_probe is store-free, so a
malformed invocation already fails clean with no store litter. The gate
therefore belongs INSIDE validate_and_register_axes, after axis validation and
before the first store write — a single chokepoint shared by all four verbs —
not at the dispatcher top.

Fork — usage error vs. project gate ordering. For a malformed dissolved
verb run outside a project (e.g. mc --real --axis bogus.name), two refusals
compete: exit 2 (unknown axis, a usage error) and exit 1 (no project). Decision:
the usage error wins. Basis: derived — a malformed command is malformed in any
environment, so a usage error is project-independent and already produces no
litter; placing the gate at the dispatcher top instead preempts the
axis-validation refusal, which (a) gives a worse diagnostic ("no project" for a
command that is itself broken) and (b) flips the exit-2 usage-refusal tests
(e.g. mc_dissolved_refuses_an_unknown_axis_name,
walkforward_campaign_refuses_a_raw_form_axis_name) that already pin "a refused
axis name must not start a real run". Gate-at-first-store-touch preserves the
ordering; gate-at-dispatcher-top does not.

Error channel. The gate refuses via a new AxisRegisterError::NoProject
variant, which exit_axis_register_error maps to exit 1 (mirroring its existing
Registry → exit 1 arm). The message carries the verb name and the substring
"needs a project", mirroring run_campaign's wording
(campaign_run.rs:322-333).

Collateral — test migration. A well-formed dissolved verb run to completion
in a bare temp cwd was the pre-#218 behaviour a set of cli_run.rs tests pinned
as correct — the execution/grade tests (sweep_real_…,
walkforward_real_e2e_…, mc_r_bootstrap_real_e2e_…, generalize_real_e2e_…,
plus the *_dissolves_* family). Those migrate onto a project fixture (the
built_project()/project_lock() pattern) so they still pin their grades — a
well-formed verb now correctly requires a project. The exit-2 usage-refusal
tests stay unchanged under the corrected gate placement. The exact migration set
is measured against the corrected placement, not the ~30 tests the
dispatcher-top attempt flipped.

The first implementation line (gate at the dispatcher top, via a RED-first
mini-fix) dead-ended on this collateral and was wound back to the green base;
the work re-enters as a spec-driven cycle. The outside-project refusal is pinned
per verb; the RED assertion the mini-fix produced is folded into that spec's
tests.

## Design + approach (specify entry, /boss) The triage located the fix as a project gate in the four `run_*_sugar` entry points. Implementing that surfaced a load-bearing refinement and one large collateral consequence; both are resolved here before spec production. **Refinement — gate placement.** The first store touch on the dissolved-verb path is not in `run_*_sugar` but earlier, in `validate_and_register_axes` (`put_blueprint`, main.rs:2529). That function validates the `--axis` names first (main.rs:2515-2522, returning `UnknownAxis` → exit 2) and only then writes the blueprint. `Registry::open` is lazy (registry lib.rs:59-61 — it stores a path, no I/O until a write) and `blueprint_axis_probe` is store-free, so a malformed invocation already fails clean with no store litter. The gate therefore belongs INSIDE `validate_and_register_axes`, after axis validation and before the first store write — a single chokepoint shared by all four verbs — not at the dispatcher top. **Fork — usage error vs. project gate ordering.** For a malformed dissolved verb run outside a project (e.g. `mc --real --axis bogus.name`), two refusals compete: exit 2 (unknown axis, a usage error) and exit 1 (no project). Decision: the usage error wins. Basis: derived — a malformed command is malformed in any environment, so a usage error is project-independent and already produces no litter; placing the gate at the dispatcher top instead preempts the axis-validation refusal, which (a) gives a worse diagnostic ("no project" for a command that is itself broken) and (b) flips the exit-2 usage-refusal tests (e.g. `mc_dissolved_refuses_an_unknown_axis_name`, `walkforward_campaign_refuses_a_raw_form_axis_name`) that already pin "a refused axis name must not start a real run". Gate-at-first-store-touch preserves the ordering; gate-at-dispatcher-top does not. **Error channel.** The gate refuses via a new `AxisRegisterError::NoProject` variant, which `exit_axis_register_error` maps to exit 1 (mirroring its existing `Registry` → exit 1 arm). The message carries the verb name and the substring "needs a project", mirroring `run_campaign`'s wording (campaign_run.rs:322-333). **Collateral — test migration.** A well-formed dissolved verb run to completion in a bare temp cwd was the pre-#218 behaviour a set of `cli_run.rs` tests pinned as correct — the execution/grade tests (`sweep_real_…`, `walkforward_real_e2e_…`, `mc_r_bootstrap_real_e2e_…`, `generalize_real_e2e_…`, plus the `*_dissolves_*` family). Those migrate onto a project fixture (the `built_project()`/`project_lock()` pattern) so they still pin their grades — a well-formed verb now correctly requires a project. The exit-2 usage-refusal tests stay unchanged under the corrected gate placement. The exact migration set is measured against the corrected placement, not the ~30 tests the dispatcher-top attempt flipped. The first implementation line (gate at the dispatcher top, via a RED-first mini-fix) dead-ended on this collateral and was wound back to the green base; the work re-enters as a spec-driven cycle. The outside-project refusal is pinned per verb; the RED assertion the mini-fix produced is folded into that spec's tests.
Author
Owner

Spec auto-signed (/boss, grounding-check PASS)

The spec for the dissolved-verb project gate was auto-signed on a
grounding-check PASS with no human in the loop. An independent fresh-context
agent ratified every load-bearing assumption against currently-green tests: the
axis-validation-before-store-touch ordering in validate_and_register_axes,
run_campaign's existing provenance gate, the 24 execution tests that today run
a verb to completion outside a project, the exit-2 usage-refusal tests, and the
built-project fixture pattern.

Scope confirmed empirically: a throwaway probe of the corrected gate placement
(inside validate_and_register_axes, after axis validation) flips exactly 24
cli_run.rs execution tests — all of which pin the pre-#218 behaviour of a verb
running outside a project — while the 6 exit-2 usage-refusal tests stay green.
Those 24 migrate onto the built-project fixture (grades re-pinned in-project,
topology_hash held constant as the no-topology-drift guard); the gate lands
after the migration so the suite stays green throughout.

Design settled and grounded. This comment records the signature for
after-the-fact veto.

## Spec auto-signed (/boss, grounding-check PASS) The spec for the dissolved-verb project gate was auto-signed on a grounding-check PASS with no human in the loop. An independent fresh-context agent ratified every load-bearing assumption against currently-green tests: the axis-validation-before-store-touch ordering in `validate_and_register_axes`, `run_campaign`'s existing provenance gate, the 24 execution tests that today run a verb to completion outside a project, the exit-2 usage-refusal tests, and the built-project fixture pattern. Scope confirmed empirically: a throwaway probe of the corrected gate placement (inside `validate_and_register_axes`, after axis validation) flips exactly 24 `cli_run.rs` execution tests — all of which pin the pre-#218 behaviour of a verb running outside a project — while the 6 exit-2 usage-refusal tests stay green. Those 24 migrate onto the built-project fixture (grades re-pinned in-project, `topology_hash` held constant as the no-topology-drift guard); the gate lands after the migration so the suite stays green throughout. Design settled and grounded. This comment records the signature for after-the-fact veto.
Author
Owner

Cycle closed (/boss, drift-clean)

Landed as two commits: the e2e-test migration into the built project fixture
(2162bd6) and the gate itself (f6d8048). The four dissolved verbs now refuse
outside a project (exit 1, <verb> needs a project, no store) at the shared
validate_and_register_axes chokepoint — placed after axis validation, so a
malformed --axis still exits 2 regardless of project. Four per-verb pins assert
the refusal; the six exit-2 usage-refusal tests stay green. Workspace suite green
(62 groups / 0 failed), clippy -D warnings clean.

Cycle-close audit: drift-clean against the ledger — gate parity with
campaign_run restored, no new invariant, no doc drift. One debt item surfaced
and filed forward as #223: the built_project/project_lock fixture helpers are
now duplicated across three test binaries that share one runs/ store; the
shared tests/common extraction was deliberately deferred to keep this bug fix's
blast radius inside cli_run.rs.

## Cycle closed (/boss, drift-clean) Landed as two commits: the e2e-test migration into the built project fixture (2162bd6) and the gate itself (f6d8048). The four dissolved verbs now refuse outside a project (exit 1, `<verb> needs a project`, no store) at the shared `validate_and_register_axes` chokepoint — placed after axis validation, so a malformed `--axis` still exits 2 regardless of project. Four per-verb pins assert the refusal; the six exit-2 usage-refusal tests stay green. Workspace suite green (62 groups / 0 failed), clippy `-D warnings` clean. Cycle-close audit: drift-clean against the ledger — gate parity with `campaign_run` restored, no new invariant, no doc drift. One debt item surfaced and filed forward as #223: the `built_project`/`project_lock` fixture helpers are now duplicated across three test binaries that share one `runs/` store; the shared `tests/common` extraction was deliberately deferred to keep this bug fix's blast radius inside `cli_run.rs`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#218