590 lines
25 KiB
Markdown
590 lines
25 KiB
Markdown
|
|
# Milestone 1 Development Plan — Representation Extraction
|
|||
|
|
|
|||
|
|
This document is the implementation plan for Milestone 1 of the roadmap.
|
|||
|
|
The roadmap names it "Define the Representation Stack"; in practice the
|
|||
|
|
work is **build a tool that observes every representation level the spec
|
|||
|
|
names, for any Rust project, and emits the result as one JSON artifact.**
|
|||
|
|
That artifact is the coordinate system every later milestone is built on.
|
|||
|
|
|
|||
|
|
The spec is `milestone-1-representation-stack.md`. This plan does not
|
|||
|
|
restate it; it explains how to deliver it.
|
|||
|
|
|
|||
|
|
## What this milestone produces
|
|||
|
|
|
|||
|
|
A single CLI:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
cstat extract --path <rust-project> --output state.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`state.json` matches §7 of the spec exactly: a `vector` of dotted-key
|
|||
|
|
scalars, per-entity records under `entities`, level-by-level status,
|
|||
|
|
provenance, schema version, toolchain stamp. Every representation level
|
|||
|
|
in §2.1–§2.9 of the spec (excluding §2.10) has working extraction code
|
|||
|
|
backing it.
|
|||
|
|
|
|||
|
|
Out of scope (be explicit so we don't drift):
|
|||
|
|
|
|||
|
|
- No moves, no optimizer, no policy decisions about what to change.
|
|||
|
|
- No human-facing summary, no scoring, no "is this codebase healthy"
|
|||
|
|
judgment. JSON is the output.
|
|||
|
|
- No behavioral testing — that's Milestone 4.
|
|||
|
|
- Rust only. Host triple only. Debug build only (release is a flag we
|
|||
|
|
surface but don't tune for).
|
|||
|
|
- No caching layer beyond what cargo and rustc do natively. If
|
|||
|
|
re-extraction is slow we'll address it after we see the numbers.
|
|||
|
|
|
|||
|
|
## Architectural commitments
|
|||
|
|
|
|||
|
|
These are the load-bearing decisions. Everything downstream assumes them.
|
|||
|
|
|
|||
|
|
**Greenfield code.** Do not try to preserve the existing cstat
|
|||
|
|
subcommands (`dump`, `complexity`, `deps`, `flow`, etc.). They were
|
|||
|
|
useful as a probe; they are not the design target. Write what the spec
|
|||
|
|
calls for and let the old surface fall away.
|
|||
|
|
|
|||
|
|
**rustc isolation boundary.** rustc-internal IRs (HIR, THIR, MIR,
|
|||
|
|
borrowck, mono-items) are accessed via `rustc_driver` running in a
|
|||
|
|
separate nightly sub-binary. That binary translates rustc's internal
|
|||
|
|
data structures into our own plain serde-friendly mirror types and
|
|||
|
|
writes JSON. The rest of the workspace never sees a `TyCtxt`, never
|
|||
|
|
sees a `'tcx` lifetime, and stays on stable Rust. When rustc churns,
|
|||
|
|
exactly one file changes — the translator — and the rest of the system
|
|||
|
|
is unaffected. This is the whole point of the boundary.
|
|||
|
|
|
|||
|
|
**One workspace, multiple crates** — separation by stability surface,
|
|||
|
|
not by feature:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
crates/
|
|||
|
|
cstat-schema/ stable. §5/§6/§7 types, JSON serde.
|
|||
|
|
cstat-ir/ stable. Mirror types for HIR/THIR/MIR/borrowck.
|
|||
|
|
cstat-orchestrate/ stable. Level trait, runner, output assembly.
|
|||
|
|
cstat-extract-stable/ stable. Every level not needing rustc internals.
|
|||
|
|
cstat-extract-rustc/ NIGHTLY. rustc_driver-driven extractor binary.
|
|||
|
|
cstat-cli/ stable. CLI entry point.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
A `rust-toolchain.toml` inside `cstat-extract-rustc/` pins it to nightly;
|
|||
|
|
the workspace root stays on stable. Cargo respects per-directory toolchains.
|
|||
|
|
|
|||
|
|
**Output schema is the contract.** The JSON shape in §7 of the spec is
|
|||
|
|
the API. Every later milestone reads it. Schema-version per §10. Don't
|
|||
|
|
"clean it up" mid-implementation; if it needs changing, bump the version
|
|||
|
|
and update the spec.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 1 — Scaffolding
|
|||
|
|
|
|||
|
|
Goal: workspace exists, schema types compile, CLI runs end-to-end with
|
|||
|
|
zero levels registered, and the resulting JSON validates against §7.
|
|||
|
|
Nothing measures anything yet — but the envelope is real.
|
|||
|
|
|
|||
|
|
### `cstat-schema`
|
|||
|
|
|
|||
|
|
The serializable surface. Pure data types, serde derive throughout.
|
|||
|
|
|
|||
|
|
- `Vector` — `BTreeMap<String, f64>` newtype. BTreeMap so iteration is
|
|||
|
|
deterministic for snapshot tests.
|
|||
|
|
- `EntityRecord { ids: IdSet, metrics: BTreeMap<String, f64> }`.
|
|||
|
|
- `IdSet { file: Option<FileId>, module: Option<ModuleId>,
|
|||
|
|
ast_func: Option<AstFuncId>, symbol: Option<SymbolId>,
|
|||
|
|
mono: Option<MonoId> }`. Per §6, every entity carries whichever IDs
|
|||
|
|
it's known by.
|
|||
|
|
- IDs are plain structs; `AstFuncId` includes a structural hash of the
|
|||
|
|
AST item so it survives reformatting but not real edits.
|
|||
|
|
- `OracleOutput` matching §7 field-for-field: `schema_version`,
|
|||
|
|
`cstat_version`, `toolchain`, `config_digest`, `wall_clock_seconds`,
|
|||
|
|
`levels` (status per level), `vector`, `entities`, `provenance`.
|
|||
|
|
- Schema version starts at `0.1.0`. Bump rules per §10.
|
|||
|
|
|
|||
|
|
### `cstat-orchestrate`
|
|||
|
|
|
|||
|
|
The runner. Knows nothing about specific levels — only the trait.
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
trait Level: Send + Sync {
|
|||
|
|
fn id(&self) -> &'static str;
|
|||
|
|
fn deterministic(&self) -> bool;
|
|||
|
|
fn extract(&self, ctx: &Context) -> Result<LevelOutput, LevelError>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Context` carries the project root, the user's config, and a
|
|||
|
|
`SharedArtifacts` cache. `SharedArtifacts` is a lazy memoizer for
|
|||
|
|
expensive intermediate products that multiple levels read — `cargo
|
|||
|
|
metadata` output, the LLVM IR text, the parsed object file, the
|
|||
|
|
`cstat-ir` dump from the nightly extractor. First level to ask pays
|
|||
|
|
the cost; the rest hit a cached reference. Keep this dumb: a few
|
|||
|
|
`OnceCell`s in a struct is enough.
|
|||
|
|
|
|||
|
|
`LevelOutput` carries the level's contribution to the vector
|
|||
|
|
(`Vec<(String, f64)>`), per-entity records, and optionally the set of
|
|||
|
|
key prefixes it claims to own. The runner cross-checks claims against
|
|||
|
|
emissions and fails loudly on collision — this is how we keep the
|
|||
|
|
namespace honest without a central registry.
|
|||
|
|
|
|||
|
|
Runner runs levels sequentially in v1. Don't parallelize until we
|
|||
|
|
measure that we need it; the rustc subprocess and codegen dominate
|
|||
|
|
wall-clock time and they're not parallelizable inside one project
|
|||
|
|
anyway.
|
|||
|
|
|
|||
|
|
### `cstat-cli`
|
|||
|
|
|
|||
|
|
One subcommand: `extract --path <PATH> [--output <FILE>]
|
|||
|
|
[--enable-rustc-internal] [--toolchain <CHANNEL>]`. Writes JSON to file
|
|||
|
|
or stdout. No pretty-printer in v1 — debugging is `jq`.
|
|||
|
|
|
|||
|
|
### Completion gate
|
|||
|
|
|
|||
|
|
A toy Rust project (check in `examples/single-file-lib/` from §13) runs
|
|||
|
|
through `cstat extract` and produces a JSON file. All levels report
|
|||
|
|
`status: "skipped"` with reason `"not implemented"`, but the envelope
|
|||
|
|
parses against `OracleOutput`. `cargo test` in the workspace passes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 2 — Stable-Rust extractors
|
|||
|
|
|
|||
|
|
Goal: every level reachable without rustc internals has a working
|
|||
|
|
`Level` impl in `cstat-extract-stable`. This is the bulk of the work
|
|||
|
|
by volume but most of it is mechanical — pick the right crate, walk
|
|||
|
|
the right tree, emit the right keys.
|
|||
|
|
|
|||
|
|
Group the work by the file or artifact each level reads from. Levels
|
|||
|
|
in the same group share intermediate state via `SharedArtifacts`.
|
|||
|
|
|
|||
|
|
**Group A — Filesystem and source text** (levels 1–6, plus 47).
|
|||
|
|
One pass walks the project tree once, holds each file's bytes briefly,
|
|||
|
|
forks downstream into: filesystem stats, raw-byte stats, line-level
|
|||
|
|
stats, tokens (via `proc_macro2`), comments (token filter), whitespace
|
|||
|
|
(rustfmt diff via `cargo fmt -- --check`). Don't re-read files.
|
|||
|
|
|
|||
|
|
**Group B — Syntactic AST** (levels 7, 8, 9).
|
|||
|
|
Level 7: `syn::parse_file` per `.rs` file; parse errors recorded under
|
|||
|
|
`entities.ast.file_errors` and the file excluded from AST metrics per
|
|||
|
|
§11. Level 8: walk the parsed CSTs to assemble the module skeleton and
|
|||
|
|
visibility distribution. Level 9: shell out to `cargo expand` (preferred
|
|||
|
|
because it's stable) for macro-expanded source, then re-run level 7's
|
|||
|
|
pass over the expanded text. The level-9 metrics are the same as level
|
|||
|
|
7's; what's interesting is the delta.
|
|||
|
|
|
|||
|
|
**Group C — Cargo and project metadata** (levels 41–44).
|
|||
|
|
Direct calls to `cargo metadata --format-version 1`, `rustc --print
|
|||
|
|
cfg`, and direct file reads of `Cargo.lock` and `rust-toolchain.toml`.
|
|||
|
|
This group is essentially free; do it early so the toolchain stamp is
|
|||
|
|
available for the output envelope.
|
|||
|
|
|
|||
|
|
**Group D — Codegen** (levels 20–24).
|
|||
|
|
Invoke `cargo rustc -- --emit=llvm-ir -C no-prepopulate-passes` for
|
|||
|
|
the pre-opt IR (level 20), `--emit=llvm-ir` for the optimized form
|
|||
|
|
(level 21), `--emit=asm` for assembly (level 22). The build also
|
|||
|
|
produces object files (level 23) and a linked binary (level 24) which
|
|||
|
|
the orchestrator locates via `cargo build --message-format=json`. LLVM
|
|||
|
|
IR is parsed with the `llvm-ir` crate (text-format reader). Per-function
|
|||
|
|
metrics: instruction count, basic-block count, instruction-kind
|
|||
|
|
histogram, call-site count.
|
|||
|
|
|
|||
|
|
**Group E — Binary artifacts** (levels 25–30).
|
|||
|
|
The `object` crate handles 25 (symbol table), 27 (sections), 28
|
|||
|
|
(relocations). `rustc-demangle` runs over level 25 to produce 26 (the
|
|||
|
|
demangled view, from which source-to-symbol ratio falls out). Level 29
|
|||
|
|
uses `gimli` over the debug build's DWARF — this is where the
|
|||
|
|
AST↔Symbol identity join from §6 becomes possible, so the join logic
|
|||
|
|
lives here. Level 30 parses the linker map file (`-Wl,-Map=...`).
|
|||
|
|
|
|||
|
|
**Group F — Type-system views** (levels 16, 17, 19).
|
|||
|
|
Level 16 is the AST-approximation in v1: walk syn types per function,
|
|||
|
|
emit per-function distinct-type count. Levels 17 and 19 come from
|
|||
|
|
rustdoc JSON (`cargo +nightly rustdoc -- --output-format json`), parsed
|
|||
|
|
with the `rustdoc-types` crate. Yes, rustdoc JSON is nightly-only — but
|
|||
|
|
it's a separate process, not in-process rustc, so it doesn't compromise
|
|||
|
|
the isolation boundary. Treat it like any other tool invocation.
|
|||
|
|
|
|||
|
|
**Group G — Graph derivations** (levels 31, 32, 34, 36–40).
|
|||
|
|
Build module dependency graph (31) and intra-crate call graph (32)
|
|||
|
|
from Group B's name-unresolved AST. CFG-per-function (34) walks each
|
|||
|
|
function body's syn tree into a basic-block graph (pre-MIR; MIR's CFG
|
|||
|
|
arrives in Phase 3 as a separate level). Type-usage graph (36) lifts
|
|||
|
|
Group F's level-16 output to a graph. SCCs (37) via Tarjan. Communities
|
|||
|
|
(38) via Louvain or label-propagation, with modularity Q emitted. Power-
|
|||
|
|
law fit (39) is a utility: takes any graph, fits a degree-distribution
|
|||
|
|
exponent via MLE plus a KS goodness-of-fit. Self-similarity (40) takes
|
|||
|
|
any per-entity scalar and reports the distribution distance (KS) across
|
|||
|
|
aggregation scales (function → file → module).
|
|||
|
|
|
|||
|
|
**Group H — External tools** (levels 45, 46, 48, 49).
|
|||
|
|
`cargo clippy --message-format=json` for 45. Group F already does 46.
|
|||
|
|
`cargo test -- --list --format json -Z unstable-options` for 48; the
|
|||
|
|
doc-test variant for 49.
|
|||
|
|
|
|||
|
|
### Completion gate
|
|||
|
|
|
|||
|
|
Run `cstat extract` on the §13 worked example. Every level in this
|
|||
|
|
phase reports `status: "ok"`. The vector contains keys for each of
|
|||
|
|
those levels. Approximate value match against §13 is fine — the spec
|
|||
|
|
example was illustrative, and we can correct §13's numbers once we
|
|||
|
|
have ground truth. What must hold: shape matches, IDs join where §13
|
|||
|
|
shows them joining (the AST↔Symbol example), provenance is populated.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 3 — rustc_driver extractor (the isolated nightly binary)
|
|||
|
|
|
|||
|
|
Goal: levels 10–15 and the exact form of 18, accessed in-process via
|
|||
|
|
rustc, exposed to the rest of the system as plain JSON mirror types.
|
|||
|
|
|
|||
|
|
### The boundary
|
|||
|
|
|
|||
|
|
`cstat-ir` defines the mirror types. One module per IR layer:
|
|||
|
|
`cstat_ir::hir`, `cstat_ir::thir`, `cstat_ir::mir`, `cstat_ir::borrowck`,
|
|||
|
|
`cstat_ir::monoitems`. Each module has plain serde-derive structs
|
|||
|
|
capturing the fields we measure — *not* a full mirror of rustc's
|
|||
|
|
internals. Enums use string discriminants for forward-compat with rustc
|
|||
|
|
shape changes. No lifetimes, no rustc deps, no internal types leak
|
|||
|
|
through.
|
|||
|
|
|
|||
|
|
A minimal example shape (final form decided during Phase 3):
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// cstat_ir::mir
|
|||
|
|
pub struct Body {
|
|||
|
|
pub def_path: String,
|
|||
|
|
pub arg_count: u32,
|
|||
|
|
pub local_count: u32,
|
|||
|
|
pub basic_blocks: Vec<BasicBlock>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub struct BasicBlock {
|
|||
|
|
pub id: u32,
|
|||
|
|
pub statement_kinds: Vec<String>, // string discriminants
|
|||
|
|
pub terminator: String,
|
|||
|
|
pub predecessors: Vec<u32>,
|
|||
|
|
pub successors: Vec<u32>,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The orchestrator and `cstat-extract-stable` may depend on `cstat-ir`.
|
|||
|
|
Neither depends on rustc.
|
|||
|
|
|
|||
|
|
### The nightly binary
|
|||
|
|
|
|||
|
|
`cstat-extract-rustc` is its own bin crate, pinned to nightly via
|
|||
|
|
`rust-toolchain.toml` in that crate's directory.
|
|||
|
|
|
|||
|
|
Architecture:
|
|||
|
|
- Parse args: project path, output path, which IR levels to emit.
|
|||
|
|
- Construct an `rustc_driver::RunCompiler` with our `Callbacks` impl.
|
|||
|
|
- The `after_analysis` callback has `TyCtxt`. Walk the bodies we need,
|
|||
|
|
call the per-level translator, accumulate `cstat-ir` values.
|
|||
|
|
- Write JSON to the output path. Exit.
|
|||
|
|
|
|||
|
|
The orchestrator's Level impl for rustc-derived levels invokes this
|
|||
|
|
binary as a subprocess. Subprocess failure (no nightly installed, rustc
|
|||
|
|
panic, project doesn't build) → each affected level reports `status:
|
|||
|
|
"failed"` with reason. The rest of the run continues.
|
|||
|
|
|
|||
|
|
### Translators
|
|||
|
|
|
|||
|
|
One per IR layer, all living in `cstat-extract-rustc`:
|
|||
|
|
|
|||
|
|
- `translate_hir(tcx)` — produces `cstat_ir::hir::Item` records.
|
|||
|
|
- `translate_thir(tcx, body)` — typed HIR per body; we emit
|
|||
|
|
per-expression type info aggregated into per-function metrics
|
|||
|
|
(polymorphism density, generic-arg counts, trait-object density).
|
|||
|
|
- `translate_mir(body)` — `cstat_ir::mir::Body`. Run twice: once on
|
|||
|
|
pre-opt MIR (level 13), once on optimized MIR (level 14). The delta
|
|||
|
|
is its own observable.
|
|||
|
|
- `translate_borrowck(tcx, def_id)` — region/lifetime constraint
|
|||
|
|
count, NLL conflict density.
|
|||
|
|
- `translate_monoitems(tcx)` — `tcx.collect_and_partition_mono_items`
|
|||
|
|
rolled up to per-generic-item instantiation count.
|
|||
|
|
|
|||
|
|
### Metric extraction
|
|||
|
|
|
|||
|
|
Lives back in `cstat-extract-stable` (or a sibling stable crate; pick
|
|||
|
|
during implementation). Takes `cstat-ir` values and emits
|
|||
|
|
`(vector_key, f64)` pairs. Pure functions, unit-testable against
|
|||
|
|
hand-built mirror values — this is the natural place for property tests
|
|||
|
|
(see Testing).
|
|||
|
|
|
|||
|
|
### Levels covered
|
|||
|
|
|
|||
|
|
- 10 (name-resolved AST): import graph, shadowing structure from HIR.
|
|||
|
|
- 11 (HIR): branch counts, expression-kind histogram, sugar density.
|
|||
|
|
- 12 (THIR): type information rolled into metrics.
|
|||
|
|
- 13 (MIR pre-opt): per-function basic-block count, statement count,
|
|||
|
|
terminator-kind histogram.
|
|||
|
|
- 14 (MIR opt): the same, after MIR optimization passes.
|
|||
|
|
- 15 (borrowck): lifetime constraint count, region count, NLL density.
|
|||
|
|
- 18 (generic instantiation, exact): from mono-items.
|
|||
|
|
|
|||
|
|
### Completion gate
|
|||
|
|
|
|||
|
|
Subprocess runs on the §13 example, emits a `cstat-ir` JSON, the
|
|||
|
|
orchestrator distributes it into the vector. All nightly levels show
|
|||
|
|
`status: "ok"` when nightly is installed and `"skipped"` with reason
|
|||
|
|
`"nightly toolchain unavailable"` otherwise.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 4 — Derived levels + validation
|
|||
|
|
|
|||
|
|
Goal: the levels that compose other levels' outputs, plus the test
|
|||
|
|
harness that proves the whole thing reproduces.
|
|||
|
|
|
|||
|
|
### Derived levels (33, 35)
|
|||
|
|
|
|||
|
|
- Level 33 (inter-crate / monomorph call graph): joins level 26
|
|||
|
|
(demangled symbols) with level 20 (LLVM IR `call` instructions
|
|||
|
|
resolved to callee symbols). Output: a graph keyed by `MonoId`.
|
|||
|
|
- Level 35 (data-flow per function): from level 20's SSA form,
|
|||
|
|
build def-use chains; emit per-function dataflow density.
|
|||
|
|
|
|||
|
|
These are pure compositions over previously extracted artifacts; they
|
|||
|
|
don't invoke any new tooling.
|
|||
|
|
|
|||
|
|
### Validation harness
|
|||
|
|
|
|||
|
|
Lives in `cstat-orchestrate/tests/` and `cstat-cli/tests/`. Per
|
|||
|
|
preferences: scenario tests, property tests, contract tests — no
|
|||
|
|
white-box "insert-then-lookup" style.
|
|||
|
|
|
|||
|
|
- **Worked-example snapshot.** The §13 toy crate is checked in under
|
|||
|
|
`examples/single-file-lib/`. A scenario test runs `cstat extract`
|
|||
|
|
on it and snapshots the resulting JSON. The snapshot is reviewed
|
|||
|
|
on diff. Numeric values in §13 may need correction the first time
|
|||
|
|
this runs; that's a spec edit, not a code bug.
|
|||
|
|
- **Determinism property test.** For any project in the examples
|
|||
|
|
directory, two runs in a row produce byte-identical JSON for every
|
|||
|
|
key whose owning level declares `deterministic: true`. Non-
|
|||
|
|
deterministic keys are inspected separately.
|
|||
|
|
- **Failure-mode contract tests.** One small fixture project per row
|
|||
|
|
of §11: an unparseable `.rs` file, a missing `Cargo.toml`, a panicky
|
|||
|
|
proc macro (if one can be constructed), a missing nightly toolchain
|
|||
|
|
(simulated by an env var the runner respects). Each fixture asserts
|
|||
|
|
the expected `status` and `reason` shape — not the internal control
|
|||
|
|
flow.
|
|||
|
|
- **Schema roundtrip property test.** `OracleOutput → JSON → Value →
|
|||
|
|
OracleOutput` is lossless for any output the runner can produce.
|
|||
|
|
Catches accidental schema drift early.
|
|||
|
|
|
|||
|
|
### Real-target smoke
|
|||
|
|
|
|||
|
|
Run `cstat extract` against itself (this workspace) and at least one
|
|||
|
|
external Rust crate (a small one — `serde_json`, `ripgrep`, or
|
|||
|
|
similar). The vector should populate for every required level with
|
|||
|
|
no `failed` statuses. This is exploratory, not a pass/fail gate, but
|
|||
|
|
the first time we see the numbers will shape what we want to add or
|
|||
|
|
adjust in §5's aggregate set.
|
|||
|
|
|
|||
|
|
### Completion gate
|
|||
|
|
|
|||
|
|
All four test categories pass. `cstat extract` on at least two real
|
|||
|
|
crates produces a complete JSON with no `failed` statuses (only
|
|||
|
|
`skipped` where genuinely unavailable, e.g., missing optional tooling
|
|||
|
|
on the dev machine).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Testing strategy in one paragraph
|
|||
|
|
|
|||
|
|
No white-box tests. No "construct an AST, call the metric, assert the
|
|||
|
|
metric returns what the AST says." Tests fall into three buckets:
|
|||
|
|
**scenario** (run the tool on a real or fixture project, assert
|
|||
|
|
properties of the output), **property** (determinism, schema
|
|||
|
|
roundtrip, key-namespace non-collision), **contract** (failure-mode
|
|||
|
|
behavior matches §11's catalog). Tests should survive a refactor of
|
|||
|
|
any individual level's internals — they observe the JSON, not the
|
|||
|
|
code. The §13 worked example doubles as our primary scenario fixture
|
|||
|
|
and as the spec's anchor; if the spec's numbers change, the snapshot
|
|||
|
|
changes with them.
|
|||
|
|
|
|||
|
|
## Environment prerequisites
|
|||
|
|
|
|||
|
|
- Stable Rust (workspace default).
|
|||
|
|
- Nightly Rust installed alongside (for `cstat-extract-rustc`,
|
|||
|
|
`cargo expand`, rustdoc JSON, `cargo test --list -Z`). Pinned via
|
|||
|
|
`rust-toolchain.toml` inside the crate that needs it.
|
|||
|
|
- `cargo` on PATH.
|
|||
|
|
- Standard system tools: `cargo`, `rustc`, `rustup`, and a linker.
|
|||
|
|
Object/DWARF/symbol parsing is all in-process via crates (`object`,
|
|||
|
|
`gimli`, `rustc-demangle`); no `nm`/`objdump` shell-outs.
|
|||
|
|
- Optional: `cargo-expand` (a cargo subcommand); the orchestrator
|
|||
|
|
detects absence and falls back / skips.
|
|||
|
|
|
|||
|
|
## Sequencing and parallelism
|
|||
|
|
|
|||
|
|
Phases are listed in dependency order. Phase 0 must land before any
|
|||
|
|
code so the spec is stable. Phase 1 must land before Phases 2–4 (they
|
|||
|
|
need the scaffold). Phases 2 and 3 are mostly independent — different
|
|||
|
|
crates, different toolchains, different problems — and can be worked
|
|||
|
|
in parallel by different agents if convenient. Phase 4 needs both 2
|
|||
|
|
and 3 to be far enough along that the example reproduces.
|
|||
|
|
|
|||
|
|
A natural single-track ordering: 0 → 1 → 2 → 3 → 4. A parallel-track
|
|||
|
|
ordering: 0 → 1 → (2 ‖ 3) → 4.
|
|||
|
|
|
|||
|
|
## Anchor tests (write before any extractor)
|
|||
|
|
|
|||
|
|
These exist to keep implementation honest across what will be a
|
|||
|
|
multi-week, possibly multi-agent build. Each one targets a specific
|
|||
|
|
architectural commitment from the plan; failing the test means the
|
|||
|
|
commitment slipped, not that a metric is off by one. Specify them
|
|||
|
|
before writing the code they exercise — they describe what success
|
|||
|
|
looks like, independent of how the code is structured.
|
|||
|
|
|
|||
|
|
None should read as "the test echoes the code." If a future reader
|
|||
|
|
can rewrite the implementation behind any of these tests and the
|
|||
|
|
test still passes for the right reasons, it's doing its job.
|
|||
|
|
|
|||
|
|
### 1. §13 worked example reproduces — scenario
|
|||
|
|
|
|||
|
|
The toy crate from spec §13 lives in `examples/single-file-lib/`.
|
|||
|
|
The test runs `cstat extract` on it and snapshots the JSON. The
|
|||
|
|
snapshot is reviewed on diff, not regenerated automatically.
|
|||
|
|
|
|||
|
|
**Anchors:** the spec's own example is the canonical description of
|
|||
|
|
expected behavior. The snapshot covers output shape, key naming, the
|
|||
|
|
AST↔Symbol identity join (§13 shows both IDs on the `classify`
|
|||
|
|
entity), and per-level coverage in one artifact. If §13's numbers
|
|||
|
|
are wrong, the spec gets edited; if the code is wrong, the code
|
|||
|
|
gets fixed. Either is fine; the snapshot makes the choice explicit.
|
|||
|
|
|
|||
|
|
**Does not assert:** extractor internals or call counts.
|
|||
|
|
|
|||
|
|
### 2. Every required level is present — property
|
|||
|
|
|
|||
|
|
For any non-empty Rust project, every level listed in spec §4 as
|
|||
|
|
required appears in `levels` with status in `{ok, skipped, failed}`.
|
|||
|
|
No required level is absent.
|
|||
|
|
|
|||
|
|
**Anchors:** the biggest risk in a long build across agents is that
|
|||
|
|
a level gets harder than expected and silently disappears. This
|
|||
|
|
test makes a punt *visible* in the output as a `skipped` or `failed`
|
|||
|
|
status with a reason. Hard-to-implement levels can't quietly vanish.
|
|||
|
|
|
|||
|
|
**Does not assert:** that the level's metrics are correct, only that
|
|||
|
|
it reported something.
|
|||
|
|
|
|||
|
|
### 3. Determinism — property
|
|||
|
|
|
|||
|
|
For any project in `examples/`, two consecutive runs produce byte-
|
|||
|
|
identical JSON for every key whose owning level reports
|
|||
|
|
`deterministic: true`.
|
|||
|
|
|
|||
|
|
**Anchors:** trajectories in Milestone 6 are built from these
|
|||
|
|
vectors. If extract is non-deterministic for keys we claim are
|
|||
|
|
deterministic, downstream data is noise. Hash-map iteration order,
|
|||
|
|
embedded timestamps, parallel-scheduling effects all surface
|
|||
|
|
immediately.
|
|||
|
|
|
|||
|
|
**Does not assert:** anything about non-deterministic levels, which
|
|||
|
|
are inspected separately.
|
|||
|
|
|
|||
|
|
### 4. Schema roundtrip — property
|
|||
|
|
|
|||
|
|
`OracleOutput → JSON → serde_json::Value → JSON → OracleOutput` is
|
|||
|
|
lossless. Generated outputs validate against a JSON schema derived
|
|||
|
|
from §7.
|
|||
|
|
|
|||
|
|
**Anchors:** the JSON shape is the contract Milestone 2+ read.
|
|||
|
|
Schema drift is invisible until the next milestone breaks. This
|
|||
|
|
test surfaces drift the moment it's introduced.
|
|||
|
|
|
|||
|
|
**Does not assert:** values, only structural integrity.
|
|||
|
|
|
|||
|
|
### 5. No two levels claim the same key — property
|
|||
|
|
|
|||
|
|
Across every registered level, the set of vector keys emitted is
|
|||
|
|
pairwise disjoint between levels. Provenance for each key names
|
|||
|
|
exactly one owning level.
|
|||
|
|
|
|||
|
|
**Anchors:** the namespace stays meaningful (`ast.*` is AST, `mir.*`
|
|||
|
|
is MIR, `sym.*` is symbol table). Two levels silently double-writing
|
|||
|
|
the same key would mean last-writer-wins and half the data lost
|
|||
|
|
without notice. Phase 1's orchestrator cross-checks claim vs.
|
|||
|
|
emission; this test makes that check observable.
|
|||
|
|
|
|||
|
|
**Does not assert:** that the partitioning is the right
|
|||
|
|
partitioning. That's a design decision baked into the spec.
|
|||
|
|
|
|||
|
|
### 6. Unparseable file — contract (§11 row 4)
|
|||
|
|
|
|||
|
|
Fixture: a project with one well-formed `.rs` file and one
|
|||
|
|
syntactically broken one. Run extract.
|
|||
|
|
|
|||
|
|
Assert: the broken file appears as a record under
|
|||
|
|
`entities.ast.file_errors`; AST-level metrics for the good file are
|
|||
|
|
present; `meta.files_unparseable.count == 1.0`; the process exits
|
|||
|
|
zero.
|
|||
|
|
|
|||
|
|
**Anchors:** it's tempting to propagate a `syn` parse error upward
|
|||
|
|
and abort. The spec says no — file-level failures degrade
|
|||
|
|
gracefully. This makes that rule enforceable.
|
|||
|
|
|
|||
|
|
**Does not assert:** the reason string verbatim, only that the
|
|||
|
|
record exists and the meta counter increments.
|
|||
|
|
|
|||
|
|
### 7. Nightly unavailable — contract (§11 "tool missing")
|
|||
|
|
|
|||
|
|
Fixture: any project, run with an env override that simulates
|
|||
|
|
missing nightly (or actually unset, in a CI lane without nightly).
|
|||
|
|
|
|||
|
|
Assert: every level in §2.3 (rustc-internal IRs), level 9, level
|
|||
|
|
18's nightly path, and the rustdoc-JSON-derived levels (17, 19, 46)
|
|||
|
|
report status in `{skipped, failed}` with a reason. Every stable-
|
|||
|
|
Rust level reports `ok`. Process exits zero.
|
|||
|
|
|
|||
|
|
**Anchors:** the architectural commitment that the stable
|
|||
|
|
orchestrator works without nightly is a runtime requirement, not
|
|||
|
|
just a build-time concern. Missing nightly produces a usable
|
|||
|
|
vector, not a crash.
|
|||
|
|
|
|||
|
|
**Does not assert:** the reason string verbatim, only that status
|
|||
|
|
is one of the documented values and a reason field is non-empty.
|
|||
|
|
|
|||
|
|
### 8. Isolation boundary — architectural check
|
|||
|
|
|
|||
|
|
A CI step runs `cargo tree -p cstat-orchestrate` (and the other
|
|||
|
|
stable crates) and asserts that no `rustc_*` crate appears anywhere
|
|||
|
|
in the transitive dependency graph. The only crate permitted to
|
|||
|
|
depend on `rustc_*` is `cstat-extract-rustc`.
|
|||
|
|
|
|||
|
|
**Anchors:** the load-bearing decision of the whole plan. If
|
|||
|
|
`rustc_driver` sneaks into the stable side once, undoing it later
|
|||
|
|
means recompiling on every rustc bump forever. This catches the
|
|||
|
|
slip on the first PR that introduces it.
|
|||
|
|
|
|||
|
|
**Does not assert:** anything about behavior. It's a structural
|
|||
|
|
invariant on the build graph.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Tests 1, 6, and 7 use checked-in fixtures (one toy crate, one
|
|||
|
|
broken-file crate, one nominal crate for the nightly-unavailable
|
|||
|
|
case). Tests 2, 3, 4, 5 are universally quantified — they run
|
|||
|
|
against every fixture and every example project we check in. Test 8
|
|||
|
|
is a CI script, not a Rust test.
|
|||
|
|
|
|||
|
|
The full set should be specifiable in a single afternoon before any
|
|||
|
|
extractor lands. Treat them as the contract Phase 1 must satisfy
|
|||
|
|
the shape of, and Phases 2–4 must satisfy the substance of.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## What "done with Milestone 1" looks like
|
|||
|
|
|
|||
|
|
- Spec is internally consistent (Phase 0).
|
|||
|
|
- Workspace builds on stable; `cstat-extract-rustc` builds on its
|
|||
|
|
pinned nightly.
|
|||
|
|
- `cstat extract --path <any-rust-project>` produces a JSON matching
|
|||
|
|
§7 with every required level reporting `status: "ok"` when the
|
|||
|
|
prerequisites are present.
|
|||
|
|
- The §13 worked example reproduces (modulo spec corrections to the
|
|||
|
|
numbers).
|
|||
|
|
- Validation tests pass.
|
|||
|
|
- No moves, no optimizer, no scoring — just the coordinate system,
|
|||
|
|
fully observable.
|