fix(studio): load the app module by computed specifier in the law-4 arm - #525
fix(studio): load the app module by computed specifier in the law-4 arm#525KnockOutEZ wants to merge 1541 commits into
Conversation
The type-gate CI job deliberately does not build, so a static import of `apps/studio/src/main/run-view-model.ts` from `tests/integration` put a module whose own `wigolo/studio` specifier resolves through `dist/` into the root type-check program — three green legs locally, three red in CI, for a resolution fact rather than a type error. The app is a Bundler-resolution project and the root program is nodenext; the two are not meant to see each other. The runtime import is unchanged — same module, same arm — and the surface it drives is restored as two local shapes, so the seam stays typed. `run-view-model` goes back to the extensionless specifier its 34 siblings use.
…replay-ra fix(studio): three adopt/replay races leave the app projection permanently stale or law-4-violating
The SD1 exit gate compares REST, the menu, SQLite and the JSONL for the run id but pins only the badge's NULL branch, so truncating the id or reading it off runs[0] left every suite green while law 8's shared address space quietly broke.
K7 falsified 'with no change in the store to do it' — listRuns decides the filter on the projection. The header was rewritten in 49ac2c9; the title was not, so it still advertised a premise the row no longer asserts.
…ting `before-quit` was fire-and-forget, so Electron tore the process down while `shutdown()` was still awaiting `endRun`'s append. On the most ordinary path in the product — Cmd-Q, or the last window closing — a live session's run stayed `running` in the durable log forever, and boot `reconcile()` rewrites only visibility, never status. The log and every projection of it then disagree permanently, which is precisely what the run spine exists to prevent. The first quit is now cancelled and re-issued once shutdown resolves, bounded by a 5s deadline: a wedged broker child must not be able to make the app unquittable. A crash is already survivable; an app the user cannot close is not. The e2e forces the race rather than hoping for it — one session lands inside the window Electron's own teardown leaves open, so the arm opens eight and asserts every one comes back terminal, read through a daemon that never saw them. The SIGKILL arm alongside it is the outside signal: crash-orphaned runs stay `running`, so the graceful arm cannot be passing on a store default.
Two defects on the /v1/runs events surface, both invisible in the happy case. The drain wait had no deadline. `waitForDrain` resolved only on 'drain' or 'close', and a client that keeps the TCP connection open while it stops reading emits neither: the await parks, so no further byte is handed over and `stalledBytes` cannot reach its budget; the heartbeat returns early on `needsDrain`, so no ping is written and the silence reconcile never runs; and the events route is exempt from the router's slot and deadline discipline by design. Nothing was left to notice. The connection slot was then held until the daemon restarted, and the default 32 of them 429 the route permanently for any authed client whose run's log is long enough to reach the pace gate. The wait now carries its own clock — four heartbeat intervals, so the knob that already governs "how long may this stream say nothing" governs this too — and an expiry takes the same door every other back-pressure case takes: `endStalled`, which ends the stream and gives the slot back, and the client resumes from Last-Event-ID against the durable log. `pumpDurable` treated any short page as end-of-log. The requested page size is what this process asks for, not what it gets: the broker clamps every read to MAX_EVENTS_PAGE whatever the limit says, so setting WIGOLO_STUDIO_RUN_REPLAY_PAGE above the clamp silently truncated every host-side replay mid-log, and the heal path then looped end/reconnect at the client's backoff. It now stops only on an EMPTY page, which is the contract run-view-model.ts already documents and enforces, with a non-advancing-cursor guard taking over the accidental termination the short-page check provided. The broker states the clamp as a contract at its own end. Tests force both conditions rather than hoping for them: a response whose write returns false and which never emits 'drain' or 'close', and a store that clamps its pages below what the route asked for. A must-not-fire control pins that an ordinarily slow but draining client is not killed.
`""` passed validation and was persisted verbatim: the `?? DEFAULT_SPACE_ID` substitution downstream fires only on `undefined`, so the run was then invisible to `?spaceId=default` — the filter every surface lists with — while still being a live run holding tabs. The client strings failed the other way: the store rebuilds the badge with `name && version`, so an empty one was accepted on write and erased on read, and the caller got a 201 for a driver identity no surface would ever show. Law 3 makes the driver shown identically everywhere, and a field that evaporates between write and read cannot be. Both now 400 with the field named. Validated on the trimmed value and persisted verbatim, which is what `task` already does — trimming here would quietly rewrite a caller's identifier, and the durable log is the wrong place for that. Behaviour on `undefined` is unchanged. The test enumerates the shapes the schema ACCEPTS rather than only the revealing one — the blanks include a tab, a newline, a CRLF and a U+00A0 — and lists the shapes that must still work beside them, including a spaceId whose padding is blank but whose value is not, and the non-string types a guard written as `!spaceId` would newly let through.
…re append path Every append recompiled five statements inside BEGIN IMMEDIATE — the write lock on a database search caching, embeddings and artifacts also queue behind — because better-sqlite3 does not cache them. cost.recorded is one event per browser action by design, so a long run paid the compiler continuously. Statements now resolve through a WeakMap keyed by Database handle, so the broker child, the daemon and every test database keep their own. Only constant sql goes through it; the two reads whose text varies with their argument count keep their own prepare. Measured 20k appends on this machine: the transaction alone 74.0 -> 17.4 us/append (76.5% of it was compilation), and the held write lock 106.8 -> 47.6 us/append.
`task` carried `minLength: 1` from the start; `spaceId` and the client strings carried only a maximum, so the served document advertised a blank as valid input for both. A generated client would emit one and take a 400 the contract said could not happen — and before the router refused it, the worse outcome: a 201 for a run invisible to the default space filter, or for a driver badge the store erases on read. The new row is a drift gate rather than a restatement: it reads the served document, so relaxing either side alone goes red.
…il-badge-s test(studio): pin the rail badge's positive render; retitle a falsified fanout row (SD1 exit-7)
…an guessed The 5s bound was written on the assumption that `shutdown()` is sub-second. The demo falsified it: eight live sessions cost ~4.2s, because the walk is serial and every detach and append is a broker round trip. At 5s the backstop would have started truncating at roughly ten sessions — a fleet size that is not unusual — which turns a bound meant for a wedged broker into a routine truncation with a timer. 10s is 2.4x the measured cost and still under the 15s the e2e lane bounds `close()` at, so a slow quit cannot become a red lane either. A-98-1 updated with the measurement and with the thing to fix before the bound is raised again: the serial walk itself.
…ared-state perf(studio): cache prepared statements on the run-store append path (SD1 exit-7, #102)
…it-needs fix(daemon): SSE drain wait needs a deadline; replay short-page contract; empty spaceId validation
…uit-must fix(studio): a graceful quit must land the terminal events before the process exits
…us index The list page reads studio_runs by keyset and orders by (created_at, id). With no index over those columns SQLite planned SCAN + TEMP B-TREE: the whole table read and the whole table sorted, per page, on a table that grows forever by design. Two indexes because the read has two live shapes and neither can use the other's — an unscoped read cannot use an index led by space_id, and a space-scoped read cannot seek without one. Both cost a b-tree write only at INSERT, once per run, since created_at, id and space_id are fixed at creation. idx_studio_runs_status is the opposite trade and is now dead: nothing selects studio_runs by status since the filter moved onto the projection, but the index still had to be rewritten by the status UPDATE on every append.
Bounding the READS was never a bound on the WORK. #94 moved the status filter onto the projection, correctly, but the scan loop then fully projected every row it looked at — so a filter matching nothing paid up to eight pages of unbounded per-run reads to return an empty page, synchronously, with no await for the router's deadline to fire in. Measured at 2000 runs x 122 events, ?limit=200&status=cancelled: 163 ms of blocked event loop. Two changes, and the pair is the bound: - The scan is two-phase. A row's status comes from its seeds — bounded newest-row seeks plus the auto-deny-windowed decision read — and only survivors are projected. Rows fully projected per call is limit + 1, filtered or not. The seeds are computed once and handed to the projection rather than re-read there: re-reading would put a second status answer in front of the row it admitted, which is the exact defect K7 deleted. - The eight reads now share one row ceiling, so pages get shorter as the caller's page gets longer and the ceiling belongs to the store rather than to the request. The first read is still the full limit + 1, so an unfiltered call is the one read it always was. The keyset predicate is a row value rather than the equivalent OR form: against the OR SQLite declined the index constraint and planned SCAN ... USING INDEX, walking from the newest row on every page. The row value plans SEARCH ... ((created_at,id)<(?,?)) and seeks straight to the cursor. 163 ms -> 2.5 ms on the no-match case. The docstring quantified only the look-ahead row at 2%, which understated the reachable 8x multiplier; it now states the bound it actually has.
runListLogs learned an oversized run's serialized size by materializing it — up to 2000 rows read, every payload JSON.parsed, the array re-serialized — and on failing the char check returned the projection without spending either budget. charsLeft was therefore still the full 4M for the next run on the page, which read itself in full to fail the same way, so a page of fifty 5M-char runs moved a quarter of a gigabyte to answer with fifty few-hundred-byte projections, and the next hydration page did it again. Two changes. A SUM(LENGTH(payload)) probe decides the char bound before the read where it can: it under-states the serialized size by construction, so a run it rules out could not have fitted and the accepted path is decided by exactly the check it was decided by before. And the budgets are now charged for the READ rather than for the acceptance, which terminates the overrun the probe's soundness margin still admits — charsLeft goes non-positive and the guard stops the reads for the rest of the page. Forced test, five oversized runs on one page: 5 full-log reads of 83 rows and 20,505,815 payload chars before, 0 materializations and 870 chars after. A second row pins the charging arm on its own, with runs built into the band the probe cannot reject.
…t run end Three races in the decision mirror, each producing a durable log that contradicts what actually happened. A human double-submit double-wrote. `settle` dropped the `runOf` link and then awaited the append, so a second submit inside that await missed the link — and the `runForDecision` fallback still found the card pending, because the first fold had not landed. `resolveDecision` appends unconditionally, so nothing downstream deduped the second resolution. An answer arriving while the card was still being recorded was dropped. `runOf` is set only after `requestDecision`'s round-trip, and the renderer was told about the card before `parked` ran at all, so a fast click found neither the link nor a projection entry and returned without writing. Two minutes later the log said `auto_denied` for a card the broker approved. Both are one mechanism: the mirror now takes one turn at a time per card. The second submit runs after the first append has folded and finds nothing left to write; the early answer waits for the request to land and then settles it. Per card, not global, so a slow append never holds up another card's answer. `onParked` records before it notifies, so the claim on the card's turn exists before any answer can arrive. And a card can outlive its run: `endRun` has no channel into the mirror, so a timer parked when the run ended appended `decision.resolved` after `run.completed` — out-of-order in an append-only log, and a forced full re-read on a condensed run. Refused at the write rather than announced at the call site, through the same `isTerminal` every other seam uses, so every path in is covered by one check instead of by remembering to notify.
The block said a size 'would still have to read and parse an arbitrary number of rows to find out what it was'. That was the justification for the pair of bounds and it is no longer true: the size now comes from a SUM that never parses a payload. States what each bound actually does, and that a read is charged whether or not its envelopes ship.
The union test's POST_MERGE_ADDED list is the file's own designed growth point — its comment says an entry there is a deliberate addition and an unrecognised name in neither released set is still a failure. Every new migration has to land in it or the count assertions red.
…-hydration perf(daemon): charge the hydration char budget when the read happens, not when it is accepted
…filtered-l perf(studio): bound the filtered list scan's projection cost; index the list read; drop the dead status index (SD1 exit-7)
…rror-doubl fix(studio): decision mirror — double-submit double-write, dropped fast resolve, timers past run end
…y takes Law 4's enforcement seam was half-serialised. `attachTab` queued per tab because the ownership check reads the projection while the append is a round-trip; `detachTab` and `endRun`'s release loop read `ownerOf` and appended off that queue entirely, so two ordinary sequences reached the durable log. A human closing a tab while its attach was still on the wire — the broker can take seconds — found no owner, returned, and let the attach commit behind it: the run then owned a destroyed tab permanently, listed by `agentVisibleTabs` and focused by `promote()`. And a human close racing `endRun`'s `run_ended` release had both sides read the owner before either folded, writing one detachment to the append-only log twice. Attach and detach now share ONE lane per tab, because the pairs that race are mixed — two lanes would serialise each kind against itself and leave both sequences as exposed as no queue at all. `endRun` keeps its membership read outside the lane: it names the tabs to try, and the lane decides per tab whether anything is still there to release. Also: the coalescing window was a raw `setTimeout` with no handle kept, so `dispose()` could not clear it. Shutdown ends every live run, so a terminal append landing in the final 16 ms fanned out into listeners one line after the tray was destroyed. It is on the injected timer now, tracked, and released with the horizons. Tests: forced-race arms for both sequences at the durable store, a pin that a global FIFO lock fails, and a dispose arm that drives a change into the window.
…ayload The tests/ debt ratchet type-checks this arm under a looser program than the root type-check does, where `RunEventInput.payload` is optional.
…d-endrun fix(studio): detachTab and endRun must take the per-tab queue attachTab relies on
studio_open's agent-supplied startUrl reached the tab.attached payload
verbatim, so a magic link's token landed in an append-only log with no
prune path and was served over GET /v1/runs/{id}/events and the SSE tail
to every client past the REST gate. The audit path already strips the
query unconditionally, on the same rationale: a secret can ride the query
of any page and no classifier catches them all.
Narrow at the event constructor rather than the call site, so every path
that reaches attachTab with a url is covered. The rule moves to one
url-policy module shared with the audit path, and its parse-failure
branch now cuts at the first ?/# instead of returning the string whole —
an unparseable url is not a harmless one.
…-s-startur fix(studio): store only the origin of an attach url in the run log (SD1 exit-7)
…rship A run condensed at boot is condensed because it is LONG, and a live long run is exactly the one that emits next — so the first live envelope after boot routed through adopt(replace) to a readLog from seq 0 with no total cap, and then to a retain with no projection, so nothing condensed it again. At a hundred thousand envelopes that is two hundred sequential broker round-trips, a JSON.parse per envelope on the thread that paints, and every envelope retained for the rest of the run's life: the whole boot bound handed back one envelope after boot. The bound is re-applied on replay, on the same quantity the store decides it on. A condensed run still past it is answered the way boot answered it — the store's own projection, one round-trip and a few hundred bytes — and stays condensed. A condensed run UNDER it is materialized, because a short run at the end of a full boot page is condensed by the page's budget rather than by its own size and is better off folding its next envelope for free. The clock-driven status re-read of a condensed run is now bounded to once per tail: with the run staying condensed, asking again at the same tail cannot learn anything the last answer did not carry, and a store that disagreed about an expiring card by a hair would otherwise narrow and ask forever. Same fold seam: ownerOf walked every run and projected it to ask whether that run held the tab, and ipc-host's state() asks it once per tab per broadcast — tabs × runs × tabs-per-run on the thread that paints, growing with every run the machine had ever seen. It now answers from a tab-to-run index maintained on the fold by projectRun's own two rules and rebuilt from the projection whenever a log is replaced.
A decline arrived back through the shared promise as a bare null, so it was remembered for 60s exactly like a launch that ran and timed out. The commonest decline is the substrate presence TTL disagreeing with an uncached record read, which a reinstall resolves in the next second — so a fixed machine was locked out for the rest of the window. The pass now reports whether anything was started, and only that case is remembered. memoMs 0 is honoured at the read as well as the write, so the documented escape hatch is not silently declined by another caller's window. normalizeLaunch throws on a thenable or on an object with no boolean started, instead of reading undefined as a decline and skipping the poll.
…-memo-reme fix(studio): memoize only a launch that was actually attempted
…tracts Three arms, all red on this tip: normalizeLaunch keeps a truthy non-function `failed` (the `??` merge only replaces nullish), the poll's first `failed()` call sits outside the try/catch so that shape rejects out of a path documented never to throw, and defaultLaunch's env spread leaves two hidden-flag keys when the parent carried another casing.
normalizeLaunch now rejects a defined non-function `failed` the same way it rejects a non-boolean `started`. The `??` merge only replaced a nullish probe, so a truthy non-function reached the handle poll, and the poll's first `failed()` call sits a tick past the try/catch that guards the launcher: the TypeError rejected the shared promise, the memo mapper had no rejection branch, and `await inFlight` handed it to studioBridgeFetch, which awaits with no catch. Thrown at the seam instead, it is the synchronous throw the try/catch already turns into a clean decline — so the never-throws contract holds and the non-memo rule for an unattempted launch still applies. defaultLaunch strips the inherited hidden flag in any casing before re-adding the canonical one, mirroring runStudio's mechanism. The env spread collapses win32's case-insensitive proxy into a plain object, so an inherited `wigolo_studio_hidden=0` left two keys and the child resolved whichever the OS handed back first — a window could surface for a session that exists only to serve the agent.
…-honours-i fix(studio): auto-launch honours its own seam contracts — validate LaunchOutcome.failed, strip HIDDEN case-insensitively
…type contracts The injected-source seam never passes `readSubstrateManifest`, so the type established there does not hold for a manifest handed in through `deps.source` — the seam a published channel re-enters with a manifest of its own. The pre-flight guards answer from outside the try, and `staysInsideItsDirectory` calls `isAbsolute()`, which throws `ERR_INVALID_ARG_TYPE` on a non-string straight past a documented NEVER THROWS into `warmup`'s unguarded await. The discriminator is settle-vs-reject rather than an outcome string, because that is the property the contract is about: an assertion on `outcome` alone cannot tell a refusal apart from a crash. `readSubstrateRecord` checks truthiness where `readSubstrateManifest` establishes `typeof === 'string'`, so a numeric `version` reads back PRESENT and flows into the detail text, the logs and every doctor row downstream. Red against unfixed source: three executable-shape arms reject, and the numeric record version reads back present.
…the try `8f465f67` moved the join inside the try so NEVER THROWS would be true of the function's shape rather than of its guards, and then left the guards themselves outside — where `staysInsideItsDirectory` calls `isAbsolute()`, which throws `ERR_INVALID_ARG_TYPE` on a non-string. The class reopened one line higher, on the one seam a manifest can still arrive mistyped through: `deps.source`, which never passes `readSubstrateManifest` and which is where a published channel plugs in. `warmup` awaits this call unguarded on the strength of the contract, so the throw took the browser, model and search phases with it. Both guards now sit inside the try, and the type is established at the seam the way `readSubstrateManifest` establishes it for a manifest on disk — so the refusal names its own reason instead of surfacing as a generic install failure. What the guards refuse is unchanged; only where they answer from. Same file, the sibling asymmetry: `readSubstrateRecord` checked truthiness where `readSubstrateManifest` establishes `typeof === 'string'`. The other two fields threw their way to null through its own catch, so the shape that got through was a numeric `version`, reading back as PRESENT and flowing into the detail text, the log line and every doctor row downstream as a number.
…trate-pre fix(studio): acquireSubstrate's pre-flight guards answer from inside the try
…casing
Both arms are red against the source in the parent commit.
The plant helper writes a forged acquisition record at `substrateRoot()`'s
ambient answer and its caller `rmSync(root, { recursive: true, force: true })`s
that directory. `substrateRoot()` follows the memoized data dir, and
`tests/setup.ts` repoints `WIGOLO_DATA_DIR` only when unset — deliberately — so
a developer who exported the documented knob and ran `npm test` lost their real
substrate root while the suite reported 37/37 and exited 0. The new arm forces
the export, proves the forcing took effect via `resetConfig()`, and asserts the
plant lands in the per-test dir with the decoy's file surviving both the plant
and the cleanup. It reds naming the decoy root as the plant target.
The off switch compares exact-match against two lowercase literals, so `False`,
`FALSE` and `Off` all read as "not disabled" and the substrate spawns detached
and hidden against the operator's stated intent. Eight spellings plus two
anti-vacuity arms — an unset variable and a set-but-unrecognised one must both
still launch, or a bare `return null` satisfies the whole suite.
…e-insensitively The plant helper repoints `WIGOLO_DATA_DIR` at its per-test temp dir and drops the memoized config, so `substrateRoot()` — which takes no data dir, because `studioLaunchable()` reaches it through `substratePresent()` — answers inside a directory this file created. Its caller's recursive remove then targets that, not a root the suite never made. The containment check stays as the assert that the repoint took effect: a dropped `resetConfig()` or a config that stopped reading the var would silently restore the original defect, and the refusal is what fails loudly instead. It also moves the SIGKILL residue — a forged record naming a real on-disk executable — out of the path the launcher reads. The off switch was exact-match against two lowercase literals, so `False`, `FALSE` and `Off` read as "not disabled" and the substrate spawned detached and hidden against the operator's stated intent. It now matches lowercased and trimmed against `0`/`false`/`off`. `envBool` (src/config.ts) is the house comparison and normalises for the same reason; the two `WIGOLO_STUDIO_HIDDEN` fixes this phase were the env KEY's casing, which no value comparison inherits. Both widenings past the house set — `off`, and the trim — are recorded with their reversal conditions as A-189-1 and A-189-2.
…-exported fix(studio): auto-launch — confine the substrate plant, and read the off switch case-insensitively
…o-invalidation contracts Six arms, all red on tip: a throwing failed/sleep/readHandleFn rejects out of ensureStudioRunning; a falsy non-false launcher answer reads as started and pays the full poll budget; a handle observed inside the poll leaves the negative memo standing.
…memo-invalidation contracts The handle poll calls failed(), the handle reader and sleep() a tick past the try that guarded the launcher, and the top-of-call read sat outside every try, so a throw from any of them rejected out of ensureStudioRunning into a caller that awaits with no catch — and skipped the memo write, making the fan-out re-pay the poll budget per URL. Both are guarded now: the top read falls through to the launch path, and a poll fault resolves as an attempt that published nothing. normalizeLaunch ended in "anything else means started", so null/0/''/NaN bought a 30s poll for a process nobody launched plus a 60s lockout. The legal answers are now enumerated and anything else throws at the seam, which the launcher catch already turns into a zero-tick decline. A handle observed inside the poll now clears the negative memo, which only the top-of-call read did — so a substrate that came up on the second attempt left a stale window that declined the next caller after its session ended.
…-seam-cont fix(studio): auto-launch seam contract holes — throwing probe rejects past NEVER-THROWS, falsy launch reads as started, poll-observed handle leaves stale memo
WIGOLO_SKIP_PREPARE is gated on bare truthiness, so =0/=false/=off all mean SKIP — the inverse of the operator's intent, and the opposite of the rule autoLaunchDisabled establishes one file over for the same class. Two arms: the off spellings must BUILD and must not print the skip line, and the values CI actually sets must still skip and still print it.
…ng on them The opt-out read any non-empty value as "skip", so a leaked WIGOLO_SKIP_PREPARE=0 made a local npm ci exit 0 with an unbuilt tree and surfaced weeks later as module-not-found. Normalize the way the phase's own rule does: trim, lowercase, and treat 0/false/off as off. Every value CI sets still skips, and still prints the line those legs grep.
…prepare-is fix(build): WIGOLO_SKIP_PREPARE honours 0/false/off instead of skipping on them
…ressed root npm publish Red against the tip classifier: its alternation enumerates install verbs, so a planted un-suppressed `npm publish` at the repo root is classified as not prepare-firing and the sweep reports an empty set. The arm plants the shape in a scratch workflow directory under $TMPDIR and runs the real parser over it, because every other arm in this block can only agree with the workflows that already exist.
…m publish `npm publish` packs, and packing fires the root `prepare` hook, so the release publish step is a root prepare-firing step — but the sweep's alternation listed only install verbs, so the step never entered the affected set and the list a reader takes as "every root step that fires the hook" omitted it. Adds `publish` to the alternation and the release publish step to the expected set. Deleting its `WIGOLO_SKIP_PREPARE` now reds two arms naming the step instead of one, and the enumeration no longer implies the publish path is exempt. Corrects the pack/publish block's claim that the install sweep cannot see the shape. No change to the workflow.
…-step-s-pr fix(ci): the install-register classifier and its enumeration cover npm publish
…ainment escapes Both shapes reach the record that backs the prompt-less substrate spawn, and both pass every containment check the module has: the symlinked root because each side resolves through the link, the nested path because it really is inside the root. Red at tip, with controls that the escapes are real and that a linked ANCESTOR of the root still reads.
… not root/<version> The record backs a prompt-less spawn, and its docstring justified that by saying the acquirer writes path as substrateRoot()/<version> and nothing else. Two shapes made that false. A root that is itself a link resolved along with the candidate, so every containment comparison agreed for a tree outside the data dir; readSubstrateRecord and acquireSubstrate now both refuse it via lstat, which is blind to a linked ancestor and so keeps the macOS /var -> /private/var shape working. And path was only required to be somewhere under the root; it must now resolve to join(root, version), with version required to be one directory name so the join cannot be '.' or a traversal. Two fixtures planted a path the acquirer cannot produce and are corrected.
…he SEC-2 arm The nested-path arm was green against a build with the equality deleted: with no real <root>/<version> on disk the refusal came from the expected location being absent, which is the weaker half of the rule. It now plants a legitimate install alongside the decoy, so both sides resolve and only the inequality refuses; the absent-location case keeps its own arm. Dropping isInside(raw.path, root) likewise changed no test — the equality dominates it and the executable check is what refuses a linked <root>/<version> — so it is removed rather than left to be trusted.
…, not on the platform The nine skipIf(win32) arms cited 'creating a symlink needs elevation', which this same file contradicts by planting an unskipped junction. The accurate rule is about link TYPE: a junction needs no elevation but is a directory link that Node normalises to an absolute target, and the arms driving acquireSubstrate have their plant re-created by cpSync with no type hint, so a junction does not survive the copy. The two record-level arms need no copy, so they now plant junctions and run unconditionally on all three shipped OSes; the executable arm links bin/ rather than bin/run to make that possible. The seven copy-driven arms ask the machine whether it can make ordinary links instead of assuming Windows cannot, so they run on a Windows runner with Developer Mode. Un-skipping them blindly would have been worse than the skip: four expect outcome 'failed', which is also what a cpSync EPERM produces. A probe arm keeps a silently-false capability answer from vacating the family.
…ontainment fix(studio): substrate containment defeated by a symlinked root and a non-version path (PX0 exit SEC-1+SEC-2)
The absolute-link containment arm pinned the refusal with /bin\/run/, but the message names the offending entry via relative(), so the separator is the host's: bin/run on POSIX, bin\run on win32. Both Windows jobs at public tip 5fc9287 were red on this assertion while the refusal it exists to pin had fired correctly; every other arm passed on win32. Match either separator. The arm still requires the message to name the entry, so no kill is lost: stripping the entry from the refusal reds this assertion and nothing else.
…parator-in fix(test): match the substrate refusal separator-agnostically on win32
|
Important Review skippedToo many files! This PR contains 787 files, which is 487 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (7)
📒 Files selected for processing (787)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Closed by supervisor (2026-08-31) — accidental PR, forbidden base, zero new work at risk. This PR was cut from the shared Why it cannot merge:
Exposure check, for the record: the diff contains no Runner note: the verifier's forbidden-base alarm only scans |
The type-gate CI job deliberately does not build, so a static import of
apps/studio/src/main/run-view-model.tsfromtests/integrationput a modulewhose own
wigolo/studiospecifier resolves throughdist/into the roottype-check program — three green legs locally, three red in CI, for a resolution
fact rather than a type error. The app is a Bundler-resolution project and the
root program is nodenext; the two are not meant to see each other.
The runtime import is unchanged — same module, same arm — and the surface it
drives is restored as two local shapes, so the seam stays typed.
run-view-modelgoes back to the extensionless specifier its 34 siblings use.