fix(cli,supply-chain): parse package.json as JSON, and send fatal errors to stderr - #323
fix(cli,supply-chain): parse package.json as JSON, and send fatal errors to stderr#323Mark2Mac wants to merge 2 commits into
Conversation
|
Cross-PR note: this PR conflicts with #302, which also touches The reconciliation is one line — the JSON parser here calls that predicate instead of its own |
rng1995
left a comment
There was a problem hiding this comment.
[Automated SkillSpector Review]
Requesting changes. Parsing package.json as JSON fixes the compact-manifest blind spot and the fallback preserves best-effort handling of malformed manifests. The stderr change is incomplete, however: both generic --verbose exception branches still call console.print_exception(), so a fatal traceback is written to stdout. I reproduced this at the current head with a forced graph exception: exit 2, full traceback in stdout, empty stderr. Please route those tracebacks through err_console and add a stream-separation regression for the verbose path.
| # Fatal errors go to stderr. Anything driving the CLI from a script separates the two streams, | ||
| # and with the message on stdout the only diagnosis available was thrown away: a failed scan | ||
| # left an empty error log and the caller had nothing to act on. | ||
| err_console = Console(stderr=True) |
There was a problem hiding this comment.
Blocking: the generic --verbose handlers in both scan() and baseline() still call console.print_exception(), which writes the fatal traceback to stdout. I reproduced exit 2 with the full traceback in stdout and empty stderr. Please use err_console.print_exception() for those branches and add a regression that asserts stdout/stderr separation under --verbose.
There was a problem hiding this comment.
Fixed in f617861 — the objection was correct, and it was the same defect this PR set out to fix, left in the one branch where it is hardest to notice.
Both generic handlers now print through err_console, so scan --verbose and baseline --verbose behave like the one-line error paths. Reproduced your case first: with graph.invoke raising, the traceback was in stdout and result.stderr was empty.
Regression covers both commands (tests/unit/test_cli.py): exit code 2, RuntimeError present in stderr and absent from stdout. Verified it fails on the previous commit.
grep -rn print_exception src/ now returns only those two lines, both on err_console. Full suite: 1572 passed, 12 skipped, 6 xfailed.
f617861 to
01a92a4
Compare
|
Rebased onto The blocker. You were right, and I had reproduced it the same way: The reconciliation with #319, which is the reason for the rebase. #319 landed One test moved with it: The defect is still live on >>> _extract_packages_from_package_json('{"name":"x","dependencies":{"express":"^4.18.0","lodash":"4.17.21"}}')
[]A valid one-line manifest yields no dependencies at all, silently — no error, no warning, just an empty supply-chain surface for that unit. Full suite on the rebased branch: |
rng1995
left a comment
There was a problem hiding this comment.
[Automated SkillSpector Review]
Re-review: the previous stderr blocker is resolved—both verbose fatal traceback paths now use err_console, with stream-separation regressions—and the JSON package parser is well covered. However, the exact head fails ruff format --check: tests/unit/test_patterns_new.py would be reformatted. Please run Ruff format and update the PR; the focused functional suites otherwise pass (289 tests).
| assert versions["shell-quote"] is None | ||
| assert versions["semver"] is None | ||
| assert versions["glob"] is None | ||
| def test_package_json_on_a_single_line_is_not_invisible(self) -> None: |
There was a problem hiding this comment.
Blocking CI issue: the exact head fails ruff format --check; Ruff reports this test file would be reformatted. Please run ruff format tests/unit/test_patterns_new.py and commit the result. The prior stderr blocker is resolved and the focused tests pass.
|
@Mark2Mac - Please address review comments and resolve merge conflicts. |
01a92a4 to
bf5792e
Compare
|
Both points addressed. New head The lint failure. You were right, and the CI run on The formatting is squashed into the commit that introduces those tests rather than added as a separate The conflicts. Rebased onto Verified on the exact head:
The defect this PR fixes is still live on >>> _extract_packages_from_package_json('{"name":"x","dependencies":{"express":"^4.18.0","lodash":"4.17.21"}}')
[]A valid single-line manifest yields no dependencies at all — no error, no warning, just an empty supply-chain surface for that unit. |
package.json was scanned line by line. A manifest written on a single line — valid JSON, and what several generators emit — never entered the dependency section, so it produced *no* dependencies at all and the file passed silently. That is not noise, it is blindness: the scanner reports nothing and the caller cannot tell the difference from a clean manifest. It is now parsed as JSON. Version extraction is unchanged, including the caret handling: only the parsing changes. Line numbers survive the switch — the entry is located from the section header onwards, so a name that also appears in "scripts" does not steal the position — and a manifest that does not parse still falls back to the previous scan rather than going blind. Tests: one-line manifest, compact manifest, line numbers preserved, a name shadowed by "scripts", invalid JSON falling back, a non-object manifest, and non-string specs ignored. Signed-off-by: Mark2Mac <Mark2Mac@users.noreply.github.com>
bf5792e to
f8bfc34
Compare
Anything driving the CLI from a script separates the two streams and parses stdout. A diagnostic printed there is lost as a diagnostic — a failed scan left an empty error log and nothing to act on — and corrupting as output, since it lands in the same stream as the report. The mechanism already exists: err_console arrived with the author-shipped baseline notices, which correctly go to stderr. This commit only moves the diagnostics onto it. Thirteen call sites: every message that prints and then raises typer.Exit, the two print_exception() calls in the --verbose branches, and the per-skill error inside the multi-skill loop. --version stays on stdout, because that is program output rather than a diagnostic. Tests enumerate all thirteen paths and assert the message reaches stderr and never stdout. Verified red against the unmodified module: fourteen failures. The last test is the reason the others are not enough. Twelve of these sites already existed when this change was first written and it moved only eight of them; a thirteenth arrived later, in the same commit that introduced err_console. The invariant has no enforcement, so it regenerates. The test parses cli.py and fails when error-styled output is written to the default console. Signed-off-by: Mark2Mac <Mark2Mac@users.noreply.github.com>
f8bfc34 to
232a226
Compare
|
Both points addressed, and the second half of the PR is smaller than it was. New head The blocking comment. The conflict. It was the Two things I got wrong, both found while rebasing. When this PR was first opened there were twelve A thirteenth arrived afterwards — in except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(code=2) from eThat is the argument for the last test better than anything in the PR body. The invariant regenerates in both directions because nothing enforces it, so The thirteen behavioural cases were verified red against the unmodified module: fourteen failures, one per path plus the guard. Gates, run locally before pushing:
Also merged with #344 to check the two do not fight over Split into two commits along the two fixes, so the CLI change can be read on its own. CI has not run on a head of this PR since |
Two correctness fixes, both surfaced while driving the CLI from automation. They are independent of each other but small enough to review together; happy to split if you prefer.
Rebased onto
mainat27fd962(2.9.5). The second half changed shape since the last review — see "What changed in this rebase" at the end.1.
package.jsonwas scanned line by line_extract_packages_from_package_jsonwalked the file looking for a line containing"dependencies", then read entries until a line starting with}. A manifest written on a single line — valid JSON, and what several generators emit — never enters the section:That is not a noisy result, it is a blind one: the output is indistinguishable from a clean manifest.
It is now parsed with
json.loads. Specifically:"scripts"does not steal the line.2. Fatal diagnostics were written to stdout
console = Console()writes to stdout, and everyError:went through it. Any caller that separates the two streams — which is what automation does — throws the diagnosis away:The message lands in the file that was supposed to hold the report, and the error log is empty. It is lost as a diagnostic and corrupting as output.
The mechanism now already exists.
err_console = Console(stderr=True)arrived with the author-shipped baseline notices in #286, and those correctly go to stderr. This PR no longer introduces anything — it moves the diagnostics onto the console that is already there. Thirteen call sites: every message that prints and then raisestyper.Exit, the twoprint_exception()calls in the--verbosebranches, and the per-skill error inside the multi-skill loop.--versiondeliberately stays on stdout: that is program output, not a diagnostic. TwoWarning:messages also stay where they are — same stream, but a different question, and I would rather not widen this PR to decide it.Tests
Seven cases for the parser (one-line manifest, compact manifest, line numbers preserved, name shadowed by
scripts, invalid JSON falling back, non-object manifest, non-string specs ignored).For the CLI, thirteen parametrized cases, one per fatal path, each asserting the message reaches stderr and never stdout. Verified red against the unmodified module: fourteen failures.
The fourteenth is a guard, and it is the part I would keep even if you drop the rest. When this PR was first opened there were twelve such sites and it moved eight of them — I missed four. A thirteenth arrived afterwards, in
2d198ab, the same commit that introducederr_console: the local-target gate prints itsValueErrorto stdout. The invariant has no enforcement, so it regenerates in both directions.test_cli_writes_no_error_styled_output_to_stdoutparsescli.pyand fails when error-styled output is written to the default console.Gates, run locally
make lint(ruff check src/ tests/)make format-check(ruff format --check)make test-unit2210 passed, 14 skipped, 4 xfailedtests/docker/smoke.sh(image built from this branch)Interaction with #344 measured rather than assumed: merged the two branches, no conflict,
2225 passed.What changed in this rebase
err_consoledefinition is gone from the diff —mainhas it now, and that was the only line this PR was colliding on.--mcp-registryargument checks, the registry scan handler, and--baselinewith--recursive) and the one that arrived with the local-target gate in 2.9.4.