Skip to content

Implement the v3 output schemas: structured why + doctor reports#51

Open
kjanat wants to merge 6 commits into
masterfrom
feat/schema-v3
Open

Implement the v3 output schemas: structured why + doctor reports#51
kjanat wants to merge 6 commits into
masterfrom
feat/schema-v3

Conversation

@kjanat

@kjanat kjanat commented Jun 12, 2026

Copy link
Copy Markdown
Owner

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 --json v3 (default for why)

Report restructured around {task, match} candidate pairs plus a
decision block. Tasks carry identity (fqn = root:<kind>:<name>,
provider, kind — cargo aliases labeled cargo-alias), origin
(source file, source_pointer key path), and resolution data
(definition, resolved preview, cwd, sibling aliases). The match
half exposes the exact run-time selection key; decision.strategy names
the branch taken (single-candidate / ranked / filtered /
exec-fallback).

doctor --json v3 (default for doctor)

Flat v2 dump becomes a structured diagnostic inventory:
invocation/environment/runner provenance, per-ecosystems decisions
graded by confidence (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, ranking), flattened diagnostics, and a self-describing
resolution policy block.

Verification

  • why v3 real output reproduces the former draft example verbatim
    (semantic diff: identical); promoted to schemas/why.v3.example.json.
  • doctor v3 real output validates against the original draft schema
    and the committed generated one (ajv, draft 2020-12); example committed.
  • Conflict detection proven on real data: this repo's runner task is
    defined by both the justfile and cargo-aliases; the report names
    root:just:runner the winner with the ranking that decided it.
  • 642 lib + 24 integration tests (15 new), clippy --all-targets --all-features clean, zero lint suppressions, gen-schema drift-stable.

Review deltas from the drafts (documented in module docs)

  • tasks[].resolved / source are nullable — PM resolution can fail;
    the draft would have forced lying.
  • sources[].kind aligned to the why-v3 label convention
    (cargo-alias), fixing the drafts' cross-surface inconsistency.
  • Draft shapes nothing can emit yet (rich dependency edges, workspace
    identity, probe-error status, unused severities/tool kinds) are
    deferred, not declared — contracts describe output, not ambition.

Versioning

Surfaces version independently: doctor and why are at v3, list
stays at v2 and rejects --schema-version 3 rather than mislabel an
unchanged payload. v1/v2 output unchanged and reachable via
--schema-version. Human output untouched (doctor human path pins to the
v2 builder).

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`.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are limited based on label configuration.

🚫 Review skipped — only excluded labels are configured. (2)
  • wip
  • cr:skip

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31c4a77d-23ff-4957-bab4-fbefd5b53baa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR rolls out JSON schema version 3 as the new default output format for runner doctor --json and runner why --json commands. The implementation splits schema version management per-command (doctor and why each get independent current-version constants and validators), updates the CLI to accept schema versions 1–3, and adds complete v3 payload implementations. Doctor v3 expands the report with structured invocation metadata, environment details, ecosystem resolution signals, task inventory with effective commands, tool probing results, conflict detection, and flattened diagnostics. Why v3 restructures the output around task/match pairs with a decision block containing strategy and rationale, including provider/source labelling and optional PM-resolved commands. Both are accompanied by committed JSON Schema documents and realistic examples. Backward compatibility is preserved: v1 and v2 are accessible via --schema-version.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • kjanat/runner#22: Introduced initial runner doctor/runner why JSON implementations; this PR extends those with v3 schema support and default routing.
  • kjanat/runner#37: Modified schema-generation plumbing; related to the schema emission changes in this PR.
  • kjanat/runner#50: Earlier work on why/schema generation that this PR builds upon to add v3 contracts and builders.

"Yo-ho, the schemas be polished and the JSON be sharpened,
Doctor and Why now sail on v3, no longer half-harboured,
Cargo aliases get their own little flag to fly,
Tests and examples snug in the chest — shipshape, aye aye!
Arr, go fetch yer review, or walk the plank, matey!"

🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Semver Version Bump Validation ⚠️ Warning Rust/code files changed (e.g., src/, schemas/) but Cargo.toml version is unchanged: origin/master 0.13.0 vs PR HEAD 0.13.0; no required SemVer bump detected. Bump the SemVer version in Cargo.toml (MAJOR for breaking default JSON output contract) to a new MAJOR.MINOR.PATCH value; commit the updated version file.
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarises the main change: implementing v3 output schemas for why and doctor reports.
Description check ✅ Passed The description is directly related to the changeset, providing detailed context about the v3 schema implementations, verification steps, and versioning approach.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Changelog Update ✅ Passed Non-doc code files (e.g., src/.rs, schemas/.json) changed and CHANGELOG.md was updated under ## [Unreleased] with a new ### Added section describing v3 doctor/why defaults.
Agents.Md Documentation Updated ✅ Passed No AGENTS.md files exist in the repository (0 found), so there’s no corresponding AGENTS.md that could be required to update for this PR’s CLI/workflow changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/schema-v3

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 12, 2026
coderabbitai[bot]

This comment was marked as resolved.

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.
kjanat added 2 commits June 13, 2026 03:31
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.
@kjanat kjanat self-assigned this Jun 13, 2026
kjanat added 2 commits June 13, 2026 03:48
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.
@kjanat kjanat added the cr:skip Skip CodeRabbit review label Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 74ce1f2 and 19979e5.

📒 Files selected for processing (22)
  • .github/workflows/pages.yml
  • CHANGELOG.md
  • schemas/doctor.v1.example.json
  • schemas/doctor.v2.example.json
  • schemas/doctor.v2.schema.json
  • schemas/doctor.v3-draft.schema.json
  • schemas/doctor.v3.example.json
  • schemas/doctor.v3.schema.json
  • schemas/list.v1.example.json
  • schemas/list.v2.example.json
  • schemas/why.v1.example.json
  • schemas/why.v2.example.json
  • schemas/why.v3.example.json
  • src/cli.rs
  • src/cmd/doctor.rs
  • src/cmd/schema.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/doctor_v3.rs
  • src/schema/mod.rs
  • src/schema/project.rs
  • src/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.rs
  • src/schema/v3.rs
  • src/schema/project.rs
  • src/cli.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/mod.rs
  • src/schema/doctor_v3.rs
  • src/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.rs
  • src/schema/v3.rs
  • src/schema/project.rs
  • src/cli.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/mod.rs
  • src/schema/doctor_v3.rs
  • src/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.rs
  • src/schema/v3.rs
  • src/schema/project.rs
  • src/cli.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/mod.rs
  • src/schema/doctor_v3.rs
  • src/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.rs
  • src/schema/v3.rs
  • src/schema/project.rs
  • src/cli.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/mod.rs
  • src/schema/doctor_v3.rs
  • src/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.rs
  • src/schema/v3.rs
  • src/schema/project.rs
  • src/cli.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/mod.rs
  • src/schema/doctor_v3.rs
  • src/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.rs
  • src/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.rs
  • src/schema/v3.rs
  • src/schema/project.rs
  • src/cli.rs
  • src/lib.rs
  • src/resolver/mod.rs
  • src/schema/mod.rs
  • src/schema/doctor_v3.rs
  • src/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: false be the missing piece that makes yer glob pattern schemas/*.schema.json actually 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!

Comment thread src/cmd/doctor.rs
Comment on lines +45 to +52
// 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
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread src/schema/doctor_v3.rs
Comment on lines +495 to +510
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()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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": {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why rm it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cr:skip Skip CodeRabbit review enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant