You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(vscode): treat missing dependencies as a state, not an errorfix(vscode): 将缺失的依赖项视为一种状态,而非错误 (#29)
* fix(vscode): treat missing dependencies as a state, not an error
A nested rstack.config.* whose own dependencies are never installed
(create-rstack's template-* beside their generator) made the Rstest
stack log '[error] Failed to initialize project config' with a full
stack trace per template, on every detection pass.
'Not installed' is now reported uniformly across the three stacks
(new AGENTS.md rule): a disabled status whose reason names the restart
command as the way out, plus one warn line in the output channel —
never a crashed status, a stack trace, or a notification.
- shared/notInstalled.ts owns the wording for all three stacks (the
formatVersionMismatch precedent); the restart hint derives from the
new stackCommandTitle, checked against the manifest in tests.
- The Rstest worker classifies a config import failure on Node's own
error code (the IPC channel drops it) and returns the verdict as
data (NormalizedConfigResult); Project branches on it and latches a
per-project disabled status that installs clear and dispose forgets.
- StatusHolder gains a notInstalled latch ranked below crash and
version mismatch, idempotent across refresh repaints.
- Lint's report moves wholly into the onDocumentFailure hook, so the
upstream-tracked RuntimeManager only defers to it; missing rstack
logs one warn line instead of an error with a stack.
- Missing @rstest/core now reports through the same path (warn +
disabled status) at all three master resolution sites.
* fix(vscode): keep the not-installed classification honest across stacks
Review follow-ups on the uniform not-installed policy:
- lint: a missing native @rslint/core is the not-installed state, not a
crash; the code-to-package mapping (missingPackageOf) is shared by the
status and the warn line, and the warn names the runtime a document
keeps. A misconfigured rslint corePath now throws invalid-package so a
wrong setting is never reported as "install your dependencies".
- rstest worker: @rstest/core is loaded before the classified config
load, so a broken core install reports its real error instead of
"a config dependency is missing".
- rstest bridge: the not-installed latch clears the moment the rstack
package resolves, and an install that ships no Rstest shim latches a
version-mismatch instead of painting the folder healthy.
* fix(vscode): classify only bare package imports as not installed
Second review round on the uniform not-installed policy:
- worker: missingDependencyCauseOf replaces isMissingDependencyError —
Node's code alone also covers a typo'd relative import or a missing
generated file, which installing dependencies cannot fix, so only a
bare (package-name) specifier counts and anything else keeps the full
error report. The returned cause is the message's first line, keeping
the warn to one line without the CJS require stack.
- shared: the config-dependency log line moves into shared/notInstalled
(formatConfigDependencyMissingLog), deriving its consequence from
STACK_LABELS, so no stack owns its own wording.
- rstest status: StatusHolder latches now supersede each other per
source (one source, one verdict) — a stale higher-ranked crash or
mismatch can no longer paint over a newer not-installed observation,
and raise sites need no manual cross-latch cleanup.
* fix(vscode): keep independent failure facts from masking each other
Third review round on the uniform not-installed policy:
- rstest bridge: the missing-rstack warning goes through the shared
formatNotInstalledLog instead of its own sentence.
- rstest status: a package-state observation (mismatch or not-installed)
restates its root — it retires the other kind AND a stale crash,
whose only other exit (workerSpawned) cannot fire while the package
is unusable. The config-dependency verdict moves to its own
config-deps: latch key (the nodeRuntimeStatusSource precedent), so it
coexists with the core version check instead of erasing it.
- worker classifier: a bare-looking subpath of an installed package
(require('pkg/missing')) is a source error, not the not-installed
state — confirmed against the physical node_modules with the same
uncached walk-up the rest of the stack resolves packages with.
* docs(vscode): scope the config-import case to Rstest, tracked in #30
Copy file name to clipboardExpand all lines: packages/vscode/AGENTS.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,6 +23,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
23
23
24
24
-**Pre-1.0.0 the extension breaks freely.** No compatibility is owed with earlier unpublished states of this extension — settings, command ids and behavior may change without deprecation paths, and dead compat code for them is removed, not kept. Only the **latest released**`rstack`, `@rstest/core` and `@rslint/core` need support: whenever a change touches a floor in `SUPPORT_MATRIX`, set it to the latest release at that time — do not reason about which older release would still work — and raise it without a transition story (the floor status names the required version). No settings migration exists either — not for earlier states of this extension, and not for the two retired standalone extensions (removed in #15; users re-enter their settings under `rstack.*`).
25
25
-**The three tools are treated uniformly by default.** Detection, dependency-change retry, restart semantics, version gating and status reporting follow one shared pattern across the lint/test/fmt stacks; a stack diverges only when its tool forces it, and the divergence is recorded here as a gotcha. When adding behavior to one stack, first ask whether it belongs to all three. This is about behavior, not code — the upstream copies still must not be deduplicated.
26
+
- **Not installed is a state, not an error — uniformly.** A folder or project whose dependencies are not installed (no `rstack`, no `@rstest/core`, no `@rslint/core`, a config importing a package that is not there) is the normal state of a fresh clone and of scaffolded templates beside their generator (`create-rstack`'s `template-*`, which declare their own dependencies and are never installed). Every stack reports it the same way: a `disabled` status whose reason names the restart command as the way out (ADR 0002: an install that changes no lockfile fires no detection pass), one `warn` line in the output channel without a stack trace, never a `crashed` status and never a notification. The words come from one place, `shared/notInstalled.ts` (the `formatVersionMismatch` precedent) — each stack keeps its own status machinery, none its own wording; the restart hint is derived from `stackCommandTitle`, which `tests/extension.test.ts` checks against the manifest. Lint's report lives in the `onDocumentFailure` hook (`stacks/lint/index.ts`), which owns the log line too, so the upstream-tracked `RuntimeManager` only defers to it. Rstest classifies the config-import case in the worker (`missingDependencyCauseOf`: Node's `code`, a bare — package-name — specifier, and for a subpath a walk-up proving the package really is absent, so a typo'd relative import or a missing subpath of an installed package stays a real error) because the IPC channel drops the `code` — `NormalizedConfigResult` carries the verdict as data end to end, and `Project` branches on it. The config-import case is implemented for Rstest only today — lint and fmt load configs inside their own servers and cannot classify there yet (#30).
26
27
- One stack failing to register or crashing must never take another stack (or the shell) down.
27
28
- The shell always activates; per-folder config detection decides which stacks start, and re-runs on config/lockfile changes without a window reload. Enable-settings are coarse kill switches only.
28
29
- Reconciles and restarts share one serialized queue (`enqueue`); a reconcile leaves a live stack alone, so the restart path — the commands, and the full pass any relevant settings change triggers — is the only thing that rebuilds one. Do not add a second queue.
@@ -38,7 +39,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
38
39
39
40
- The lint × `rstack.config.*` bridge stays thin on purpose: only a root Rstack config can claim a bridged folder, any native config anywhere in the folder wins ownership, and the worker evaluates rstack's published shim from the folder root. Never generate a shim, load the Rstack config in the extension host, or interpret `define.lint()` ourselves.
40
41
-**Yarn Plug'n'Play is unsupported by decision, extension-wide.** Every stack resolves through physical `node_modules` (`shared/packageResolve.ts`, `resolution.ts`'s rstack → `@rslint/core` chain, the fmt bin probe, the rstest package lookup) and the lint worker's own `createRequire` from the core directory does too. Lint once carried a `.pnp.cjs` branch for the find-`@rslint/core` hop only; nothing after that hop (config evaluation, plugin resolution, the other stacks) had PnP hooks, so it never produced a working folder, and upstream removed its own PnP path in the same refactor that introduced `corePath`. Real support would be a PnP editor-SDK-shaped project across all three stacks, not a resolver branch — do not reintroduce one.
41
-
- **A Lint runtime lives as long as a document needs it, and a folder with none is `running: idle`.** Since the #1617 sync, `RuntimeManager` refcounts each runtime by open document: the first document to resolve a core starts one, the last to release it closes it, so a detected folder with nothing open holds zero workers and zero Go processes. That folder still reports `running` — with the detail `idle` — because it is live and will start a runtime on the next `didOpen`; do **not** add a `StackState` kind for it (the shell's status bar and `when` clauses read the kinds, and idle is not a kind of health). A folder's state is the **worst of** its runtimes plus any document whose core resolution currently fails (last-good: that document keeps the runtime it already had), so one failing core is never masked by a healthy sibling — the same invariant fmt pins across folders, applied inside one and across them alike (lint's rank table matches fmt's: `disabled` there means "no `rstack`", not the kill switch). Triggers: the shell's detection pass (which already covers lockfiles) plus one lint-owned watcher on `node_modules/@rslint/core/package.json` — upstream's glob minus the lockfiles detection owns. Failures report through the status only: upstream's `window.showWarningMessage` is dropped, since stacks own no UI chrome. Consequently `whenStackActive('rslint')` means "the controller registered its folders", not "a server is up" — E2E suites open a document and await diagnostics.
42
+
- **A Lint runtime lives as long as a document needs it, and a folder with none is `running: idle`.** Since the #1617 sync, `RuntimeManager` refcounts each runtime by open document: the first document to resolve a core starts one, the last to release it closes it, so a detected folder with nothing open holds zero workers and zero Go processes. That folder still reports `running` — with the detail `idle` — because it is live and will start a runtime on the next `didOpen`; do **not** add a `StackState` kind for it (the shell's status bar and `when` clauses read the kinds, and idle is not a kind of health). A folder's state is the **worst of** its runtimes plus any document whose core resolution currently fails (last-good: that document keeps the runtime it already had), so one failing core is never masked by a healthy sibling — the same invariant fmt pins across folders, applied inside one and across them alike (lint's rank table matches fmt's: `disabled` there means "a package is not installed" — no `rstack`, or no `@rslint/core` — not the kill switch). Triggers: the shell's detection pass (which already covers lockfiles) plus one lint-owned watcher on `node_modules/@rslint/core/package.json` — upstream's glob minus the lockfiles detection owns. Failures report through the status only: upstream's `window.showWarningMessage` is dropped, since stacks own no UI chrome. Consequently `whenStackActive('rslint')` means "the controller registered its folders", not "a server is up" — E2E suites open a document and await diagnostics.
42
43
- The lint worker is deliberately vscode-free so it can move upstream whole. It takes explicit `--core` / `--config` native paths, writes logs only to stderr because stdout is LSP, and owns the Go child plus config/plugin lifecycles. Config edits use `rslint/configRefresh` with the same pinned path; a native ↔ bridged ownership change replaces the whole folder runtime because protocol 2 locks that choice for the process lifetime.
43
44
- The test × `rstack.config.*` bridge stays thin on purpose: it points the upstream machinery at rstack's shipped shim and lets the shim interpret the config inside the worker, same as the CLI. Bridged projects resolve `@rstest/core` from the resolved rstack package directory, mirroring lint, so rstack's dependency remains visible under isolated installs. Never re-implement rstack config semantics in the extension.
44
45
- The fmt stack is an LSP client: one `rs fmt --lsp` server per detected workspace folder, spawned at the **folder root** even when a deeper `rstack.config.*` exists. Deepest-config-wins was removed deliberately — `rs fmt` loads one config from its cwd with no upward walk, so anchoring deeper made the editor disagree with `rs fmt` in a terminal; a subproject that needs its own fmt config becomes its own workspace folder. The stack registers **no** `DocumentFormattingEditProvider`: the client registers the provider from the server's `documentFormattingProvider` capability, and adding one by hand would double-register. A config create/change/delete **restarts** the owning folder's server (the server caches its config for its process lifetime and has no config-change message), which is also why the stack watches `RSTACK_CONFIG_GLOB` itself instead of relying on detection — a detection signature records which config files exist, not their contents. A detection pass keeps healthy servers and restarts failed ones in place (`isFailedFmtState`) — lockfile events notify even when the folder set is unchanged, precisely so a completed install or upgrade is retried without a manual restart. There is no stdin fallback below `SUPPORT_MATRIX.rstack`; that is a version gate, not an omission. **Nested workspace folders are a documented limitation, by decision**: when a folder and its subdirectory are both workspace folders and both detect fmt, the parent's per-folder selector also matches the nested folder's files, and which server VS Code hands the request to is not defined — the supported shape is subprojects as _sibling_ workspace folders (or only the subproject opened), not parent-plus-child. Routing (lint's `WorkspaceDocumentRouter` shape) was considered and deferred. Why all of it: `docs/adr/0002-fmt-lsp-on-user-node-runtime.md`.
0 commit comments