ts-qa-ci
| Language | JavaScript and TypeScript |
|---|---|
| Kind | toolchain |
| Readiness | 🟢 |
| Detector conformance | 🟡 |
| Toolchain conformance | 🟡 |
| Project conformance | 🟡 |
| Checked | 2026-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.
- Phase 0, fast fail: eslintConfigParity. A check of the toolchain’s own that the project’s ESLint configuration matches what the pipeline resolves. Not a defence host. Reported through the pipeline verdict only.
- Phase 0, fast fail: supplyChain. A dependency bake-window audit. Not a defence host. Reported through the pipeline verdict only.
- Phase 0, fast fail: oxlint. Can host a bespoke defence through its JS plugin mechanism, though ts-qa-ci uses it as a pre-filter with built-in rules only, so no defence is routed through it and it is graded as a check. Its
oxlint-disabledirectives are banned by the bundledts-qa/no-eslint-disablerule;ts-qa rule,ts-qa rule-docandts-qa rulesdo not cover oxlint findings. - Phase 1, code modification: prettier. A formatter; cannot host a defence. Not wrapped.
- Phase 1, code modification: eslintFix. ESLint in write mode over the same resolved configuration as the report lane; hosts bespoke defences, and is wrapped by all three of
ts-qa rule,ts-qa rule-docandts-qa rules. - Phase 2, lint and validation: eslintReport. ESLint in report mode; the lane every bundled and bespoke plugin defence runs in, and the one
ts-qa rulenarrows to a single identifier. Wrapped byts-qa rule,ts-qa rule-docandts-qa rules. - Phase 2, lint and validation: remarkValidateLinks. A Markdown link checker; a fixed catalogue, cannot host a defence. Not wrapped.
- Phase 2, lint and validation: knip. Unused file, export and dependency detection; a fixed catalogue, cannot host a defence. Not wrapped.
- Phase 3, static analysis: tsc. The compiler’s fixed catalogue; cannot host a defence. Its
@ts-directives are banned byts-qa/no-eslint-disable. Not otherwise wrapped. - Phase 3, static analysis: dependencyCruiser. Hosts a bespoke architectural defence in the project’s own configuration, and the toolchain ships one of its own,
no-circular, at error severity. Not wrapped byts-qa rule,ts-qa rule-docorts-qa rules, and the lane ignores-p. - Phase 4, tests: vitest. A test runner; cannot host a defence. Runs the toolchain’s own self-audit test. Not wrapped.
- Phase 4, tests: playwright. An end-to-end test runner; cannot host a defence. Not wrapped.
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.