-
Notifications
You must be signed in to change notification settings - Fork 3
kithara devtools
Documentation reviewed from source revision 19ca073f2. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
Owning-crate contracts for the reusable, project-agnostic xtask command core.
README.md is the overview. Command names, flags, and output
contracts belong to the root justfile, .config/just/, and
../../docs/guides/tooling.md; repo-wide rules
belong to ../../AGENTS.md. This file owns only what neither
the code nor a test already states.
Ctx::load and Ctx::load_from_manifest resolve the workspace root once via
cargo metadata --no-deps, parse the project and similarity configs, and retain
the metadata. Ctx::new builds a metadata-less context: a command reaching
ctx.metadata() there gets a typed error instead of a second shell-out to cargo.
Commands take &Ctx and must not re-resolve the root or re-parse config. The one
sanctioned exception is common::walker — the scoped walkers reload
ProjectConfig themselves because the lint namespaces run without a Ctx.
.config/xtask.toml is optional: a missing file yields documented code defaults,
not a fallback chain, and init scaffolds it plus empty per-namespace baselines.
project.name is derived from cargo metadata when absent; it is a human-facing
label (report titles, temp-dir prefixes), so that derivation is a sanctioned
user-facing default rather than state resolution.
[ext] ownership rule. The core schema names generic concerns only. A project
may fill those generic shapes with its own values, but a concern needing
project-specific keys lives under [ext.*] — a raw table the core never
interprets and the consuming bin deserializes into its own typed view (kithara:
xtask/src/config.rs). Every core section is deny_unknown_fields, so a consumer
section at top level is a typed parse error naming the token.
tests/config_contract.rs pins the defaults, the rejection, the passthrough, and
the requirement that a stress config name the directory it builds into.
[tools] ownership rule. A role is a job the core needs done
(ast-grep, typos, samply), never a binary the project happens to
install. program is what a project puts in front of that role — an absolute
path, or a name for PATH to resolve — and defaults to the role name, so a
project that installs the expected tools configures nothing. pin, when
given, names the .config/ci-pins.toml key that fixes the version; the
consuming bin cross-checks the two so a role cannot claim a pin that does not
exist. Version-control programs and the Rust toolchain are deliberately absent:
git, cargo, gh and rustup are the ground a project stands on, and a
config able to redirect them is a hazard, not a feature.
[workspace-scan] exclude globs apply in the scoped walkers only; raw .rs
discovery stays a pure directory walk.
The nextest profile named by [perf] must expose a JUnit path — the perf matrix
copies that report out of the target directory into the run data — and a lane
without one is skipped and fails the command.
[quality.assessment] declares only project-specific deep-stage execution: no
source relationships and no metric values, because delegated commands keep their
own budgets and output schemas.
CoreCommand is flattened into the consumer's own subcommand enum and dispatched
by run. Command names, flags, and help text are the public surface — treat a
change as an API change for the consuming project.
audit-clippy keeps workspace selection unless an explicit package selector
replaces it, and --fix applies machine-applicable suggestions over that same
scope before re-reporting; a failed fix stops before reporting and preserves
cargo's exit status. Both passes disable the workstation sccache wrapper and keep
incremental builds, matching the repository's Clippy cache contract. Pinned in
audit_clippy.rs.
Baseline compares on a line-insensitive canonical key, so reformatting never
re-fingerprints an unchanged violation, while a positional-only key keeps its line
as its sole handle. A violation with no baseline entry fails only at
Severity::Deny. Pinned in common/baseline.rs.
Exclusion runs in three complementary passes — path globs, AST #[cfg(test)]
ranges (only test-keyed cfgs; a feature cfg is untouched), and inline-module
globs. An unparseable file contributes no ranges, so its violations are kept
rather than silently dropped, and scan_all_rules names ast-grep rule IDs that
re-run over the full tree, tests included. Pinned in common/exclude.rs.
thin_wrapper_economy owns synchronous one-expression free helpers, and its
threshold is net savings after paying for the definition. Even a scoped run scans
the whole workspace before filtering findings, so omitted callers cannot make a
metric look exact, and ambiguous symbols get no invented savings. The check is
diagnostic-only and must stay so: a syntax-only rewrite cannot preserve trait
lookup, never coercion, or #[track_caller], so an autofix needs compiler-backed
callee resolution. Attributed helpers are excluded because an attribute can change
call or code-generation semantics; multi-statement logging proxies belong to the
ast-grep rule.
One lossless source-evidence graph is built, then scope and level of detail
project it into artifacts below target/architecture/<revision>/. View and crate
selectors are projections of that graph, not independent analyzers: a crate scope
needs no runtime scenario and no extra discovery command, and never expands
external packages. Static call targets stay candidates until rust-analyzer
resolves them.
LOD is independent from scope. Concrete types own their impl methods, traits are
contracts, and free functions belong to one module-functions abstraction. Page
selection is navigation, not evidence truncation: every source contour stays in
contours.json, there is no diagram node budget, and the manifest records
complete visible-node coverage. An endpoint hidden by LOD lifts to its nearest
visible owner; equal visible endpoint/kind pairs aggregate while retaining the
original evidence, and relation kinds stay distinct. Metrics come from the same
contracted relations as the diagram, and the aggregate index is diagnostic and
alters no CI budget.
Project defaults and the repeatable CLI filters compile into one additive
projection filter: it removes matching symbols before semantic selection and
matching contours plus incident edges from the DiagramModel, never alters raw
evidence, and never disables an excluded package used as a runtime scenario.
A module pattern matching any ancestor of a canonical package::module path
excludes the descendant, and relations never lift through an excluded endpoint. An
emptied projection is an error, not an empty diagram.
Configured runtime scenarios are the only project-specific runtime evidence input.
The schema is strict and tagged — Cargo tests, Cargo binaries, or existing trace
paths — and targets are checked against Cargo metadata, then launched with
structured arguments, a bounded timeout, captured logs, and
ARCHITECTURE_TRACE_PATH. No shell command is ever stored in config. Producers
write through the public, domain-neutral viz::trace JSONL API; a cross-thread
send connects only through an explicit correlation identifier. Source matching
enriches existing syntax nodes, and an unmatched record stays a visible runtime
event rather than a guessed static target. A manually supplied trace carries the
Manual evidence class. Runtime enrichment precedes semantic resolution, so a
selected scenario limits rust-analyzer work to the functions the trace observed.
The Markdown report derives from the visible DiagramModel, so every finding
points at a visible contour.
Missing, timed-out, or failed semantic resolution yields the same static classification, cause kept in diagnostics. Degraded runtime observation cannot invalidate the static projection: optional degradation warns and succeeds, while explicitly required semantic, scenario, or trace evidence errors after preserving the artifacts. Truncation is explicit, applies only to evidence collection, and never removes nodes because of diagram size.
Each stage runs a tool pinned and installed by .config/ci-pins.toml and
xtask/src/ci/image.rs. ENV_SKIP_MARKERS lets a stage whose provisioned tool is
transiently missing read as SKIP rather than a false FAIL, and .strict()
withholds that pass from a stage whose tool the fleet never provisions. Stage
invocations and their rationale live in health.rs and are pinned by its tests.
What is not visible there:
-
semver-checkscompares against a git baseline, so CI has to fetch it.actions/checkoutbrings one commit of one branch; without an explicit fetch the stage dies oncouldn't parse revision. The standalone lane insemver.rsreads the baseline lockfile first and names any configured package that has no earlier surface to compare. -
geigerstays.advisory()because cargo-geiger exits non-zero whenever it emits a warning, and here it always does: it cannot match the workspace's own path packages. -
lockbud-deadlockis a rustc driver, not a crates.io package, so it has no version pin: the image installs it from git and exports the nightly it was built against asKITHARA_LOCKBUD_TOOLCHAIN, read by the stage and both recipes. Its-land-bcrate filters do not restrict what it reports — on the pinned commit all three flag forms print the same findings across workspace and dependency crates — so the verdict parses the per-crate summary lines and counts only workspace members, spelled as a compiled crate is (underscores, not hyphens). -
macheteis handed the directories to walk because cargo-machete 0.9 takes no exclude flag and otherwise walks the whole tree. The list comes from cargo metadata, so a new crate is covered without being named anywhere. -
workspace-unused-pubneeds therust-analyzerrustup component. rustup ships the proxy binary either way, so an image without the component does not report a missing tool — it reports a non-zero exit.docker/ci.Dockerfileadds the component for that reason alone.
Independent sccache processes cannot reliably share a disk backend while they
remain running, because each process holds its own cache index. CI therefore uses
a host-local S3-compatible backend for compiler artifacts. The backend is
provisioned through just ci cache; its Compose service is pinned by
.config/ci-pins.toml and keeps credentials outside the repository.
Each trust class has a separate bucket and writer credential. The service binds only to host loopback and the CI Docker network. A hard quota and seven-day expiry constrain retained artifacts; quota exhaustion rejects new writes rather than evicting old entries. This backend shares compilation outputs only: Cargo target reuse and final binary linking remain separate concerns.
stress run is the sole portable lifecycle owner for repeated-test evidence,
writing one fresh raw directory per run. The project's [stress] section solely
owns modes, test features, child environment, paths, limits, and evidence markers;
devtools applies that policy without embedding product feature or environment
names. The manifest freezes the resolved test runner, its arguments, and effective
features so the independent reporter can reject controller or config drift. The
inventory-by-iteration contract, not nextest's last iteration status, owns the
primary verdict.
A run owns the directory it builds into. [stress].build_dir names it relative to
the checkout a lane compiles, and the run exports it as CARGO_TARGET_DIR to
every child after the lane's own environment, so no mode can name it away; a
relative value is refused before a child inherits it. An inherited target
directory is shared with everything else on the host, and a stress run lasts hours:
binaries cleared mid-run turn every remaining repeat into a millisecond exec
failure, and the lane then reports nothing about the revision it was asked about.
The price is one cold build per run per tree. The manifest records the resolved
directory as an observation, not provenance — the reporter has no such directory
and is never asked to agree about one.
Report anchors are the exception — they stay on the checkout that runs the tests,
because nextest's store is rooted at the workspace root and does not follow
CARGO_TARGET_DIR. An anchor placed under the build directory reads a path
nextest never writes, and one run lost all six lanes' evidence to exactly that.
The lane holds a lease on the build directory for as long as it owns it: a shared
claim on .kithara-job-lease there, which a build-cache budget elsewhere must
take exclusively before reclaiming — the one request a shared holder refuses.
Exporting the directory to the children cannot stand in for it: the children are
cargo, which claims nothing, and a directory no budget can see is one no budget
can reclaim.
Linux runner targets cross the Colima bind mount, where file locks do not reach
the macOS host. The public CI command therefore refreshes
.kithara-job-heartbeat beside the lock, and host cleanup accepts only a recent
heartbeat. Normal exit removes it; a killed job becomes reclaimable after the
bounded freshness window.
Pressure sampling ends after the test and evidence phase so the reporter consumes a closed stream, and its end marker records the primary exit status, while the manifest's exit code is the later combined verdict and can additionally reflect staging or supplemental-evidence errors.
stress report independently consumes an uploaded raw directory: it compares the
manifest against trusted checkout and workflow inputs, checks that pressure
sampling ended healthy, correlates configured evidence by exact nextest attempt,
and returns non-zero for failed, missing, partial, duplicate, malformed, or
mismatched evidence. GitHub Actions owns only authorization, immutable checkout
selection, job isolation, artifact transfer, and publishing the rendered summary.
A lane is barred from the cross-lane comparison only by what its JUnit report cannot account for: a missing iteration, a quarantined repeat, a selected test the report never names. A partial evidence overlay - a census past the line pass's record bound, a lost output tail, an absent envelope directory - is a diagnosis caveat and stays one, because the counts, the rates and the verdict are read from the JUnit report alone. The reason an excluded lane carries is the sentence that lane recorded, never a cause re-derived at the summary.
quality assess is an artifact federation layer: the lint, architecture,
similarity, health, Quality Lab, test, dependency, concurrency, performance, and
platform commands remain the canonical owners, and the assessment normalizes and
correlates their output while reimplementing none of their metrics.
The complete profile disables project-default architecture and similarity
exclusions and pulls in integration tests plus test and tooling crates; an
explicitly selected crate or canonical package::module
scope is included even when defaults exclude it. Standard depth executes each
portable gate separately, so stage evidence is attributable without paying for the
heavyweight sections of health; deep runs the full health pipeline plus the
registered rare stages. A configured stage is advisory unless it records an
already-established project gate.
Artifacts land under a revision-and-profile directory. A dirty worktree gets a content-digest suffix and records that digest: committed Quality Lab output, Cha's especially, must not claim coverage of dirty content. Reuse rebuilds the report from stage artifacts already on disk and rejects malformed stage evidence.
The workspace debt target is zero and the refactor threshold is 100; a smaller
scope scales that by LOC and never reaches zero. Existing lint baseline entries
count as debt, and baseline growth is a regression. A hard invariant, debt at or
above threshold, debt regression against a baseline, or same-location
corroboration by two independent tools yields refactor; otherwise diagnostic
findings yield investigate, remaining debt stable-with-debt, uncovered signals
evidence-gap, and a clean run healthy. Verdicts are advisory and do not fail a
complete command; a broken stage marks the analysis partial, while invalid input
or broken required analysis preserves a partial artifact and returns an error.
The tool coverage matrix must account for every known signal. A tool the project declares not-applicable stays visible in the matrix with its reason and can never be scheduled as a deep stage.
similarity owns native source-level comparison of Rust abstractions: it parses
the selected production sources, writes its own revision-scoped artifacts, then
runs the external similarity-rs function-copy profile. Native
findings are diagnostic and do not change the thresholds that profile owns. Only
the strict profile includes test paths and #[cfg(test)] items; the other two
keep the production-only policy in both passes.
Built-in type families carry conservative similarity degrees; a dependency family
activates only when Cargo metadata shows the dependency, and
.config/similarity.toml may add project families of two or more members, or
directional pair relations with substitution caveats. Generic and local names are
erased where they carry no semantics; domain types, constructors, significant
macro symbols, and effects remain. An impl block in a separate file attaches
only when its owner resolves uniquely in the workspace or, failing that, uniquely
within the owning crate. Partial state overlap without matching behavior is a review finding;
composition is recommended only when aligned impl behavior supports it.
The JSON report and graph are exhaustive; the Mermaid view aggregates candidates by crate pair, so rendering stays useful without a node or finding limit. The manifest records the exact roots and whether project-default exclusions were disabled, so an assessment never reuses evidence from another profile or scope. Proc-macro output is not expanded, similarity never proves substitutability, and the caveats must be checked before refactoring.
quality lab owns heavyweight external analysis that must stay outside the fast
lint path, the normal audit, and pre-commit. Its .config/quality-lab.toml is
required, loaded exactly once, and carries a strict versioned schema
independent of .config/xtask.toml. Every external tool version must match its pin
before analysis runs.
- The coverage profile owns the cargo-crap coverage-risk gate. A production run without a baseline emits the absolute artifact; a pull-request run supplies it and gates regressed entries plus new high-risk functions. The wrapper judges the delta JSON rather than the tool's own regression exit code, which does not cover new functions. A failing instrumented test run still writes its coverage reports and still runs cargo-crap: the combined stage preserves the test exit as findings instead of losing the risk evidence. The rendering carries no verdict of its own and never takes the baseline, because a delta narrows what the gate accepts while the report states the whole picture.
- The scheduled profile's findings are advisory; a missing tool, invalid report, version mismatch, or timeout is a tool error.
- The manual profile adds a read-only repo score. A missing executable is skipped and findings stay advisory. A direct non-coverage tool run follows this policy.
- Cha runs only from a clean, non-shallow worktree and analyzes a disposable local clone whose revision is verified against HEAD. The clone is deleted afterwards so Cha cache state cannot leak into the source checkout.
An orphan is a file no mod declaration in its package names. cargo modules orphans answers a narrower question — what one resolved configuration loads — and
pairs a file with its parent by directory convention, so a module behind an unset
cfg, or one reached through #[path] from a sibling, reads as unreferenced to it.
The sweep therefore treats its findings as candidates and settles each against the
source: declared.rs walks the package tree, resolves #[path] and cfg_attr
paths against the directory of the declaring file and plain mod declarations
against the directory that file owns, and drops any candidate the source names.
Drops are printed per package, never silent — that filter is the reason the sweep
can be green at all.
The tool selects one target per run and offers no selector beyond library and binary, so the sweep enumerates both for every package and folds a package's targets into one verdict: a file one target reports and another declares is not an orphan. That is what lets the exclude list stay empty — a package without a library is swept through its binaries instead of dropped.
One run loads the whole workspace into a rust-analyzer database and peaked at
3.0 GiB here, so concurrency is a property of the job rather than a constant: the
sweep takes the smaller of the cores it may use and its cgroup memory cap divided
by that budget, capped at orphans.max_parallelism. A CI job container bounded at
8 GiB and three cores exhausted its cgroup under a fixed four, and the kernel
killed the step before any verdict. The chosen count and the numbers behind it are printed, because a sweep
quietly running one at a time is otherwise indistinguishable from a slow one.
Without an explicit deny the run is advisory.
ci-report consolidates one CI run's archived quality artifacts into a single
markdown document. It reads artifacts, never tools, so it cannot disagree with what
a job measured, and locates inputs by file name rather than by an upload's
directory layout. A section whose input never arrived says so — an omitted section
would read as "nothing to report" from a run that reported nothing. Long tables are
carried as a capped prefix and the omission is stated, because a step summary has a
size limit.
Promoting a unique external check into syn, Cargo metadata, git, or ast-grep
requires repeated actionable, deterministic evidence and two comparison runs before
the adapter is retired.
lint and viz are default-on cargo features gating the syn-heavy command
modules. Gates live only at the lib.rs module-declaration, enum-variant, and
match-arm sites — never as an inline cfg inside a logic file. syn and
proc-macro2 stay non-optional because common uses them unconditionally; lint
additionally turns on the optional quote dependency. The features gate the check
modules, not the AST stack.
common is intentionally public so a consumer can build custom checks on the
shared infrastructure; internal helpers stay pub(crate), as do all viz modules
except viz::trace.