Implement the v3 output schemas: structured why + doctor reports#51
Implement the v3 output schemas: structured why + doctor reports#51kjanat wants to merge 6 commits into
Conversation
The v3 draft shipped in #50 as a hand-written example with no emitter behind it. Implement the data model for real: `why --json` now defaults to schema v3, restructured around `{task, match}` candidate pairs plus a `decision` block. Tasks carry identity (`fqn`, `provider`, `kind` — cargo aliases labeled `cargo-alias`), origin (`source` file, `source_pointer` key path), and resolution data (`definition`, `resolved` preview, `cwd`, sibling `aliases`, `dependencies`); the match half exposes the run-time selection key and `decision.strategy` names the branch taken. Real output reproduces the former draft example verbatim (semantic diff: identical), so the draft is promoted to `why.v3.example.json` and validates against the generated `why.v3.schema.json` (ajv, draft 2020-12). Surfaces now version independently: `WHY_CURRENT_VERSION = 3` while doctor/list stay at `CURRENT_VERSION = 2` and reject `--schema-version 3` rather than mislabel an unchanged payload — their v3 drafts remain under review. v1/v2 why output is unchanged and still reachable via `--schema-version`.
|
Important Review skippedAuto reviews are limited based on label configuration. 🚫 Review skipped — only excluded labels are configured. (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR rolls out JSON schema version 3 as the new default output format for Sequence Diagram(s)sequenceDiagram
participant User as User CLI
participant Dispatch as lib.rs<br/>Dispatch
participant Validator as schema::validate_*<br/>schema_version
participant DoctorCmd as cmd::doctor<br/>why()
participant Builder as DoctorReportV3<br/>WhyReportV3
participant JSON as serde_json<br/>serialize
User->>Dispatch: doctor --json --schema-version 3
Dispatch->>Validator: doctor_schema_version_for_json(3)
Validator-->>Dispatch: Ok(3)
Dispatch->>DoctorCmd: cmd::doctor(..., schema_version=3)
DoctorCmd->>Builder: if schema_version >= 3: build_report_v3(ctx)
Builder-->>DoctorCmd: DoctorReportV3 with expanded metadata
DoctorCmd->>JSON: serde(report_v3)
JSON-->>DoctorCmd: pretty JSON string
DoctorCmd-->>User: print to stdout
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 7 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (7 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Comment |
Same treatment why v3 got: review the draft against the codebase, then make it real. `doctor --json` now defaults to schema v3 — a structured diagnostic inventory replacing the flat v2 dump: invocation/environment/ runner provenance, per-ecosystem decisions graded by `confidence` (mapped from the typed `ResolutionStep`: override/manifest/lockfile = high, PATH probe = medium, legacy npm fallback = low, failure = none), first-class `sources`, `fqn`-keyed tasks with effective `resolved` commands, PATH-probed `tools`, duplicate-task-name `conflicts` (winner, shadowed, and the ranking that decided it), flattened `diagnostics`, and a self-describing `resolution` policy block. The real output validates against both the generated `doctor.v3.schema.json` and the original `doctor.v3-draft` (retired here). Review deltas, all documented in `schema::doctor_v3`: nullable `tasks[].resolved`/`source` instead of lying when PM resolution fails; source kinds aligned to the why-v3 label convention; speculative draft shapes nothing can emit (dependency edges, workspace identity, probe errors, unused severities) deferred rather than declared. `list` still caps at v2 and rejects v3; human doctor output is unchanged, pinned to the v2 builder. RFC 3339 timestamps via dependency-free civil-date math.
Same sweep the v3 examples got: every committed example is a hand-maintained fixture with placeholder values (`/path/to/project`, `/home/user/…`), not captured output — real home paths and the Volta tool inventory leaked in via the v1/v2 fixtures. All six still validate against their schemas. `gen-schema` never writes example files, so the drift guard cannot reintroduce real paths.
`git sparse-checkout set` runs in cone mode by default, which only accepts directories — `schemas/*.schema.json` failed the checkout with "specify directories rather than patterns". Set `sparse-checkout-cone-mode: false` so the glob is treated as a pattern, matching the convention release.yml and npm-release.yml already use.
The previous scrub only renamed the username, leaving the actual machine fingerprint in place: real Volta shim layout, installed-tool inventory, and exact versions (npm 11.6.2, yarn 1.22.22, node 24.14.1). Replace all probe-derived data with invented values — `/usr/bin`-style probe paths, round fake versions, `volta_shims` dropped (optional field), generic `path_entries` — so the fixtures describe no real host at all. All eight examples still validate against their schemas.
The v3 signals object had copied v2's `volta_shims` field name unexamined — baking one shim manager into the contract when asdf, mise, proto, fnm, and corepack shims are the same construct. v3 is unreleased, so rename to `shims`: keyed by tool, each entry carrying the shim `manager` as data (`volta` is just the first one the prober classifies) plus `resolved`. New managers slot in without a contract change. v2's `volta_shims` spelling stays frozen — that contract already shipped.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cmd/doctor.rs`:
- Around line 45-52: The non-JSON doctor renderer should be pinned to an
explicit v2 contract instead of the global CURRENT_VERSION: change the ternary
that sets build_version (the if json { schema_version } else {
crate::schema::CURRENT_VERSION }) to use a dedicated doctor-human v2 constant
(e.g. crate::schema::DOCTOR_HUMAN_V2) for the else branch so the human renderer
remains stable; add that constant in crate::schema if it doesn't exist and use
the symbol DOCTOR_HUMAN_V2 in place of CURRENT_VERSION in the build_version
assignment while leaving the json branch using schema_version unchanged.
In `@src/schema/doctor_v3.rs`:
- Around line 495-510: ecosystems_v3 currently seeds ecosystems only from
ctx.package_managers which lets Node be dropped even when the resolver/tasks
imply Node is present; update ecosystems_v3 (and the matching logic in tools_v3)
to compute a unified "node context present" predicate that considers both
ctx.package_managers and resolver/task signals (e.g., presence of a Node
resolution, package.json-derived decisions, or tasks that depend on Node), then
use that predicate when deciding to include Ecosystem::Node (so
node_ecosystem_v3 is called) and when emitting the Node entry in tools_v3;
ensure both functions call the same helper/predicate so the inclusion logic is
consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 22e721fb-3b64-4181-bf64-d17620a8b660
📒 Files selected for processing (22)
.github/workflows/pages.ymlCHANGELOG.mdschemas/doctor.v1.example.jsonschemas/doctor.v2.example.jsonschemas/doctor.v2.schema.jsonschemas/doctor.v3-draft.schema.jsonschemas/doctor.v3.example.jsonschemas/doctor.v3.schema.jsonschemas/list.v1.example.jsonschemas/list.v2.example.jsonschemas/why.v1.example.jsonschemas/why.v2.example.jsonschemas/why.v3.example.jsonsrc/cli.rssrc/cmd/doctor.rssrc/cmd/schema.rssrc/lib.rssrc/resolver/mod.rssrc/schema/doctor_v3.rssrc/schema/mod.rssrc/schema/project.rssrc/schema/v3.rs
💤 Files with no reviewable changes (1)
- schemas/doctor.v3-draft.schema.json
📜 Review details
⏰ Context from checks skipped due to timeout of 18000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: verify
- GitHub Check: Analyze (rust)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (actions)
🧰 Additional context used
📓 Path-based instructions (2)
.github/**/*.{yml,yaml}
⚙️ CodeRabbit configuration file
Do not ever warn about stylistic yamllint shit. Do warn about security related shit. Insecure shell handling of user supplied / defined strings: think of branch names, inputs, pr content, anything needs to be string interpolation and permission safe.
Files:
.github/workflows/pages.yml
**/CHANGELOG.md
📄 CodeRabbit inference engine (Custom checks)
**/CHANGELOG.md: If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, CHANGELOG.md MUST also be modified in the same PR.
If a version bump is detected, CHANGELOG.md MUST contain a new section header matching the exact new version number in the format: '## [X.Y.Z] - YYYY-MM-DD'.
If NO version bump is detected, the changes in the PR MUST be added under the existing '## [Unreleased]' section in CHANGELOG.md. The entry MUST describe the changes (e.g., Added, Changed, Fixed, Removed).
Files:
CHANGELOG.md
🧠 Learnings (8)
📚 Learning: 2026-05-04T15:23:38.296Z
Learnt from: kjanat
Repo: kjanat/runner PR: 5
File: .github/workflows/release.yml:73-73
Timestamp: 2026-05-04T15:23:38.296Z
Learning: For GitHub Actions workflows in this repository (kjanat/runner), maintain the maintainer’s preference for “floating” action tags (e.g., `v6`, `v7`) instead of pinned full commit SHAs. During code review, do not flag action usages in `.github/workflows/**/*.yml` for not being pinned to full commit SHAs or suggest switching to SHA pinning.
Applied to files:
.github/workflows/pages.yml
📚 Learning: 2026-03-26T20:05:44.851Z
Learnt from: kjanat
Repo: kjanat/runner PR: 1
File: src/cmd/mod.rs:67-75
Timestamp: 2026-03-26T20:05:44.851Z
Learning: In Rust, `std::process::Command` does not provide getters for stdio configuration, so whether `Stdio::inherit()` (or other `Stdio::*` settings) was applied cannot be asserted via pure unit tests without spawning a process and inspecting OS-level fds. When reviewing Rust code, do not flag “missing unit test coverage” for stdio configuration on `std::process::Command` as a code issue—treat the explicit `Command::stdin/stdout/stderr` setter calls in the source as the meaningful guarantee.
Applied to files:
src/cmd/doctor.rssrc/schema/v3.rssrc/schema/project.rssrc/cli.rssrc/lib.rssrc/resolver/mod.rssrc/schema/mod.rssrc/schema/doctor_v3.rssrc/cmd/schema.rs
📚 Learning: 2026-04-21T15:16:40.277Z
Learnt from: kjanat
Repo: kjanat/runner PR: 4
File: src/tool/just.rs:132-133
Timestamp: 2026-04-21T15:16:40.277Z
Learning: In Rust, if a method like `ExtractedTask::name() -> &str` returns a borrowed `&str` tied to `&self`, then using `sort_unstable_by_key(|t| t.name())` should be avoided because `by_key` requires the key to not borrow from the element being sorted (it will fail to compile due to the key’s lifetime). Do not recommend `sort_unstable_by_key` as a simplification in this situation. If you need an allocation-free and idiomatic comparison, use `sort_unstable_by(|a, b| a.name().cmp(b.name()))` instead. Using `.to_owned()` inside `by_key` is an alternative but allocates a `String` per element.
Applied to files:
src/cmd/doctor.rssrc/schema/v3.rssrc/schema/project.rssrc/cli.rssrc/lib.rssrc/resolver/mod.rssrc/schema/mod.rssrc/schema/doctor_v3.rssrc/cmd/schema.rs
📚 Learning: 2026-05-04T23:28:17.947Z
Learnt from: kjanat
Repo: kjanat/runner PR: 5
File: src/lib.rs:222-223
Timestamp: 2026-05-04T23:28:17.947Z
Learning: When reviewing Rust `rustdoc` comments (`/// ...`), treat a trailing backslash (`\`) at the end of a comment line as valid CommonMark syntax for a hard line break (rendered as `<br>`). Do not flag such trailing backslashes as papercuts or recommend removing them unless there is clear evidence they are unintended (e.g., they are not in the `rustdoc` comment context or the surrounding formatting contradicts the intended CommonMark hard-break usage).
Applied to files:
src/cmd/doctor.rssrc/schema/v3.rssrc/schema/project.rssrc/cli.rssrc/lib.rssrc/resolver/mod.rssrc/schema/mod.rssrc/schema/doctor_v3.rssrc/cmd/schema.rs
📚 Learning: 2026-06-01T17:42:48.461Z
Learnt from: kjanat
Repo: kjanat/runner PR: 34
File: Cargo.toml:61-61
Timestamp: 2026-06-01T17:42:48.461Z
Learning: In this repo, do not flag `actions_rs::log::GroupGuard` or `actions_rs::log::group_guard` as missing/non-existent when they are referenced in Rust code, because the `actions-rs` crate (dependency `actions-rs = "0.1"` / published v0.1.x) exports `pub struct GroupGuard` and `pub fn group_guard` from `actions_rs::log` (and also exports `actions_rs::env::is_github_actions`). This avoids false positives from outdated/incorrect web search results. If the repo does not depend on `actions-rs = "0.1"` in `Cargo.toml`, then normal missing-import/item checks can apply.
Applied to files:
src/cmd/doctor.rssrc/schema/v3.rssrc/schema/project.rssrc/cli.rssrc/lib.rssrc/resolver/mod.rssrc/schema/mod.rssrc/schema/doctor_v3.rssrc/cmd/schema.rs
📚 Learning: 2026-06-11T18:52:28.233Z
Learnt from: kjanat
Repo: kjanat/runner PR: 45
File: src/lib.rs:659-661
Timestamp: 2026-06-11T18:52:28.233Z
Learning: In Rust, it’s acceptable to use `matches!(cli.command, Some(cli::Command::Doctor { .. }))` (or similar) when the scrutinee comes from a field accessed through a shared reference (e.g., `cli: &Cli`). If the match pattern uses `..` and binds zero fields (so only the enum discriminant is matched), Rust match ergonomics will avoid moving out of the referenced value. Reviewers should not flag such code as a compile-blocking move; confirm with `cargo check` rather than forcing changes like `.as_ref()` when compilation succeeds.
Applied to files:
src/cmd/doctor.rssrc/schema/v3.rssrc/schema/project.rssrc/cli.rssrc/lib.rssrc/resolver/mod.rssrc/schema/mod.rssrc/schema/doctor_v3.rssrc/cmd/schema.rs
📚 Learning: 2026-05-14T15:35:39.922Z
Learnt from: kjanat
Repo: kjanat/runner PR: 27
File: src/cmd/run.rs:206-212
Timestamp: 2026-05-14T15:35:39.922Z
Learning: For Go projects using the canonical `cmd/` layout, when `runner` dispatches a local Go tool by its bare name (e.g., `some-cli`) under `PackageManager::Go`, resolve it relative to `ctx.root` by probing `ctx.root/cmd/<task_name>/`. If it exists, run the tool with the filesystem-path form: `go run ./cmd/<task_name>` (note the leading `./`). Do NOT generate `go run <task_name>` because bare names are treated as Go import paths, not local directories. While `go run <module>/cmd/<task_name>` can work, it’s unnecessary for local development—prefer `./cmd/<task_name>`.
Applied to files:
src/cmd/doctor.rssrc/cmd/schema.rs
📚 Learning: 2026-05-15T01:31:48.037Z
Learnt from: kjanat
Repo: kjanat/runner PR: 27
File: src/types.rs:584-585
Timestamp: 2026-05-15T01:31:48.037Z
Learning: In the `kjanat/runner` Rust codebase, remember that `just` treats its `justfile` name as officially case-insensitive and allows the hidden variant: `justfile`, `Justfile`, `JUSTFILE`, etc., and `.justfile`. `TaskSource::Justfile` detection should recognize these filename variants. Also note that `from_label` in `src/types.rs` is for parsing user-supplied qualifier prefixes (e.g. `justfile:build`), not for detecting the on-disk justfile. If you add/adjust qualifier label emission for `TaskSource::Justfile`, it’s not strictly required for backward compatibility because `TaskSource::label()` previously only emitted lowercase `"justfile"`, but adding additional casing/hidden variants as a defensive UX improvement is consistent with `just`’s official support.
Applied to files:
src/cmd/doctor.rssrc/schema/v3.rssrc/schema/project.rssrc/cli.rssrc/lib.rssrc/resolver/mod.rssrc/schema/mod.rssrc/schema/doctor_v3.rssrc/cmd/schema.rs
🪛 zizmor (1.25.2)
.github/workflows/pages.yml
[warning] 10-10: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 10-10: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🔍 Remote MCP GitHub Grep
Based on the GitHub Grep searches performed, let me compile the findings:
Code Integration Validation via GitHub Grep
I searched for real-world code patterns to validate the implementation approaches used in this PR against industry conventions. Here are the relevant findings:
JSON Schema and Structured Reporting Patterns
JSON Schema draft 2020-12 is actively used in real-world projects, confirming the PR's choice of schema version. The PR uses standard $defs and $schema declarations consistent with modern JSON Schema practices found across multiple repositories including the official JSON Schema test suite.
Diagnostic/Report Serialization Patterns
Searches for similar structured report implementations across Rust projects (Biome, Deno, Bevy, Move Compiler) show that:
- Collecting diagnostics in vectors (
pub diagnostics: Vec<...>) is a conventional pattern for structured reporting - Separate builder functions (like
DoctorReportV3::build) are used to assemble complex report types from context objects - Decision/strategy enums are commonly serialized with strategy names (
single-candidate,ranked, etc.)
Version-Gated Output Logic
The PR's pattern of dispatching based on schema_version using if json && schema_version >= 3 matches conventions found in config reloaders and CLI tools that handle multiple output formats. This is a standard approach for maintaining backward compatibility while adding new schemas.
Source Label Conventions
Source label functions returning static strings (fn source_label() -> &'static str) are standard in Rust for describing where configuration/data originates, which aligns with the PR's source_label implementation for cargo-alias distinction.
PATH Probing and Tool Detection
The PR's ProbeSignals and path_probe structures for detecting tools are consistent with patterns found in real projects that need to discover executables (network proxies, shell environments, build tools). Making these public (pub(super)) for module-level access is appropriate for sibling module integration.
Code Generation and Schema Stability
The PR's use of schema_url(command, version) for canonical URL generation and schema post-processing (patching $defs for version-specific constraints) matches patterns used by projects like biome for maintaining schema stability across versions.
🔇 Additional comments (19)
CHANGELOG.md (1)
20-51: LGTM!schemas/doctor.v1.example.json (1)
3-43: LGTM!schemas/doctor.v2.example.json (1)
3-43: LGTM!schemas/list.v1.example.json (1)
3-3: LGTM!schemas/list.v2.example.json (1)
3-3: LGTM!schemas/why.v1.example.json (1)
14-26: LGTM!schemas/why.v2.example.json (1)
14-26: LGTM!.github/workflows/pages.yml (1)
10-10: Arrr, yer sparse-checkout needed this all along! 🏴☠️The
sparse-checkout-cone-mode: falsebe the missing piece that makes yer glob patternschemas/*.schema.jsonactually work, matey. Cone mode be fer directory prefixes only, but ye be needin' file pattern matchin' here. Ship shape!src/schema/mod.rs (1)
27-33: LGTM!Also applies to: 40-46, 59-67, 96-114, 134-136, 226-240
src/cli.rs (1)
851-855: LGTM!Also applies to: 859-865
src/lib.rs (1)
586-596: LGTM!Also applies to: 598-606, 792-793
src/schema/v3.rs (1)
1-6: LGTM!src/cmd/schema.rs (1)
60-63: LGTM!Also applies to: 156-156, 254-254, 267-268
schemas/why.v3.example.json (1)
4-4: LGTM!Also applies to: 13-13, 19-19, 39-39, 45-45
src/resolver/mod.rs (1)
50-53: LGTM!src/schema/project.rs (1)
525-533: LGTM!schemas/doctor.v2.schema.json (1)
405-405: LGTM!schemas/doctor.v3.schema.json (1)
1-672: LGTM!schemas/doctor.v3.example.json (1)
1-610: LGTM!
| // The human renderer still reads the flat v2 `Project` shape, so a | ||
| // non-JSON call always builds at the v2 contract regardless of the | ||
| // requested (or defaulted) version. | ||
| let build_version = if json { | ||
| schema_version | ||
| } else { | ||
| crate::schema::CURRENT_VERSION | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Pin the human doctor renderer to an explicit v2 contract constant.
This currently works, but using crate::schema::CURRENT_VERSION couples human output to a shared global version that may drift for another surface later. Please pin the non-JSON build path to an explicit doctor-human/v2 constant so this path can’t regress accidentally.
Suggested patch
+const DOCTOR_HUMAN_PROJECT_SCHEMA_VERSION: u32 = 2;
+
pub(crate) fn doctor(
@@
- let build_version = if json {
+ let build_version = if json {
schema_version
} else {
- crate::schema::CURRENT_VERSION
+ DOCTOR_HUMAN_PROJECT_SCHEMA_VERSION
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/cmd/doctor.rs` around lines 45 - 52, The non-JSON doctor renderer should
be pinned to an explicit v2 contract instead of the global CURRENT_VERSION:
change the ternary that sets build_version (the if json { schema_version } else
{ crate::schema::CURRENT_VERSION }) to use a dedicated doctor-human v2 constant
(e.g. crate::schema::DOCTOR_HUMAN_V2) for the else branch so the human renderer
remains stable; add that constant in crate::schema if it doesn't exist and use
the symbol DOCTOR_HUMAN_V2 in place of CURRENT_VERSION in the build_version
assignment while leaving the json branch using schema_version unchanged.
| let mut seen = Vec::new(); | ||
| for pm in &ctx.package_managers { | ||
| let eco = pm.ecosystem(); | ||
| if !seen.contains(&eco) { | ||
| seen.push(eco); | ||
| } | ||
| } | ||
|
|
||
| seen.into_iter() | ||
| .map(|eco| match eco { | ||
| Ecosystem::Node => node_ecosystem_v3(ctx, node_pm, resolve_shims), | ||
| Ecosystem::Python => python_ecosystem_v3(ctx, overrides), | ||
| other => single_pm_ecosystem_v3(ctx, other), | ||
| }) | ||
| .collect() | ||
| } |
There was a problem hiding this comment.
Node can disappear from v3 diagnostics despite a successful Node resolution.
ecosystems_v3 seeds ecosystems only from ctx.package_managers. In scenarios where resolver still derives a Node decision (e.g. package.json present, no detected PM), Node is omitted from ecosystems, and the same gating then skips the node runtime entry in tools. That yields internally inconsistent output (tasks may resolve via Node PM while Node ecosystem/tool metadata is missing).
Please derive “Node context present” from resolution/task signals as well (not only detected PM list), then apply that same predicate in both ecosystems_v3 and tools_v3.
Suggested direction
fn ecosystems_v3(
@@
) -> Vec<EcosystemV3> {
let mut seen = Vec::new();
@@
for pm in &ctx.package_managers {
let eco = pm.ecosystem();
if !seen.contains(&eco) {
seen.push(eco);
}
}
+
+ let has_node_context =
+ node_pm.is_ok()
+ || ctx.tasks.iter().any(|t| matches!(t.source, TaskSource::PackageJson))
+ || detect_pm_from_manifest(&ctx.root).is_some();
+ if has_node_context && !seen.contains(&Ecosystem::Node) {
+ seen.push(Ecosystem::Node);
+ }
@@
fn tools_v3(ctx: &ProjectContext) -> Vec<ToolV3> {
@@
- if ctx
- .package_managers
- .iter()
- .any(|pm| pm.ecosystem() == Ecosystem::Node)
- {
+ let has_node_context =
+ ctx.package_managers.iter().any(|pm| pm.ecosystem() == Ecosystem::Node)
+ || ctx.tasks.iter().any(|t| matches!(t.source, TaskSource::PackageJson));
+ if has_node_context {
tools.push(probe_tool(
"node",
DependencyKindV3::Runtime,Also applies to: 809-823
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/schema/doctor_v3.rs` around lines 495 - 510, ecosystems_v3 currently
seeds ecosystems only from ctx.package_managers which lets Node be dropped even
when the resolver/tasks imply Node is present; update ecosystems_v3 (and the
matching logic in tools_v3) to compute a unified "node context present"
predicate that considers both ctx.package_managers and resolver/task signals
(e.g., presence of a Node resolution, package.json-derived decisions, or tasks
that depend on Node), then use that predicate when deciding to include
Ecosystem::Node (so node_ecosystem_v3 is called) and when emitting the Node
entry in tools_v3; ensure both functions call the same helper/predicate so the
inclusion logic is consistent.
| "pnpm": "/home/kjanat/.volta/bin/pnpm", | ||
| "yarn": "/home/kjanat/.volta/bin/yarn" | ||
| }, | ||
| "volta_shims": { |
What
Implements the schema v3 data models that shipped in #50 as unreviewed
drafts. Both drafts got the same treatment: compare against the actual
codebase, implement, validate the real output against the draft, retire the
draft.
why --jsonv3 (default forwhy)Report restructured around
{task, match}candidate pairs plus adecisionblock. Tasks carry identity (fqn=root:<kind>:<name>,provider,kind— cargo aliases labeledcargo-alias), origin(
sourcefile,source_pointerkey path), and resolution data(
definition,resolvedpreview,cwd, siblingaliases). Thematchhalf exposes the exact run-time selection key;
decision.strategynamesthe branch taken (
single-candidate/ranked/filtered/exec-fallback).doctor --jsonv3 (default fordoctor)Flat v2 dump becomes a structured diagnostic inventory:
invocation/environment/runnerprovenance, per-ecosystemsdecisionsgraded by
confidence(typedResolutionStep→ override/manifest/lockfile= high, PATH probe = medium, legacy npm fallback = low, failure = none),
first-class
sources,fqn-keyedtaskswith effectiveresolvedcommands, PATH-probed
tools, duplicate-task-nameconflicts(winner,shadowed, ranking), flattened
diagnostics, and a self-describingresolutionpolicy block.Verification
whyv3 real output reproduces the former draft example verbatim(semantic diff: identical); promoted to
schemas/why.v3.example.json.doctorv3 real output validates against the original draft schemaand the committed generated one (ajv, draft 2020-12); example committed.
runnertask isdefined by both the justfile and cargo-aliases; the report names
root:just:runnerthe winner with the ranking that decided it.clippy --all-targets --all-featuresclean, zero lint suppressions,gen-schemadrift-stable.Review deltas from the drafts (documented in module docs)
tasks[].resolved/sourceare nullable — PM resolution can fail;the draft would have forced lying.
sources[].kindaligned to the why-v3 label convention(
cargo-alias), fixing the drafts' cross-surface inconsistency.identity, probe-error status, unused severities/tool kinds) are
deferred, not declared — contracts describe output, not ambition.
Versioning
Surfaces version independently:
doctorandwhyare at v3,liststays at v2 and rejects
--schema-version 3rather than mislabel anunchanged payload. v1/v2 output unchanged and reachable via
--schema-version. Human output untouched (doctor human path pins to thev2 builder).