fix(cli,fresh-ui): ui:add emits a resolvable import for subpath registry deps, and pins the current SDK release - #957
Conversation
…e two-cause fix Research proves the filed root cause is necessary but not sufficient: correcting the stale beta.10 pins still leaves `@netscript/sdk/desktop` unresolvable, because `ui:add` strips the subpath from the import-map key but keeps it in the value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… deps Proves that a registry dependency carrying an export subpath installs an alias Deno can resolve. The alias must address the package root: Deno appends the remainder of a bare specifier to the mapped value, so an alias pointing at `jsr:@netscript/sdk@<v>/desktop` resolved `@netscript/sdk/desktop` to `./desktop/desktop` and failed every render (#953). Add and remove now derive the entry from one helper, so pruning matches what installation wrote and two items sharing a package keep their single import. Refs #953 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hips The desktop items pinned sdk@0.0.1-beta.10, a release with no `./desktop` or `./auto-update` export at all — so a beta.11 install resolved a subpath that could not exist. The lifecycle tests now assert the installed import matches packages/sdk/deno.json, so the pin cannot silently fall a release behind again. Refs #953, #956 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ubpath is real Shape validation passed the defect through: `scanned=2206 failures=0` while fresh-ui shipped a beta.10 SDK pin inside a beta.11 workspace. The guard now compares every exact `@netscript/*` pin against the version that workspace member declares, and every export subpath against that member's `exports`. Range pins are listed as notes rather than failures — they still resolve, so rewriting one is a release-policy call, not a bug fix. The release cut calls the guard after the bump, where `findVersionResidue` cannot see a pin left in TypeScript. Refs #953, #956 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lockstep-residue reads the JSON file set the bump rewrites, so a stale @netscript/* pin left in TypeScript passed every release gate. The specifier check now fails on it and on an unexported subpath, and lists range pins as evidence rather than swallowing them. Refs #953, #956 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
[PHASE: RESEARCH] The filed root cause is real but not sufficient — fixing the pins alone leaves #953 broken. Verified against
|
| Import-map value | Result |
|---|---|
jsr:@netscript/sdk@0.0.1-beta.10/desktop |
error: Unknown export './desktop/desktop' for '@netscript/sdk@0.0.1-beta.10' — and beta.10's export list contains neither ./desktop nor ./auto-update |
jsr:@netscript/sdk@0.0.1-beta.11/desktop |
error: Unknown export './desktop/desktop' for '@netscript/sdk@0.0.1-beta.11' — the version fix alone does not help |
jsr:@netscript/sdk@0.0.1-beta.11 |
Check main.ts — clean |
mergeDenoJsonImports strips the export subpath when deriving the import-map key but leaves it in the value (registry-deno-json.ts:22-26,41-51). Deno appends the remainder of a bare specifier to the mapped value, so @netscript/sdk/desktop resolves to ./desktop/desktop. That failure is version-independent. The stale pin made it louder — beta.10 had no such export at all — and supplied the beta.10 string in the reported error.
So the two issues are the same defect from two sides, and each side is a separate cause.
Why every existing guard missed it
findVersionResidue(bump-version.ts:41-66) inspects only*.jsonanddeno.lock:if (!entry.path.endsWith('.json') && relativePath !== 'deno.lock') continue;.discoverVersionFilesrewrites only workspace manifests, the lock, andscaffold.plugin.json. A pin inside a.tsfile is invisible to both.auditLockstepAndResidueinpublish-readiness.tsdoes compare internal specifiers to the release — over that same JSON-only file set.version-drift_test.ts:18,20matches only0.0.1-alpha.\d+and only walkspackages/cli/src/**. It cannot seebeta.10, and it cannot seepackages/fresh-ui.
Not reproducible
#956's third observation, "the MCP server advertises beta.9": packages/mcp/deno.json:3 and publish-assets.generated.ts:5 both read 0.0.1-beta.11 on main. Nothing to fix there.
Skew that is not breakage
Eighteen range-pinned @netscript/* specifiers (^0.0.1-alpha.12 and friends) satisfy 0.0.1-beta.11 under SemVer and resolve correctly. Grok's "CLI reports beta.11, scaffold pins older" scar is this class. Deliberately out of scope for a fix PR — see #973.
Full findings F1–F12 in .llm/runs/fix-freshui-registry-sdk-pin--953/research.md.
|
[PHASE: IMPL] Five slices landed; full gate set green. Slices
The fix
The guard
Reverting one pin on this branch reproduces the shipped defect as a failure: A first draft added this as a separate Regression proof
Gates
One honest caveat: the root Next
|
|
[PHASE: IMPL] Diagnostic note from the #963 investigation — this PR's red Run 30635836952 died mid-suite at Every gate it reached passed. The earlier run on this branch, 30633728754, passed This PR also does not share a failure with #963. For the record on the #953 side: |
behavior.app-home PASSED 225ms, passed=62 failed=0. Closes drift D-4 (the suite had never been executed) and D-7 (local port collision). #957 passes the same lane on re-run with no code change.
…ver-render (#963) * plan(aspire): #954 root-caused to a missing Aspire health check on generated apps Records the harness run for fix/aspire-app-health-probe: research (including a verified `aspire restore` against SDK 13.4.6 confirming `ExecutableResource.withHttpHealthCheck` and its options-object signature), the plan, and the Design checkpoint with five commit slices. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(aspire): declare a health-probe path contract for app resources Proves an app entry can name the HTTP path Aspire should probe, and that the scaffold has a single named default for it instead of a literal buried in a generator template. `RESOURCE_DEFAULTS.AppHealthCheckPath` sits beside `HttpEndpointName`, which the probe reuses as its endpoint name. `AppEntry.HealthCheckPath` is optional and accepts `false`, so an app that serves no health route can opt out rather than sit permanently Unhealthy. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): generated apps report Healthy only once they can server-render Proves an Aspire resource for a generated Fresh app is no longer considered ready the instant its process spawns. Aspire treats a resource with no registered health check as ready as soon as it reaches `Running`. The Aspire helper generator registered none, so an app whose every request failed during SSR still showed green on the dashboard and satisfied `aspire wait` (#954). `generateRegisterApps` now emits `withHttpHealthCheck({ path, endpointName })` for `app` entries that expose a port, immediately after the endpoint whose base address the probe resolves against. Only the `app` type gets a probe: `tauri`, `desktop`, and `task` own no HTTP page contract, and `desktop` is not given an endpoint at all. The emitted call uses the options-object form. Aspire's published docs show `withHttpHealthCheck('/health')`, but the TypeScript SDK generated by `aspire restore` for SDK 13.4.6 declares `withHttpHealthCheck(options?: WithHttpHealthCheckOptions)` on `ExecutableResource`; the positional form would throw at AppHost start. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): pin SSR as the fall-through of the scaffolded health route Proves the route the AppHost probes still renders through the page layer, so the probe added in the previous slice keeps exercising the SSR pipeline rather than a JSON short-circuit. Aspire's probe sends no `Accept` header, so it lands on the server-rendered branch. Narrowing that branch later — returning JSON for an unspecific Accept, say — would silently restore "Healthy while every page returns 500" without touching the generator at all. The route's own doc comment now states the contract for the next person to edit it. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(cli): scaffold.runtime now asks the generated app for a page Proves the merge-readiness suite would catch a recurrence of #954 rather than pass straight through it. The suite started the whole AppHost, waited on every database, cache, and plugin resource and probed their HTTP health — but never waited on the generated app and never issued a single request to any app route. `behavior.ui-render`, the only app-shaped gate, renders AI payload components in-process and never touches the running server. An app that returned 500 to every request passed the suite. Two paired gates close that hole: `runtime.wait.dashboard` blocks on the app's new HTTP health probe (300s, covering Vite's cold start and first render), and `behavior.app-home` fetches the home page and requires a 2xx that is actually HTML. Asserting the status alone would not do — a 500 error page is `text/html` too. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(harness): record gate evidence and the slice log for #954 Closes the run's Gate phase: fmt/lint/check/test/arch/doc-lint results with counts, the fail-before output for the new generator guard, and the `aspire restore` verification that fixed the emitted SDK call shape. `scaffold.runtime` is recorded as NOT RUN rather than skipped — this host has no database containers, so the two new E2E gates are proven by registration tests only. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(llm): session record for the #954 Aspire app health probe run Captures the two reusable findings: Aspire's docs disagree with the generated 13.4.6 TypeScript SDK on withHttpHealthCheck's signature (and how to settle that in two minutes with `aspire restore`), and scaffold.runtime brought up the whole AppHost without ever requesting a page from the generated app. Refs #954 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): the app-home gate asks the project which port the app is on `behavior.app-home` shipped probing a hardcoded `http://127.0.0.1:8000/` (`PORT_RANGES.APP.start`). The scaffold publishes the app on 8010 — `PORT_RANGES.APP.start + 10`, offset on purpose so the Aspire proxy does not collide with Vite's own default of 8000. Nothing has ever listened on 8000, so all 60 attempts were refused: 60 refusals at 1s apart is the 60182ms CI failure, and a refused connection reads exactly like an app that cannot render. The app was fine throughout. `runtime.wait.dashboard` passed in the same run, and a `curl` at the app's real port returns HTTP 200 `text/html`, 130KB. The probe now takes a project root and an app name and resolves the URL from that project's `appsettings.json` — the same file the helper generator reads when it emits `withHttpEndpoint({ port })`. A literal cannot drift from the artifact it describes if there is no literal. It moved from an inline `deno eval` string to a script module so the gate's command factory stays a pure function of the run context; resolving inside the factory broke the suite-runner test that builds the real suite against a faked executor. `SCAFFOLD_APP_PORT` replaces the `PORT_RANGES.APP.start + 10` expression that was spelled out in three scaffold call sites. Generated output is unchanged. Refs #954 * chore(harness): record the clean-host scaffold.runtime verdict for #954 behavior.app-home PASSED 225ms, passed=62 failed=0. Closes drift D-4 (the suite had never been executed) and D-7 (local port collision). #957 passes the same lane on re-run with no code change. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
🤖 Augment PR SummarySummary: Fixes scaffolded Fresh UI workspaces failing to resolve Changes:
Technical Notes: The guard reads workspace member 🤖 Was this summary useful? React with 👍 or 👎 |
| ? text.slice(markerIndex + ALLOW_MARKER.length).trim() | ||
| : undefined; | ||
|
|
||
| if (afterPackage === '@') { |
There was a problem hiding this comment.
(.llm/tools/validation/check-netscript-jsr-specifiers.ts:335) This treats any specifier with an @ immediately after the package name as “versioned”, but a malformed value like jsr:@netscript/sdk@/desktop (empty version) would still take this path and avoid the rule-1 versionless failure. Is it worth ensuring an empty parsed.version is still flagged so the guard can’t silently pass broken specifiers?
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| details: result.allowances.map((entry) => | ||
| `${entry.path}:${entry.line} ALLOW ${entry.reason}` | ||
| ), | ||
| `${result.scannedFiles} framework source files carry only versioned, current NetScript JSR specifiers`, |
There was a problem hiding this comment.
(.llm/tools/release/publish-readiness.ts:154) This summary says the repo carries only “current” NetScript JSR specifiers, but scanSpecifiers explicitly allows range pins (reported in details) which aren’t exact/current pins. Consider rewording so the PASS message doesn’t imply ranges were validated as lockstep pins.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
Summary
@netscript/sdk/desktopand/auto-updatefail to load in a scaffolded workspace for twocompounding reasons, not one. The filed root cause — stale
beta.10pins in the fresh-ui registrymanifest — is real but not sufficient: with the pins corrected the import still fails, because
ui:addstrips the export subpath when deriving the import-map key but keeps it in the value,so Deno resolves
@netscript/sdk/desktopto./desktop/desktop. This PR fixes both causes andextends the guard that let the stale pin ship.
Proven by execution rather than by reading — three
deno checkruns against published JSR:ui:addwritesjsr:@netscript/sdk@0.0.1-beta.10/desktop(shipped)Unknown export './desktop/desktop'— and beta.10 exports neither./desktopnor./auto-updatejsr:@netscript/sdk@0.0.1-beta.11/desktop(pin fix only)Unknown export './desktop/desktop'— still brokenjsr:@netscript/sdk@0.0.1-beta.11(this PR)Check main.ts— cleanScope
packages/cli), overlay frontend (packages/fresh-ui), plus.llm/tools/{validation,release}@netscript/*specifiers)What changed
Cause 1 — the import-map merge (
packages/cli).importEntryForDependencyis now the singledefinition of the import-map entry a registry dependency contributes, and it normalises the value to
the package root.
mergeDenoJsonImportswrites it andremoveUiRegistryItemprunes by it. Thatsymmetry matters twice: pruning previously matched the raw specifier against the map value, so
normalising the write alone would have orphaned imports on
ui:remove; and two items depending ontwo subpaths of one package now share one entry, which the old raw-string
stillRequiredcheckwould have deleted out from under the survivor.
Cause 2 — the pins (
packages/fresh-ui). The two SDK dependencies read0.0.1-beta.11.The guard (
.llm/tools).check-netscript-jsr-specifiersvalidated specifier shape, which iswhy it reported
scanned=2206 failures=0while the defect was in the tree. It now also proves:JSR-NETSCRIPT-CURRENT— an exact@netscript/*pin equals the version that workspace memberdeclares;
JSR-NETSCRIPT-EXPORT— a literal export subpath exists in that member'sexports;JSR-NETSCRIPT-RANGE— range pins are listed as notes, never failed.It runs in the existing
deno task check:netscript-jsr-specifiers, already aci:qualitydependency, so drift is caught per-PR and not only at a cut.
publish:readinessfails the release onthe two failing classes —
lockstep-residueandfindVersionResidueboth read the JSON-only fileset the bump rewrites, which is exactly why a pin left in TypeScript survived the beta.11 cut.
What deliberately did not change
@netscript/*specifiers (^0.0.1-alpha.12in six plugin adapters,^0.0.1-alpha.18in the contracts scaffold template,^0.0.1-beta.5in the manifest,^0.0.1-alpha.0in the plugin skeleton). They satisfy0.0.1-beta.11under SemVer and resolvecorrectly, so they are skew rather than breakage — Grok's "CLI reports beta.11, scaffold pins
older" scar is this class. Converting a range to an exact pin changes what a consumer workspace
resolves to over time, which is a release-policy decision. The guard now lists them; fix(release): range-pinned @netscript/* specifiers still name alpha/beta releases #973 carries
the decision.
version:bump.replaceVersionFilesdoes a blind whole-filereplaceAll; running that over arbitrary TypeScript would rewrite unrelated version mentions. Afailing guard is the correct instrument.
@netscript/sdk's exports (beta.11 already exportsboth subpaths).
Issue accuracy
#956's third observation — "the MCP server advertises
beta.9" — does not reproduce onmain:packages/mcp/deno.json:3andpublish-assets.generated.ts:5both read0.0.1-beta.11. It wasobserved against a running artefact. Both issues carry a closing keyword because everything in them
that exists on
mainis resolved here, with the range-pin remainder split to #973.Slices
447b9ff35ui:add/ui:removeemit a resolvable import-map entry for subpath dependencies —cf73024efec71664882fe7c4a14publish:readinessblocks a pin the release no longer ships —a34f4db5053672586aDefinition of Done
@netscript/sdk/desktopresolves from an import map produced byui:add(executed proof, table above)0.0.1-beta.10manifest pins read0.0.1-beta.11deno task check:netscript-jsr-specifiersfails on a stale@netscript/*pin and on a subpath that is not a real exportfmt:check,lint,check,test,arch:checkrecorded with real resultsValidation
deno task fmt:checkfilesSelected=1869 findings=0deno task lintfilesSelected=1724 occurrences=0deno task checkfilesSelected=2458 batches=21 failedBatches=0cd packages/fresh-ui && deno task checkcheckexcludes fresh-uideno task test2243 passed (507 steps) / 0 failed / 12 ignored, 3m15sdeno task arch:checkFAIL=0deno task quality:scanok:true findings:[] allowCount:7(pre-existing)deno task check:netscript-jsr-specifiersscanned=2206 allowances=1 ranges=18 failures=0deno task check:scaffold-versionsE-12 OK — 11 scaffold pin(s) are stabledeno task publish:dry-runSuccess Dry run completescaffold.runtime)ui:addis outside that suiteThe root
lint.excludecoverspackages/cli/and.llm/, sodeno task lintlints neither rootthis run changed — repo policy, not a gap introduced here.
deno fmt --checkanddeno checkdocover both.
Harness
.llm/runs/fix-freshui-registry-sdk-pin--953/NOT_RUN.run-loop.md§4/§7 require a separate evaluator session;this run was assigned to a single session and does not self-certify either gate. It stays at
status:impluntil an IMPL-EVAL verdict exists.Drift / Debt
drift.mdentry 1.main— entry 2.