The VS Code extension — the editor as a first-class authoring surface
Status: design accepted 2026-07-07 (roadmap Phase 54, a cross-cutting developer-experience
track like Phase 38). All four slices are delivered — Phase 54 is complete and
milestone M19 is met (vscode-extension/, tesseraql lint --format json, the
vscode-extension CI job, and the scaffolder recommendation). The marketplace publish
remains the recorded operator step; until then the CI-built .vsix installs from file.
TesseraQL already has three authoring surfaces, each riding the same engines: Studio
in the browser (Track J), the MCP dev-tools for coding agents (Phase 24), and the
CLI for scripts and CI. The plain editor is the fourth — and today it is only
half-served. Phase 43 wired schema-driven completion into every scaffolded app
(.vscode/settings.json associates the committed tesseraql-v1.schema.json with the
route trees through redhat.vscode-yaml), and the linter learned to position its
findings (LintFinding.line/column, the source:line:column clickable form). But a
JSON Schema can only check shape. Everything the real linter knows — SQL file
references, security policies, connector names, i18n keys, the whole TQL-* taxonomy —
never reaches the editor; the developer round-trips through a terminal or Studio to see
it. Phase 54 closes that loop with vscode-extension/: a thin editor shell over the
engines that already exist.
The no-duplication stance first
Section titled “The no-duplication stance first”- The extension holds no validation logic.
AppLinteris the single lint engine; the extension runs the project-selected CLI and renders what it reports. A rule added to the framework reaches the editor with no extension release, and the two can never disagree — the drift risk that killed every “editor plugin with its own parser” design is structurally absent. - The contract already exists. The MCP dev-tools’
linttool has emitted{errors, warnings, findings: [{code, severity, source, message, line, column}]}since Phase 24. Slice 2 teachestesseraql lintto print exactly that shape with--format json, making it the one cross-surface findings contract (CLI, MCP, editor) rather than a new one. - Zero runtime dependencies. The extension is TypeScript compiled by
tsc, no bundler, no runtime npm packages — the JDK-only instinct applied to the npm world, and the docs-site precedent for toolchain (pnpm, Node 22). All logic that does not need thevscodeAPI (contract parsing, app-home discovery, the explorer tree model) lives in editor-free core modules unit-tested withnode:test; thevscodeglue stays thin. That factoring is deliberate: a future language server grows out of the core, not beside it. - The CLI is the app’s, not the extension’s. The extension resolves the
tesseraqlbinary from thetesseraql.cliPathsetting (default:tesseraqlonPATH), so the findings always come from the version the project actually builds and serves with. The extension version is independent of the framework version; the JSON shape above is the only coupling.
What ships
Section titled “What ships”Slice 1 — this design and the roadmap entry.
Slice 2 — the CLI contract. tesseraql lint --app <dir> --format json prints the
MCP-shape findings document on stdout (one JSON object, nothing else on stdout; exit
semantics unchanged, --fail-on-warning included). --format text is the explicit
name for today’s output and stays the default.
Slice 3 — the extension MVP, under vscode-extension/:
- App discovery. A workspace folder — or a direct child directory — holding
config/tesseraql.ymlis an app home. Multi-root and multi-app workspaces resolve through a quick-pick; a single app resolves silently. - Diagnostics. On save of any file under an app home (debounced), on activation,
and on an explicit TesseraQL: Lint command, the extension runs the JSON lint and
publishes every finding to the Problems panel at
source:line:column(position-less findings anchor at the top of their file). Findings clear when the next run no longer reports them. If the CLI is missing or predates--format json, one actionable warning points at thetesseraql.cliPathsetting — never a modal, never a crash. - Commands. Serve, Test, Migrate, Admission, and Package run the corresponding CLI verb in the integrated terminal (visible, cancellable, credentials prompted by the CLI itself); Lint runs headless into the Problems panel.
- The explorer. A TesseraQL tree view over the app layout — routes grouped by
kind (
web/,consume/,batch/,mcp/), views,db/**/migrationtrees, andtests/suites — built from the documented directory contract (docs/app-layout.md), refreshed on file events, one click to the source. No CLI call needed to navigate. - Error-code hovers. Hovering a
TQL-<DOMAIN>-<n>literal in an app file links to its entry in the published error-code reference on the docs site — the generated index from the documentation portal, now one hover away from the finding that cited it. - Snippets for the blessed route shapes, kept few and exactly aligned with
docs/reference-yaml-surface.md. - Schema completion stays Phase 43’s. The extension declares
redhat.vscode-yamlas a recommendation and leaves the scaffolded schema wiring alone — it complements the JSON Schema, it does not replace it. - CI. A
vscode-extensionjob mirroring the docs-site job (pnpm, Node 22): typecheck, unit tests, and avsce packagesmoke so a broken manifest can never merge.
Slice 4 — the adoption wiring. The scaffolder’s .vscode/extensions.json (and the
committed examples/scaffold-demo-app copy) recommends the extension alongside
redhat.vscode-yaml; the cookbook documents install and use. Publishing to the
Visual Studio Marketplace / Open VSX is recorded as an operator step (the
Cloudflare-dashboard precedent from the docs site): until the publisher account
exists, the CI-built .vsix installs from file.
Deliberately out of scope (the ladder above the MVP)
Section titled “Deliberately out of scope (the ladder above the MVP)”Recorded so they are chosen, not implied: a full language server (cross-file
go-to-definition, project-aware completion beyond the JSON Schema, embedded-SQL
analysis against the introspected catalog), Test Explorer integration over
tesseraql test, deep links into a running Studio, auto-registering the Phase 24 MCP
server with editor AI clients, and marketplace publish automation in release.yml.
Each has an obvious seam in the MVP (the editor-free core, the findings contract, the
explorer) and none blocks it.
Milestone M19 — in a fresh tesseraql new app opened in VS Code with the
extension installed: an edit that breaks a route surfaces the real TQL-* finding in
the Problems panel at its line and column within seconds of save, and clears on fix;
serve, test, and migrate run from the command palette; the explorer navigates to every
route, view, migration, and test suite — all without opening Studio or a terminal by
hand.
Phase 55 — editor authoring depth
Section titled “Phase 55 — editor authoring depth”Status: design accepted 2026-07-08 (roadmap Phase 55, the first rungs of the ladder
above). All five slices are delivered — Phase 55 is complete and milestone M20 is
met (reference links, tesseraql test --format json, Test Explorer, SQL coverage,
the serve status bar; extension 0.2.0). Four capabilities, in the order they pay off, under the same Phase 54 stance:
the extension computes nothing the framework already knows. Reference resolution
follows the documented app layout, test results and coverage come from run contracts
the framework already writes, health comes from the Phase 45 probe.
Slice 1 — this design.
Slice 2 — reference navigation (the pre-LSP rung). A document-link provider makes
file:, view:, and template: values in app YAML clickable, resolved against the
document’s directory exactly as the runtime resolves them (a frags.html::fragment
suffix links to the file). Links appear only when the target exists — a broken
reference stays a lint finding, not a dead link. No YAML semantics enter the
extension: the provider matches the documented key shapes line-by-line, in the
editor-free core, under node:test. The full go-to-definition ladder (message keys,
policies, named queries) stays with the language server rung.
Slice 3 — the test-run contract. tesseraql test --app <dir> --format json
prints one JSON object on stdout:
{passed, failed, results: [{name, passed, message}], sql: [{file, lineRatio, branchRatio, coveredLines, coverableLines}]} — the complete per-case results
(report.json only carries cases joined to a route) plus per-file SQL line/branch
coverage with the 1-based line lists the portal already renders. --format text
names today’s output and stays the default; exit semantics (1 on failure, 2 on the
opt-in regression gate) are identical in both formats.
Slice 4 — Test Explorer and SQL coverage. The extension discovers suites from
tests/**/*.yml (case names and lines only — presentation, not semantics) into the
native Test Explorer; a run executes the slice-3 contract against the app’s
datasource and maps results back by case name. The same run feeds VS Code’s test
coverage API: coverableLines minus coveredLines renders covered/uncovered SQL
lines in the editor — the 2-way SQL branch story, visible where the SQL is written.
Single-case filtering server-side is deferred (the CLI runs the whole app; runs are
seconds on a scaffold); it joins the ladder.
Slice 5 — serve status. A status-bar item polls the Phase 45 readiness probe
(/_tesseraql/health/ready) on the configured base URL (tesseraql.serverUrl,
default http://localhost:8080) while an app home is open: up, DOWN (503), or
unreachable, one click to open the app. Nothing new server-side — the probe is the
contract.
Milestone M20 — in a scaffolded app: Ctrl+click on a file:/view: value jumps
to the SQL or view source; the Test Explorer lists every suite case, a run marks
pass/fail inline and paints covered/uncovered lines in the route’s SQL files; the
status bar tracks tesseraql serve up and down — still no Studio, still no
hand-typed terminal.
Phase 56 — editor intelligence: the remaining ladder
Section titled “Phase 56 — editor intelligence: the remaining ladder”Status: design accepted 2026-07-08 (roadmap Phase 56). All five slices are
delivered — Phase 56 is complete and milestone M21 is met (single-case runs,
Studio deep links, MCP registration, the tesseraql symbols language layer;
extension 0.3.0). The four remaining ladder
items, in the order they pay off: single-case runs, Studio deep links, MCP
registration, and the language layer. One design decision up front:
The “full LSP” ships as in-extension providers over a CLI symbols contract, not as a language-server process. Everything a language server would know about a TesseraQL app — declared policies, message keys, route ids — only the framework can resolve, so the intelligence lives in a new CLI contract and the editor stays a thin renderer (the Phase 54 stance). Inside VS Code, definition/completion providers over that contract are functionally identical to an LSP client and cost one process less. The LSP wire protocol becomes worthwhile only when a second editor is targeted; that rung stays on the ladder, and the editor-free core keeps the seam open.
Slice 1 — this design.
Slice 2 — single-case runs. tesseraql test gains a repeatable --case <name>
filter (exact case names): the runner executes only the matching cases, and the
--format json document reports only them. The Test Explorer passes the filter when
a run request names specific cases, so one failing case re-runs alone — the
seconds-long whole-app run stops being the only granularity.
Slice 3 — Studio deep links. TesseraQL: Open in Studio on an app file (editor
context, explorer context, and the command palette) opens
/_tesseraql/studio/ui/source?path=<app-relative path> on tesseraql.serverUrl —
the same source view every Studio surface links to. The editor and Studio stop being
parallel worlds: the file under the cursor is one click from its live, hot-reloading
counterpart.
Slice 4 — MCP registration. TesseraQL: Register MCP Server writes the Phase 24
dev-tools server (<cliPath> mcp --app <home>, stdio transport) into the chosen
client configuration: .vscode/mcp.json (VS Code MCP clients) and/or the repo-root
.mcp.json (Claude Code), merging with any existing servers and never overwriting a
foreign tesseraql entry without confirmation. One command, and any connected agent
sees manifest, lint, tests, and scaffolding.
Slice 5 — the language layer. A new CLI contract, tesseraql symbols --app <dir> --format json, prints what the framework declares:
{policies: [{name, source, line}], messages: [{key, source, line}], routes: [{id, source, path, recipe}]} — policies from the app config, message keys from the
default-locale catalog (flattened dotted keys with their source lines), routes from
the manifest; sorted, deterministic. Over it the extension adds, per app home and
refreshed on save: completion for policy: values (declared policies) and
message: values (catalog keys), and go-to-definition from a policy: value to
its declaration in config/tesseraql.yml and from a message: value (or a view
title:/label: that names an existing key) to its line in the catalog. Unknown
references stay lint findings — the providers navigate, they do not judge.
Milestone M21 — in a scaffolded app: a single failing case re-runs alone from
the Test Explorer; right-click on a route file opens that source in Studio; one
command registers the MCP server and a connected agent lists the dev tools; typing
policy: offers the app’s declared policies and Ctrl+click on one lands on its
declaration, same for message keys — the editor knows what the framework knows.
Phase 57 — the Studio–editor boundary, kept honest
Section titled “Phase 57 — the Studio–editor boundary, kept honest”Status: design accepted 2026-07-08 (roadmap Phase 57). All three slices are
delivered — Phase 57 is complete and milestone M22 is met (principle 6, the
reverse deep link, framework-derived form options; the live auth: public drift
fixed). Phases 54–56 grew the editor
alongside Studio; this phase names the boundary that emerged and closes its two
gaps. The principle, now extension principle 6 in the roadmap: Studio owns the
live runtime side (data, state, audited draft/apply writes under runtime
governance); the editor owns the source-of-truth loop (files, git, tests,
findings); both are thin renderers over the same engines, meeting through deep
links — neither re-implements the other’s trust model.
Slice 1 — this design, and the principle recorded in the roadmap.
Slice 2 — the reverse deep link. The Studio source view gains Open in editor
(vscode://file/<absolute path>) next to Edit as form — the counterpart of the
editor’s Open in Studio, closing the round trip: see a finding or a failing test
in Studio, land in the editor on the same file. Best-effort by design: the protocol
link assumes the browser and the files share a machine (the normal dev loop); a
remote or containerized Studio simply leaves the button inert, and no configuration
is added until someone needs a different editor scheme.
Slice 3 — framework-derived form options. The route form’s dropdowns are today
hand-coded lists in the runtime and the template — and they have already drifted:
the form offers four auth modes where the framework accepts five (auth: public is
missing). The fix follows the knownRouteRecipes() precedent (exposed exactly so
the shipped JSON Schema could be drift-tested): the recipe options come from
AppLinter.knownRouteRecipes(), and two new surfaces — knownAuthModes() and
knownInputTypes() — feed the auth and input-type selects, land as real enums in
the shipped JSON Schema (today security.auth is free-text with examples, so the
editor gains completion too), and extend SchemaSyncTest so the schema, the
linter, and the form can never disagree again. Deliberately not chosen: generating
the whole form from the schema. The form is a curated subset — id, recipe,
security, inputs — with the text editor as the escape hatch; full generation would
couple its UX to schema structure for little gain, and the choices, not the layout,
are where drift lives.
Milestone M22 — in a running scaffolded app: the Studio source view jumps to
the same file in VS Code; the route form offers exactly the recipes, auth modes
(including public), and input types the framework accepts — the schema, the
linter, and the form provably in sync.