ts-qa-ci

LanguageJavaScript and TypeScript
Kindtoolchain
Readiness🟢
Detector conformance🟡
Toolchain conformance🟡
Project conformance🟡
Checked2026-09-08, main at commit 46fac6a, declaration merged at ea2020b

ts-qa-ci is an orchestrated QA pipeline for TypeScript and React projects, published as @longtermsupport/ts-qa-ci (repository). One command runs oxlint as a pre-filter, then Prettier, ESLint with a bundled plugin of Component-Driven Development rules, the TypeScript compiler, dependency-cruiser, knip and the test suite, auto-fixing locally and failing the gate on anything it cannot fix. It is the reference toolchain for this specification in the JavaScript ecosystem, and every grade below was checked by running its commands against a fixture rather than by reading its README. Under toolchain specification 0.1.0 it graded conforming; the 0.2.0 document measures every detector a defence is routed through and every suppression route, and on that measure three gaps appear that the earlier document did not ask about.

Tools it bundles

The pipeline runs in five phases, fail-fast between them. For each tool: whether it can host a bespoke defence under the method, and which of the toolchain’s own mechanisms wrap it.

The three conformance mechanisms cover the ESLint lanes in full and nothing else. That is enough for every defence a practitioner writes as a plugin rule, and it leaves the one bundled dependency-cruiser defence with an identifier that prints, blocks, and leads nowhere.

How it is conformant

Bespoke rules under detector clause 4.1 enter through tsQaConfig/eslint.config.js, which is merged into the resolved flat config, so any ESLint plugin object the project writes is a first-class defence. Clause 4.2 is met by ts-qa rule <identifier> <path>, which runs the ordinary lint lane over one path and reports whether that one rule fired, exiting 1 with locations when it did; a fixture carrying an eslint-disable comment made ts-qa/no-eslint-disable fire and a clean rule stay silent on the same file. Identifiers are ts-qa/<name>, chosen once in the plugin map and printed by ESLint with every finding, which is clause 4.3. Section 5 is met by the entry point, its -p <path> subset flag and the --llm verdict table, which names the failing tool and the path to the full result. Clauses 6.1 to 6.3 are met for every bundled plugin rule by ts-qa rule-doc <identifier>, which resolves the identifier exactly as printed to its section of the shipped docs/cdd-rules.md and prints it, offline, and a test in the suite walks every plugin rule to prove the page is there. That closes for the toolchain what ESLint alone fails, since ESLint’s own rule pages are online only.

On the toolchain document, the listing under clauses 5.1 and 5.2 is ts-qa rules, which walks the resolved configuration and prints each active defence’s identifier, severity, a terse description and the on-disk route, then the project record beneath it in the same listing; the repository’s own rule against its own source appears alongside the bundled ones. The project record under 6.1 is tsQaConfig/tier-a-exemptions.json, loaded by the toolchain itself; an entry without a justification throws, and so do “legacy” and “needed for now”, each rejection naming the entry and asking for the hazard and the scope, which is 6.2 (configuration). Inline suppression is forbidden under 4.3 by ts-qa/no-eslint-disable, which fires on every eslint-disable, oxlint-disable and @ts- directive form and points the reader at the record. Clause 4.5 holds: phases 0 to 3 are detectors, phase 4 is the test runner, and a run that fails at phase 1 runs nothing after it. Clause 8.2 is the continuous integration workflow, which runs the toolchain on the package itself and fails on anything it cannot fix. The declaration under 9.2 is the defenceBeforeFix key in package.json, carrying both levels of clause 9.1 with their gaps recorded, so neither level claims conformance.

How it is not conformant

Three causes, each verified through the entry point.

The dependency-cruiser lane is outside the three commands. The shipped no-circular defence prints its identifier with a two-file cycle and fails the run, but ts-qa rule-doc no-circular exits 2 as unknown, ts-qa rules does not list it, the lane ignores -p, and the release self-audit walks plugin rules only. Through the wrapping, dependency-cruiser fails detector clauses 4.2, 5.2 and 6.1, which fails toolchain 4.1, and the toolchain fails 4.2, 4.4, 5.1, 5.3 and 8.1 on the same fact. ts-qa rule no-circular src compounds it by answering “did not fire” with exit 0 for an identifier the harness cannot see.

ESLint’s bulk suppressions file is an open route. A file containing the literal PLACEHOLDER fails the lint lane. With an eslint-suppressions.json in the project root naming that file and ts-qa/no-placeholder, the same run passes with every phase clean and the project record unchanged. Nothing disables the file or fails on its presence, so toolchain 4.3 fails, though every inline form is banned.

Resolution of a core rule stops at a URL. The always-on as/enum ban is routed through ESLint’s no-restricted-syntax, and ts-qa rule-doc no-restricted-syntax resolves to a one-line description and eslint.org, not to the toolchain’s own shipped page for the ban. Detector 6.2 and 6.3 fail for that identifier and toolchain 4.2 with them.

The derived listing also exposed a defect: ts-qa/no-naive-datetime-template is declared Tier A in the source and the documentation and is absent from the shipped generic configuration, so it is neither listed nor enforced anywhere, which fails 8.2 as a shipped rule inactive on the package itself. At project level, two of the bundled rules, no-duplicate-section-ids and no-placeholder, have no fixture test, so the red proof method clause 3.3 requires is not in the repository for them. Two SHOULDs remain open from the earlier audit: detector 4.4, a rule over the rules, and toolchain 7.2, since deploy-skills delivers static files rather than the generated listing.

Clause by clause

Document Clause Result Evidence
Detector 4.1 Yes tsQaConfig/eslint.config.js merged into the resolved config; a project dependency-cruiser config replaces the shipped one
Detector 4.2 Partial ts-qa rule ts-qa/no-eslint-disable src/probe.ts exited 1 with the location and a silent rule exited 0; no route for a dependency-cruiser rule
Detector 4.3 Yes ts-qa/<name> chosen in the plugin map and printed unaltered; no-circular printed with its cycle
Detector 4.4 No No rule over the rules
Detector 5.1 Yes npx ts-qa, no infrastructure
Detector 5.2 Partial -p <path> and the single-path rule command for the ESLint lane; the dependency-cruiser lane ignores -p
Detector 5.3 Yes --llm verdict table names the failing tool and the path to the full result
Detector 5.4 Yes CI runs the same command
Detector 6.1 Partial rule-doc ts-qa/no-eslint-disable printed the shipped section; rule-doc no-circular exited 2 as unknown
Detector 6.2 Partial docs/ ships and the printed route is a local path for plugin rules; a core rule resolves to eslint.org only
Detector 6.3 Partial Every plugin rule has a section in docs/cdd-rules.md, pinned by a test; no page for no-circular, none shipped for core rules
Detector 7.1 Yes Directives detected by ts-qa/no-eslint-disable; the known-violations baseline needs a flag the toolchain never passes
Detector 7.2 No A directive’s reason is optional and a baseline entry carries none
Toolchain 4.1 No dependency-cruiser plus the wrapping fails detector 4.2, 5.2 and 6.1 and a defence is routed through it
Toolchain 4.2 No no-circular unresolvable; no-restricted-syntax resolves to an upstream URL, not the shipped as/enum ban page
Toolchain 4.3 No A PLACEHOLDER finding failed the lint lane; with eslint-suppressions.json naming it the run passed, all phases clean
Toolchain 4.4 No Entry point meets detector 5.1 to 5.4 for the ESLint lane; no subset for the dependency-cruiser lane
Toolchain 4.5 Yes Detectors in phases 0 to 3, tests in 4; a failure at phase 1 ran nothing after it
Toolchain 5.1 No ts-qa rules printed eighteen defences with identifier, description and route; no-circular absent
Toolchain 5.2 Yes The listing walks the resolved ESLint configuration, and showed a declared Tier A rule that the configuration never enables
Toolchain 5.3 No The repository’s own plugin rule is listed; its dependency-cruiser defence is not
Toolchain 6.1 Yes tsQaConfig/tier-a-exemptions.json, loaded by the toolchain on every run
Toolchain 6.2 Yes Missing, “legacy” and “needed for now” justifications each rejected with the entry named
Toolchain 6.3 Yes The project record is printed by ts-qa rules beneath the defences
Toolchain 6.4 Yes The always-on tier and its reasoning are documented in docs/cdd-rules.md and docs/coding-standards.md
Toolchain 7.1 Partial One line per defence with identifier and route, phrased as a description
Toolchain 7.2 Partial deploy-skills delivers static skills, not the generated summary
Toolchain 8.1 No The self-audit test walks plugin rules only; no-circular is outside it
Toolchain 8.2 No CI runs the toolchain on itself, but ts-qa/no-naive-datetime-template ships and is active nowhere
Toolchain 9.2 Yes package.json defenceBeforeFix: method 1.0.0, toolchain 0.2.0 at both levels, with the gaps above in knownGaps
Method 7 3.3 Partial Twenty-five bundled rules have a fixture test; no-duplicate-section-ids and no-placeholder have none
Method 7 record Yes Two justified, scoped exemptions, printed on every run and by rules

Notes for a practitioner

Install the package, run npx ts-qa init to scaffold tsQaConfig/, and write your defence as an ESLint plugin rule in tsQaConfig/eslint.config.js with a section in your own documentation, because that is the lane the harness, the resolver and the listing all cover. Prove it with ts-qa rule <identifier> <fixture>, check it appears in ts-qa rules, and put any irreducible exception in tier-a-exemptions.json with the hazard and scope written down, since the toolchain will reject anything less. Keep architectural rules in dependency-cruiser only if you are willing to document and prove them by hand, and do not let an eslint-suppressions.json into the repository, since the toolchain will not notice it for you yet.