Skip to content

Commit 9920399

Browse files
feat(nix): package the CLI as a flake and add a nix lane
Distribution is the flake only — there is no crates.io release — so this is how the CLI is consumed. `nix build .#intent` packages it; `nix flake check` gates fmt, clippy, the crate's test suite, a `--help` smoke test, and a proof that the packaged binary reads a real corpus. Two attributes are needed to package a crate that is not at the repository root, and they are not the same knob. `buildAndTestSubdir` moves only the build and test phases; `cargoSetupPostPatchHook` still reconciles the vendored lockfile against the repository root and fails with "Missing Cargo.lock from src". `cargoRoot` is what points that reconciliation at the crate. `src` stays the whole repository on purpose, so the corpus at `intent/` remains visible to a check aimed at it from the same source tree. The nix lane is a separate workflow rather than a job inside `ci.yml`. Folding a multi-minute Nix build in beside the fast corpus gates would couple them, and running the corpus gates through `nix flake check` would collapse `corpus-strict` and `semantic-review-fixtures` into a single check named `check` — a run would no longer show which gate concluded and how. This lane packages the CLI and re-gates nothing. Adopting the formatter and the lint required two changes to the lifted source: - The block comments carried hand-aligned continuation lines that rustfmt de-indents to column 0, which reads worse than what it replaced. Converted to line comments, which rustfmt leaves alone, so the wording is unchanged and the gate is real rather than permanently red or absent. - `automated_context_indicator` becomes the `Iterator::find` it was written as by hand. `require_section` keeps its eight arguments under an explicit `allow` with the reason inline: this crate is a lift whose acceptance bar is behaving identically, and grouping those arguments is a refactor motivated only by a style lint, better done on its own where a regression would be attributable. Verified locally: `nix flake check` passes all five checks, the packaged binary reports 246 graph nodes against the real corpus, and after these source edits the strict check still reports 0 diagnostics with the graph unchanged at 246/215. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-tool: Claude Code agent-tool-version: 2.1.220 agent-runtime: Claude Code 2.1.220 agent-session-lookup: unavailable tooling-profile: dotfiles@unknown-dirty
1 parent 3f513f5 commit 9920399

7 files changed

Lines changed: 294 additions & 29 deletions

File tree

.github/workflows/nix.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: nix
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: nix-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
# Deliberately its OWN workflow rather than a job inside `ci.yml`. The corpus
18+
# gates there are fast and must stay separately named and independently
19+
# readable; folding a multi-minute Nix build in beside them would couple the
20+
# two, and collapsing them behind `nix flake check` would leave a run showing
21+
# a single check named `check` instead of which corpus gate concluded and how.
22+
# This lane is additive: it proves the CLI packages and that the packaged
23+
# binary works, and it re-gates nothing.
24+
check:
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 30
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: DeterminateSystems/determinate-nix-action@v3
30+
# Builds the package — which runs the crate's test suite via doCheck — and
31+
# evaluates every `checks.*`: fmt, clippy, the `--help` smoke test, and the
32+
# proof that the packaged binary reads a real corpus.
33+
- run: nix flake check --print-build-logs

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ target/
66
# Artifacts the CI steps write into the checkout root while running.
77
/report.json
88
/graph.json
9+
10+
# `nix build` output symlinks.
11+
/result
12+
/result-*

crates/intent/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,13 @@ than assume either location; `.github/workflows/ci.yml` does exactly that.
6262
cargo build --locked --manifest-path crates/intent/Cargo.toml
6363
cargo test --locked --manifest-path crates/intent/Cargo.toml
6464
```
65+
66+
The repository is also a flake, which is the only supported distribution — there
67+
is no crates.io release. `nix build .#intent` packages the CLI, `nix flake check`
68+
runs fmt, clippy, the test suite and a proof that the packaged binary reads a
69+
real corpus, and `nix develop` gives you the toolchain plus `jq` and
70+
`check-jsonschema` that the corpus gates use.
71+
72+
Note that `rust-toolchain.toml` pins the channel for rustup users only. A Nix
73+
build uses whichever toolchain nixpkgs pins and does not read that file; the two
74+
are not expected to agree on a patch version.

crates/intent/src/lib.rs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ use std::io::{self, Write};
88
use std::path::{Path, PathBuf};
99
use std::process::{Command, ExitCode};
1010

11-
/* Positions INSIDE a corpus, so they hold for any repository that adopts the
12-
layout. Resolving the review assets relative to the corpus root rather than the
13-
repository root is what keeps tool and corpus co-located: `review` reads both
14-
from the filesystem at runtime, so a corpus that moves takes them with it. */
11+
// Positions INSIDE a corpus, so they hold for any repository that adopts the
12+
// layout. Resolving the review assets relative to the corpus root rather than the
13+
// repository root is what keeps tool and corpus co-located: `review` reads both
14+
// from the filesystem at runtime, so a corpus that moves takes them with it.
1515
const SEMANTIC_REVIEW_SUBDIR: &str = "15-evaluation/semantic-review";
1616
const REVIEW_PROMPT_ASSET: &str = "16-enforcement/review-prompt.md";
1717
const REVIEW_SCHEMA_ASSET: &str = "16-enforcement/review-result.schema.json";
@@ -208,8 +208,8 @@ impl Defaults {
208208
arg.unwrap_or_else(|| self.corpus_root.clone())
209209
}
210210

211-
/* Fixtures live at a fixed position INSIDE the corpus, so one caller-supplied
212-
corpus root determines both defaults and they cannot drift apart. */
211+
// Fixtures live at a fixed position INSIDE the corpus, so one caller-supplied
212+
// corpus root determines both defaults and they cannot drift apart.
213213
fn fixtures_or_default(&self, arg: Option<PathBuf>) -> PathBuf {
214214
arg.unwrap_or_else(|| self.corpus_root.join(SEMANTIC_REVIEW_SUBDIR))
215215
}
@@ -1200,10 +1200,10 @@ pub fn graph_root(root: &Path) -> Result<GraphReport, Box<dyn std::error::Error>
12001200
})
12011201
}
12021202

1203-
/* Corpus-relative only. The old second branch guessed `context/vrs/.decisions` to
1204-
cover being handed a repository root instead of a corpus root — a guess that was
1205-
silently wrong for any repository laid out differently, and that let a misaimed
1206-
invocation look like a clean one. Pointing this at a corpus is the caller's job. */
1203+
// Corpus-relative only. The old second branch guessed `context/vrs/.decisions` to
1204+
// cover being handed a repository root instead of a corpus root — a guess that was
1205+
// silently wrong for any repository laid out differently, and that let a misaimed
1206+
// invocation look like a clean one. Pointing this at a corpus is the caller's job.
12071207
fn meta_vrs_decision_dir(root: &Path) -> PathBuf {
12081208
root.join(".decisions")
12091209
}
@@ -1537,6 +1537,11 @@ fn check_reference_shape(
15371537
Ok(())
15381538
}
15391539

1540+
// Eight arguments, one over clippy's threshold. Left as-is deliberately: this crate
1541+
// is a lift of `axe vrs`, whose acceptance bar is that it behaves identically, and
1542+
// grouping these into a struct is a refactor whose only motivation is a style lint.
1543+
// Worth doing later, on its own, where a regression would be attributable.
1544+
#[allow(clippy::too_many_arguments)]
15401545
fn require_section(
15411546
root: &Path,
15421547
path: &Path,
@@ -1646,7 +1651,7 @@ fn corpus_asset(root: &Path, relative: &str) -> Result<PathBuf, String> {
16461651
}
16471652

16481653
fn automated_context_indicator() -> Option<&'static str> {
1649-
for name in [
1654+
[
16501655
"CI",
16511656
"GITHUB_ACTIONS",
16521657
"BUILDKITE",
@@ -1659,12 +1664,9 @@ fn automated_context_indicator() -> Option<&'static str> {
16591664
"CODEBUILD_BUILD_ID",
16601665
"DRONE",
16611666
"PRE_COMMIT",
1662-
] {
1663-
if std::env::var(name).is_ok_and(|value| !value.is_empty() && value != "false") {
1664-
return Some(name);
1665-
}
1666-
}
1667-
None
1667+
]
1668+
.into_iter()
1669+
.find(|&name| std::env::var(name).is_ok_and(|value| !value.is_empty() && value != "false"))
16681670
}
16691671

16701672
fn visit_markdown(
@@ -2228,9 +2230,9 @@ mod tests {
22282230
.any(|d| { d.rule == "VRS.ENF.meta-decision-shape" && d.severity == Severity::Error }));
22292231
}
22302232

2231-
/* The extraction's acceptance bar is that `axe vrs` behaves identically, and the
2232-
only thing holding that up is the caller keeping its own default. Locked here
2233-
because a regression is silent: the wrong root still exits 0. */
2233+
// The extraction's acceptance bar is that `axe vrs` behaves identically, and the
2234+
// only thing holding that up is the caller keeping its own default. Locked here
2235+
// because a regression is silent: the wrong root still exits 0.
22342236
#[test]
22352237
fn an_absent_argument_falls_back_to_the_callers_layout() {
22362238
let axe = Defaults::corpus_root("context/vrs");
@@ -2254,12 +2256,15 @@ mod tests {
22542256
let defaults = Defaults::corpus_root("context/vrs");
22552257
let explicit = PathBuf::from("/somewhere/else");
22562258
assert_eq!(defaults.root_or_default(Some(explicit.clone())), explicit);
2257-
assert_eq!(defaults.fixtures_or_default(Some(explicit.clone())), explicit);
2259+
assert_eq!(
2260+
defaults.fixtures_or_default(Some(explicit.clone())),
2261+
explicit
2262+
);
22582263
}
22592264

2260-
/* Covers the branch that replaced the `context/vrs` sentinel. It is only ever
2261-
reached where there is no `.git` — a Nix build sandbox or a vendored source
2262-
tree — so it is invisible to any interactive run. */
2265+
// Covers the branch that replaced the `context/vrs` sentinel. It is only ever
2266+
// reached where there is no `.git` — a Nix build sandbox or a vendored source
2267+
// tree — so it is invisible to any interactive run.
22632268
#[test]
22642269
fn review_workspace_falls_back_to_the_corpus_when_there_is_no_git() {
22652270
let tempdir = tempfile::tempdir().unwrap();
@@ -2280,8 +2285,8 @@ mod tests {
22802285
assert_eq!(review_workspace(&corpus), repo);
22812286
}
22822287

2283-
/* The assets travel with the corpus rather than the repository: that co-location
2284-
is the reason the tool was moved next to the corpus in the first place. */
2288+
// The assets travel with the corpus rather than the repository: that co-location
2289+
// is the reason the tool was moved next to the corpus in the first place.
22852290
#[test]
22862291
fn enforcement_assets_resolve_under_the_corpus_not_the_repository() {
22872292
let tempdir = tempfile::tempdir().unwrap();

crates/intent/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use clap::Parser;
22
use std::process::ExitCode;
33

4-
/* The binary is a thin shell over the library entry point on purpose: `axe vrs`
5-
calls `intent::run` directly, so anything that lived here would be behavior the
6-
embedded caller silently does not get. */
4+
// The binary is a thin shell over the library entry point on purpose: `axe vrs`
5+
// calls `intent::run` directly, so anything that lived here would be behavior the
6+
// embedded caller silently does not get.
77
fn main() -> ExitCode {
88
intent::run(intent::VrsCli::parse())
99
}

flake.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)