GitNexus/.github/workflows
glier c6b24162d9
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
perf(kotlin): index import resolution instead of scanning per import (#2872)
* perf(kotlin): index import resolution instead of scanning per import

`resolveKotlinImportTarget` walked the entire workspace on every import.
Its four tiers — exact/suffix, directory child, package fan-out and
progressive prefix strip — each ran `for (const raw of allFilePaths)` with a
`replace(/\\/g, '/')` and several string scans per entry, and they are tried
in cascade, so one unresolved import cost two to four full passes.

Across a repository with tens of thousands of Kotlin files that is
O(imports x files): on the order of 10^10 string operations on a single
thread. It does not look like a hot loop from the outside - analyze sits at
exactly 1.00 core with a completely flat heap and emits nothing for hours,
because every allocation is a short-lived string and nothing accumulates to
hint at progress. Small repositories hide it entirely: at a few hundred files
each pass is free.

Three maps, built once per `allFilePaths` Set and memoized on its identity,
make each tier O(1): stem -> path for the exact tier, every component-suffix
of the stem for the suffix tier, and directory -> direct children for both the
fan-out and the first-child fallback. Cost becomes O(files) once plus O(1) per
import. This mirrors the existing Python index (`getPythonFileIndex`), down to
the WeakMap keying and the build counter.

Semantics are unchanged, including the parts the scans expressed only through
iteration order:

  - an exact match anywhere beats a suffix match found earlier, because the
    scan returned on the first exact hit but merely remembered the first
    suffix hit;
  - "first match" stays first in set-iteration order, so both stem maps keep
    the earliest path inserted for a key;
  - a directory-name match still honours the scan's `startsWith`-then-`indexOf`
    rule, which only ever considered the FIRST occurrence of `/dir/`. A path
    like `data/src/main/kotlin/com/example/data/Repo.kt` is therefore still
    NOT a child of `data`. That is arguably wrong, but fixing it here would
    silently move edges in every Kotlin repository; it belongs in its own
    change with its own fixtures.

That claim is gated, not asserted. `bench/kotlin-import-target` fingerprints
every `fromFile | targetRaw -> result` triple over an exhaustive branch matrix
plus a deterministic fuzz, each file set resolved in BOTH iteration orders
because that is the only place the tie-breaks above are expressed. The
committed baseline is the value the PRE-INDEX implementation produces: both
implementations print
5ad605c179081505705ff7698a09dbdbdc4831080af6d9fdec5499cc6bce28ee over the same
20074 cases, 11612 of them non-null, and anyone can re-run it by pointing the
harness's module specifier at the old file.

Its second arm is the scaling ratio, `(t_large/t_small)/(1600/400)` over a
synthetic Kotlin monorepo whose imports are ~40% unresolvable — only a miss
drives all four tiers, which is where the scan was worst. The index measures
0.99 (8.0 ms / 31.7 ms); the implementation it replaces measures 3.737
(2207.8 ms / 33003.5 ms) on that same corpus, so the budget of 1.6 separates
them by a wide margin. Take the absolute times as an order of magnitude only
(~276x, ~1041x): the floor arm was run once cold because best-of-seven against
a quadratic implementation costs minutes, while the index arm is the usual
best-of-seven. The ratios are the comparable pair. Both arms run in the
existing always-on `benchmarks (GITNEXUS_BENCH)` job, next to the C++ guard
from #2788 and the Python one from #1918.

Two unit-level guards sit alongside it: a parity test pinning the curated
cases, and an integration test asserting the index is built once across many
imports — the adapter must pass the Set through, since a defensive copy would
hand a fresh WeakMap key per call and restore the old behaviour (the same trap
Python hit in PR #1918).

Two other providers have the same defect and are left alone here, having no
repository at hand to verify a change against:

  - `go/import-target.ts`: `findRootPackageFiles` and `findAllFilesInPkgDir`
    scan unmemoized, and the GOPATH fallback calls the latter once per path
    segment but the last, so a single import can trigger several full passes;
  - `dart/import-target.ts`: the `package:` branch scans once per candidate
    path — `lib/<rel>` and bare `<rel>` — and `resolveRelative` scans again in
    its suffix fallback, also unmemoized.

`csharp/import-target.ts` is a partial case worth noting: it already builds a
memoized `getWorkspaceFileIndex`, but that is reached only when a `.csproj` is
found; the no-csproj path hands the raw Set to `resolveDirectMatch` and
`resolveByProgressiveStripping`, which scan past it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(kotlin): close the blind axes in the import-resolution gate

Review of #2872 found the weak part was the gate, not the resolver: four
plausible follow-up mutations passed `--check` with a byte-identical
fingerprint, `cases` AND `non_null`. Each is now caught, and each was
re-checked against the mutation it exists to stop.

  - The hashed record carried `order | fromFile | targetRaw | result` but not
    the FILE SET, so a corpus edit that swapped the workspace under a case
    while leaving its result string alone was invisible. Leaving the resolver
    untouched and editing only the corpus, two documented load-bearing cases
    could be gutted — the "exact beats an earlier suffix" case losing its
    competing file, the repeated-directory negative case losing its file
    entirely — with the gate green. The file set is now part of the record, and
    that same edit now moves the fingerprint.
  - The corpus capped path depth at 8 components and packages at 16 files,
    which are precisely the two axes the loops this change added run on. It now
    carries 11- and 13-component paths, queries against suffix keys deeper than
    seven segments, a 40-file package, and a fuzz that spans both. Verified:
    capping suffix-key depth at 7, skipping the `dirChildren` suffix loop above
    depth 8, and capping a bucket at 17 entries each now move the fingerprint,
    where all three previously passed.
  - `non_null` was reported but never asserted; it is asserted beside `cases`.
    That closes only the "resolves nothing at all" hole — it stayed 11612 under
    all three code mutations above and under the corpus edit — so it is a
    companion to the two fixes above, not a substitute for either.
  - A ratio cannot see a constant factor, and a file-count ratio cannot see a
    depth cost. `--check` now also asserts a DEPTH ratio (file count fixed,
    paths 24 components against 8) and an absolute ceiling on the small arm: a
    full workspace scan reintroduced on 1-in-32 imports scores 1.490, inside
    the scaling budget, while running 2.8x slower.

The baseline is re-derived, not adjusted: the pre-index implementation and the
index both print
ebf1790bf1d42dad483a51f2cbdeb2351e493b9e8236e4eedeef592dd81e2c5c over the new
20106-case corpus, 13256 of them non-null.

Both test suites were shown to be non-load-bearing and now are:

  - the parity test's repeated-directory case put `data` at the LEADING
    segment, so the `startsWith` guard fired and the `indexOf` rule its own
    comment describes was never reached — a resolver with that check relaxed to
    `>= 0` passed all 18 cases. A mid-path case now pins it, and a backslash
    fan-out case pins `norm.lastIndexOf` against `raw.lastIndexOf`, which was
    also bench-only. Both mutations now fail the unit suite.
  - the index-reuse test discarded all 200 return values, so a build count of 1
    was equally true of an adapter that had stopped resolving anything. It now
    asserts results, and its docstring premise is corrected: every one of its
    imports hit the tier-1 suffix lookup and none reached the fan-out it
    claimed to exercise. Half now genuinely do. The `undefined as never` casts
    and the `?.` are gone — both trailing parameters are optional and the
    member is required.

Resolver changes, all output-identical against the differential above:

  - `dirChildren` buckets are frozen once built. `findKotlinPackageFiles` hands
    a bucket straight out of the index, and the `readonly string[]` return type
    does not survive the caller: the finalize pass normalizes with
    `Array.isArray(t) ? t : [t]`, and `isArray`'s `arg is any[]` predicate
    widens the true branch, so `tsc --strict` accepts a `.sort()` there. A
    downstream sort would permanently reorder the cached bucket and flip the
    first-child tier for every later import in the run.
  - `stripped` is computed only after tier 1 misses, with `lastIndexOf`/`slice`
    instead of `split`/`slice`/`join`. Measured -20% small arm, -21% large arm.
  - `KOTLIN_EXTENSIONS` now comes from the existing `import-resolvers/jvm.ts`
    export instead of a fourth inlined copy.
  - A note on why the shared `buildSuffixIndex` is not reused, with the four
    probes that diverge, and the measured basename-bucket comparison — the one
    place this was less documented than the Python precedent it follows, and
    the question the Go/Dart/C# follow-ups will each face.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-08 09:31:39 +00:00
..
build-tree-sitter-prebuilds.yml fix(ci): stop CI Report dying silently when the tests job fails (#2728) 2026-08-01 18:31:49 +01:00
ci-devcontainer.yml chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 2026-07-22 20:18:29 +00:00
ci-e2e.yml chore(deps): bump dorny/paths-filter from 4.0.1 to 4.0.2 (#2505) 2026-07-16 07:02:56 +01:00
ci-quality.yml chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 2026-07-22 20:18:29 +00:00
ci-report.yml fix(ci): stop CI Report dying silently when the tests job fails (#2728) 2026-08-01 18:31:49 +01:00
ci-tests.yml perf(kotlin): index import resolution instead of scanning per import (#2872) 2026-08-08 09:31:39 +00:00
ci.yml refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023) 2026-06-04 11:07:37 +01:00
claude.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
codeql.yml chore(deps): bump the codeql-action group across 1 directory with 3 updates (#2755) 2026-07-31 10:34:35 +01:00
commit-fork-prebuilds.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
dependency-review.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
docker.yml chore(deps): bump docker/login-action from 4.4.0 to 4.6.0 (#2851) 2026-08-06 08:19:19 +01:00
gitleaks.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
gitnexus-review-agent.yml fix(ci): stop the placeholder review, verify citations, repair once (#2733) 2026-07-28 18:51:01 +01:00
gitnexus-skill-evolution.yml chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 2026-07-22 20:18:29 +00:00
grammar-update-monitor.yml chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 2026-07-22 20:18:29 +00:00
impact-pdg-mutation-report.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
pr-autofix-apply.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
pr-autofix-publish.yml feat(autofix): replace inline reviewdog with /autofix ChatOps button (#1458) 2026-05-09 16:32:38 +01:00
pr-autofix.yml chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 2026-07-22 20:18:29 +00:00
pr-description-check.yml chore(deps): bump actions/github-script from 7.0.1 to 9.0.0 2026-04-15 20:17:08 +00:00
pr-labeler.yml chore(deps): bump release-drafter/release-drafter from 7.6.0 to 7.7.0 (#2853) 2026-08-06 08:18:01 +01:00
publish.yml Merge branch 'main' into dependabot/github_actions/softprops/action-gh-release-3.0.2 2026-07-23 09:23:26 +01:00
scorecard.yml chore(deps): bump ossf/scorecard-action from 2.4.3 to 2.4.4 (#2852) 2026-08-06 08:19:01 +01:00
skill-sync.yml chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 2026-07-22 20:18:29 +00:00
tree-sitter-upgrade-readiness.yml chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292) 2026-06-25 06:53:47 +01:00
triage-sweep.yml chore(deps): bump actions/setup-python from 6.3.0 to 7.0.0 (#2757) 2026-07-31 13:05:18 +01:00
trivy.yml chore(deps): bump the codeql-action group across 1 directory with 3 updates (#2755) 2026-07-31 10:34:35 +01:00
workflow-lint.yml chore(deps): bump actions/setup-python from 6.3.0 to 7.0.0 (#2757) 2026-07-31 13:05:18 +01:00