Skip to content

M0: stop fixer damage and silent zeros; fix worst TypeScript false positives - #1

Merged
cstarlea merged 86 commits into
mainfrom
m0/upstream-batch
Oct 6, 2026
Merged

cstarlea merged 86 commits into
mainfrom
m0/upstream-batch

Conversation

@cstarlea

@cstarlea cstarlea commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Milestone 0 from dev/FORK_ROADMAP.md: stop the fixers corrupting code, stop detectors silently reporting zero, and remove the worst TypeScript false positives. Merge this with a merge commit (not squash) so upstream contributors keep authorship.

Upstream PRs merged (--no-ff, authorship kept)

Fixes

Fixers that break code

  • The TS unused-imports, unused-vars, unused-params, debug-logs and empty-if-chain fixers only write with --unsafe; --dry-run still works. next, the plan and the narrative no longer suggest them.
  • The debug-logs fixer no longer deletes side-effecting declarations such as const result = await saveUser(user).
  • detect logs --fix no longer deletes lines itself.
  • Fixer writes keep CRLF, BOM, file mode and symlinks.
  • move (TypeScript):
    • builds its graph from the project root;
    • rewrites ./x.js specifiers;
    • no longer re-points imports between files that move together;
    • applies replacements in one pass;
    • aborts and lists any importer it can't rewrite (--force overrides).

Silent zeros now reported as reduced coverage

  • tsc:
    • runs the project's own compiler, never npx tsc (which downloads an unrelated npm package);
    • no longer writes a temp tsconfig into the repo;
    • parses all seven unused-symbol codes;
    • reports failures as reduced coverage.
  • Knip:
    • runs from the package root, so the default --path src works, and from the workspace root with --workspace in monorepos;
    • reads line instead of the character offset pos;
    • uses the export count as the potential;
    • says when it was skipped.
  • tree-sitter grammars that fail to load (offline download) produce a scan warning with the fix command.
  • The 50-match cap on smells is removed (TS, Python, R and Rust).

False positives and harmful advice

  • tsconfig resolution:
    • parses JSONC;
    • follows extends chains (relative paths, packages, arrays);
    • follows solution-style references;
    • uses baseUrl as a fallback root;
    • prefers the first paths target that exists.
  • The import graph includes leaf files with no imports of their own.
  • Test-quality analysis recognises AVA, tap, node:test, Playwright and type-test assertions.
  • Next.js 16 proxy.ts and the other new convention files are entry points.
  • eval matching no longer flags page.$eval, redis.eval or JSDoc examples; clearing with innerHTML = '' is no longer an XSS finding.
  • The deprecated detector only matches the JSDoc tag, and never calls exported (possibly public) API "safe to delete".
  • Unclassified unused symbols are categorised as vars, not imports.
  • Type tests, e2e suites, benchmarks, codegen output and common tool configs are zoned out of production.
  • Packaging requires tree-sitter>=0.25 (needed for QueryCursor) and excludes tree-sitter-language-pack 1.6.3, which has no importable module.

Verification

  • Full suite: 7073 passed, 18 skipped, 0 failed. main currently fails 5 tests, which chore(ci): unblock CI by gating bash tests on tree-sitter; sort review prompt glob peteromallet/desloppify#617 fixes.
  • ruff (CI select), mypy and lint-imports are clean.
  • Re-scan of sindresorhus/ky:
    • false orphans: 13 → 0;
    • test-coverage issues: 34 → 15;
    • the fake "assertion-free tests" top item is gone (retry.ts now shows 450 assertions, previously 0).
  • vercel/commerce import graph: edges 39 → 121, files with no importers 38 → 17 (the rest are Next.js route files).
  • Real tsc 5 and knip 6.39 run against a fixture project. An offline scan with an empty grammar cache now reports reduced coverage.
  • TypeScript directory move on a fixture (alias, .js specifier, scripts/ importer, sibling imports): tsc --noEmit passes afterwards, and an importer using a custom alias aborts the move with no files changed.

Not in this PR (tracked in the roadmap)

  • M0 leftovers: @generated-header zoning and directory-level zones, string-literal skipping in the security checks, a separate params unused category.
  • M1: a shared TS resolver (workspaces, package exports), AST import extraction, entry points from package.json.

🤖 Generated with Claude Code

elfensky and others added 30 commits June 1, 2026 23:43
…w prompt glob

## The problem

CI has been red on `main` since 2026-04-06, and on every PR opened
since, because of 5 pre-existing test failures that are unrelated to
any feature work. Because they live on `main`, every PR inherits them
and cannot go green without bundling a workaround. Two distinct root
causes:

  1. Three bash tests in
     `desloppify/tests/lang/common/test_bash_unused_imports.py` call
     `detect_unused_imports`, which parses via tree-sitter. tree-sitter
     is an *optional* dependency (`[project.optional-dependencies]`),
     so the `tests-core` job (which installs no extras) does not have
     it; there the function returns `[]` and the tests, asserting
     specific findings, fail. In `tests-full` (installs `.[full]`)
     tree-sitter is present and the tests pass.

  2. Two review tests in
     `desloppify/tests/review/test_review_commands.py` and its
     integration counterpart use
     `list(runs_dir.glob("*/prompts/batch-*.md"))` without sorting.
     The glob order is filesystem-dependent: on macOS it happens to
     return `batch-1.md` (the batch with `historical_issue_focus`)
     first, on Linux it returns `batch-2.md` first. The assertion
     `"Previously flagged issues" in prompt_text` then fails on Linux
     because that string only appears in `batch-1.md`.

## The solution

Two small, behaviour-preserving test fixes:

  1. Gate the bash test file on tree-sitter availability with the exact
     pattern its sibling tree-sitter tests already use
     (`test_treesitter.py`, `test_treesitter_complexity_and_integration.py`):

     ```python
     pytestmark = pytest.mark.skipif(
         not is_available(),
         reason="tree-sitter-language-pack not installed",
     )
     ```

     This is a consistency fix — the bash file was the only
     tree-sitter-dependent test file missing the guard. Coverage is
     preserved: with tree-sitter installed (`tests-full`) the tests
     run and assert real findings as before; without it (`tests-core`)
     they skip cleanly, because the feature genuinely cannot run.

  2. Wrap the review test's glob in `sorted(...)`:

     ```python
     prompt_files = sorted(runs_dir.glob("*/prompts/batch-*.md"))
     ```

     This makes the test deterministic across operating systems —
     `batch-1.md` always sorts first — and removes a latent flake that
     only passed on macOS by luck. The assertion still verifies exactly
     what it intends (batch-1's content).

## Scope

This is a standalone maintenance PR against `main`: it touches only test
files and changes no product behaviour. Landing it here gives `main` —
and therefore every open and future PR (peteromallet#614, peteromallet#616, …) — a green
baseline, instead of each PR having to carry the same workaround.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kotlin resolves some imports by convention rather than by name, so the imported
symbol never appears in the file body:

  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.setValue
  ...
  var expanded by remember { mutableStateOf(false) }

The identifier search in detect_unused_imports never sees "getValue", so every
Compose file using property delegation is reported as having 2 unused imports.
Acting on the finding breaks the build. On a Compose Multiplatform project this
was 37 of 73 unused-import findings — over half the detector's output.

Adds a TreeSitterLangSpec.implicit_import_uses field: (name_pattern, body_pattern)
pairs declaring per-language conventions where a matching body pattern means the
import is used. Kotlin declares getValue/setValue/provideDelegate (guarded on the
`by` keyword) and componentN (guarded on destructuring declarations). Other
languages are unaffected — the field defaults to empty.

Genuinely dead delegation imports are still flagged: the guard requires the
convention's syntax to actually be present in the file.
The Kotlin plugin declared no zone_rules, so it fell back to the common rules,
which look for a '/test/' or '/tests/' path segment. Kotlin projects are laid out
by Gradle source set instead, and Kotlin Multiplatform names every test source set
'<target>Test':

  shared/src/commonTest/kotlin/...
  sync/src/jvmTest/kotlin/...
  composeApp/src/androidUnitTest/kotlin/...

None contain '/test/', so every test file was zoned production. On a KMP project
that put all 241 files in the production zone, with 64 test files reported as
orphaned (a test file has no importers by definition) and hardcoded test
credentials raised as production security findings.

Adds desloppify/languages/kotlin/_zones.py covering KMP and plain Gradle/Android
test source sets, marking Gradle build scripts as config, and wires it into the
plugin via zone_rules.
The kotlin plugin declared the ktlint tool with fmt="json", which maps
to the generic parse_json parser. That parser expects a flat list of
objects with top-level file/line/message keys (like most linters), but
ktlint's --reporter=json output nests violations per file:

  [{"file": ..., "errors": [{"line", "column", "message", "rule"}]}]

Because parse_json looked for top-level "line"/"message" keys that
don't exist on ktlint's file objects, every entry was silently dropped,
so ktlint_violation always produced zero entries despite ktlint exiting
non-zero with real violations. This surfaced as a permanent
'Coverage reduced (ktlint_violation): ... tool_failed_unparsed_output'
warning and meant Kotlin projects never got real ktlint findings from
desloppify.

Add a dedicated parse_ktlint parser that understands the nested
file/errors shape, register it in PARSERS, and switch the kotlin plugin
to fmt="ktlint". Verified against a real multi-module Kotlin project:
ktlint_violation coverage is no longer reduced and the mechanical
'Code quality' score reflects real lint findings.
The csharp plugin discovered only `.cs`, so `.razor` and `.cshtml` were
invisible. Code reachable only from markup therefore looked unreferenced:
a `Widget.razor.cs` code-behind partial is reported as an orphaned file
with zero importers and a suggestion to delete it, even though deleting
it breaks the build.

Views are parsed for edges but stay out of the scored extension set,
mirroring how the typescript plugin already handles `.svelte`, `.vue` and
`.astro`. Scores for existing projects are unchanged.

Edges resolve by symbol name rather than by namespace. A view's `@using`
says which namespaces are in scope, not which files it depends on, so
linking a whole namespace would mark every file in it as live and hide
genuinely dead code.

Resolved per view:

- types the view names, via a type-name index
- extension methods it calls, which name no type at the call site
- its own code-behind partial (`Widget.razor` -> `Widget.razor.cs`)
- components it renders, including components declared in plain C#
- partials and layouts referenced by string name
- view components and tag helpers, which resolve by naming convention
- `@page` marks a view as a routable root, like `Program.cs`
- `_Imports.razor` and `_ViewImports.cshtml` usings apply to the subtree

Also zones the ambient import files as config and `.razor.g.cs` /
`.cshtml.g.cs` as generated, and stops `build_dep_graph` returning early
on a project that is all views and no `.cs`.

Measured on a ten-project Razor Pages solution: 334 views enter the
graph, contributing 1052 edges across 200 `.cs` files, with 136 routable
pages marked as roots. No file changed orphan status on that codebase,
because the existing namespace matching already linked them.
ts_build_dep_graph discarded every import edge when file_list used relative
paths, which made every file in the project look orphaned.

resolve_import returns a path in the same space as the source_file it is
handed, so a relative file_list produces relative results. The builder then
unconditionally joined those onto the absolute scan_path and tested membership
against the relative file_set, so the lookup never matched.

Reproduced on a real React Router project: same file set, relative file_list
gives 0 edges, absolute gives 142, and a db.server.js with 15 importers was
reported as having zero. Running the scanner over that project, orphaned
findings drop from 49 to 24 and code quality goes from 42.8% to 72.0%.

Resolved paths are now matched against the file set in whichever space it
uses, with the previous scan_path-relative interpretation kept as a fallback.

The existing test only covered an absolute file_list with a scan_path-relative
resolver, which is the one combination that worked. Added a regression test for
the relative case.
Route modules and framework entry points have no importers by design -- the
file-based router loads them from the filesystem -- so every one of them is
reported as an orphaned file on every project of this shape.

Mirrors the existing Next.js App Router handling rather than adding a new
mechanism: detect the framework at the scan root, then exempt its convention
files.

Detection prefers a react-router.config.* or remix.config.* file and falls back
to checking package.json for @react-router/* or @remix-run/*, because the
framework's own template ships a Vite config rather than a react-router.config.

Exempted: anything beneath an app/routes/ or src/routes/ directory, plus
root, entry.client and entry.server beside it. An ordinary module such as
app/db.server.js is deliberately still reported -- a genuinely orphaned file
has to stay visible, which is what the negative tests cover.

On a real React Router project this drops orphaned findings from 49 to 27.
The bash unused-import check treated a source directive as used only
when the sourced file's basename reappeared in the script body. Scripts
that source a function library (source ./lib.sh) and then call its
functions or read its variables never repeat the basename, so every
such script was flagged as an unused import.

Resolve the sourced path relative to the sourcing script, parse it, and
collect the symbols it defines: function definitions in both name() and
'function name' forms plus top-level variable assignments, including
export/declare wrappers while excluding function-local declarations.
The import counts as used when any defined symbol is referenced in the
script. Unresolvable paths keep the existing basename heuristic, and
per-run caching avoids reparsing a library shared by many scripts.
The unused-import detector took the last path segment as the in-code
identifier. In Go that is frequently wrong, so every affected import was
reported as unused.

Scanning a real Go repo produced 20 false positives and no true positives:

  _ "github.com/joho/godotenv/autoload"   -> "unused import: autoload"
  "gopkg.in/yaml.v3"                      -> "unused import: v3"
  "math/rand/v2"                          -> "unused import: v2"
  "github.com/go-playground/validator/v10"-> "unused import: v10"
  "github.com/anthropics/anthropic-sdk-go"-> "unused import: anthropic-sdk-go"

These are used as yaml., rand., validator. and anthropic. respectively; the
first is a blank import that exists only for its init() side effect. Note
that a hyphenated segment can never be a Go identifier at all.

There is also a general argument: go build fails on a genuinely unused
import, so a Go module that compiles cannot have one, and any finding of
this class on a compiling module is a false positive.

Go now gets its own resolver that skips blank and dot imports, strips
major-version segments (/v2, .v3), and returns None when the identifier
cannot be determined from the path alone — callers treat None as "do not
report", because a false negative is far cheaper than a false positive here.

Verified: full suite passes (5825 passed, 3 skipped); scanning the repo that
surfaced this drops its 17 unused-import findings to 0 with no new findings.
The GDScript dependency graph only followed `preload("res://x.gd")` and
`extends "res://x.gd"`. Godot code is rarely linked that way: scripts refer to
each other by their global `class_name`, and are attached to nodes through a
scene's `[ext_resource type="Script"]` block or a `project.godot` autoload
entry. None of those were resolved, so on a real project essentially every
`.gd` file reported zero importers.

`_find_project_root` also searched upward only. A Godot project is commonly one
directory inside a larger repository (engine plugins, build tooling and CI
beside it), and the scan root is that repository — so no `project.godot` was
found, every `res://` path failed to resolve, and even the preload edges that
were implemented produced nothing.

Measured on a 384-file Godot project: orphaned findings drop from 402 (one per
file, plus a matching false test_coverage finding each) to 7 genuine ones.

- follow `class_name` declarations and their use sites as graph edges, with
  comments and string literals stripped first so a name mentioned in prose does
  not fabricate a dependency
- add `find_gdscript_dynamic_imports`, reporting scripts a scene or autoload
  attaches, via the existing `dynamic_import_finder` orphan hook
- search downward for `project.godot` when no ancestor holds one
- thread `dynamic_import_finder` through `run_coupling_phase`

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Godot's project templates and documentation use PascalCase directories, so a
project's folders are `Scripts/`, `Scenes/` and `Tests/`. The zone rules matched
only the lowercase spellings, so on a real project every suite in `Tests/` was
classified production and scored as if it were shipping code, while the one file
classified as a test was `test_scene_builder.gd` — production code that builds an
in-game scene and merely starts with the Python/JS `test_` prefix. That prefix is
not a Godot convention and is dropped; a Godot suite is `<name>_tests.gd`,
`<name>_test.gd` or `<name>_suite.gd` inside a tests directory.

Test-to-source mapping had the same import blindness as the dependency graph: it
read only `preload`/`extends`, but a suite names what it exercises by global
`class_name`, so nothing mapped and every file read as uncovered.

Measured on a 384-file Godot project: test health 0.0% -> 42.1%, with the 23
suites now recognised as tests and credited against what they cover.

- match Godot-cased test directories, and `_tests.gd`/`_suite.gd` suffixes
- resolve a bare `class_name` in `resolve_import_spec` via a cached registry
  built from the production file set
- report class names from `parse_test_import_specs`, with comments and strings
  stripped so a class merely described in prose is not counted as covered

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolving class_name references made the graph correct but reported the new
edges as import cycles, including one spanning 86 files. A class_name reference
cannot cycle at load time: Godot resolves the global registry lazily, so two
scripts naming each other — a plug and its port, a cable and its socket — is
ordinary and correct. Only preload and extends can cycle while parsing.

Record class_name edges in `deferred_imports`, which `detect_cycles` already
skips, leaving preload/extends cycles reported as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A registry or catalog commonly holds a script path in a data table and load()s
it later:

    {"id": "atari_2600", "script": "res://Scripts/Objects/system_models/atari_2600_model.gd"}

No preload or extends pattern can see that, so every model registered this way
reported as orphaned even though the registry that names it is the only thing
that ever instantiates it. Comments are stripped first, so a path discussed in
prose is not counted as a reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
awdemos and others added 27 commits September 12, 2026 13:45
Follow-up to upstream PR peteromallet#682 (Razor/Blazor graph linking): views enter
the C# dependency graph as edge sources, and detect_orphaned_files never
filtered graph nodes by extension, so an unreferenced .razor/.cshtml view
over 10 LOC was reported orphaned — a class of finding that did not exist
before peteromallet#682. Apply the extension filter the function already receives, and
strengthen the razor/orphan test to use a non-routable view so it actually
exercises the filter instead of passing via the @page entrypoint path.

Co-authored-by: Daniel Grimes <dan@dbhq.uk>
The JavaScript and TypeScript tree-sitter specs matched only
(import_statement ...), so a CommonJS codebase produced an essentially empty
dependency graph. Every file then reported importer_count == 0, which the
orphaned, coupling, single_use and test_coverage detectors all read.

On a 1372-file Strapi backend (1128 files using require(), 48 using ESM
import) this reported 1090 orphaned files -- including the logger imported by
175 modules and the core chat orchestrator.

Add require('...') and dynamic import('...') patterns to both specs. The
require pattern uses an (#eq? @_require_fn "require") predicate so
notrequire() and obj.require() do not match. Same 1372-file backend now
resolves logger.js to 175 importers and drops the zero-importer set to 36.7%,
the residue being framework convention-loaded files.
ts_build_dep_graph normalized every resolved import with
join(path.resolve(), resolved) whenever it was not absolute. But file_finder
returns paths relative to the current directory, and resolve_js_import derives
its candidate from the source file, so it also returns a cwd-relative path.
Joining that onto the absolute scan root produced a doubled path that never
matched file_set, and every edge was dropped.

Key the lookup on the absolute form of each file_list entry and try the
resolved path as-is before falling back to scan-root-relative, so edges match
whether the resolver returns absolute, cwd-relative or scan-relative paths.
Edges are now stored under the file_list key rather than the normalized
string, keeping imports and importers consistent with the graph keys.

With both fixes, the 1372-file Strapi backend goes from 1090 reported orphans
to 268, and the coupling detector finds 320 single-use candidates where it
previously found 0.
…ash tests on tree-sitter; sort review prompt glob (@elfensky)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…positive fixes, runner fixes, and one orphan-detector fix (@awdemos)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…6 framework scans (@jimmybrancaccio)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unction bodies (@treygoff24)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e() as an import edge (@sandeep-patel-alepo-fifth)

Conflict resolution: graph.py keeps the relative-file-list fix from peteromallet#696
(merged via peteromallet#744), which covers the same bug peteromallet#750 fixed; the require()
query change and both regression test sets are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck 1.6.3

The extractors import QueryCursor, which only exists in tree-sitter 0.25+,
so the old >=0.21 floor installed a combination that crashes. Language
pack 1.6.3 ships without an importable module, so exclude it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
find_dead_log_variables removed any declaration whose name only appeared
in a removed log, including `const result = await saveUser(user)`, and
cut only the first line of multi-line declarations. Restrict the cascade
to complete one-line declarations with literal or identifier initializers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TypeScript unused-imports, unused-vars, unused-params, debug-logs and
empty-if-chain fixers produce broken or behavior-changing edits on
ordinary code (see dev/FORK_ROADMAP.md §2A). Until they are rebuilt on a
real parser:

- FixerConfig gains `unsafe`; unsafe fixers only write with --unsafe
  (dry-run still works).
- Unsafe fixers are left out of lang capabilities, so next/plan/narrative
  never steer agents toward them.
- The unused-imports cascade skips itself when that fixer is unsafe.
- `detect logs --fix` no longer deletes lines itself; it points to
  `autofix debug-logs`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Resolve node_modules/.bin/tsc by walking up (hoisted monorepos), then a
  global tsc. Never `npx tsc`: without a local install it downloads the
  unrelated "tsc" npm package and the scan reported zero unused symbols.
- Pass --noUnusedLocals/--noUnusedParameters on the command line instead
  of writing tsconfig.desloppify.json into the user's project; stdin is
  /dev/null and output is forced non-pretty.
- Parse every unused diagnostic (TS6133/6138/6192/6196/6198/6199/6205),
  not just 6133/6192.
- Missing compiler, wrong package, crashed runs and tsconfig errors now
  record reduced coverage (new record_reduced_coverage helper) instead of
  reading as a clean result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Find the package root (nearest package.json) and a locally installed
  knip by walking up, so the default `--path src` scan runs it. Workspace
  packages run from the monorepo root with --workspace.
- Call the knip binary directly instead of `npx --yes knip`, and drop
  --no-gitignore so ignored build output is not analysed.
- Read `line` from Knip's JSON; `pos` is a character offset and was
  being reported as the line number.
- Resolve reported paths against Knip's cwd, not the scan path.
- Potential is the export population, not the failure count.
- When Knip can't run, record reduced coverage with a remediation hint
  instead of reporting zero dead exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…raph

- Smell detectors kept only the first 50 matches per smell across the
  whole scan, so on large repos most files' smells vanished (zod: 734
  `as any`, 50 reported, all in one benchmark package).
- The TS import graph only contained files that import something, so a
  dead constants or types module could never be reported as orphaned.
  Seed every non-declaration TS file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import graph's tsconfig reader used strict json.loads, followed only
a single relative string `extends`, ignored `references` and resolved
inherited paths against the wrong directory. Any failure fell back to
`@/ -> src/`, so aliased imports produced false orphans and
"delete dead files" advice (16/16 false orphans on vercel/commerce).

- Parse tsconfig as JSONC (comments, trailing commas, BOM).
- Follow `extends` chains: relative paths, package specifiers from
  node_modules, and arrays, resolving paths/baseUrl against the config
  that defined them.
- For solution-style configs, read paths from the projects listed in
  `references` (the Vite template layout).
- Treat `baseUrl` as a fallback root for bare specifiers.
- Prefer the first `paths` target that exists, as tsc does.
- ts_alias_resolver now matches longest prefix first, like resolve_alias.

On vercel/commerce: graph edges 39 -> 121, files with no importers 38 -> 17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rtions

Test-quality analysis only knew Jest/Chai styles, so ky's retry tests
(450 t.is/t.deepEqual assertions) were reported as assertion-free and
became the #1 queue item. Recognise t.<assertion>(...), expect.soft /
expect.poll, expectTypeOf and assertType, and count test.serial /
test.only / backtick names / .each tables as test functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Next.js 16 renamed middleware.ts to proxy.ts; it (and mdx-components,
  forbidden, unauthorized, global-not-found, manifest) was reported as an
  orphan and became the top "delete dead files" action.
- The eval check matched page.$eval(), redis.eval() and JSDoc examples.
  Only bare/global eval and new Function count now, and block-comment
  lines are skipped by the line-level security checks.
- innerHTML = '' (clearing) and innerHTML comparisons are no longer XSS
  findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Match only the JSDoc @deprecated tag, not any "deprecated" text in
  strings, identifiers or comments.
- Record whether a deprecated symbol is exported and whether it is used
  elsewhere in its own file. Exported symbols with no importers inside the
  scan may be a library's public API, so they stay tier 3 with a note
  instead of a tier-1 "safe to delete". Only unexported, unused symbols
  are quick fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_categorize_unused fell back to "imports" for anything it couldn't
classify (parameters, destructured bindings, class members, unreadable
files), labelling them "unused imports" (tier 1) and routing them to
the import-rewriting fixer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These were all scored as production code: tsd/vitest type tests
(*.test-d.ts, test-d/), e2e suites (e2e/, cypress/, playwright/, *.cy.ts),
benchmarks (zod's 734 `as any` hits came from packages/bench), codegen
output (*.gen.ts, *.generated.ts, *_pb.ts, gql/, .d.mts/.d.cts) and
common tool configs (playwright, tsup, rollup, astro, svelte, nuxt,
drizzle, ...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixer output went through read_text()/safe_write_text, which converted
CRLF to LF, dropped nothing but hid a leading BOM from the first-line
import match, replaced symlinks with regular files and left rewritten
files at mode 0600. Decode as UTF-8 explicitly, give transforms
normalized text, then restore CRLF/BOM and write through the symlink
target with the original permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tree-sitter-language-pack 1.x downloads grammars at runtime. Offline or
behind a proxy, get_parser raises, every tree-sitter phase swallows the
error, and the scan read as clean. Record grammar load failures in
_get_parser and, after the detector pipeline runs, turn them into a
"treesitter" reduced-coverage warning naming the grammars and the
download command to fix it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`desloppify move` reported "Done." after breaking TypeScript projects
(roadmap §2 #12). Stopgap until move is rebuilt on the shared resolver:

- Languages that opt in (TypeScript) build the move graph from the
  project root, so importers outside src/ are seen and rewritten.
- If any importer of a moved file gets no rewrite (e.g. a custom path
  alias), the move aborts and lists them; --dry-run warns, --force
  proceeds. Languages whose imports don't depend on file location (C#)
  are unaffected.
- ESM/NodeNext specifiers with runtime extensions ('./x.js') are
  rewritten, keeping the extension.
- Directory moves no longer rewrite relative imports between files that
  move together, which were being re-pointed at the old location.
- Replacements are applied in one pass, so one rewrite can't be
  rewritten again by a later one.

Verified on a fixture (alias, ./x.js, scripts/ importer, sibling
imports): tsc --noEmit passes after a directory move, and an unknown
alias importer aborts the move with no files touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cstarlea
cstarlea merged commit 1030193 into main Oct 6, 2026
@cstarlea
cstarlea deleted the m0/upstream-batch branch October 6, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.