Fix #1299: Picker platforms values are not checked against the known process.platform set: a manifest typo silently withholds the row everywhere - #1304
Conversation
A `platforms` gate is matched against `process.platform` by string
equality, so a manifest typo ("macos", "Darwin", "win") validates
cleanly and then withholds the row on every platform with nothing said.
`loadManifest` now emits one stderr-mirrored WARN per offending row,
naming the manifest, the plugin, the row, and the unrecognized values.
Validation is unchanged: a rejection is fatal to the whole plugin, and
one mistyped display gate is not worth that. LLP 0369 records the shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ostic out of the reject path Two low findings from the review of 11251e3. `KNOWN_PLATFORMS` was the eight values the prose docs list, but `process.platform` also reports `netbsd`, `cygwin`, and `haiku` today (they are in `NodeJS.Platform`). A plugin correctly gating a row to `["netbsd"]` would have printed "which no platform reports" on every manifest load: a false diagnostic on correct data, which is the one thing this warning must not do. The set is now the full union, LLP 0369#known-set says so, and the quiet case in the test covers `netbsd`. The warn call also sat inside the `try` whose `catch` turns anything thrown into `{ ok: false, errorKind: 'manifest_invalid' }`. A throw from the diagnostic (a foreign global logger provider whose `getLogger` throws is outside `emit`'s own guard) would therefore have lost the whole plugin over one mistyped display gate, exactly the outcome LLP 0369#warn-not-reject exists to prevent. Moved after the try/catch so the invariant is structural. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1 (head
|
…es, and off the reject path both ways A partly unrecognized gate (["darwin", "win"]) still renders the row on macOS, so the warning may not tell its author the row is offered nowhere. The message now distinguishes a gate that is unrecognized end to end from one where only some values are, and a test pins the difference. Round 1 moved the warn out of the try whose catch rejects the manifest, which fixed one containment hole and opened a wider one: a throw from the diagnostic then rejected a promise `loadManifest` has never rejected, and `loadManifests` fans out over `Promise.all`, so it would have failed every plugin rather than one. The call now carries its own catch, so LLP 0369 #warn-not-reject holds on both sides. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 2 (head
|
| case | master | head 2f2f2876 |
|---|---|---|
| success, warning fires | ok:true keys [manifest,manifestPath,ok,rootDir] |
identical |
| success, no warning | ok:true keys [manifest,manifestPath,ok,rootDir] |
identical |
| invalid JSON | ok:false manifest_invalid keys [errorKind,manifestPath,message,ok,rootDir] |
identical |
| invalid shape | ok:false manifest_invalid same keys |
identical |
| missing file | ok:false manifest_invalid same keys |
identical |
Byte-identical on all five. manifest_invalid is the only member of ManifestErrorKind (src/core/types.d.ts:132), so there is no other kind to lose, and the let manifest hoist is definitely assigned because the catch returns rather than falling through.
Findings
1. Low: the warning claimed "the row is offered nowhere" for a gate that was only partly wrong. src/core/manifest.js:122 on 2f2f2876.
The detail string was built from unrecognized, but asserted something about the whole gate. For "platforms": ["darwin", "win"] (the author meant win32, and this is squarely the class of typo #1299 is about) the row still renders on macOS, and the line told its author it was offered nowhere. That sends them hunting for a row that is visible on their own machine, which is the confusion the diagnostic exists to remove. The "offered nowhere" claim only holds when unrecognized.length === platforms.length.
Fixed: the message now splits on exactly that condition. An end-to-end unrecognized gate keeps the original wording; a partial one gets picker row names <values> in its gate, which no platform reports, so the row is offered only where the rest of the gate matches. test/core/picker-platform-unknown-warning.test.js:85 pins it, and I confirmed it is not vacuous: against 2f2f2876's src/core/manifest.js that test is not ok 2, and the other three still pass.
2. Low: round 1's fix traded a contained failure for an uncontained one. src/core/manifest.js:82 on 2f2f2876.
Round 1 was right that the warn did not belong inside the try whose catch rejects the manifest. But moving it outside with no guard at all left it on no seam: loadManifest's documented contract is Promise<LoadedManifest|FailedManifest> and it has never rejected on the success path, so a throw from the diagnostic now escaped as a rejection instead. The seam is narrow but real, and it is the same one round 1 identified: logs.getLogger('hypaware.manifest') runs at src/core/observability/logger.js:158, ahead of the try that emit puts around the OTel call, and the mirror's process.stderr.write at logger.js:194 is the one step in emit with no guard around it at all. The blast radius is larger than the bug being avoided: loadManifests fans out over Promise.all (src/core/manifest.js:136) and src/core/runtime/installed.js:41 awaits it unguarded, so one throw would fail all of plugin discovery rather than the single plugin the original placement would have lost.
Fixed: the call now sits outside the manifest try and inside a try {} catch {} of its own, with a comment naming both hazards. LLP 0369 #warn-not-reject now holds on both sides: a diagnostic can neither reject the manifest nor reject the promise.
No LLP edit was needed for either. Neither changes a documented decision: finding 1 is message text the LLP does not specify (#warn-not-reject requires the line to name "the manifest path, the plugin, the row, and the unrecognized values", which it still does), and finding 2 makes the existing @ref LLP 0369#warn-not-reject [implements] more literally true rather than differently true.
Verification on the new head ca6cd2ae
npm test: 5990 pass, 0 fail, 1 skipped (25.7s), up from 5989/0/1 by the one test added.npm run typecheck: clean.node scripts/llp-numbers.js check:1 LLP number minted against refs/remotes/origin/master, no collision.llp/0368is still +1 / -0 against master (git diff --numstaton the pushed tree), theExtended-by:forward-ref and nothing else, which is what CLAUDE.md permits on an Accepted doc.- Style: no semicolons, no em dashes in either changed file.
KNOWN_PLATFORMSis not a missed reuse. The only other platform list insrc/is['darwin', 'linux']atsrc/core/commands/daemon.js:502, which is a deliberately narrower thing (the platforms the daemon supports), not the setprocess.platformcan report.claude-desktop's["darwin"]is still the onlyplatformsgate in the repo, so the third test is meaningful rather than vacuous, anddiscoverBundledPluginsdoes route throughloadManifeststoloadManifest.
Not findings, recorded for the reader
- The
Related:lines citeLLP 0130#picker-block,LLP 0011#autodetect-vs-defaultandLLP 0139#macos-only, none of which exist as literal{#anchor}marks. Pre-existing and corpus-wide (0198, 0202, 0297 and 0368 itself all cite0130#picker-blockon master today), andref-checkvalidates@refin source, not proseRelated:lines. Not this PR's to fix. - The warning still fires once per
loadManifestrather than once ever, so a third-party plugin with a typo prints a line per offending row perhypprocess. Intended for a diagnostic aimed at the plugin author; the bundled catalog is silent. - No test was added for finding 2's guard. Proving it needs a globally installed throwing logger provider, and
logs.setGlobalLoggerProvideris process-wide state that would leak into the rest of the suite. The guard is structural and reads as its own proof.
Fixes pushed
ca6cd2ae on fix/issue-1299. Verified by reading origin/fix/issue-1299 back after the push, not from the local tree: the try { warnUnrecognizedPickerPlatforms(...) } catch {} block sits after the catch's closing brace; the detail ternary branches on unrecognized.length === platforms.length; KNOWN_PLATFORMS still carries all eleven values; test/core/picker-platform-unknown-warning.test.js:85 contains the new case. Full suite green on the pushed tree.
Triage at head
|
Ship risk:
|
What neutral was doingRung This PR was not enqueued and has not been evicted. Neutral is holding it deliberately: Why it cannot proceedThe merge queue is evicting PRs for Evidence, two consecutive merge groups, both evicted:
For comparison, the same jobs on ordinary
So the suite normally finishes in 1 to 2 minutes and merge groups succeeded as recently This is repo-wide infrastructure, not a defect in this pull request. The change itself What it needs from youA call on the merge-queue CI budget. The options, as neutral sees them:
Neutral has deliberately not re-enqueued anything: with the cap unchanged, every How to unstickReply with a comment on this PR (or push to the branch); neutral monitors this thread and |
Feature or issue
A picker row's
platformsgate is compared againstprocess.platformby string equality, but nothing checks the declared values against the setprocess.platformcan report. A plugin author's typo ("macos","Darwin","win") passesvalidatePickerContributions, loads, and then matches no platform, so the row is offered nowhere with no error and no diagnostic. The author's own machine gives no signal either, because the symptom is an absent row, which is what a row nobody wrote looks like.Solution
loadManifestnow emits one WARN per offending row,manifest.picker_platform_unrecognized, naming the manifest path, the plugin, the row, and the unrecognized values, mirrored to stderr so it reaches an install with no telemetry configured.process.platformvalue Node adds later. LLP 0369 records that loosened-but-warned shape and is noted on LLP 0368'sExtended-by:line.test/core/picker-platform-unknown-warning.test.jspins the warning: it fails on the pre-fix head (no line emitted) and passes after, asserting the plugin still loads, that a real gate and the bundled catalog stay silent, and that the line carries manifest, row, and value.Code: +41 / -0 lines
Fixes #1299