Summary
Move everything ecosystem-specific in the gate behind a connector contract, so the core (ratchet, baseline, evaluation, reporting, PR comment) never knows which language it is gating. The current JS/TS support becomes the first connector, extracted with zero behavior change. A second connector for a different ecosystem proves the seam.
Motivation
Loopwright currently refuses to install without a package.json and assumes a Node toolchain throughout. I want to use it on non-JS repos (Go, and others later) without forking the engine per language.
The good news from reading the code: the seam is half there. Each collector already resolves an adapter from a registry (.loopwright/scripts/adapters/, contract { name, collector, defaultCommand, collect(ctx) }, writing normalized JSON into reports/), and evaluate.mjs works on a flat metric map. The coupling is concentrated in a few places:
| Coupling point |
Where |
Why it is JS-bound |
| Source analysis (shape, size, integrity) |
lib/analyze-source.mjs |
Built on the TypeScript compiler API. Complexity, function length, nesting, params, file LOC and every integrity.* metric come from it. Biggest coupling. |
| Coverage |
lib/collect-metrics.mjs |
Reads coverage/coverage-summary.json (istanbul shape). |
| Collector set and labels |
config.json collectors, shell.mjs REPORT_FILES |
typecheck is labeled "TypeScript errors"; report files are named after JS tools. |
| Stack detection |
detect-stack.mjs |
Only reads package.json and JS marker dirs. |
| Install |
install.sh |
Exits when there is no package.json. |
| CI |
.github/workflows/quality-gate.yml |
Sets up Node and installs host deps with npm/pnpm/yarn. |
Goals / Non-goals
Goals:
- A documented, versioned connector contract covering: detection, collector adapters, source analysis output, coverage normalization, and the CI toolchain setup step.
connectors/js extracted from the current code with no change in behavior: the existing 148 tests and this repo's own baseline pass unchanged.
- One non-JS connector working end to end on a real repo.
- Installer detects the connector, asks to confirm, and allows a manual override.
- Toolchain installation behind a provider contract, with mise as the default provider and no host forced to use it.
- A host with no mise installed gets it bundled and confined to
.loopwright/, with zero setup.
Non-goals:
- Supporting every language. Two connectors are enough to prove the contract.
- Changing gate semantics (ratchet, tolerances, hard floors, grandfathering).
- Rewriting the engine in another language. The engine is the harness runtime; whether the host needs Node in CI is an open question, not a goal.
Proposed approach
Core keeps: evaluate, baseline and ratchet, report, sticky-comment, the metric vocabulary and its semantics, quality-gate.mjs as the single blocking step.
A connector provides:
detect(hostRoot) returning a confidence and a proposed sources/collectors config.
- Adapters per collector, writing to a normalized report schema owned by core (not named after a tool).
- A source analyzer emitting the contract core already consumes:
{ functions: [{ file, line, name, complexity, lines, depth, params }], files: [{ file, codeLines }], integrity: { <kind>: [{ file, line, snippet }] } }
- Coverage normalized to the core shape (lines/branches/functions/statements, per file).
- A declaration of the tools it needs (e.g.
node@22, go@1.23, golangci-lint), in a neutral form. A connector never installs anything itself.
Toolchain providers (mise by default): installing what connectors declare is a separate pluggable layer, the same way a collector picks an adapter. mise is the default provider, the way eslint is the default linter that framework starters ship with: recommended and wired out of the box, never required. The host does not need to have mise installed to get it.
| Provider |
What it does |
mise (default) |
Installs declared tools with mise. Uses the host's own mise when present, otherwise a bundled copy (below). Same definition locally, in CI and in the runtime image. |
native |
Uses what is already on the machine, and the ecosystem's own setup actions in CI (actions/setup-node, actions/setup-go). For hosts that do not want any mise, or machines that block downloading binaries. |
custom |
Runs a user-supplied install command. The escape hatch for nix, asdf, a company base image, anything else. |
Bundled mise: when the host has no mise, loopwright brings its own, fully contained in .loopwright/:
install.sh downloads the mise version pinned by loopwright for the host's OS and architecture, verifies its checksum, and places it in .loopwright/bin/. The binary is never committed: .loopwright/bin/ and the tool cache are added to .gitignore, the same way .loopwright/node_modules/ already is. The update path replaces the binary only when the pinned version changes.
- mise's data, cache and config locations are redirected via its environment variables, so installed tools live in a gitignored cache under
.loopwright/ and the tool declaration lives in .loopwright/mise.toml. No mise.toml appears at the host root, and a global mise the user might install later is never touched.
- Loopwright invokes every tool through
mise exec --, so no shell activation is needed by anyone.
- Bumping the bundled mise version is a loopwright release decision, never something that changes on its own.
Resolution order: host mise (reads the host's existing mise.toml or .tool-versions) → bundled mise → whatever toolchain.provider the config names as an override. The CI workflow stops hardcoding Node setup, delegates to the provider, and caches the tool directory so each run does not re-download toolchains.
Migration order: define the contract and normalized schemas first, move JS code into connectors/js behind it, prove no behavior change, then build the second connector.
Detection: auto-detect plus confirm in the installer ("found go.mod, use the go connector?"), with the config file as the override. Polyglot repos map connectors to source roots.
Second connector: Go is the natural candidate because there is a real Go repo available to test on (fin-buddy), and Go's toolchain (go vet, go test -cover, golangci-lint) maps cleanly onto the collectors.
Alternatives considered
- Generic analyzers instead of per-language ones (tree-sitter for integrity patterns, lizard for complexity across many languages). Could cut per-connector work a lot. Worth evaluating as the default analyzer inside connectors rather than a replacement for them.
- Installer asks the language, no detection. Simpler, but friction on every install and wrong by default on polyglot repos.
- Keep JS-only and fork per language. Rejected, that is the coupling this RFC removes.
Risks
- Lowest common denominator. Some metrics do not exist in every language (Go has no separate typecheck step; "focused tests" is a JS idiom). Unsupported metrics must report
unconfigured, never a clean zero, consistent with the existing rule.
- Baseline compatibility. Existing hosts' metric keys must not change, or every baseline breaks.
- Integrity detection quality. Suppression and skip patterns per language are easy to get subtly wrong; false negatives silently weaken the gate.
- CI matrix growth as connectors multiply, now multiplied by providers. Supporting providers beyond
mise, native and custom should need a real request, not speculation.
- Bundled binary supply chain. Downloading an executable at install time needs a pinned version, a verified checksum and a clear failure (fall back to
native with a notice, never run an unverified binary).
- Host mise vs bundled mise. When the host has its own mise, its version may differ from the one loopwright pins and tests against. Needs a minimum supported version check.
- Provider drift. Two hosts with the same connector but different providers may resolve slightly different tool versions; the gate must record which provider and versions produced a report.
Task breakdown (link sub-issues)
To be produced by /grill-rfc.
Open questions
- Connectors in-tree (
.loopwright/connectors/<name>) or installable packages?
- Per-language analyzers, or tree-sitter/lizard as a shared default?
- One connector per repo, or per source root from day one?
- Should the host's CI still need Node to run the engine, or should the engine ship as a single binary/container later?
- How do metrics that do not exist in a language appear in the PR comment, so "not measured" never reads as "clean"?
- Neutral tool declaration format: mise's own syntax (since it is the default), or a loopwright format that each provider translates?
Generated by Claude Code
Boundary document
The core/detail boundary this RFC is argued against is #5, refined into
docs/loopwright/principles.md (task #11). Core opinions are cited as P<n>,
detail territory as T<n>.
Summary
Move everything ecosystem-specific in the gate behind a connector contract, so the core (ratchet, baseline, evaluation, reporting, PR comment) never knows which language it is gating. The current JS/TS support becomes the first connector, extracted with zero behavior change. A second connector for a different ecosystem proves the seam.
Motivation
Loopwright currently refuses to install without a
package.jsonand assumes a Node toolchain throughout. I want to use it on non-JS repos (Go, and others later) without forking the engine per language.The good news from reading the code: the seam is half there. Each collector already resolves an adapter from a registry (
.loopwright/scripts/adapters/, contract{ name, collector, defaultCommand, collect(ctx) }, writing normalized JSON intoreports/), andevaluate.mjsworks on a flat metric map. The coupling is concentrated in a few places:lib/analyze-source.mjsintegrity.*metric come from it. Biggest coupling.lib/collect-metrics.mjscoverage/coverage-summary.json(istanbul shape).config.jsoncollectors,shell.mjsREPORT_FILEStypecheckis labeled "TypeScript errors"; report files are named after JS tools.detect-stack.mjspackage.jsonand JS marker dirs.install.shpackage.json..github/workflows/quality-gate.ymlGoals / Non-goals
Goals:
connectors/jsextracted from the current code with no change in behavior: the existing 148 tests and this repo's own baseline pass unchanged..loopwright/, with zero setup.Non-goals:
Proposed approach
Core keeps:
evaluate, baseline and ratchet,report,sticky-comment, the metric vocabulary and its semantics,quality-gate.mjsas the single blocking step.A connector provides:
detect(hostRoot)returning a confidence and a proposedsources/collectorsconfig.{ functions: [{ file, line, name, complexity, lines, depth, params }], files: [{ file, codeLines }], integrity: { <kind>: [{ file, line, snippet }] } }node@22,go@1.23,golangci-lint), in a neutral form. A connector never installs anything itself.Toolchain providers (mise by default): installing what connectors declare is a separate pluggable layer, the same way a collector picks an adapter. mise is the default provider, the way eslint is the default linter that framework starters ship with: recommended and wired out of the box, never required. The host does not need to have mise installed to get it.
mise(default)nativeactions/setup-node,actions/setup-go). For hosts that do not want any mise, or machines that block downloading binaries.customBundled mise: when the host has no mise, loopwright brings its own, fully contained in
.loopwright/:install.shdownloads the mise version pinned by loopwright for the host's OS and architecture, verifies its checksum, and places it in.loopwright/bin/. The binary is never committed:.loopwright/bin/and the tool cache are added to.gitignore, the same way.loopwright/node_modules/already is. The update path replaces the binary only when the pinned version changes..loopwright/and the tool declaration lives in.loopwright/mise.toml. Nomise.tomlappears at the host root, and a global mise the user might install later is never touched.mise exec --, so no shell activation is needed by anyone.Resolution order: host mise (reads the host's existing
mise.tomlor.tool-versions) → bundled mise → whatevertoolchain.providerthe config names as an override. The CI workflow stops hardcoding Node setup, delegates to the provider, and caches the tool directory so each run does not re-download toolchains.Migration order: define the contract and normalized schemas first, move JS code into
connectors/jsbehind it, prove no behavior change, then build the second connector.Detection: auto-detect plus confirm in the installer ("found go.mod, use the go connector?"), with the config file as the override. Polyglot repos map connectors to source roots.
Second connector: Go is the natural candidate because there is a real Go repo available to test on (fin-buddy), and Go's toolchain (
go vet,go test -cover,golangci-lint) maps cleanly onto the collectors.Alternatives considered
Risks
unconfigured, never a clean zero, consistent with the existing rule.mise,nativeandcustomshould need a real request, not speculation.nativewith a notice, never run an unverified binary).Task breakdown (link sub-issues)
To be produced by
/grill-rfc.Open questions
.loopwright/connectors/<name>) or installable packages?Generated by Claude Code
Boundary document
The core/detail boundary this RFC is argued against is #5, refined into
docs/loopwright/principles.md(task #11). Core opinions are cited asP<n>,detail territory as
T<n>.