feat(basecamp): opt into the inspector build via setup --inspector - #266
feat(basecamp): opt into the inspector build via setup --inspector#266fryorcraken wants to merge 6 commits into
setup --inspector#266Conversation
The last `nix build` for a Basecamp in CI is gone. `lgs basecamp setup --inspector` (logos-co/scaffold#266) selects `.#bin-bundle-dir-inspector` and — the half that actually mattered — classifies it as the portable stack, moving `[repos.lgpm].attr` to `cli-portable` with it. Setting `[repos.basecamp].attr` by hand was expressible all along and would have built the right binary onto profiles it cannot load: lgpm left on `cli` accepts only `-dev` variants, so Basecamp declines every module with one warning line and opens to an empty UI. The rev needs no flag — scaffold reads `[repos.basecamp].pin`, which is already the rev this job wants. But the Nix store cache is still keyed on the workflow's own `BASECAMP_REV`, so the two can now drift: a new step asserts they are equal rather than letting a bumped pin silently turn every run cold. The sitometres probe workaround stays. That was the open question, and the answer is no: scaffold's `basecamp_bin` is inside a nix out-link like `result-bundle` was, read-only, with the ELF still at `.LogosBasecamp.elf` where the probe looks for `.LogosBasecamp`. Confirmed by pointing `--basecamp` straight at it and getting the same "no Basecamp with the QML inspector compiled in". `lgs` changed where the bundle comes from, not what is in it — this one is sitometres' to fix. Also from #266, both restoring what the conversion to `lgs` gave up: - `--print-output` on both workflows' module builds. A failing build now explains itself in the job log instead of pointing at a file on a runner about to be destroyed. - `setup` no longer strips `scaffold.toml` comments, so the `runtime_dir` / `sun_path` notes survive a run. Verified: a `setup --inspector` changed only the two `attr` lines. #266 is unmerged, so `LGS_VERSION` becomes `LGS_REV` — a pinned commit, not a branch, so the tool driving every build cannot move under CI and the cache key stays meaningful. Both workflows must carry the same value. Revert both to a crates.io version when #266 ships in a release; the flag names do not change. Verified locally before pushing: `setup --inspector` in a worktree, then browse.yaml 18/18 green against the resulting bundle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Dogfooded this in the downstream consumer the PR description names, fryorcraken/logos-radicle-module#5. Both flags work as described, and the last raw EvidenceLocally: In CI, all four sitometres specs ( The classification is the part I checked hardest, since it is the half that would fail silently:
Confirming the lockstep matters: with lgpm left on Comment preservation also verified against a real case — this repo's Three notes from the integrationNone are blockers; the first is the only one I would consider acting on. 1. 2. The pin is now load-bearing in a place that can drift. With 3. What still blocks full
|
weboko
left a comment
There was a problem hiding this comment.
Reviewed at a806193. Verified on Linux (x86_64-linux) with Nix 2.35.2 and a real nix on PATH.
What I checked out as correct. The classification argument holds against the pinned flake. In logos-basecamp@aa23776, flake.nix:230 is binBundleDirInspector = withMainProgram (dirBundler appDistributedWithInspector), and appDistributedWithInspector (flake.nix:180-186) is the same ./nix/app.nix import as appDistributed with portable = true; enableInspector = true. nix/app.nix:201 shows portable is what drives -DLOGOS_PORTABLE_BUILD, and it is also what selects the unwrapped $out/bin/LogosBasecamp layout — so adding bin-bundle-dir-inspector to BASECAMP_PORTABLE_ATTRS (and with it Logos/LogosBasecamp rather than Logos/LogosBasecampDev, and cli-portable) is right, not a guess. nix eval .#bin-bundle-dir-inspector.drvPath resolves cleanly at that pin.
The flag itself behaves as documented end to end: setup --inspector writes attr = "bin-bundle-dir-inspector" and [repos.lgpm].attr = "cli-portable" before the build starts, a later --no-inspector moves both back to app/cli, --no-inspector on a project that never opted in declines with a note instead of silently doing nothing, and --inspector --no-inspector resolves last-flag-wins through clap's overrides_with. 797 tests pass (600 lib / 189 CLI / 3 API / 5 doc), cargo fmt --check is clean, and cargo clippy --all-targets reports zero warnings in any file this PR touches.
The findings below are all in the save_project_config half of the change.
1. save_project_config now panics on a scaffold.toml that parse_config accepts (blocking)
Switching the writer from "render into an empty document" to "merge over the parsed original" means write_config_into now inherits whatever shape the user's file has, and the .expect("… table") calls in it stop being structurally unreachable. Previously serialize_config always started from DocumentMut::new(), so every doc.entry(k).or_insert(Item::Table(..)) genuinely inserted a Item::Table and the expects could not fire. They can now.
The trigger is a root-level inline table for any section whose parser tolerates a non-table value by falling back to defaults. parse_config reads these through Item::as_table, which returns None for an inline table, so the file parses fine — and then the writer reaches for as_table_mut() on the inline value and aborts.
Repro (x86_64-linux, PR head):
wallet = { home_dir = ".scaffold/wallet" }
[scaffold]
version = "0.2.0"
# ... otherwise minimal, valid v0.2.0 config$ lgs basecamp setup --inspector
inspector build selected (.#bin-bundle-dir-inspector) — test-only, not a release artifact
thread 'main' panicked at src/config.rs:807:27:
wallet tableConfirmed for four sections, each aborting at its own expect:
| inline key at document root | panic site |
|---|---|
wallet = { … } |
src/config.rs:807 expect("wallet table") |
framework = { … } |
src/config.rs:815 expect("framework table") |
localnet = { … } |
src/config.rs:827 expect("localnet table") |
circuits = { … } |
src/config.rs:839 expect("circuits table") |
src/config.rs:821 (expect("idl table")) is the same shape via [framework] + idl = { … }, and ensure_subtable's expect("parent is a table") becomes reachable the same way for modules = { … } once the config actually carries a module (basecamp modules rather than setup).
Two things make this worth fixing before merge rather than filing as a follow-up. It is an abort with a backtrace, not a scaffold-level error — exactly the "raw stack trace with no scaffold-side hint" that DOGFOODING.md B1 calls a UX regression. And it lands on hand-edited files specifically, which is the population this change exists to be kind to; the file that gets a comment written into it is the file most likely to have been hand-shaped in other ways too.
The unparseable-input fallback doesn't cover it, since the file parses. Either coerce a table-like item into a Table before writing, or treat "modelled key exists but isn't a Table" as the same case as unparseable input and fall back to a fresh render — but please make it a returned error or an explicit fallback rather than an expect, since these are now reachable from file content.
2. [basecamp.profiles] is the one conditional emitter the sweep missed (src/config.rs:932)
env and env_append both got the else { basecamp_table.remove(...) } branch; profiles did not. Same defect the PR describes: clearing the profiles while [basecamp] still exists leaves the old tables stranded in the file.
Confirmed with a test in config::tests:
let original = minimal_v0_2_0()
+ "\n[basecamp]\nport_base = 41000\n\n[basecamp.profiles.alice]\nruntime_dir = \"/run/user/1000\"\n";
let mut cfg = parse_config(&original).unwrap();
cfg.basecamp.as_mut().unwrap().profiles.clear();
let updated = update_config(&original, &cfg).unwrap();
// fails: `[basecamp.profiles.alice]` is still in `updated`;
// serialize_config(&cfg) emits only `[basecamp]\nport_base = 41000`.update_config_clears_keys_reset_to_defaults doesn't catch it because it sets cfg.basecamp = None, which short-circuits to doc.remove("basecamp") at line 979 and never exercises "section kept, profiles emptied". Worth extending that test with the mixed case — it is the vacuous-coverage shape the PR write-up already identified once.
While in there: write_repo_ref is only called under if let Some(repo) = &cfg.basecamp_repo / cfg.lgpm_repo with no else removing a stale [repos.basecamp] / [repos.lgpm]. Nothing sets either back to None today, so it is latent rather than a live bug, but it is the last gap in the "every conditional emitter has a removal branch" invariant the PR states.
3. An unparseable or unreadable scaffold.toml is silently overwritten
update_config does existing.parse().unwrap_or_default(), and save_project_config does fs::read_to_string(&path).unwrap_or_default(). The fallback policy is defensible, but it is silent and there is no backup, so a file that was one stray bracket away from valid is replaced by a from-scratch render — every comment and unmodelled section gone, no message. That is the exact loss the rest of this PR is built to prevent, and the user gets no signal it happened.
unwrap_or_default() on the read also collapses "no file yet" and "file exists but I couldn't read it" (permissions, I/O error) into the same path; the second case silently truncates a file that was there.
A one-line eprintln! naming the fallback would be enough for the parse case. For the read, distinguishing NotFound from other errors and propagating the rest would be better. init's migration already writes a scaffold.toml.bak; matching that here would be the belt-and-braces option.
4. DOGFOODING.md isn't updated, and CONTRIBUTING requires it
CONTRIBUTING.md:44 — "If the change affects user-facing behavior, update README.md and DOGFOODING.md in the same PR." docs/commands.md is updated (thank you — the --inspector paragraph is genuinely good), but nothing in the runbook is.
Concretely missing:
- B1 drives
basecamp setupwith no flags. Nothing exercises--inspector/--no-inspector, the[repos.lgpm].attrrealignment, or the persist-before-build ordering — and B1's "Expected Success Signals" already enumerate per-platformattrbehavior, so the inspector attr belongs alongside it. - B5 covers
basecamp build/build-portablewithout mentioning--print-output. - Nothing anywhere asserts the new comment-preservation contract. This one matters most: it silently changes the behavior of every command that rewrites
scaffold.toml, not just the ones this PR touches, and the failure mode it introduced mid-review (keys reset to defaults being stranded) is invisible unless a scenario looks for it. B1 already says to capture ascaffold.tomlexcerpt as evidence — a check that a comment placed beforesetupis still there afterward, and that a value reset to its default is actually gone, would be cheap and would have caught findings 1–3.
The "Minimum Rerun Guidance" table should probably also gain a row for changes to the config writer, since the blast radius is every config-writing command rather than just basecamp.
5. Minor: basecamp_setup_inspector_persists_both_attrs_before_the_build does real network + Nix work in cargo test
The doc comment frames this as local-only coverage that no-ops in CI, and that's accurate for CI. But on any developer machine that does have Nix — the machines most likely to be working on this code — a plain cargo test now shells out to basecamp setup --inspector, which clones the pinned basecamp repo into the shared cache root and starts a Nix build of a Qt-heavy closure. It's the only test in tests/cli.rs that reaches the network or the Nix store, and it uses the real default cache root rather than a temp one.
The assertion is worth keeping. Gating it behind an explicit opt-in env var, and pointing LOGOS_SCAFFOLD_CACHE_ROOT at the test's own tempdir, would make it safe to leave in the default run.
Findings 1 and 2 are code defects with reproductions; 3 and 4 are behavior/documentation gaps that follow from the same change. I'd hold approval on 1 in particular — an abort on a scaffold.toml that parses is a worse outcome than the comment loss the change fixes.
I could not complete a full basecamp setup here: the container's proxy answers 403 for Nix's github: flake inputs, and a direct-egress run gets past evaluation but then builds the Qt closure from source, which is the environment limitation DOGFOODING.md B1 documents as the usual B-series blocker. Everything above the Nix build — attr selection, lgpm realignment, persist ordering, config round-tripping — was verified against the real binary.
Generated by Claude Code
|
Follow-up to my review above: the Nix build did finish after all, so the caveat I ended on ("could not complete a full
Everything the PR claims about the artefact holds against the real output:
So the feature does what it says end to end. That doesn't change findings 1–5 — the panic in particular is on a different input shape than the one exercised here — but the headline functionality is verified rather than assumed, and I wanted the record to say so. Generated by Claude Code |
* Build the inspector Basecamp through lgs, not raw nix The last `nix build` for a Basecamp in CI is gone. `lgs basecamp setup --inspector` (logos-co/scaffold#266) selects `.#bin-bundle-dir-inspector` and — the half that actually mattered — classifies it as the portable stack, moving `[repos.lgpm].attr` to `cli-portable` with it. Setting `[repos.basecamp].attr` by hand was expressible all along and would have built the right binary onto profiles it cannot load: lgpm left on `cli` accepts only `-dev` variants, so Basecamp declines every module with one warning line and opens to an empty UI. The rev needs no flag — scaffold reads `[repos.basecamp].pin`, which is already the rev this job wants. But the Nix store cache is still keyed on the workflow's own `BASECAMP_REV`, so the two can now drift: a new step asserts they are equal rather than letting a bumped pin silently turn every run cold. The sitometres probe workaround stays. That was the open question, and the answer is no: scaffold's `basecamp_bin` is inside a nix out-link like `result-bundle` was, read-only, with the ELF still at `.LogosBasecamp.elf` where the probe looks for `.LogosBasecamp`. Confirmed by pointing `--basecamp` straight at it and getting the same "no Basecamp with the QML inspector compiled in". `lgs` changed where the bundle comes from, not what is in it — this one is sitometres' to fix. Also from #266, both restoring what the conversion to `lgs` gave up: - `--print-output` on both workflows' module builds. A failing build now explains itself in the job log instead of pointing at a file on a runner about to be destroyed. - `setup` no longer strips `scaffold.toml` comments, so the `runtime_dir` / `sun_path` notes survive a run. Verified: a `setup --inspector` changed only the two `attr` lines. #266 is unmerged, so `LGS_VERSION` becomes `LGS_REV` — a pinned commit, not a branch, so the tool driving every build cannot move under CI and the cache key stays meaningful. Both workflows must carry the same value. Revert both to a crates.io version when #266 ships in a release; the flag names do not change. Verified locally before pushing: `setup --inspector` in a worktree, then browse.yaml 18/18 green against the resulting bundle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Move the GitHub-authored actions off the retired Node 20 runtime Every job logged the same deprecation warning: checkout, cache and setup-node target Node 20, which GitHub has retired, so the runner was already force-running them on Node 24. Bumping to the Node-24 majors (checkout v7, cache v6, setup-node v7, upload-artifact v7, download-artifact v8) matches what was happening anyway rather than changing behaviour — it just stops the warning and removes the cliff when the forcing turns into a hard failure. `node-version` is a separate axis from the action runtime: that is the Node sitometres and the spec-schema validator actually run on. Node 20 is EOL, so it moved to 24 with them. sitometres declares `engines.node: ">=20"` — an open lower bound, not a ceiling — so 24 satisfies it and there is nothing to raise upstream. The two third-party actions are unaffected and stay pinned. Checked rather than assumed: `cachix/install-nix-action@v27` is `using: composite` (no Node runtime at all), and `nix-community/cache-nix-action@v7` is already `using: node24`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Stop a third-party apt mirror from failing the UI job Run 33826324704's `sync` job died before any real work, while its three siblings passed the identical step minutes apart: E: Failed to fetch https://packages.microsoft.com/ubuntu/24.04/prod/… 403 E: The repository '… noble InRelease' is no longer signed. The runner image preconfigures that repo; this job does not use it. All six packages installed here come from Ubuntu's own archive, so a Microsoft mirror having a bad day should not be able to fail a UI test run. Remove its list before updating, and let `update` warn instead of exiting non-zero — `install` remains the real gate and still fails loudly if a package is genuinely unavailable. Also repoints the sitometres probe-workaround comment at the upstream issue tracking its removal, paradoxcomputer/sitometres#1. The one-line fix is `hasInspector`'s candidate list gaining `.<base>.elf`; when it ships, that whole step goes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Disable extra apt sources by pattern, not by filename The first cut of this named /etc/apt/sources.list.d/microsoft-prod.list directly. That was a guess: I never confirmed the filename on a current noble runner image, and if the image has moved that source to the deb822 `.sources` format the `rm -f` silently does nothing — leaving a step that looks hardened and is not, passing only because `update` had been made non-fatal in the same change. Move every source that is not Ubuntu's own out of the way instead, by pattern, covering both `.list` and `.sources`. `find -exec` rather than a glob so that matching nothing is not an error, and `mv -v` into a disabled-sources dir rather than `rm` so the log records what was actually disabled instead of leaving it to be inferred. Also worth recording: this is not a blip that more retries would fix. The runner image sets `APT::Acquire::Retries "10"`, so apt had already retried ten times before surfacing the 403. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Derive BASECAMP_REV from scaffold.toml instead of duplicating it Before this, two places named the same Basecamp revision: the job's `env: BASECAMP_REV`, and `[repos.basecamp].pin` in scaffold.toml. That was harmless while the workflow ran `nix build` with the env var, because one variable fed both the build and the store cache key. It stopped being harmless when `lgs basecamp setup` took over the build: setup takes no --rev and reads the pin, so the workflow's literal became a second source of truth for one fact. The divergence would have been invisible. Bump the pin and forget the env var, and the cache keys on the old rev — restoring a store full of the previous Basecamp, which the new build cannot use, so it recompiles from scratch on every run. Nothing errors, the specs still pass, the job just quietly costs ~9 minutes instead of ~3. CI slowness gets blamed on runners. So read the pin from scaffold.toml and export it. `tomllib` from the stdlib (3.11+, runner ships 3.12) rather than a TOML CLI: this is one scalar, and adding a `pip install` to fetch a TOML reader — in the step right after the one hardening against package-source flakiness — is a poor trade. It parses TOML as TOML rather than regexing it, and asserts a full 40-char hex sha, since a truncated pin still yields a usable cache key while silently not being the rev anyone meant. Ordering is load-bearing: the resolve step must precede the cache step, which is why it is separate rather than folded into setup. `env.X` in a `with:` expression resolves against the accumulated step environment, so the cache key does see the $GITHUB_ENV write. The post-setup equality check stays, with its rationale narrowed. It can no longer catch a hand-editing slip — there is nothing left to mis-edit — but it still pins the assumption that scaffold builds the pin it was given, which is worth asserting while `setup --inspector` is unreleased. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Say why the pin assertion rejects uppercase hex Verified the assertion locally against every shape that would otherwise yield a plausible-looking cache key — truncated, empty, a branch name, 40 non-hex chars, and uppercase — and confirmed it accepts the real pin. Also confirmed the derivation resolves to exactly the rev the workflow previously hardcoded, so the switch to reading scaffold.toml leaves the cache key unchanged rather than forcing one cold run. The uppercase case is the non-obvious one and the one someone will otherwise "relax": the pin goes into the cache key verbatim, so `AA23…` and `aa23…` key two separate caches for the same rev and quietly halve the hit rate. Rejecting is better than normalising, since scaffold builds whatever the file says and only the file should decide the rev. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Read the Basecamp pin with tomlq, per review Replaces the inline Python. My objection to a TOML CLI was that it needs installing; the review's answer is that this job already runs apt, so add it there — which is right, and cheaper than the reasoning I used to avoid it. The apt step moves up to just before the resolve step as a result. It now serves two unrelated needs in one transaction: the graphics libraries the Basecamp bundle resolves at load time, and `tomlq` for the pin. Ordering is fenced on both sides — after apt (which provides tomlq), before the cache step (which keys on what resolve exports). Worth knowing: the runner preinstalls `yq 4.53.6`, but that is the Go yq and ships no `tomlq`. The apt `yq` is the Python one that does. Two tools sharing a name, so the install is not redundant, and `tomlq --version` runs immediately after so a future image change that swaps them fails here rather than at the resolve step. Verified locally against the real scaffold.toml: resolves the same rev the workflow previously hardcoded, so the cache key is unchanged. The validation still rejects truncated, empty, branch-name, non-hex and uppercase pins — and, new to this approach, the literal string "null", which is what `tomlq -r` prints for a missing key where Python would have raised KeyError. Confirmed that case rather than assumed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the two Basecamp assertions fail legibly, not cryptically Three review agents ran read-only over this branch. Two of them independently landed on the same step — the one that checks the built Basecamp — and for the same reason: the step whose entire job is to explain a failure produced the least legible failure in the file. `. .scaffold/state/basecamp.state` had two problems. Sourcing executes the file, and `basecamp_bin` is a path, so a value carrying $(...) would run. And scaffold writes each key CONDITIONALLY (`if !state.pin.is_empty()` in its state.rs, with a unit test asserting the minimal file omits the other two), so a missing key is a real shape — one that surfaced as a bare `pin: unbound variable` from `set -u`, with no ::error:: and no hint, at exactly the moment someone bumps LGS_REV and the state format has moved under them. Now parsed with sed and asserted non-empty, naming LGS_REV in the message. `strings … | grep -q` conflated two different failures. With no pipefail the exit status is grep's alone, so a bundle whose layout moved — `strings` writing to stderr and printing nothing — reported "no inspector" for what was really "wrong path". That matters here because the premise of this PR is that the bundle moved once already, from result-bundle/ into scaffold's cache. Split into an explicit -f test and the grep, each with its own message. Same shape in the .lgx variant loop: without nullglob an empty directory leaves the literal pattern as "$f", and the run fails blaming a missing linux-amd64 variant rather than saying build-portable produced nothing. Now nullglob plus a count assertion — two modules, so a drop to one is caught here rather than as a spec timeout later. Verified rather than assumed: each case was run against a sandbox fixture, and each fails with its own message and exit 1 while the happy path still passes. Also from the reviews, comment-only: - The resolve step's ordering note said it must run "before the apt step that installs tomlq" when the real (and correct) order is after. Rewritten as the three constraints that actually hold — after apt, before the cache step, and before `setup --inspector`, which rewrites scaffold.toml's attr keys in place. Safe today because setup never touches `pin`, but that is now written down rather than left to be rediscovered. - Both workflows note that LGS_REV lives on an unmerged PR branch, so a cargo-install failure across every job means re-pin, not debug cargo. Not hypothetical: #266 was force-pushed while this change was being written. And two CLAUDE.md fixes: - It told a reader to `cargo install logos-scaffold --version 0.3.1` and then claimed the e2e sequence "was run against released v0.3.1". Step 1 of that sequence is `setup --inspector`, which does not exist in 0.3.1 — so following the file got you an unrecognised-flag error. The pinned build is now the install instruction, with the release kept as background. - "It used to carry one" had lost its antecedent to an earlier edit and read as though the yq package carried a rev. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Re-pin LGS_REV to scaffold#266's current head #266 was force-pushed mid-session: 16b4b514 → 8f41e37e. The old pin, 7ba5f981, was two heads behind and is the head of no branch — reachable today only because the PR keeps it alive, which is precisely the hazard the comment added in f92cc18 warns about. Moving to the current head shortens that exposure rather than relying on GitHub not collecting it. All four of #266's checks are green at 8f41e37e (two `validate`, two Template E2E `render-and-build`), which is the bar for taking it. Verified before pinning CI to it, rather than assuming a same-branch commit is equivalent: * `cargo install --git … --rev 8f41e37e` builds clean (two pre-existing `unexpected cfg` warnings, no errors); * `basecamp setup --help` still carries `--inspector` — and documents the same persist-and-`--no-inspector` behaviour the workflow comment relies on; * `basecamp build-portable --help` still carries `--print-output`. So this is a rev change and nothing else: no flag renamed, no semantics moved, and no workflow step needed editing. All three sites move together — both workflows and CLAUDE.md's local install command. The two workflows must not diverge (a job pair on different scaffold builds is the desync `LGS_REV` exists to prevent), and CLAUDE.md's command is what a teammate would run to reproduce CI locally. Both cache keys change with the rev, so the next run reinstalls lgs once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin the sitometres probe fix, and delete the workaround it existed for The copy-and-symlink step is gone. `--basecamp` now points straight at scaffold's own `basecamp_bin` inside the read-only out-link: no `cp -RL`, no `chmod -R u+w`, no second filename manufactured beside the wrapper, and nothing left coupling this workflow to lgs's bundle layout. Published sitometres (0.1.0) could not run this bundle at all. `hasInspector()` probed `bin/LogosBasecamp` (a ~5 KB sh wrapper) and `bin/.LogosBasecamp` (absent), never the `bin/.LogosBasecamp.elf` that nix's dirBundler ships, and refused a build that plainly has the inspector. Filed as paradoxcomputer/sitometres#1; fixed by #3, which probes three spellings (`.<base>`, `.<base>.elf`, `.<base>-wrapped`) and resolves symlinks first. #3 is unmerged, so SITOMETRES is a git commit, not an npm version — same shape and same reasoning as LGS_REV, including pinning a COMMIT rather than the branch. A moving ref would silently change the tool gating every spec, which is exactly what scaffold#266's mid-session force-push demonstrated. `npx` on a git ref runs `prepare`, so the TypeScript compiles at install time. Verified locally rather than assumed, and the result was not what the old comment predicted: * the pinned build launches Basecamp from scaffold's path directly — inspector attached on its port, spec reached step 1; * the OLD workaround now FAILS against it. `cp -RL` dereferences the `.elf` into a real file, and the symlink beside it no longer satisfies a probe that resolves before looking: "no Basecamp with the QML inspector compiled in", before a single step runs. So deleting the step is not merely safe with this pin — it is required by it. The workflow comment says so, and says not to reintroduce it. Also removes the "built packages carry the variant" assertion, per review. It was a diagnostic, not a gate: it could not fail a correct build, and `build-portable`'s whole purpose is that variant. The failure it guarded — the wrong variant surfacing as one Basecamp warning, an empty UI and a step-one timeout — is already written up above `Build both modules (portable)`, which is where anyone hitting it will be reading. Not reintroduced under another name. Two paths became variables while touching these lines: APP_DIR (job env) and BASECAMP_BIN (already exported from the state file), so the portable directory is named once rather than in three steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bin-bundle-dir-inspector` — the Basecamp output with the QML inspector compiled in — could not be selected through scaffold, leaving a sitometres UI end-to-end suite as the last thing reaching for raw `nix build` in an otherwise fully `lgs`-driven project (#265). Surface it as an intent rather than an attr name. `basecamp setup --inspector` selects the build and says it is test-only; `--no-inspector` restores the default; neither flag leaves the configured attr alone, so a plain re-run never silently drops the opt-in. Hand-writing the flake attr in scaffold.toml would have worked too, but it makes users spell an upstream output name that upstream marks explicitly as not a release artifact. The classification itself is what was wrong: upstream builds the inspector output from the same `portable = true` derivation through the same directory bundler as `bin-bundle-dir`, differing only by a compile-time flag, so it is the portable stack by construction. Classified as dev it picked `LogosBasecampDev` for the XDG subpath and the `cli` lgpm variant, and Basecamp then declined to load every module with a single `variant ... not supported on this platform` line and opened to an empty UI. Three adjacent instances of that same quiet failure, all reachable from the new flag: - `[repos.lgpm].attr` was only defaulted when empty, so toggling the stack on an already-set-up project left a stale `cli` against a portable build. The two are one choice and now move together; an attr scaffold does not manage is preserved but warned about. - The choice was persisted only after the (long) basecamp build, so an interrupt or an unrelated failure reverted it. An explicit flag now writes both attrs before the build. - `--no-inspector` compared the scalar `attr`, so on a project with a per-platform map it did nothing and said nothing. It now compares what `effective_attr` actually resolves, and reports when it declines to act. The flags edit only the current host's entry — `setup` builds for the machine it runs on, so a Linux developer's toggle must not discard the project's `aarch64-darwin` mapping. Also from the issue's follow-up: `basecamp build` / `build-portable` accept `--print-output`, reusing the flag `install` already exposes rather than adding a second name for it. Without it there is no log-carrying module build left in CI — a failing build reports the derivation failure without the compiler output saying why. Docs record `.scaffold/state/basecamp.state` and its three keys, which `setup` has always written and nothing documented: a harness needs `basecamp_bin` rather than deriving a path whose correct entry point varies by basecamp generation and stack. Preserving `scaffold.toml` comments — the issue's other aside — is a change to the writer every command goes through, and is split out to `fix/preserve-scaffold-toml-comments`. Closes #265 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b44f875 to
cd4fe50
Compare
`--no-inspector` keyed the `[repos.lgpm].attr` realignment on `inspector.is_some()` — on "a flag was passed" rather than on "the stack actually moved". So on a project scaffold had never put on the inspector build, `--no-inspector` printed its "nothing to undo" note, left `[repos.basecamp].attr` alone as documented, and then rewrote lgpm anyway: a hand-set `cli-portable` against a dev basecamp silently became `cli`, persisted before the build. The flag appeared to do nothing while acting on the half of the pair the user cannot see, which is the exact silent mismatch the feature exists to close. `apply_inspector_selection` now returns whether this host's effective attr moved, and that gates both the realignment and the pre-build persist — so a run that changes nothing also writes nothing, rather than rewriting `scaffold.toml` to say what it already said. `--inspector` on a project already on the inspector build is likewise a no-op. Also from review: - `--inspector` / `--no-inspector` are now `conflicts_with`, matching `run`'s `--reset` / `--no-reset`. Last-wins was the wrong default for a flag that persists to `scaffold.toml`: a fat-fingered pair would have rewritten the project's stack with no diagnostic. The tri-state collapse moves into a named `inspector_selection` with its own test — the neither-flag `None` is load-bearing, and a regression to `Some(false)` would drop every project's opt-in on the next plain `setup`. - `basecamp_setup_inspector_persists_both_attrs_before_the_build` is gated behind `LOGOS_SCAFFOLD_E2E_NIX=1` and points `LOGOS_SCAFFOLD_CACHE_ROOT` at its own tempdir. On a developer machine with nix it was shelling out to a real `setup` on every `cargo test` — a clone of the pinned basecamp repo and the start of a Qt closure build. It was the only test in the suite touching the network, and the CLI target's wall time drops from 25s to 5s without it. - DOGFOODING `B1` gains the inspector round trip (both directions, the no-flag re-run, the portable `module_root` check, and the rejected flag pair); `B5` gains `--print-output`; the rerun guidance gains a row for the stack selector, whose failures are quiet by construction. Findings 1-3 of the review land on `src/config.rs`, which this branch no longer touches — that work is PR #267. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dogfooding this PR in a downstream consumer surfaced the gap the previous commit thought it had closed. `docs/commands.md` documents `.scaffold/state/basecamp.state` and its three keys — but a consumer asking "where does `setup` leave the binary" goes to `basecamp docs`, which prints `docs/basecamp-module-requirements.md`, and that doc covers the module contract thoroughly while never mentioning the state file at all. The reported outcome is exactly the one the docs exist to prevent: hand-reconstructing `~/.cache/logos-scaffold/basecamp/<pin>/app-result/ bin/…`. Move the contract to the doc that gets printed, and say the three things a consumer cannot infer from the path: - Read `basecamp_bin`; do not reconstruct it. Which file under the nix output is the entry point varies by generation and stack — a `/bin/sh` launcher on the dev `#app` build, the unwrapped binary on a portable one, inside `Contents/MacOS/` on `bin-macos-app`. Picking wrong starts an app with no Qt plugin path, which reads as a Basecamp bug. - It is a read-only out-link, and on portable builds the real ELF is a dot-prefixed sibling (`.LogosBasecamp.elf`). A harness needing a writable tree has to copy, not modify in place. - It moves when the stack moves. `--inspector` rebuilds at a different attr, so a path cached across a `setup` silently execs the previous build — the failure the flag was added to make impossible. Also folds in the pin-drift trap from the same dogfooding report. A consumer keyed on its own copy of the basecamp rev is pinning it twice and nothing reconciles the two; that is the consumer's bug to fix, but it is inherited by every repo doing this conversion, so it costs one paragraph here to warn about rather than one silent cold cache each. `docs/commands.md` keeps its summary and now points at the fuller treatment. `DOGFOODING.md` B1 reads the state file after both the plain `setup` and `--inspector` — two excerpts, so "basecamp_bin moves with the stack" is evidenced rather than asserted — and the rerun guidance gains a row saying plainly that this file is a public interface despite living under `.scaffold/`: a key rename breaks downstream CI with no scaffold-side error at all. No behavior change; the doc is `include_str!`-embedded, so `basecamp docs` picks it up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@weboko — thank you for this. The verification half in particular (checking One structural note before the findings: you reviewed at 1.
|
danisharora099
left a comment
There was a problem hiding this comment.
Reviewed at f207de3.
The in-memory logic in apply_inspector_selection and align_lgpm_attr is careful and the unit tests around it are good. The problems are at the seams: where that logic meets the config writer, the build path, and the docs. Two of them undercut the PR's own goal, so I am holding on those.
Blocking
--inspectoris silently not persisted when the project has a per-platform attr map without an entry for this host. The config writer drops the scalar whenever a map exists. Inline comment onbasecamp.rs:354.--print-outputonbuild/build-portabledoes nothing. The module build path never reads the flag. Inline comment onbasecamp.rs:141.
Should fix here, all about the same state file
- Docs and runbook say
basecamp_binmoves when the stack moves. It does not; the out-link is keyed by pin only. Inline onDOGFOODING.md:1073andbasecamp-module-requirements.md:39. - The
--inspectorthen--no-inspectorround trip replaces a hand-set portable attr with the default. Inline onbasecamp.rs:376. - Persisting before the build can leave config and
basecamp.stateon different stacks, anddoctorcannot tell. Inline onbasecamp.rs:248.
Minor
docs/basecamp-module-requirements.md:249: theLOGOS_DATA_DIRrow lists three portable attrs. The inspector attr now qualifies too.BasecampCommand::Setupchanged from a unit variant to a struct variant on the public API with no version bump. Inline onapi/mod.rs:648.- The E2E test could run in CI with a stub
nixon PATH. Inline ontests/cli.rs:3405. - The last line of the new DOGFOODING block has no
|| true. Inline onDOGFOODING.md:1079.
The existing verification on this thread ran on a project with a plain scalar attr, so it does not exercise point 1, and nobody checked that --print-output actually streamed anything. Happy to re-review once 1 and 2 are in.
| .attr_platform | ||
| .insert(system.to_string(), BASECAMP_ATTR_INSPECTOR.to_string()); | ||
| } else { | ||
| basecamp_repo.attr = BASECAMP_ATTR_INSPECTOR.to_string(); |
There was a problem hiding this comment.
Blocking: this write is lost whenever the project already has a per-platform map.
write_repo_ref (src/config.rs:1053) emits only the inline map when attr_platform is non-empty and drops the scalar. So on a project like:
[repos.basecamp]
attr = { aarch64-darwin = "bin-macos-app" }a Linux developer running setup --inspector gets lgpm.attr = "cli-portable" on disk but never bin-bundle-dir-inspector. The next plain setup defaults the scalar back to app and builds a dev basecamp against portable lgpm, which is the exact empty-UI mismatch this flag exists to prevent. A later --no-inspector reports "nothing to undo".
The unit tests only cover a map that already contains the current host, so this path is untested.
Suggested fix: when the map is non-empty, always write this host's key into the map and never touch the scalar. That also turns the --no-inspector branch below into a plain remove(system) with no scalar-repair heuristic.
| variants, | ||
| module, | ||
| print_output, | ||
| } => { |
There was a problem hiding this comment.
Blocking: --print-output is a no-op on this path.
The only nix spawn reachable from cmd_basecamp_build is run_build_portable_nix (around line 2087), which calls cmd.output() and never consults print_output_enabled(). Output is captured and discarded on success, and only trimmed stderr surfaces on failure, exactly as before the flag existed. The help text is also wrong on the other half: build never writes a .scaffold/logs file.
basecamp_build_accepts_print_output_flag only checks --help, so nothing catches this.
Either route that spawn through run_logged the way setup and install do, or drop the flag from this PR.
| # since the attrs are written before the build starts. | ||
| "$SCAFFOLD_BIN" basecamp setup --inspector | ||
| grep -n 'attr' scaffold.toml | ||
| cat .scaffold/state/basecamp.state # basecamp_bin must have moved |
There was a problem hiding this comment.
This assertion is false on Linux, and a dogfooder following it will file a bug that is not one.
build_basecamp_app always writes the out-link to <cache>/basecamp/<pin>/app-result regardless of attr, and both the 0.2.x dev launcher and the portable bundle resolve to bin/LogosBasecamp. So basecamp_bin is byte-identical before and after --inspector.
The real hazard is the inverse of what the docs describe: the shared symlink gets re-pointed, so a consumer holding the old path silently execs the new build, and the displaced stack loses its GC root so the next toggle back rebuilds. Keying the out-link by attr (app-result-<attr>) would make this line true and let both stacks coexist.
|
|
||
| - **Read it; do not reconstruct it.** The path is under the cache root (`~/.cache/logos-scaffold/basecamp/<pin>/app-result/…` by default, and wherever `LOGOS_SCAFFOLD_CACHE_ROOT` or `[scaffold].cache_root` points otherwise), but *which file* under that output is the correct entry point differs by basecamp generation and by stack. On a dev `#app` build it is a `/bin/sh` launcher that exports `QT_PLUGIN_PATH` / `QML2_IMPORT_PATH` before exec'ing the real binary; on a portable build (`bin-bundle-dir`, `bin-bundle-dir-inspector`, `bin-appimage`) it is the unwrapped binary, because the bundle supplies those paths itself; on macOS `bin-macos-app` it is inside `LogosBasecamp.app/Contents/MacOS/`. Picking the wrong name starts an app that cannot find its Qt platform plugin or QML imports — a failure that looks like a basecamp bug, not a path bug. `setup` resolves this for you and writes the answer. | ||
| - **It is a nix out-link, so it is read-only**, and on a portable build the real ELF sits beside it as a dot-prefixed sibling (`.LogosBasecamp.elf`). A harness that needs a writable copy, or one that probes for a specific file layout, has to account for that — copy the tree out rather than expecting to modify it in place. | ||
| - **It moves when the stack moves.** `setup --inspector` rebuilds at a different flake attr, so `basecamp_bin` points somewhere else afterwards. Re-read the file after any `setup`; caching the path across a stack change silently execs the previous build. |
There was a problem hiding this comment.
Same as the DOGFOODING comment: the path does not change between stacks today, because the out-link is keyed by pin only and both stacks resolve to bin/LogosBasecamp. Either key the out-link by attr so this becomes true, or reword this bullet to say the symlink is re-pointed in place.
| // nothing at all (the map was the only source for this host). | ||
| basecamp_repo.attr_platform.remove(system); | ||
| let resolved = basecamp_repo.effective_attr(system); | ||
| if resolved.is_empty() || resolved == BASECAMP_ATTR_INSPECTOR { |
There was a problem hiding this comment.
The round trip loses a hand-set portable attr, silently.
Start from attr = "bin-appimage", lgpm.attr = "cli-portable". --inspector overwrites the scalar. --no-inspector then lands here, sets app, and align_lgpm_attr drags lgpm to cli. The user is now on the dev stack with no note, because the "left alone" note above only fires when the effective attr was not the inspector. A per-host map entry gets the same treatment: deleted rather than restored. inspector_flags_touch_only_the_current_host_entry asserts this outcome as correct, and docs/commands.md says hand-set values survive.
Restoring the previous value would mean remembering it somewhere, so I am not asking for that here. But the docs should not claim the opposite, and a note when --no-inspector lands on a different stack than the user started from would help.
| // hand-set attr has nothing to persist, and rewriting `scaffold.toml` to | ||
| // say exactly what it already said is churn the user did not ask for. A | ||
| // plain `setup` still writes once, at the end. | ||
| if stack_changed { |
There was a problem hiding this comment.
The comment above says nothing downstream depends on the build having finished. That is not quite right: launch, paths, and doctor derive the XDG subpath and variant key from config, but take the binary from basecamp.state, which does not record the attr.
After an interrupted setup --inspector, config says portable while the binary on disk is still the dev launcher. launch seeds portable profiles and execs the dev app, and doctor reports PASS because the path exists.
Recording the built attr in basecamp.state and having doctor compare it to effective_attr would close this, and also answers the "which build is this?" question from the basecamp_bin comments.
Smaller: on a toggle this early save plus the unconditional save at the end write scaffold.toml twice with identical bytes. Guarding the trailing save with !stack_changed would keep it to one.
| - `run` combines build (which chains `setup`), IDL build, localnet start, wallet topup, and deploy into a single command — the inner loop for day-to-day development. It works with no configuration. If a `[run]` section with `post_deploy` is present in `scaffold.toml`, each hook is executed after deploy via `sh -c` (cwd = project root) with `SEQUENCER_URL`, `NSSA_WALLET_HOME_DIR`, `LEE_WALLET_HOME_DIR`, `SCAFFOLD_PROJECT_ROOT`, `SCAFFOLD_IDL_DIR`, `SCAFFOLD_TOPUP_SKIPPED`, and `SCAFFOLD_DEPLOY_SKIPPED` env vars; when the project has exactly one deployable program, `SCAFFOLD_PROGRAM_ID` and `SCAFFOLD_GUEST_BIN` are also set. If a localnet is already running it is reused; otherwise it is started, and deploy is skipped when the guest binaries + IDL + config and the sequencer instance are unchanged. `--profile NAME` selects a named pipeline from `[run.profiles.<name>]`; `--reset` wipes sequencer state + wallet and re-seeds before the run (`--no-reset` overrides a config-set default); `--post-deploy <cmd>` (repeatable) overrides the configured hooks and `--no-post-deploy` skips them entirely; `--watch` re-runs the pipeline on file changes. `run` covers the deploy loop only — it does not run `wallet -- check-health` or any `basecamp` command. | ||
| - `spel -- ...` forwards raw spel CLI arguments to the project-vendored `spel` binary so any spel subcommand (`inspect`, `pda`, `generate-idl`, …) runs against the project's pinned version without a global install. | ||
| - `basecamp setup` pins basecamp + `lgpm` (read from `[repos.basecamp]` / `[repos.lgpm]` — both `build = "nix-flake"`), builds both (logged to `.scaffold/logs/<timestamp>-setup-*.log`), and seeds per-profile XDG directories for `alice` and `bob` under `.scaffold/basecamp/profiles/`. The two pins move as a set: scaffold's default `lgpm` rev is the one the pinned basecamp release locks, because that same package-manager library is what the app uses to read the modules `lgpm` installed. Existing projects keep whatever they pinned in `scaffold.toml` — bumping means editing both pins and re-running `setup`. Runtime config (`port_base`, `port_stride`) is in `[basecamp]`. | ||
| - `basecamp setup` pins basecamp + `lgpm` (read from `[repos.basecamp]` / `[repos.lgpm]` — both `build = "nix-flake"`), builds both (logged to `.scaffold/logs/<timestamp>-setup-*.log`), and seeds per-profile XDG directories for `alice` and `bob` under `.scaffold/basecamp/profiles/`. The two pins move as a set: scaffold's default `lgpm` rev is the one the pinned basecamp release locks, because that same package-manager library is what the app uses to read the modules `lgpm` installed. Existing projects keep whatever they pinned in `scaffold.toml` — bumping means editing both pins and re-running `setup`. Runtime config (`port_base`, `port_stride`) is in `[basecamp]`. Setup also records what it built as `key=value` lines in `.scaffold/state/basecamp.state` — `pin`, `basecamp_bin`, `lgpm_bin` — which is the supported way to find the binary from a script. Use `basecamp_bin` verbatim rather than reconstructing a path: which file under the nix output is the right entry point differs by basecamp generation and by stack — on a dev `#app` build it is a `/bin/sh` launcher that exports `QT_PLUGIN_PATH` / `QML2_IMPORT_PATH`, and picking the raw binary beside it yields an app that cannot find its Qt platform plugin or QML imports. `setup` resolves that for you. `lgs doctor` prints the same path for humans. [basecamp-module-requirements.md](./basecamp-module-requirements.md#what-setup-leaves-behind--scaffoldstatebasecampstate) — which `basecamp docs` prints offline — carries the full contract for script consumers, including the read-only out-link layout and the fact that the path moves when the stack does. |
There was a problem hiding this comment.
Two corrections in this paragraph:
lgs doctordoes not print the binary path. Onlylgs basecamp doctorreadsbasecamp.state.- The
--inspectorbullet says the flag persists "overriding a per-platform attr map, since the inspector output is built for every supported system", and a few sentences later says both flags "edit only the current host's entry". The code does the second. The first reads as if the whole map is replaced.
| pub enum BasecampCommand { | ||
| /// Build/install the pinned basecamp binary and seed profiles. | ||
| Setup, | ||
| Setup { |
There was a problem hiding this comment.
This changes BasecampCommand::Setup from a unit variant to a struct variant on the public API, so any embedder writing BasecampCommand::Setup stops compiling. Fine if intended, but it deserves a version bump or at least a note, since nothing in tests/ exercises the API surface and CI will not notice.
| /// developer's shared `~/.cache/logos-scaffold` — B1's "do not pollute the | ||
| /// user's home" rule applies to the test suite too. | ||
| #[test] | ||
| fn basecamp_setup_inspector_persists_both_attrs_before_the_build() { |
There was a problem hiding this comment.
This could run unconditionally in CI. ensure_nix_present is only a which("nix"), and the file already stubs binaries on PATH via write_wallet_stub. A stub nix plus a [repos.basecamp] source that fails to clone immediately would prove the write ordering in milliseconds, and the two contains assertions stay as they are. Then the opt-in gate and the nix caveat in the PR description both go away.
| "$SCAFFOLD_BIN" basecamp paths alice | grep -i module_root | ||
| "$SCAFFOLD_BIN" basecamp setup --no-inspector | ||
| grep -n 'attr' scaffold.toml | ||
| "$SCAFFOLD_BIN" basecamp setup --inspector --no-inspector # must be rejected |
There was a problem hiding this comment.
This line is meant to exit non-zero, but unlike the other expected-failure lines in this scenario it has no || true. Under a set -e capture script the block aborts here. Same for the setup --inspector line above on a host where the clone fails: the comment says to run the greps regardless, but the greps come after a command that may exit non-zero.
…ized Review of #266 found two defects that made the feature silently not work, plus a docs claim this branch introduced that was false. `--inspector` wrote the scalar `attr` whenever the per-platform map lacked the current host. But `write_repo_ref` emits the inline map *instead of* the scalar whenever the map is non-empty, so on a project carrying only a colleague's `aarch64-darwin` entry the choice reached disk as nothing at all: `[repos.lgpm].attr` moved to `cli-portable`, the next plain `setup` defaulted the scalar back to `app`, and the result was a dev basecamp built against portable lgpm — precisely the empty-UI mismatch the flag exists to prevent. A later `--no-inspector` then reported "nothing to undo". The decision now keys on whether the project uses a map at all rather than on whether that map happens to mention this host, which also reduces the undo path to a plain `remove(system)` and drops the scalar-repair heuristic. `--print-output` was a no-op on `build` / `build-portable`: `run_build_portable_nix` called `cmd.output()` and never consulted `print_output_enabled()`, so nix's log was captured and discarded exactly as before the flag existed. Only stderr is now forwarded when the flag is set — stdout carries the `--print-out-paths` store paths this function parses, so routing the whole spawn through `run_logged` (which redirects both) would break the build instead of fixing the flag. The help text claimed a `.scaffold/logs` file that `build` never writes; corrected on both verbs. The `basecamp_bin` docs asserted the path moves when the stack moves, which was false: the out-link was keyed by pin alone, so both stacks resolved to the same `app-result/bin/LogosBasecamp`. Rather than only rewording, the out-link is now keyed by attr (`app-result-<attr>`), which makes the claim true and is the better behaviour anyway — a shared link is re-pointed by every toggle, so a consumer holding the old path silently execs the new build and the displaced stack loses its GC root, forcing a rebuild on the way back. Both stacks now coexist under one pin. Also: `--no-inspector` restores the default stack rather than the attr configured beforehand, which scaffold does not remember; it now says so instead of moving the user's stack in silence, and `docs/commands.md` no longer claims hand-set values survive. The trailing `save_project_config` is skipped on a toggle, since the pre-build save already wrote those bytes. Tests: the nix-gated e2e test now runs unconditionally in CI. A stub `nix` satisfies `ensure_nix_present` (only a `which`) and an unresolvable source fails the clone in milliseconds, which is well inside the pre-build window under test — no network, no nix store, 0.01s. Added a sibling covering the map-without-current-host case, verified to fail against the previous code with the reported symptom, and a behavioral `--print-output` test that distinguishes streamed output from the failure-path echo, so deleting the implementation fails it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — this was a genuinely good catch list. Both blocking findings were real, and one of them meant the feature silently did nothing on a common project shape. Pushed as 6b71700. Blocking 1 — the attr write is lost when a per-platform map exists — fixedConfirmed exactly as described. Took your suggested fix. The decision now keys on whether the project uses a map at all, not on whether that map happens to mention this host:
One wrinkle worth flagging: Tests:
Blocking 2 —
|
`BasecampCommand::Setup` became a struct variant in this PR so it could
carry the `--inspector` tri-state. `BasecampCommand` is public on a
published crate with no `#[non_exhaustive]`, so an embedder writing
`BasecampCommand::Setup` stops compiling — and nothing in `tests/`
exercises the API surface, so CI does not catch it. Raised in review; it
was left implicit until now.
Recorded in ADR.md, which is where this repo already documents
consumer-facing surfaces and their migrations (see "Hook Env Contract is a
Documented Public Surface" and the 0.2.0 schema-migration entries). There
is no CHANGELOG and no dedicated API doc, and CONTRIBUTING.md says to add
to ADR.md rather than rewrite it, so this follows the existing convention
instead of introducing a new file. The entry gives the one-line migration:
`BasecampCommand::Setup { inspector: None }` is the previous behaviour.
Also noted in the rustdoc on the variant itself, so it is visible from the
generated API docs without finding this PR.
No version bump here: this repo bumps in standalone `chore:` commits, so
one is expected to follow separately. `#[non_exhaustive]` deliberately not
added — it would itself be a breaking change and is a separate decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@danisharora099 — resolution on the public-API point you raised, pushed as 27ffa8b. The breaking change is now documented rather than implicit, in three places:
The migration, for the record: // before
project.basecamp(BasecampCommand::Setup)?;
// after — `None` is the previous behaviour
project.basecamp(BasecampCommand::Setup { inspector: None })?;No version bump in this PR, deliberately. The repo's convention is standalone
Docs-only commit; CI green on all four checks. |
danisharora099
left a comment
There was a problem hiding this comment.
Re-reviewed at 27ffa8b. Both blockers are fixed and covered by tests that fail against the previous code, and the out-link change makes the docs true rather than just rewording them. Thanks for taking the E2E test suggestion further than I asked, running it in CI unconditionally is the right outcome.
Verified locally: cargo fmt --check clean, lib tests green, the three new CLI tests pass in about a second. One CLI test failed on my first full run (deploy_program_path_json_includes_program_id, a ureq panic) but passes on rerun and on master, so that is a pre-existing flake unrelated to this PR.
Two non-blocking notes inline. Deferring the basecamp.state attr field to a follow-up is fine by me. The version bump is a maintainer call, and the ADR entry gives them what they need to make it.
| // different stack than the user started from is exactly the sort | ||
| // of quiet change this feature exists to surface. | ||
| let restored = basecamp_repo.effective_attr(system); | ||
| if restored != BASECAMP_ATTR { |
There was a problem hiding this comment.
Non-blocking. This condition fires in the opposite case from the one it was added for. The lossy round trip lands on the default after displacing a hand-set value, and there restored == BASECAMP_ATTR, so the note stays silent. In the scalar branch above attr is always set to the default, so the note never fires there. In the map branch it fires only when the scalar is non-default, which cannot be serialized alongside a map. In practice it is close to unreachable.
Printing the note unconditionally on a successful undo would be simpler and would actually cover the case it describes. Fine as a follow-up.
| // would rebuild from scratch. Per-attr links let both stacks coexist under | ||
| // one pin and make `basecamp_bin` genuinely move when the stack does, | ||
| // which is what `basecamp.state`'s consumers are told to rely on. | ||
| let link = out_dir.join(format!("app-result-{attr}")); |
There was a problem hiding this comment.
Non-blocking. lgpm-result in build_lgpm just below is still one shared link across cli and cli-portable, so it has the same re-pointing behaviour this change fixes for basecamp: a toggle replaces the other stack's lgpm and drops its GC root. Much smaller cost since lgpm builds in seconds, but keying it the same way (lgpm-result-<attr>) would keep the two consistent and make lgpm_bin in basecamp.state distinct per stack too. One-liner for a follow-up.
The env table's `LOGOS_DATA_DIR` row enumerated the portable `[repos.basecamp].attr` values as `bin-macos-app`, `bin-appimage`, `bin-bundle-dir` — the set as it stood before this branch added the inspector attr. `BASECAMP_PORTABLE_ATTRS` now carries a fourth entry (`BASECAMP_ATTR_INSPECTOR` = `bin-bundle-dir-inspector`), and the `LOGOS_DATA_DIR` export is gated on `is_portable_basecamp`, which reads exactly that constant. So on a macOS host after `setup --inspector`, `launch` does export `LOGOS_DATA_DIR` and the table said it would not. The same file's prose at line 37 already listed `bin-bundle-dir-inspector` among the portable builds, so the doc contradicted itself. This is also the text `basecamp docs` prints from its embedded copy, so the stale list shipped offline to consumers. Docs-only; the code was already correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
One finding from @danisharora099's review body was missed in my earlier responses — flagging it rather than leaving it silently dropped.
Fixed in 231889c (docs-only, one line; the code was already correct). |
|
Further dogfooding on this, and I've hit something the review discussion hasn't covered: I don't think this is a bug in the implementation. I think it's the flag's shape. What happenedWorking in a module repo, I ran $ lgs basecamp install
...
error: the `.lgx` has no variant for this stack — a dev basecamp loads
`<host>-dev` variants and a portable one loads bare `<host>` variants.
Rebuild with the matching flake output (`#lgx` for dev, `#lgx-portable`
for portable) or align `[repos.basecamp].attr`.Correct behaviour, given the state. So the two things a module developer does most often — run the app, run the e2e specs — are now mutually exclusive modes, and switching between them means re-running Why I think it's the shape, not the implementationThe PR's strongest argument is that But it's also the tell. If selecting this build has to drag lgpm's attr with it, and changes the XDG subpath the profiles are seeded under, and invalidates the recorded A flag on
Each fix is right, and each one is scaffold absorbing another consequence of two modes wearing one command's clothes. What I'd suggest@fryorcraken's framing, and I agree with it: make it a separate verb rather than a flag — $ lgs basecamp setup # the dev stack: install, launch, doctor
$ lgs basecamp setup-inspector # the portable+inspector stack: specsThat would change the shape of the problem in three ways:
If the two stacks could coexist on disk (the attr-keyed out-link from this PR is most of the way there), the verbs could even be idempotent enough to alternate without a full re-setup. That's a bigger change and I'm not asking for it here. Not asking to blockThis PR delivered what it set out to: the last raw But I'd flag One smaller thing that would help regardless of the outcome: |
The end-to-end layer built `#bin-bundle-dir-inspector` through `lgs basecamp setup --inspector`, on the belief that it was the only Basecamp output with the QML inspector compiled in. That belief was wrong, and it is what kept both workflows pinned to a commit on the unmerged logos-co/scaffold#266. Reading logos-basecamp's flake.nix at the pinned rev: `appDistributed` (which feeds #bin-bundle-dir, appimage and macos) passes `enableInspector = false`, while the dev `app` inherits logosQtMcp and sets no such flag. The inspector is off in the SHIPPING outputs — which is what the flake's own comment says — not off in the dev build. Upstream's inspector-driven integration-test and shutdown-test checks both use `appPkg = app`. Verified with the same probe sitometres uses, searching for the literal "[QmlInspector] Inspector server listening on port": #app bin/.LogosBasecamp present #bin-bundle-dir-inspector bin/.LogosBasecamp.elf present #bin-bundle-dir (shipping) bin/.LogosBasecamp.elf ABSENT The shipping row is the control: it proves the probe discriminates rather than matching whatever it is pointed at. ui-tests.yml now asserts the #app row on every run, so a future Basecamp flipping enableInspector off for the dev build fails loudly instead of silently removing what this layer depends on. Switching to the dev app removes three things rather than trading them: * `basecamp setup`, which also built lgpm and seeded alice/bob profiles the job never used, and which rewrites scaffold.toml stripping every comment — the reason the pin had to be read before it ran. That ordering constraint is gone. Running specs no longer conflicts with `lgs basecamp launch` either, since neither `attr` key is touched. * `--variant linux-amd64`. sitometres' hostVariant() defaults to linux-amd64-dev, exactly what `.#lgx` produces; the override existed only because the bundle demands portable variants. * the dev-vs-portable classification problem that is the whole subject of scaffold#265 — it only ever arose from choosing the bundle. With `--inspector` unneeded, both workflows return to released scaffold. The other flag they pinned for, `--print-output` on `basecamp build`, is sugar for LOGOS_SCAFFOLD_PRINT_OUTPUT=1, and print_output_enabled() is read inside run_logged on every build path regardless of subcommand — so a job-level env var is the whole feature on the release. sitometres returns to a published version too. Its git pin existed for the probe bug (paradoxcomputer/sitometres#1), which only ever affected the bundle: the released probe looks for bin/.LogosBasecamp, and the dev app ships exactly that. browse.yaml was run green on 0.1.0 and 0.1.2 against the dev app before making the switch; 0.1.2 is what both the workflow and run-local-e2e.sh now pin. That pin was not free. #266 moved three times while this repo tracked it, and one move reverted a feature already documented here as fixed (save_project_config's comment preservation), silently reintroducing the scaffold.toml comment stripping. Both retracted upstream asks would have been avoided by reading the dependency's source first, which CLAUDE.md now says. Verified before committing: browse.yaml 18/18 green against the dev app on both sitometres versions; run-local-e2e.sh 38/39, the one failure being the step local.yaml documents as seeded-profile-specific and expected to fail against a developer's own node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This is actually likely to be unnecessary, will close once confirmed. |
The end-to-end layer built `#bin-bundle-dir-inspector` through `lgs basecamp setup --inspector`, on the belief that it was the only Basecamp output with the QML inspector compiled in. That belief was wrong, and it is what kept both workflows pinned to a commit on the unmerged logos-co/scaffold#266. Reading logos-basecamp's flake.nix at the pinned rev: `appDistributed` (which feeds #bin-bundle-dir, appimage and macos) passes `enableInspector = false`, while the dev `app` inherits logosQtMcp and sets no such flag. The inspector is off in the SHIPPING outputs — which is what the flake's own comment says — not off in the dev build. Upstream's inspector-driven integration-test and shutdown-test checks both use `appPkg = app`. Verified with the same probe sitometres uses, searching for the literal "[QmlInspector] Inspector server listening on port": #app bin/.LogosBasecamp present #bin-bundle-dir-inspector bin/.LogosBasecamp.elf present #bin-bundle-dir (shipping) bin/.LogosBasecamp.elf ABSENT The shipping row is the control: it proves the probe discriminates rather than matching whatever it is pointed at. ui-tests.yml now asserts the #app row on every run, so a future Basecamp flipping enableInspector off for the dev build fails loudly instead of silently removing what this layer depends on. Switching to the dev app removes three things rather than trading them: * `basecamp setup`, which also built lgpm and seeded alice/bob profiles the job never used, and which rewrites scaffold.toml stripping every comment — the reason the pin had to be read before it ran. That ordering constraint is gone. Running specs no longer conflicts with `lgs basecamp launch` either, since neither `attr` key is touched. * `--variant linux-amd64`. sitometres' hostVariant() defaults to linux-amd64-dev, exactly what `.#lgx` produces; the override existed only because the bundle demands portable variants. * the dev-vs-portable classification problem that is the whole subject of scaffold#265 — it only ever arose from choosing the bundle. With `--inspector` unneeded, both workflows return to released scaffold. The other flag they pinned for, `--print-output` on `basecamp build`, is sugar for LOGOS_SCAFFOLD_PRINT_OUTPUT=1, and print_output_enabled() is read inside run_logged on every build path regardless of subcommand — so a job-level env var is the whole feature on the release. sitometres returns to a published version too. Its git pin existed for the probe bug (paradoxcomputer/sitometres#1), which only ever affected the bundle: the released probe looks for bin/.LogosBasecamp, and the dev app ships exactly that. browse.yaml was run green on 0.1.0 and 0.1.2 against the dev app before making the switch; 0.1.2 is what both the workflow and run-local-e2e.sh now pin. That pin was not free. #266 moved three times while this repo tracked it, and one move reverted a feature already documented here as fixed (save_project_config's comment preservation), silently reintroducing the scaffold.toml comment stripping. Both retracted upstream asks would have been avoided by reading the dependency's source first, which CLAUDE.md now says. Verified before committing: browse.yaml 18/18 green against the dev app on both sitometres versions; run-local-e2e.sh 38/39, the one failure being the step local.yaml documents as seeded-profile-specific and expected to fail against a developer's own node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Run the e2e layer on the dev #app, and CI on released scaffold The end-to-end layer built `#bin-bundle-dir-inspector` through `lgs basecamp setup --inspector`, on the belief that it was the only Basecamp output with the QML inspector compiled in. That belief was wrong, and it is what kept both workflows pinned to a commit on the unmerged logos-co/scaffold#266. Reading logos-basecamp's flake.nix at the pinned rev: `appDistributed` (which feeds #bin-bundle-dir, appimage and macos) passes `enableInspector = false`, while the dev `app` inherits logosQtMcp and sets no such flag. The inspector is off in the SHIPPING outputs — which is what the flake's own comment says — not off in the dev build. Upstream's inspector-driven integration-test and shutdown-test checks both use `appPkg = app`. Verified with the same probe sitometres uses, searching for the literal "[QmlInspector] Inspector server listening on port": #app bin/.LogosBasecamp present #bin-bundle-dir-inspector bin/.LogosBasecamp.elf present #bin-bundle-dir (shipping) bin/.LogosBasecamp.elf ABSENT The shipping row is the control: it proves the probe discriminates rather than matching whatever it is pointed at. ui-tests.yml now asserts the #app row on every run, so a future Basecamp flipping enableInspector off for the dev build fails loudly instead of silently removing what this layer depends on. Switching to the dev app removes three things rather than trading them: * `basecamp setup`, which also built lgpm and seeded alice/bob profiles the job never used, and which rewrites scaffold.toml stripping every comment — the reason the pin had to be read before it ran. That ordering constraint is gone. Running specs no longer conflicts with `lgs basecamp launch` either, since neither `attr` key is touched. * `--variant linux-amd64`. sitometres' hostVariant() defaults to linux-amd64-dev, exactly what `.#lgx` produces; the override existed only because the bundle demands portable variants. * the dev-vs-portable classification problem that is the whole subject of scaffold#265 — it only ever arose from choosing the bundle. With `--inspector` unneeded, both workflows return to released scaffold. The other flag they pinned for, `--print-output` on `basecamp build`, is sugar for LOGOS_SCAFFOLD_PRINT_OUTPUT=1, and print_output_enabled() is read inside run_logged on every build path regardless of subcommand — so a job-level env var is the whole feature on the release. sitometres returns to a published version too. Its git pin existed for the probe bug (paradoxcomputer/sitometres#1), which only ever affected the bundle: the released probe looks for bin/.LogosBasecamp, and the dev app ships exactly that. browse.yaml was run green on 0.1.0 and 0.1.2 against the dev app before making the switch; 0.1.2 is what both the workflow and run-local-e2e.sh now pin. That pin was not free. #266 moved three times while this repo tracked it, and one move reverted a feature already documented here as fixed (save_project_config's comment preservation), silently reintroducing the scaffold.toml comment stripping. Both retracted upstream asks would have been avoided by reading the dependency's source first, which CLAUDE.md now says. Verified before committing: browse.yaml 18/18 green against the dev app on both sitometres versions; run-local-e2e.sh 38/39, the one failure being the step local.yaml documents as seeded-profile-specific and expected to fail against a developer's own node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix run-write-e2e.sh, and share its setup with run-local-e2e.sh Review found that `run-write-e2e.sh` was left on the old world by the previous commit, which is the exact failure `run-local-e2e.sh`'s own comments warn about, reproduced in the file next door. It still pinned the sitometres fork commit, read `.scaffold/state/basecamp.state`, pointed at `.scaffold/basecamp/portable`, and passed `--variant linux-amd64` — and it told the user to run `lgs basecamp setup --inspector`, a flag that does not exist on the released scaffold this branch now pins. Following its own error message got an unrecognised-flag error. `write.yaml` still points readers at it, and neither wrapper runs in CI, so the twelve green checks were no evidence whatever about either. Rather than apply the same four edits twice, the shared half moves to `e2e-env.sh`: the sitometres pin, the Basecamp path with its build hint, the app dir, and both preflight checks. The two wrappers now differ only in the profile they point the run at, which is the only thing that ever distinguished them. Keeping them in step by hand has now failed twice; this makes it structural. Verified by running both, which is the point: run-write-e2e.sh 36/36 passed (was unrunnable before this commit) run-local-e2e.sh 38/39 passed (unchanged; the one failure is the step local.yaml documents as seeded-profile- specific and expected to fail against a developer's own node) Also from review, all stale references the previous commit missed: * scaffold.toml's runtime_dir warning justified itself with "#266's current head, which is what both CI workflows pin via LGS_REV" — a vanished env var. The warning is still true, so it keeps the released-version framing instead, plus the note that no CI job runs `setup` any more. * CLAUDE.md's verb table advertised `build-portable` as "(e2e, AppImage)". The e2e layer no longer uses portable at all, so a reader following the table landed on exactly the dev/portable mismatch the same file spends three paragraphs warning about. Split into the two rows it should have been. * ui-tests.yml's BASECAMP_REV and store-cache comments both still explained themselves in terms of what `setup` builds. * .gitignore's dist/ note named the portable directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing this, together with #265 which it implements — the motivating issue turned out to rest on a false premise of mine. Detail in #265's closing comment; the short version is that The consuming repo now runs its whole end-to-end suite against What is being discarded, and what I would keepTwo separable things landed here. I want to be explicit that only one of them is moot:
One thing worth recordingThe revert of
Apologies for the churn, and thanks for the review time already spent on this. |
|
Link for anyone arriving here later: the downstream change that removed the need for this is fryorcraken/logos-radicle-module#38, now merged. It switches that repo's end-to-end layer to the dev |
Closes #265.
What this does
bin-bundle-dir-inspector— the Basecamp output with the QML inspector compiled in — could not be selected through scaffold, leaving a sitometres UI end-to-end suite as the last thing reaching for rawnix buildin an otherwise fullylgs-driven project.The issue proposed appending the attr to
BASECAMP_PORTABLE_ATTRS. That fixes the build selection but leaves the opt-in as a flake attr name the user has to know exists and spell correctly — for an output upstream deliberately marks as not a release artifact. This surfaces it as an intent instead:--inspectorselects the build and persists the choice, so later runs stay on it without repeating the flag.--no-inspectorrestores the default. It only undoes our attr — a hand-setbin-appimagesurvives, and scaffold says so rather than appearing to act.setupre-run never silently drops the opt-in.setupbuilds for the machine it runs on, so a Linux developer's toggle must not discard the project'saarch64-darwinmapping.What this unblocks
The consumer is a sitometres UI end-to-end suite, which drives Basecamp headlessly through the QML inspector (via
logos-qt-mcp). The inspector is a compile-time feature, deliberately off in every shipping output, so no release artefact can be driven this way.Downstream context: fryorcraken/logos-radicle-module#4 converted both CI workflows to
lgs, with its UI-test job pointing sitometres'--app-dirat.scaffold/basecamp/portable/and running 18/18 steps green. The single rawnix buildit could not convert is the inspector bundle.The classification was the actual bug
Confirmed against the pinned flake (
aa23776, tag 0.2.3):bin-bundle-dir-inspectorisdirBundler appDistributedWithInspector, built from the sameportable = truederivation asbin-bundle-dirand differing only byenableInspector. It is the portable stack by construction — the issue's reasoning holds.Classified as dev it picked
LogosBasecampDevfor the XDG subpath and theclilgpm variant, so Basecamp declined to load every module with onevariant ... not supported on this platformline and opened to an empty UI.Three adjacent instances of that same quiet failure, all reachable from the new flag and fixed here:
[repos.lgpm].attr. Only defaulted when empty, so toggling the stack on an already-set-up project leftcliagainst a portable build. The two are one choice and now move together; an attr scaffold doesn't manage is preserved but warned about.--no-inspectorwas a silent no-op on a project with a per-platformattrmap — it compared the scalar, not whateffective_attrresolves. Now compares the effective attr, and reports when it declines.Also included
From the issue's follow-up comment:
--print-outputonbasecamp build/build-portable. Reuses the flaginstallalready exposes rather than adding--print-build-logsas a second name for the same thing. As the follow-up notes, there was no log-carrying module build left in CI: a failing build reported the derivation failure without the compiler output explaining why.Docs also record
.scaffold/state/basecamp.stateand its three keys.setuphas always written it and nothing documented it — which left--inspectortelling a harness author to build something while leaving them to derive its path, and that path is exactly what they must not derive (the correct entry point under the nix output varies by basecamp generation and stack).That contract now lives in
docs/basecamp-module-requirements.md— the docbasecamp docsprints — rather than only indocs/commands.md. Dogfooding this PR downstream showed why: a consumer looking for "where doessetupleave the binary" goes tobasecamp docs, finds the module contract, and reconstructs a cache path by hand when it isn't there. The section covers the three things a consumer cannot infer — read it rather than reconstruct it, it is a read-only nix out-link whose real ELF is a dot-prefixed sibling on portable builds, and it moves whenever the stack moves (so--inspectorinvalidates a cached path).Breaking change for library embedders
BasecampCommand::Setupgoes from a unit variant to a struct variant so it can carry the tri-state.BasecampCommandis a public enum on a published crate, so any embedder constructing it stops compiling. The CLI is unaffected, and nothing intests/exercises the API surface, so CI does not catch this.Recorded in
ADR.mdand in the rustdoc on the variant itself. No version bump in this PR — this repo bumps in standalonechore:commits, so one is expected to follow separately.#[non_exhaustive]is deliberately not added: it would itself be a breaking change and is a separate decision.Split out of this PR
The issue's other aside —
setuppreservingscaffold.tomlcomments — is a change to the config writer that every scaffold command goes through, so it is on its own branch (fix/preserve-scaffold-toml-comments) with its own PR. It is unrelated to selecting a basecamp build, and reviewing them together would let a problem in either block the other.Testing
cargo fmt --checkclean;cargo clippy --all-targetswarning count unchanged from the base commit (62 pre-existing, 0 new — an earlier revision of this description said 64, which was wrong). None of the 62 is in a file this PR touches.--inspectorpersistsattr = "bin-bundle-dir-inspector"andlgpm.attr = "cli-portable"even when the build is interrupted.The earlier
LOGOS_SCAFFOLD_E2E_NIX=1gate is gone (review feedback).ensure_nix_presentis only awhich("nix"), so a stubnixon PATH satisfies it and an unresolvable[repos.basecamp]source fails the clone in milliseconds — which is well inside the pre-build window the test is actually about.basecamp_setup_inspector_persists_both_attrs_before_the_buildnow runs unconditionally in CI in 0.01s with no network and no nix store, alongside a sibling covering the per-platform-map case.Docs updated per CONTRIBUTING:
docs/commands.mdfor the flags,docs/basecamp-module-requirements.md(whatbasecamp docsprints) for the.scaffold/state/basecamp.statecontract, andDOGFOODING.mdforB1/B5plus the rerun-guidance rows.🤖 Generated with Claude Code