Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Glossary of terms used across rstack-editor. Code, docs, commit messages and rev

- **Stack** — one tool integration (lint, test, fmt) hosted by the extension shell. A stack registers against the shell and reports status through it; stacks never own UI chrome.
- **Shell** — the always-activating extension core: detection, status bar, output channels, stack lifecycle.
- **Detection** — the per-workspace-folder scan deciding which stacks a folder lights up. The decision is per folder, but native lint's config signal may come from an ancestor directory. Detection signals are config files and installed tool binaries, never user settings.
- **Detection** — the per-workspace-folder scan deciding which stacks a folder lights up. The decision is per folder, but native lint's config signal may come from an ancestor directory, and lint then picks each document's config source by **Ownership**. Detection signals are config files and installed tool binaries, never user settings.
- **Gate** — the per-stack activation condition: detected, workspace trusted, and that stack's enable setting on.

## Runtimes
Expand All @@ -25,7 +25,7 @@ Glossary of terms used across rstack-editor. Code, docs, commit messages and rev
- **Shim** — the module rstack-cli ships per tool that loads the Rstack config and exposes that tool's section through the tool's ordinary explicit-config channel. The extension points upstream machinery at the shim rather than re-implementing Rstack config semantics.
- **Bridged project** — a test project the extension synthesizes for a directory whose test signal is a Rstack config, wired to the shim. _Avoid_: virtual project, rstack project.
- **Config root** — the directory a tool's config is loaded from, which is also the directory the tool's process stands in. For the fmt server the editor anchors it at the workspace folder root, so it loads the config a terminal opened on that folder would, and a subproject that needs its own config becomes its own workspace folder. The test stack does not share this anchor: a project's cwd is set per project (for native configs, upstream's config-file-directory rule). _Avoid_: config directory, project root.
- **Ownership** — the editor-side rule choosing one config source for a tool's unit of work when both a native config and a Rstack config are present: the atomic tool's native config wins and the bridge yields. The unit is the tool's own — a project for test (one per config directory), a workspace folder for lint (one config choice per folder, locked for each lint runtime's lifetime). This rule exists only in the editor; upstream CLIs never face the choice, since each reads only its own config.
- **Ownership** — the editor-side rule choosing one config source for a tool's unit of work when both a native config and a Rstack config are present. The unit is the tool's own — a project for test (one per config directory; the native config wins and the bridge yields), a document for lint (the nearest config on the document's own ancestor chain wins: an `rslint.config.*` up to the folder root, then a root `rstack.config.*`, then an `rslint.config.*` above the folder; a document with none is not linted). A config that is not an ancestor of the document never counts. The choice is locked per lint runtime, not per folder, so one folder can run both kinds. This rule exists only in the editor; upstream CLIs never face the choice, since each reads only its own config.

## test

Expand All @@ -36,8 +36,8 @@ Glossary of terms used across rstack-editor. Code, docs, commit messages and rev
- **Rslint core** — one `@rslint/core` package directory, identified by its real path (two copies of the same version are two cores; a symlink to one copy is that copy). Everything a lint runtime runs — the Go binary, config host, protocol version, plugin host — derives from one Rslint core. _Avoid_: core (bare), installation, binary.
- **Lint runtime** — the lint machinery serving one Rslint core inside one workspace folder: one lint worker, the Go process it spawns, and one language client. Unrelated to the Node runtimes above. _Avoid_: server, instance, coordinator.
- **Lint worker** — the process the extension ships and runs for one lint server: it hosts Rslint's JS side (config evaluation, plugin rules) on a User Node runtime with its cwd at the workspace folder root, and fronts the Go `rslint --lsp` process it spawns, so the editor sees one language server. _Avoid_: lint host, lint proxy, lint server (that is what the worker presents, not what it is).
- **Bridged folder** — a workspace folder whose lint runs against the Rstack config: no native `rslint.config.*` anywhere in the folder, a `rstack.config.*` at its root, and the lint worker pinned to rstack's shipped shim for its whole lifetime. _Avoid_: bridged workspace, rstack folder.
- **Native folder** — a workspace folder whose lint runs against its own `rslint.config.*`, exactly as the standalone Rslint extension would.
- **Bridged runtime** — a lint runtime that runs against the folder's root `rstack.config.*`, its lint worker pinned to rstack's shipped shim for its whole lifetime. It serves the documents of its folder that no `rslint.config.*` between them and the folder root governs. _Avoid_: bridged folder, bridged workspace, rstack folder.
- **Native runtime** — a lint runtime that runs against `rslint.config.*` discovery, exactly as the standalone Rslint extension would. It serves the documents whose ancestor chain reaches an `rslint.config.*` first. _Avoid_: native folder.
- **Inline directive** — a source comment that toggles lint rules for a scope: `rslint-disable`, `rslint-enable`, `rslint-disable-line`, `rslint-disable-next-line`, with the `eslint-` prefix accepted as an exact equivalent. Rule ids are comma-separated; a bare directive applies to all rules. _Avoid_: disable comment, suppression comment.
- **Rule docs link** — the documentation URL derived from a rule id alone (one base URL plus the id, no per-rule data). Best-effort by design: a mistyped or brand-new rule id yields a dead link, never an error. _Avoid_: rule doc URL, docs href.

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0003-lint-through-editor-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Rslint's language server is two halves: the Go process (`rslint --lsp`) lints na
- **Lint gains a Node floor it never had.** A native folder that lints on VS Code's Node today reports `version mismatch` and starts nothing when no User Node runtime clears the ADR 0001 floor. Accepted deliberately: one worker, one path, one floor (ADR 0001 already rejected per-project floors), and this is the debt that ADR named.
- **Resolution follows one chain, mirroring `rs lint`.** For a bridged folder: `rstack` from the folder root → `@rslint/core` from rstack's directory (the transitive copy `rs lint` itself imports; a pnpm project declaring only `rstack` cannot resolve `@rslint/core` from its root) → the Go binary through that core's `resolveRslintBinary()`. For a native folder the chain starts at `@rslint/core` from the **document's own directory** (the #1617 sync; before it, from the folder root). The extension walks the chain as far as the **core directory** — `fs.stat`, `package.json` reads and semver comparisons, no project code loaded, so inside the load bound and legitimately on the VS Code Node runtime, exactly as fmt resolves the `rs` bin — and gates there; the worker receives `--core <dir> [--config <shim>]` and takes the last hop itself, calling that core's `resolveRslintBinary()` on the User Node runtime, since it is a JS export of the project's package. Floors follow the evidence in `SUPPORT_MATRIX`: `@rslint/core >= 0.8.0` is the first release with explicit protocol-2 config selection, while `rstack >= 0.7.0` is the first release whose shim supplies `basePath` and itself pins `@rslint/core` 0.9.0; one answer to "which rstack does the extension support" is worth more than keeping older releases running.
- **One override, and it names a core, not a binary.** `rstack.rslint.binPath` / `customBinPath` are removed in favour of `rstack.rslint.corePath` — the setting upstream introduced in rslint #1617: a path to an `@rslint/core` package directory (or its `package.json`), resource-scoped, from which the binary, config host, protocol version and plugin host all derive. In a bridged folder it overrides the rstack → `@rslint/core` hop only; the shim stays rstack's. A binary chosen independently of its core cannot be supported: the two must speak the same protocol. The rest of #1617 — per-document core resolution, one runtime per physical installation — has since been synced (issue #13): a **Lint runtime** is now one Rslint core inside one workspace folder, resolved per open document and refcounted by it, so a folder runs as many workers as its files have distinct cores (a bridged folder always exactly one, rstack's) and none at all while nothing is open. The worker never noticed: it still takes explicit `--core` / `--config` paths, which is precisely why that change did not touch it.
- **Ownership is per folder, native wins.** One server holds one config choice for its lifetime (the supported config protocols lock `configPath` per process), and explicit and automatic modes cannot mix, so a folder is bridged only when no `rslint.config.*` exists anywhere in it and a `rstack.config.*` sits at its root; a subdirectory `rstack.config.*` lights nothing (`rs lint` in a terminal reads its cwd only — the same reason ADR 0002 rejected deepest-config-wins for fmt). Detection lights a bridged folder on the file's presence and never reads it: a `rstack.config.*` without `define.lint()` runs an empty config, as `rs lint` does.
- _Superseded by [ADR 0006](0006-per-document-lint-ownership.md): "folder" here is the workspace folder, a narrower unit than the intended one; ownership follows each document's ancestor chain._ **Ownership is per folder, native wins.** One server holds one config choice for its lifetime (the supported config protocols lock `configPath` per process), and explicit and automatic modes cannot mix, so a folder is bridged only when no `rslint.config.*` exists anywhere in it and a `rstack.config.*` sits at its root; a subdirectory `rstack.config.*` lights nothing (`rs lint` in a terminal reads its cwd only — the same reason ADR 0002 rejected deepest-config-wins for fmt). Detection lights a bridged folder on the file's presence and never reads it: a `rstack.config.*` without `define.lint()` runs an empty config, as `rs lint` does.
- **Config changes refresh, mode changes restart.** Rslint has a live refresh (`rslint/configRefresh` with the same `configPath`), unlike `rs fmt --lsp`, so the extension keeps its watcher-driven refresh — extended, for a bridged folder, with the root `rstack.config.*` — and the worker re-stamps `protocolVersion` and its `configPath` on every refresh (the extension does not know either). Only a native ↔ bridged flip, or a dependency change the refresh cannot absorb, restarts the server. This is the "diverge only when the tool forces it" rule: rslint can refresh, fmt cannot.
- **Failure states mirror fmt.** No `rstack`, no `@rslint/core`, or a config importing an absent package → `disabled`; `rstack` or the chained `@rslint/core` below floor, or no Node clearing the floor → `version mismatch`; worker or Go dying → `crashed`. Config-import failures are classified where the worker still has the loader's structured error and carried to the editor as data; the live Go server remains available for a later refresh.
- The lint copy diverges further from upstream: the reverse-request adapter and plugin pool move into the worker unchanged in logic, and the extension-side `Rslint.ts` keeps only the language-client half. Recorded as an adaptation in `packages/vscode/AGENTS.md`.
36 changes: 36 additions & 0 deletions docs/adr/0006-per-document-lint-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
status: accepted
---

# Lint ownership follows the document's ancestor chain

ADR 0003 wrote lint ownership down as a workspace-folder decision: any `rslint.config.*` anywhere in the folder made the whole folder native, and only a folder with none could be bridged to a root `rstack.config.*`. That wording pinned ownership to the editor's folder, which is narrower than the rule it meant to state — a config governs the directory tree below it — and the implementation followed the wording. Issue #85 shows the gap. A root `rstack.config.*` with `define.lint()` stops working as soon as one subdirectory adds its own `rslint.config.*`: every file in the folder goes to the native Go server, the files outside that subdirectory fall outside every config boundary, and they get no diagnostics while the status bar still says `running`.

The constraint ADR 0003 cited is real but does not force the folder granularity. Rslint's server locks its config choice per **process** (`internal/lsp/server.go`: "Changing that choice requires a new process"), not per folder. Since the rslint #1617 sync, `RuntimeManager` resolves a runtime per document and keys it on `folder + core identity + shim`, so one folder can already run a bridged runtime and a native runtime side by side. The folder-level decision was the only thing preventing it.

**Decision.** Ownership is decided per document, from the configs on the document's own ancestor chain (`decideDocumentMode` in `stacks/lint/resolution.ts`). Walking up from the document's directory:

1. The first `rslint.config.*` between the document's directory and the workspace folder root, both included → **native**.
2. Otherwise, a `rstack.config.*` at the folder root → **bridged** (rstack → `@rslint/core` from rstack's directory, shim pinned, as in ADR 0003).
3. Otherwise, the nearest `rslint.config.*` above the folder, skipping `node_modules` and counting only while `@rslint/core` resolves from the folder (#89) → **native**.
4. Otherwise the lint stack does not serve the document: no `didOpen`, no diagnostics.

A sibling or descendant config never counts. A root `rslint.config.*` beside a root `rstack.config.*` still makes every document native, by rule 1. Detection stays per folder and only collects the signals; there is no folder mode any more.

Rule 4 matches the `rslint` CLI, which lints only files inside some config's boundary: run at the root of a tree whose only config is `packages/legacy/rslint.config.ts`, it lints the two files under `packages/legacy` and never touches `src/index.ts`.

## Considered options

**Keep folder ownership and document the limitation** — leaves #85 as is: a mixed repository gets silent gaps with a healthy status. Rejected.

**Let the root `rstack.config.*` win whenever it exists** — breaks the documents a nested `rslint.config.*` is meant to govern, and contradicts the `rslint` CLI run in that subdirectory. Rejected.

**Per-document ownership over the whole folder's configs** (any config in the folder counts, nearest by distance) — a sibling directory's config would decide documents it can never govern in either CLI. Rejected in favour of the ancestor chain.

## Consequences

- **One folder, several runtimes of both modes.** A bridged and a native runtime can serve one folder at once. A new `rslint.config.*` in a subdirectory changes the detection signature; the reconcile moves only the documents under it to a native runtime, and the bridged runtime keeps serving the other documents without a restart (`e2e/lint/suite-document-ownership`).
- **Known divergence from `rs lint`.** `rs lint` run at the folder root lints nested files with the root Rstack config. The editor lints a file under `packages/legacy/rslint.config.ts` with that config instead. The editor cannot match both CLIs; the nearest config matches the CLI a user runs in that directory (`rslint` in `packages/legacy`), and it is what the native config's author asked for. Accepted.
- **Documents outside every config are not linted.** Before, a folder with a nested config handed every document to the native server, which linted nothing outside that config anyway; now the editor does not open them on a server at all, as the CLI does not lint them.
- **Detection reads every `rslint.config.*`.** The folder's native config list is now an ownership input rather than a detection signal, so its `findFiles` is unbounded like the Rstack config list.
- ADR 0003's "Ownership is per folder, native wins" consequence is superseded. Its other consequences (resolution chains, refresh vs restart, failure states) are unchanged; a native ↔ bridged flip still replaces the runtime, now for the documents that flipped only.
Loading
Loading