0.3.6 — TypeScript through every door
TypeScript through every door. The backend has scored TypeScript since 0.2.12, and
0.3.5 warned when a mixed baseline was being gated on its Python half — but the
officially shipped CI path could not obey that warning: typescript was not a config
key, the Action had no way to pass --typescript, and its install step never installed
the [typescript] extra. This release makes the switch a config key, teaches every
entrance the last three releases hardened — the Action, the pytest plugin, init,
explain, doctor — to honour it, closes the TypeScript silences that surfaced on the
way, and makes check say what it did not check. The first Added heading since
0.3.0.
Added
typescript,ts_coverage,ts_entryare[tool.riskratchet]keys. Validated
(typescript = "yes"is exit2), shown byconfig show, inconfig.schema.json,
and in the README table — a trip-wire test now requires every allowed key to be
documented there. The two lists anchor to the config directory likecoverage; CLI
values stay cwd-relative.--no-typescriptis the explicit off that beats
typescript = trueand the deprecated--experimental-typescriptalias, on every
command: an option config can turn on must be turn-off-able by flag.- Action inputs
typescript,ts-coverage,ts-entry. Passthroughs:true→
--typescript,false→--no-typescript, empty → nothing, so the CLI resolves
config; anything else fails the step before the CLI runs. The two path inputs are
space-separated likepaths, one repeated flag per entry, workspace-relative. The
Action now installsriskratchet[typescript]on every install path (PyPI latest, a
pinned version, a local wheel), so a config-driven repo never sees the install hint in
CI. Config-driven TypeScript therefore needs the Action atv0.3.6or later: an
olderaction.ymlwith an unpinned version installs the 0.3.6 CLI without the extra
and exits2with the hint. - Pytest plugin options
--riskratchet-typescript,--riskratchet-no-typescript,
--riskratchet-ts-coverage,--riskratchet-ts-entry. Both switches off means config
decides. explain --ts-coverage/--ts-entry, for symmetry with the keys.initrecognises a TypeScript tree. Whensrcholds.tsfiles the starter block
gainstypescript = trueand a commentedts_coveragehint, and the next steps become
pip install 'riskratchet[typescript]'→ the runner's coverage command →baseline/
checknaming only the reports the tree needs.init --with-baselinebuilds the first
baseline with the resolved TypeScript settings. Python-only output is byte-identical.doctorcheckts-coverage(thedoctor --jsoncheck-name enum is extended),
only when TypeScript is on: nothing configured → WARN "TypeScript functions score as
uncovered" with the vitest +ts_coverageremediation; configured but missing or
malformed → FAIL; older than a.tsunder the paths, or zero overlap with the scanned
.tsset → WARN; else PASS.checksays how much of the baseline it compared. Every format carries
Baseline: N entries · M compared · K not seen this run;--summaryprints the same
counts;check --jsongains an additivebaselineobject (entries,compared,
removed; absent in--fail-abovemode).- The unscanned-files warning. When baseline entries live in files that still exist
under the scanned paths but were not scanned this run — aninclude/exclude
hiding them, not a deletion and not a deliberate subset such ascheck packages/api—
check,diff, and the pytest plugin warn with counts only (safe under
private_comment), and never fail. The0.3.4carry-over: three of four baseline
entries hidden by anexcludeprinted "No risk regressions detected." and exited0;
only the PR comment's collapsed diff saidRemoved: 3. - The rename + edit note. When the diff holds both new and removed entries,
check
prints one stderr note naming the rule: a renamed function whose body also changed is
gated as new againstfail_new_above, not as a regression against its old score, and
riskratchet difflists both sets. The README gains the "Rename matcher: known limits"
paragraph that lived only inAGENTS.md. skipped_generated_filesin every report summary (scan --json, the text and
markdown headers, the summary line): the files a@generatedheader removed from
scoring, in both languages (see Changed).check --jsonregression objects carrygroup(the scan and diff payloads already
did);explain --summary --jsoncarrieslanguage(the one identity field the summary
lost). A scan path outside the config directory is named once on stderr.
Changed
- A TypeScript-only project runs. On
0.3.5,scan,baseline, and
check --typescriptall exited2on a tree with no.pyfiles: auto-coverage tried
to runpytestto cover nothing, and--allow-missing-coveragenever reached the
unstartable-runner branch. When TypeScript is on and nothing under the scan paths is
Python, Python coverage is not applicable — no auto-coverage, no exit2, one stderr
line — anddoctorreports the coverage row the same way, skipping the overlap and
branch-data rows.summary.totalindoctor --jsontherefore moves for
TypeScript-only projects. allow_missing_coveragedowngrades an unstartable test command (pytestoff the
PATH) to a warning. The flag promises "continue when coverage is absent", and a runner
that cannot start yields absent coverage.- Missing TypeScript reports follow the
requiredsplitcoveragealready had. A
report named on the CLI (--ts-coverage) is exit2everywhere, unchanged. A report
named in config (ts_coverage) warns onscan/explainand is exit2on
baseline/check/diff, downgraded byallow_missing_coverage— which on
0.3.5returned early and let the strict loader exit2anyway, so the remediation it
printed was not one. explain --typescriptwithout the[typescript]extra is exit2with the
install hint; it was exit1plus a traceback.explainandinit --with-baselinenow
build through the same boundary as every other command.doctorFAILs when config enables TypeScript and the extra is absent, with the
install command; it tries the import, not just the dist metadata, becausecheck
exits2on exactly that import. When only the baseline records a TypeScript identity
the row stays a WARN. Withtypescript = truea scan path holding.tsfiles is PASS
instead ofWARN no .py files; with it off,.tsfiles under the paths WARN that they
are not scored, naming the key.- A
# @generatedPython file leaves the scored set. The TypeScript backend has
honoured a comment-anchored@generatedheader since0.2.12and its docstring
claimed the Python backend mirrored it; it did not, so such a file was scored in full.
Both backends now skip the file's functions, count it inskipped_generated_files, and
list it infiles[]with zero functions. The*.pb.ts/*.gen.tsname rule is
deliberately not mirrored for Python (codegen headers sayDO NOT EDITin a hundred
spellings); the README recipe isexclude = ["**/*_pb2.py"]. Entries the header
removes appear as removed indiff, never as regressions. files[]lists every file the scan reached. A Python file with a syntax error or a
@generatedheader is listed with zero functions, as the TypeScript backend already
did, sototal_filesmoves for repos that have them — a skipped population shows up in
the count rather than vanishing.- One key per file. A file outside the config directory is keyed by one
../…
spelling whatever cwd or spelling it was passed with;0.3.5kept whatever spelling
the caller used, so the identity changed with the invocation. A symlinked scan root
keeps itssrc/x.pykey. SARIFartifactLocation.uriis that key verbatim, so it
equalsproperties.pathalways — it used to re-derive an absolute path against the
process cwd. A baseline written from an absolute out-of-root spelling carries across
through the fingerprint matcher;riskratchet baselinesettles the keys. - The pytest plugin gates through
diff+regressions_from_diff, ascheckdoes,
instead ofcompare. Same verdict (the0.3.5property test still pins it); the diff
is what knows what was not seen, which is how the plugin prints the unscanned-files
warning. - The "baseline holds N typescript entries but this run analyzed no typescript" hint
names both ways to enable the backend; the Python-side "analyzed no python" warning
namesinclude/excludeas the likely cause.
No exit-code change above can turn a green gate red: each turns an exit 2 into 0 or
a warning for a project that could not run, or reports a setup failure that was already
failing. Two score changes can, and only for a TypeScript project whose ts_entry is
@generated or does not parse (see Fixed): its functions regain their real
public_surface, the first check after upgrade says so, and riskratchet baseline
accepts the corrected scores.
Fixed
config.schema.jsonsaidmissing_coverage ∈ {pessimistic, skip, zero}; the
enum isoptimistic, soconfig show --jsonfailed its own schema for one legal
value. A per-policy test now sweeps the enum.- A
@generatedTypeScript file was dropped with no warning and no counter, and a
@generatedentry barrel demoted every TypeScript function to internal
(public_surface100 → 0) behind the ordinary "public surface narrowed" line. A
generated file is valid TypeScript: it is now parsed for its real exports and only its
functions are skipped, soexport * from './generated'keeps resolving and a generated
entry narrows correctly — guarded by a test that a good barrel re-exporting a generated
module narrows exactly as on0.3.5. - An entry file that did not parse narrowed the public surface to nothing. A
syntax-broken or unreadablets_entrydemoted every function to internal the same
way. Narrowing now refuses on an unproven graph:checkwarns
entry index.ts did not parse; the surface can't be bounded — keeping file-level export flagsand leaves the flags untouched. A file that is only re-exported keeps the
empty-module behaviour. - TypeScript discovery descended into
node_modules; only hidden directories were
skipped. Anode_modulesnamed explicitly as the scan path is still honoured. doctoron a TypeScript-only package warnedno .py fileswith a remediation
about package roots, prescribed pytest for coverage, never checked TypeScript coverage,
and keyed itstypescriptrow off the baseline identity alone (see Added and
Changed).doctorandchecknow agree on whether such a project can run.- The pytest plugin had no TypeScript path. It called
engine.analyzedirectly, so
a mixed baseline undertypescript = truewould have been gated on its Python half
only, against the invariantAGENTS.mdstates. It now builds through
pipeline.build_reportwith the CLI's pieces it never had: the missing-report rule,
the exception boundary (ImportError/FileNotFoundError/ValueErrorfail the
session with the message, never a traceback), the grammar-bump identity guard with the
exact re-baseline command, and the "baseline holds N typescript entries" warning. - The regression hint printed the raw baseline path under
--private-comment
beside a hashed stdout; it now prints<baseline.json>.
Internal
config.resolve_gate_settings/GateSettingscarrytypescript,ts_coverage,
ts_entry, andallow_missing_coverage;resolved_typescript,resolved_ts_paths,
usable_ts_coverage, andmissing_ts_report_messageare the public halves every entry
point resolves through.languages_not_scannedandunscanned_baseline_fileslive in
baseline.classify(a leaf) so the CLI and the plugin render the same facts;
DiffReport.baseline_entriesfeeds the baseline line.dogfood-action.ymlgains a second job that stages a TypeScript-only project and gates
it throughaction.ymlin baseline mode — the mode in which a missing--typescript
or a missing extra is exit2rather than a silent pass — proving the install and
TypeScript scoring on a runner. The Action's check step is executed under bash against
a stubriskratchetin the test suite, so input handling is tested as behaviour, not
grepped.- Adding a config key is a four-place change (allowed keys, type rule, schema +
config showpayload, README table), each with a trip-wire.AGENTS.mdrecords the
invariants this release added: an option config can turn on must be turn-off-able by
flag; Python coverage is not applicable when there is nothing to cover; the Action
installs the extra on every path; a skipped file is still a file the scan reached;
narrowing refuses on an unproven graph; the gate must say what it did not check; one
key per file. - Tests 1081 → 1176; coverage 92.16% → 92.02%.