mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-16 23:43:12 +00:00
220 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
45f97045e0 |
refactor(eval): simplify sweep internals and needrestart check (#2785)
Reuse the incumbent skill digest instead of walking the tree twice, and keep the needrestart grep a literal match that actionlint accepts. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
b7e621df4c |
fix(ci): gate evolution runs on runner readiness (#2785)
Prevent paid scheduled work until host survival protections and the proven three-worker rollout are explicitly in place. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
999b7bbede |
fix(eval): close skill evolution review gaps (#2785)
Keep promotion decisions monotonic and evidence-bound while preserving paid sweep results, redacting live failures, and hardening prior-run seeding. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
9e905a3606
|
Merge branch 'main' into fix/skill-evolution-gate | ||
|
|
cc066656df
|
chore(deps): bump the codeql-action group with 3 updates (#3151)
Bumps the codeql-action group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.7 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits]( |
||
|
|
c3eb5991c1
|
fix(eval): preserve complete evolution evidence | ||
|
|
aeb853b9cb
|
fix(ci): harden evolution evidence reuse | ||
|
|
d232671278
|
Merge origin/main into fix/skill-evolution-gate | ||
|
|
e1e2464960
|
feat(kotlin): bind Spring config consumers on Kotlin sources (#3126)
* feat(kotlin): bind Spring @Value and ConfigurationProperties consumers Kotlin sources were skipping the Java-only config-binding attach path, so mixed JVM apps under-reported blast radius for Kotlin placeholders. Capture from the live AST, serialize on the existing side channel, and reuse the shared binder. (U1-U4) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(review): require exact Spring annotation FQNs and skip raw-string escapes Reject similarly named third-party imports and leave Kotlin triple-quoted bodies undecoded so capture stays fail-closed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): use only grammar-valid Kotlin string and class-name nodes The coverage shard failed the #1920 literal gate on invented string node types and a Java-style name field. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): fail closed on non-literal prefixes and persist unresolved config markers Reject constant and boolean @ConfigurationProperties arguments, scope nested Value shadows to their owner, and rewrite drifted consumer files when a config key is deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * test(kotlin): add config-consumer capture benchmark and fix JVM feature stamps The Kotlin capture path had no performance or behavior guard: a file-wide lexical shadow regression silently dropped two of every three facts. The new bench arm fingerprints @Value / @ConfigurationProperties facts from an explicit-import control against a wildcard-import corpus whose files each declare a sibling nested `Value` type, so parity between the arms is the regression gate, and CI runs it with scaling + widening budgets. Broadening spring.config-bindings to .kt also means Kotlin-only JVM repos now stamp the feature, which the two exact-map orchestration expectations still denied. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): let an explicit import shadow the Spring wildcard import importedAs accepted a star import of the Spring package even when the same simple name was explicitly bound to another type, so a file importing com.example.Value alongside the Spring annotation package emitted a false @Value consumer fact. Kotlin resolves the explicit import first, so the wildcard branch now only applies when the name is otherwise unbound. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
72ff2b9453
|
feat(jvm): fold Java static wildcards and Kotlin star imports for route constants (#3110)
* feat(java): expand wildcard static imports + POSIX-space import resolution - `import static a.b.C.*` records the class FQN at extract time (ModuleConstants.wildcardImports) and expandJavaWildcardStaticImports materializes the bare-name bindings once the repo constants map exists, mirroring explicit single-member static imports. Wired in both the group-side prepareRepo fold and the ingestion-side parse-impl pass so the two query surfaces cannot diverge. Without this, wildcard-imported route constants (~693 routes on our monorepo) silently failed to fold and their provider contracts were dropped. - resolveJavaImport compares in POSIX space (backslash-normalized repo keys) — on Windows the '/'-joined class file never matched a backslash-keyed repo (observed: 675 calls, zero hits). - Wildcard bindings never overwrite single imports (a member shadowing its own wildcard is honored); unresolved wildcards degrade to the existing skip floor. Rebased onto current main: the parse-worker gate this originally carried is superseded by the provider moduleConstantHeuristic architecture; only the resolver-side wildcard expansion and POSIX tolerance remain. * chore(cache): claim SCHEMA_BUMP 83 (82 taken upstream by Spring lookup facts) * style: prettier * fix(java): make wildcard static imports actually resolve route constants extractJavaModuleConstants never populated wildcardImports, so `import static a.b.C.*;` was inert: the asterisk is a sibling of scoped_identifier in tree-sitter-java, not a path segment. Record the class FQN there and stop binding the class simple name as a field. Wildcard-only files also fell through every harvest gate (the java provider heuristic regex, the parse worker emit check, and the group prepareRepo filter), so the constants never reached either layer. Expansion now resolves targets against constant-defining files only, keeping ingestion and group in parity (#2980 R4). Adds unit coverage for extraction, expansion/shadowing, the unresolved skip floor, the harvest heuristic, Windows path keys, and group <-> ingestion parity. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(kotlin): fold package-star imports for route constants Kotlin `import pkg.*` now records star scopes and resolves unique top-level names after local and explicit imports, matching the Java wildcard path without accepting invalid object-star imports. Reuse a Java constant-file suffix index across expansion so wildcard materialization stays linear as both constants and importers scale. Add named-vs-star benches and CI --check gates. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(jvm): fold wildcard imports without shadowing locals or skipping same-package names Keep Java unfoldable declarations from being resurrected by static wildcards, bind only the target type's members, and prefer Kotlin same-package names over package-star imports. Move repo-wide preparation behind a language-provider hook so the shared parse phase stays language-agnostic. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3110) - Stop treating a Java static wildcard as a type import for qualified refs. - Harvest only class and static (including on-demand) imports, not import pkg.*. - Let harvested Kotlin top-level names shadow same-package star imports. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3110) - Fold Kotlin classifier-star imports (`import Type.*`) the same way package stars already fold. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3110) - Rebuild the Kotlin constant index when overlay replaces a contributing file. - Document the wildcard shape in the Java pipeline e2e fixture. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: l.cx <l.cx@winning.com.cn> Co-authored-by: ChunxueLi <mecoloud@users.noreply.gitee.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
678a0e11c9
|
fix(server,web): honor the grep tool contract — real regex, fileFilter, caseSensitive (#3109)
* fix(server,web): honor grep tool contract — real regex, fileFilter, caseSensitive (Patch 12)
Background
==========
The web chat's grep tool schema has always promised regex search with an
optional path-substring fileFilter and caseSensitive control, but the
GET /api/grep handler escapeRegExp()'d every pattern into a literal
substring (a ReDoS hardening from
|
||
|
|
4aa6bddd0a
|
feat(jvm): synthesize Lombok and Kotlin JVM accessor methods (#2885)
* feat(java): synthesize Lombok @Data/@Getter/@Setter accessor methods * fix(lombok): resolve class identity by AST node id, not simple name Root-cause fix for the bot review's name-ambiguity findings: 1. Cross-file collision: the owner map was rebuilt per file from result.symbols, which accumulates across the whole language group — a later Java file with the same simple class name resolved to the earlier file's class node. The map is now filled INSIDE the capture loop (per-file scope) and keyed by the class_declaration AST node id (SyntaxNode.id), which is unique by construction. 2. Same-tail nested classes (Outer.A vs Other.A): a name-keyed map overwrote one with the other; AST-node-id keys cannot collide. 3. Synthesized method ids now follow the SAME convention real nested member ids use (keyed by the class's own simple name, matching findEnclosingClassInfo().className), so call resolution can hit synthesized accessors exactly like hand-written ones. 4. Lombok semantics: setters are no longer generated for final fields (Lombok never emits those) and @Setter(AccessLevel.NONE) now suppresses setters, symmetric to the existing getter suppression. Also tightens two vacuous test loops flagged by the bot (empty-array for..of passed trivially): counts are asserted before property loops, and a new regression test pins distinct owners for same-tailed nested classes plus the real id convention for nested accessors. * feat(java): synthesize Lombok accessors via provider hook and scope dual-path Replace the worker language===Java branch with LanguageProvider.synthesizeStructureMembers, align MethodRegistry ownership through scope captures, and bump parse-cache schema to 83 so warm caches cannot replay pre-synthesis worker output. Co-authored-by: Cursor <cursoragent@cursor.com> * test(java): cover Lombok synthesis semantics, cache replay, and CI bench Add unit/integration matrices (including durable cold/warm/historical parse-cache), a permanent no-Lombok vs Lombok-heavy harness with fingerprint budgets, and a CI --check step. Document that Kotlin→Java member CALLS remains a pre-existing gap. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(lombok): drop dead state and redundant scans from accessor synthesis Collapse Lombok import provenance into one compilation-unit scan with a cached wildcard flag, remove unused planned-accessor fields and the duplicate @Data enable flag, and plan scope captures without wrapping a fake Parser.Tree. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#2885) Give each Lombok accessor a unique scope range so multi-declarator fields do not share @scope.function IDs, and type the owner map as ReadonlyMap to match the provider hook. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(bench): pin the real Lombok synthesis fingerprint (#2885) The committed baseline held a fingerprint no revision of this branch ever produced, so the CI guard failed on every push. Re-pin it to the value the synthesizer deterministically emits and correct the method count the comment claims (800 x 4 x 2 = 6400, not 12800). Co-authored-by: Cursor <cursoragent@cursor.com> * feat(kotlin): synthesize JVM accessors using shared beanspec helpers (#2885) Kotlin val/var properties now emit the same JavaBeans get/set Methods as Lombok, via jvm/beanspec + jvm/synthetic-accessors. SCHEMA_BUMP 84 invalidates warm caches that would omit those callables. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): match kotlinc JVM accessor ABI (#2885) Emit custom getters, preserve is-prefix names, and convert synthetic graph lines to 0-based so same-name accessors resolve to the owner. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#2885) Restrict Lombok provenance to lombok/experimental FQNs and match Kotlin existing methods by exact JVM name. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(jvm): consolidate accessor synthesis (#2885) Keep language-specific discovery in Java and Kotlin adapters while centralizing owner orchestration, collision policy, graph emission, and captures. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(jvm): align accessor synthesis with compiler ABI (#2885) Match Lombok and kotlinc provenance, companion owners, and collision arity so mixed-JVM CALLS bind to the Methods compilers actually emit. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#2885) Mark Kotlin interface accessors abstract, pin the Lombok case-fold collision test, and document the non-lowercase is-prefix rule. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#2885) Honor explicit Getter/Setter over @Data regardless of order, and let field @Accessors replace class-level fluent/chain. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): pin Kotlin scope-capture fingerprint after interface accessors (#2885) Invalidate warm parse cache so interface property Methods are not replayed as concrete. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ChunxueLi <mecoloud@users.noreply.gitee.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
19f6731c34
|
feat(java): resolve SpringContextUtil.getBeans(X.class) dynamic lookups (#2886)
* feat(java): resolve SpringContextUtil.getBeans(X.class) dynamic lookups * fix(ingestion): make Spring dynamic lookups graph-correct Capture Java and Kotlin lookups from ASTs and resolve them through scoped type bindings and transitive JVM assignability so emitted INJECTS edges are attributable, cache-safe, and production-tested. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(ingestion): keep Spring lookup capture linear Reuse Java and Kotlin scope-query call nodes instead of rewalking each AST, cache DI subtype closures, and enforce linear scaling with production-path benchmarks in CI. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
43a842724d
|
fix: bind Razor ViewComponent names to in-repo classes (#3104)
* fix: bind Razor ViewComponent names to in-repo classes
Index Component.InvokeAsync("Name") and in-repo ViewComponent("Name")
as CALLS to workspace ViewComponent classes so impact sees real callers
instead of an empty graph. SDK types stay unresolved.
Fixes #2991
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix: scan Razor and C# ViewComponent names without regex holes
Use string-aware lexers so combined Name= aliases, code-block calls,
this/base helpers, and escaped @@ markup match ASP.NET instead of
emitting false or missing CALLS.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* perf: skip Razor scans without ViewComponent tokens
Preserve the lexer correctness fixes while avoiding per-character work for
the common view that cannot contain a supported invocation.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: gate Razor ViewComponent extractor scaling in CI
Wire mixed-corpus tripwire + GITNEXUS_BENCH loader/scaling checks into the dedicated ci-tests benchmarks job so the #2991 lexer cannot regress without a wall-clock gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: read Razor views through one file handle
CodeQL js/file-system-race: the size gate stat'd the path and the read
re-resolved it, so a template swapped in between could be read past the
size ceiling. Both now go through the same handle.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||
|
|
31c9d9223e
|
chore(deps): bump the codeql-action group with 3 updates (#3056)
Bumps the codeql-action group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.6 to 4.37.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits]( |
||
|
|
414687ad10
|
chore(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#3057)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](
|
||
|
|
c056d136ad
|
chore(deps): bump actions/attest-build-provenance from 4.1.1 to 4.2.2 (#3005)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 4.1.1 to 4.2.2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](
|
||
|
|
b77d6f662b
|
fix(kotlin): resolve imports from declared packages (#2990)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
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
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Has been cancelled
|
||
|
|
7f0ab16ffe
|
feat(routes): support JS data route tables (#2972)
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 / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
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
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
|
||
|
|
02008e0288
|
chore(deps): bump the codeql-action group with 3 updates (#2947)
Bumps the codeql-action group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.3 to 4.37.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits]( |
||
|
|
8c2452a4e8
|
chore(deps): bump dorny/paths-filter from 4.0.2 to 4.0.3 (#2948)
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](
|
||
|
|
18bc51dfd2
|
perf(import-resolvers): index every scanning resolver, consolidate the memo, gate every registered language (#2911)
* perf(import-resolvers): build buildSuffixIndex's dirMap lazily (#2903) `buildSuffixIndex` eagerly built three maps. `dirMap` is the array-valued one — one entry per directory suffix per file, so O(files x depth) in entries and array churn — and only four call sites ever read it, all via `getFilesInDir`: `import-resolvers/{php,csharp,jvm}.ts` and `import-resolvers/configs/python.ts`. Ruby (through workspace-file-index), the TypeScript scope resolver, Vue's import-target and the include-extractor never ask a directory question, and built it anyway. Since #2880 these indexes are retained for a whole resolution pass rather than rebuilt per import, so that waste is now resident memory. Deferring it to the first `getFilesInDir` call is behaviour-identical — same key, same descending-suffix order, same per-bucket push order, same `substring(lastIndexOf('.'))` extension clamp. The builder assigns the MAP on completion, so a repeated miss cannot rebuild it. Measured on `buildSuffixIndex` alone, 32k paths, index built and `getFilesInDir` never called: C# layout, 13 segments 79,018,680 -> 66,580,488 B -15.74% Ruby layout, 11 segments 60,752,792 -> 48,656,856 B -19.91% and on the whole retained WorkspaceFileIndex the bench measures: csharp 32k 73.62 -> 61.76 MiB ruby 32k 55.26 -> 43.69 MiB When `getFilesInDir` IS called the footprint is unchanged, so the deferral is never a loss. No new retention: all five construction sites already hold both input arrays alive beside the index. The laziness is pinned structurally rather than by timing. The test's corpus is a `string[]` whose elements are accessor properties, so an indexed read is observable and the read count IS the pass count: 14 after construction, still 14 after any number of get/getInsensitive, 28 after the first `getFilesInDir`, 28 after five more. Memoizing the decision instead of the map would read 42. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * perf(php): resolve imports from a per-run index, not a scan per import (#2901) PHP was the last language whose import resolution scanned the workspace per import. Both `resolvePhpImportTarget` and `resolvePhpImportTargetInternal` materialized two full arrays from the Set on every call, then passed `undefined` as the `index` argument — so `resolvePhpImportInternal` fell through to `suffixResolve`'s linear `findIndex`, once per extension per path part. Measured at 20,000 files: 96.40 ms per import. **Handing it the shared SuffixIndex would have moved IMPORTS edges.** All three index-fed sites answer a different question than the scan they short-circuit, each found by differential with a concrete witness: 1. `getInsensitive` — the scan leg is `allFiles.has(path)`, exact whole-path with no case-insensitive counterpart; the shared index answers a ci SUFFIX probe. 2. `getFilesInDir` — the scan is root-anchored `startsWith(nsDir + '/')`; `dirMap` is keyed on every directory SUFFIX, so a vendor copy can win. 3. `suffixResolve` — the scan's `endsWith('/' + S)` matches only a PROPER suffix; `buildSuffixIndex` indexes j=0, so a root-level `Foo.php` starts resolving `use Foo` where it returned null. 3b. the scan's `endsWith(p) || lower.endsWith(lower(p))` has a second disjunct that subsumes the first, so it is purely first-in-Set-order and case-insensitive; `get(S) || getInsensitive(S)` lets a case-exact hit anywhere beat an earlier ci hit. So this is not Ruby's #2880 shape. Both sites take `getWorkspaceFileIndex` for the memoized arrays and hand the internal resolver a PARITY `SuffixIndex` memoized on the same Set identity: `getInsensitive` disabled, `get` implementing the scan's real rule via the shared ci lookup plus one O(files) whole-path correction map, `getFilesInDir` root-anchored in Set order. no composer.json 96.40 -> 0.036 ms/import steady state with composer.json 100.19 -> 0.068 ms/import steady state Also closes PHP's last per-import traversal, in `import-resolvers/php.ts`: its namespace-directory scan ran whenever `getFilesInDir` came back EMPTY, not merely when no index was supplied — despite the comment above it claiming "only when SuffixIndex unavailable". An empty bucket is already the answer, so the scan could only confirm it, at one full pass per import whose namespace matches a PSR-4 prefix but whose directory has no direct `.php` child (measured 11 traversals for 10 imports; now 1). Moving it into the `else` is safe because the bucket is a SUPERSET of what the scan finds — a root-anchored direct child `nsDir/<x>.php` has its directory exactly equal to `nsDir`, and a directory is always one of its own suffixes, so both index shapes contain it. Nine mutations of the new code are caught, including M1 "pass the raw shared index" (the naive fix) at 23 arms. The adapter guard reads 600 instead of 1 under a defensive `new Set(allFilePaths)` — the #1918 P1 hazard the unit differential is structurally blind to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * perf(java): index import resolution instead of scanning per import (#2908) Java scanned the whole workspace twice per import: once for the three-tier direct match, and again INSIDE the progressive prefix-stripping loop — so a single unresolvable import cost one full pass per stripped segment. No WeakMap, no index, and it is registered in `SCOPE_RESOLVERS`, so it ran in production. This is byte-for-byte the C# shape #2878 fixed, so Java now reads the same machinery: `getWorkspaceFileIndex` for `normToRaw` + the segment-suffix index, and a Java-owned `PackageDirIndex` WeakMap over `buildPackageDirIndex(_, n => n.endsWith('.java'))` read through `firstFileDirectlyInPkgDir`. Structure mirrors C#'s `narrowContext` / `resolveDirectMatch` / `resolveByProgressiveStripping`. 20k files, 256 imports, 7-in-8 unresolvable: 8.05 -> 0.62 ms/import steady state once the index is built: 0.0036 ms/import Tie-breaks preserved, and Java's are NOT identical to C#'s: - tier 1 `break`s on the exact match, so an exact whole-path hit wins even when a suffix or directory-child hit came earlier in iteration order — hence `normToRaw.get` before `index.get`, which conflates them; - the stripping loop instead returns at the FIRST hit of `f === tailFile || f.endsWith('/' + tailFile)` and only yields its directory child after the scan completes, so the conflated `index.get` is the correct lookup THERE. Applying tier 1's exact-wins rule inside the loop is a real behaviour change (mutation M6); - `.*` wildcard stripping stays ahead of everything; - `firstFileDirectlyInPkgDir` reproduces Java's at-root/at-nested predicate exactly, including the first-`indexOf` rule — proved algebraically rather than assumed: the `atRoot` branch matches iff `dir === pathLike`, which is `D.indexOf(P) === 0 === D.length - P.length`, and the `atNested` branch's first occurrence in `f` is the first occurrence in `D` shifted by one. Six mutations are caught; a seventh (swapping the two index builds) is a true equivalence and is recorded as such. Hand-derivation also corrected four cases where the legacy code resolves and I had predicted null — including `java.util.List` reaching a local `util/List.java`, because Java has no in-repo-namespace gate like C#'s #1881. That is preserved here and filed separately as #2910; the parity test pins it so the fix is visible. The adapter guard reads 800 instead of 2 under a defensive `new Set(allFilePaths)`. Two traversals is correct: the workspace index and the package-dir index are separate WeakMaps and each iterates the Set once, the same accounting as C#. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * perf(cobol): index COPY resolution instead of two scans per statement (#2908) `cobolScopeResolver.resolveImportTarget` ran two full workspace scans per `COPY`, each calling `path.extname` + `path.basename` + `.toUpperCase()` on every entry: tier 1 over `.cpy`/`.copybook`, tier 2 over `.cbl`/`.cob`/ `.cobol`. No WeakMap, no index, and registered in `SCOPE_RESOLVERS`. Two uppercased-basename maps, one per tier, filled in a SINGLE pass over the Set and memoized on Set identity. Lookup is `copybooks.get(upper) ?? sources.get(upper) ?? null`. 20k files, 500 COPY operands: 3879-4082 -> 10.5-11.7 us/import (~350-369x) steady state once built: 0.253 us/import Tie-breaks preserved: - TIER ORDER. A `.cpy` match beats a `.cbl` match even when the source file appears EARLIER in Set-iteration order. This is the one a naive single-map rewrite silently breaks, so it gets its own fixture. - Within a tier, first in Set-iteration order wins (`if (!tier.has(...))`, mirroring the scans' first-match return). - The key is built with the identical call sequence, `basename(fp, extname(fp).toLowerCase()).toUpperCase()`, so `Foo.CPY` still keys under `FOO.CPY` rather than `FOO`. - `path` stays in the loop rather than hand-rolled `/`-slicing, so backslash handling is unchanged on every platform — pinned by a `dir\sub\BOOK.cpy` case. All six mutations are caught: collapsing the tiers, within-tier last-wins, dropping the target uppercase, dropping the extension lowercase, hand-rolled slicing, and the adapter's defensive copy. The first five are caught by the differential and are invisible to the adapter guard; the sixth is the reverse, which is the layering working as intended — the guard reads 600 instead of 1. `COBOL_SOURCE_EXTENSIONS` was being re-allocated on every call; hoisted to module scope beside `COPYBOOK_EXTENSIONS`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * perf(csharp): index the csproj leg's namespace-directory scan (#2902) #2878 moved C#'s no-csproj leg onto memoized indexes; the csproj leg kept a per-import full scan in `resolveCSharpImportInternal` step 3, measured at ~1.10 ms per import at 50,000 `.cs` files. **The fix the issue proposed would have moved edges.** It suggested skipping the fallback when an exhaustive index is available, on the assumption that step 2's `getFilesInDir` answers the same question. It does not: step 2's `dirMap` is keyed on segment-aligned directory suffixes, while step 3's `normalized.indexOf(dirPrefix + '/')` is an UNANCHORED substring match, so step 3 finds a strict superset — and it runs only when step 2 came back empty, so those extra hits are observable, not shadowed: dirPrefix 'ubModels' step 2 [] step 3 ['src/SubModels/Widget.cs'] dirPrefix 'rc/Models' step 2 [] step 3 src/Models/* AND vendor/mysrc/Models/* So the predicate is kept byte-for-byte and made fast instead. It depends only on the file's directory (the needle ends with `/`, so every occurrence lies wholly inside `D + '/'`), which reduces to the `package-dir-index` formula minus the anchoring leading slash. `PackageDirIndex` itself cannot be reused for the same reason — its matcher is anchored. The index is memoized on the `normalizedFileList` array identity and built lazily at the point step 3 is first reached, so BCL usings — which `continue` out at the root-namespace gate — never pay for it. Candidates come from an exact last-segment bucket when `dirPrefix` contains a slash, a last-segment key sweep when it does not, and `singleSegmentDirs` when it is empty. Positions rather than paths, merged and sorted when several directories match, so file-list order survives. App.Missing @ {App, src} 1103.0 -> 7.6 us (145x, and flat in file count: 7.3 @10k, 7.6 @50k, 8.4 @200k) App.Missing @ {App, ''} 626.7 -> 108.5 us App @ {App, ''} 1077.9 -> 2.0 us (539x) App.Ns8 @ {App, src} 0.6 -> 0.6 us (step-2 hit, untouched) `relative === ''` is preserved exactly, including the no-`projectDir` case where the needle is a bare `/` and the answer is "every `.cs` whose directory has no slash of its own" — `getFilesInDir('', '.cs')` cannot answer that over repo-relative paths, so it has its own arm. 13 of 14 mutations are caught, including M1, the naive skip-when-indexed cleanup, at 9 arms. The survivor drops the empty-prefix fast path and is a true equivalence. M9 initially survived and exposed a real corpus gap — no non-`.cs` file lived inside a directory — now covered. The remaining non-constant term is the slash-free sweep, O(distinct last segments): 456 us at 200k files on a unique-name layout, but 7.9 us on a `SrcN/Models` layout, which is how C# repos are actually laid out. Closing the unique-name case needs a character-suffix map over segments — the O(files x depth) memory shape `package-dir-index.ts` cites #2649 to avoid — so it is documented in the code as a design change rather than tuned here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * test(scope-resolution): assert index reuse for every registered language (#2909) Index reuse was asserted by nine hand-written per-language files, so the guarantee existed exactly for the languages someone remembered — and #2908 is the proof that is not good enough: Java and COBOL were registered, quadratic and unguarded until this branch. `resolveImportTarget` is a required member of `ScopeResolver` with one signature and 16 registrations, so "calling it N times against a stable `allFilePaths` must not traverse the set N times" is a property of the CONTRACT. `import-target-index-reuse.contract.test.ts` drives every entry of `SCOPE_RESOLVERS`, modelled on `construction-syntax-wiring.test.ts` — the established shape here for a property plus a justified inventory. Measured counts, all memoized: c 1 cobol 1 cpp 1 csharp 2 dart 1 go 1 java 2 javascript 2 kotlin 1 php 1 python 1 ruby 1 rust 0 swift 1 typescript 2 vue 2 **`KNOWN_UNINDEXED` is empty.** The audit that produced it also cleared C, C++, Rust, Swift, TypeScript, Vue and JavaScript by hand — Rust's memo lives in `qualified-call.ts::moduleIndexFor`, C's and Swift's loops are inside their WeakMap builders. The empty map stays as a mechanism: a 17th language cannot opt out silently, and the inventory arm fails when a registered resolver has no fixture. Two things the assertion had to get right: - it is `scans(200) === scans(2)`, not `scans === 1`. Per-language counts legitimately differ (C# and Java build two indexes), and comparing two counts needs no per-language expected value. - Rust legitimately scans ZERO times — it answers every leg with `allFilePaths.has(candidate)` probes — so the floor is a per-language `minimumScans`, 1 for fifteen languages and 0 for Rust with the reason on the interface. Paired with a `hitTarget` that must resolve non-null, so the property cannot pass vacuously on a resolver that stopped answering. Miss targets are distinct per import, which defeats the TS/JS/Vue per-target `resolveCache`. Also unifies the instrument. Kotlin and Python counted index BUILDS from production; the other seven count traversals of a `CountingSet`. The build counter is strictly weaker — a scan added BESIDE a reused index moves no build count, which is exactly the mutation `baselines.json` `_blind_spot` records as invisible to every timing arm — and it costs two production modules that ship in the bundle purely for tests, holding module-global state every test must `reset()`. Both guards migrate to `CountingSet`, and `languages/{kotlin,python}/index-stats.ts` plus both call sites are gone, for -59 lines of shipped source. (Mechanical note: the two `index-stats.ts` file deletions appear in the #2901 commit rather than this one. They were staged with `git rm` while a concurrent commit swept the index. The final tree is correct; only that attribution is off, and rewriting a sibling commit to move them was not worth the risk.) Coverage went up in the swap: Kotlin's old "rebuilds when the file set is a different object" arm (3 sets, 3 builds) would have PASSED under a defensive adapter copy. Its replacement fails, as do all six arms across the two files. Verified by mutation: `new Set(allFilePaths)` inserted into the kotlin, python and go adapters fails exactly those three and no others — `python: 200 imports cost 201 traversals, 2 cost 3`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * test(import-target): gate the four newly-indexed resolvers, retighten heap The bench covered go/csharp/dart/ruby/kotlin. The four resolvers indexed on this branch shipped unmeasured, and #2903's memory win was not locked in. **php, java and cobol join the shared corpus**, each with the two load-bearing properties the header requires: imports scale with file count, and most imports MISS so the full cascade runs (resolve rates php 36.0%, java 34.4%, cobol 36.0%). Java's miss families were measured rather than assumed, since it has no in-repo-namespace gate (#2910): `java.*` 1041 imports and `com.google.*` 1006, both resolving 0. COBOL's collide layout repeats a bookname across BOTH extension tiers, so it reaches the copybook-over-source tie-break rather than only the basename map. **`csharp_csproj` is a sixth LANGS entry**, not a new arm dimension — an entry needs five small additions and inherits all five arms and all seven gates, where a context axis would have to be threaded through `buildRepo`, `resolveAll`, `identityPass`, the report shape and every gate. `buildFiles` aliases it to `csharp`, so the two share one corpus by construction and cannot drift. Two configs (`{App, 'src'}`, `{Lib, ''}`) produce all three `dirPrefix` shapes — slashed, slash-free and empty — in five arms instead of ten: App.Ns{d} 30.6% src/Ns{d} step 2 hit App.Missing{n} 25.5% src/Missing{n} step 3, last-segment bucket Lib 14.0% (empty) step 3, singleSegmentDirs Lib.Missing{n} 12.0% Missing{n} step 3, KEY SWEEP — the one non-constant path BCL / Ghost 12.4% — root-namespace-gate control **2221 of 3200 imports reach the indexed leg**, only 12.4% `continue` out. What that arm pins is stated plainly rather than overclaimed: step 3 answers null for all 2221 here (the hits land at step 2), so it gates that leg's COST and its null answers; its positive tie-breaks stay pinned by the unit parity test. **Heap ceilings retightened.** #2903 dropped the measured figures, leaving the 1.5x ceilings at ~1.9x — a straight revert to the old size would have passed: csharp 116,000,000 -> 98,000,000 B (measured 61.76 MiB) ruby 87,000,000 -> 69,000,000 B (measured 43.69 MiB) php new 106,000,000 B (measured 67.29 MiB) java new 154,000,000 B (measured 97.32 MiB, the largest in the file — Maven layout is 18 segments) php and java are gated because both retained NOTHING across imports at BASE and now retain the O(files x depth) suffix index — the same argument that gates C#. cobol is not: two `Map<basename, path>`, O(files) with no depth term, and its retained delta does not clear measurement noise, so a ceiling would gate nothing. `csharp_csproj` is not: same corpus, same index, a duplicate number — its one distinguishing footprint, the lazily-built `dirMap` its `getFilesInDir` forces back, is measured at +20.8% and recorded as a residual instead, because gating it would licence eager-dirMap everywhere. csharp's `depth_ratio` also fell 3.318 -> 2.31 (the no-csproj leg never asks a directory question, so the deep arm stopped paying an eager dirMap build). Budget 5 -> 3.5, restoring the file's 1.5x convention — and `_arms_note` says plainly that 3.5 does NOT lock that win in, because locking it needs ~2.9, which is 1.25x over a 1.05x spread and the kind of tightening `_triage` warns buys flake rather than signal. All five pre-existing languages are byte-identical: 25 cells x 5 fields = 125 values, 0 mismatches. The new arms were proven live by a doctored baseline (cobol ceiling 0.01, php heap 1000 B, java resolved 999) producing three correctly-worded failures and exit 1. Wall-clock 10.9 -> 26.1 s, php and csharp_csproj ~11 s of it — both cascades end in `suffixResolve`'s ~50-extension probe, and both gate the two largest wins on this branch, so neither is a candidate to drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * perf(javascript): build the suffix index JS resolution never had JavaScript's `PassCache` was TypeScript's minus one field: `index`. So JS called the shared `resolveTsTarget` with `ctx.index === undefined`, and `import-resolvers/standard.ts` fell through to `suffixResolve`'s linear `findIndex` — scanning the materialized path list once per extension (~39) per path part, per import. 2000 files 6448.9 -> 28.5 us/import (TypeScript: 25.0) 8000 files 25972.6 -> 27.4 us/import (TypeScript: 27.0) Per-import scaling over 4x the files: 4.12x -> 1.09x. **Every instrument on this branch was blind to it.** `CountingSet` counts traversals of the Set; this walked the array the adapter had already materialized — the blind spot `counting-file-set.ts` documents in its own header and `baselines.json` records under `_blind_spot`. Under mutation M1, which drops `index` and reproduces the shipped defect exactly, the sixteen- language contract test stays GREEN for javascript, because the pass cache is still reused and `files.scans` reads 2 either way. Two new arms do catch it: a `suffixResolve` linear-branch counter that runs the legacy adapter first as its control (135 entries legacy, 0 now), and a mock-free behavioural assertion that a repo-root module resolves by bare specifier. Adding an index moves output, exactly as it did for PHP in #2901, so it was characterized rather than assumed — 211,200 pairs (400 corpora x 3 importers x 176 targets) plus 184 hand cases. **Two classes move and there is no third:** A null -> repo-root file (108) `require('config')` with root `config.js`. The scan tests `endsWith('/' + suffix)`, so a path with no slash has no proper suffix and was unreachable through that leg — while `./config` from the root already resolved via the exact `Set.has` branch. JS was internally inconsistent. B file -> different file (5679) `import 'app/main'` was resolving to `node_modules/dep0/lib/main.js`; the scan skipped the whole-path candidate at the 2-segment suffix and fell through to the 1-segment `/main.js`, taking the first such file in Set order. C hit -> null ZERO, and impossible: proper-suffix keys are a subset of the index's keys. Both moved classes are JS being wrong. **JS-new agrees with TypeScript on all 211,200 pairs and every corpus case, 0 disagreements** — which is the intended design, since JS delegates to the TS resolver and differed only by this field. Also swaps the single-slot `let cached: PassCache | null` in JS, TS and Vue for a module-level `WeakMap`, matching every other language. Two alternating file sets rebuilt everything on every call: 12.0 -> 1438.2 ms at 4000 files x 400 imports (120x); after, 11.0 -> 15.7 ms. This is LATENT, not live — `pipeline/run.ts:673` builds one Set per provider pass and the three are separate providers — but it is why these were the only languages that could not carry the standard distinct-set guard. They can now: the arm fails on HEAD for all three (`expected 42 to be 2`) and passes after. Six mutations caught, including a global `resolveCache` (M5), which needed a new arm — `expectDistinctFileSetsGetOwnIndex` builds two IDENTICAL corpora, so a stale answer carried between them is also the right answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * refactor(ingestion): one per-file-set memo primitive, twenty-one call sites Every language that indexes its import resolution hand-rolled the same memo: declare a module-level `WeakMap` keyed on the file-set object, `get`, `if undefined` build and `set`, return. One concept, written twenty-one times, and this branch had just added five more. `import-resolvers/per-file-set.ts` exports it once: perFileSet<K extends object, T extends object>(build: (key: K) => T): (key: K) => T Two decisions, both recorded in the file. `T extends object` rather than `has`-then-`get`: `WeakMap.get` returning `undefined` cannot distinguish "not built" from "built as undefined", and the `has` form needs a cast or a non-null assertion, both banned here — the constraint makes the ambiguous case unrepresentable instead, and a future caller wanting `string | null` gets a compile error pointing at the decision. A throwing build stores nothing and runs again next call, so failures are not memoized and a half-filled index is never published — inert for these pure builders, and the safer direction. `K extends object` rather than `ReadonlySet<string>` is what lets C#'s `readonly string[]`-keyed cache share the helper. Twenty-one sites migrated across `import-resolvers/` and fifteen languages. Every existing doc comment was re-homed onto the new call rather than deleted — several record real invariants (the Set-identity contract, the #1918 pass-through rule, why Rust's memo lives on a different hook). TypeScript, JavaScript and Vue additionally had byte-identical `PassCache` interfaces and builders. `import-resolvers/pass-cache.ts` now holds the one builder, taking a single argument — every difference the three have lives in the CONSUMER (`tsconfigPaths`, the extension list), not the builder. The builder is shared, the memo deliberately is not: each adapter keeps its own `perFileSet`, hence its own index and its own `resolveCache`, because the three disagree about what a specifier resolves to and one shared cache would hand a language another language's answers. It buys no runtime reuse and the module says so — each provider pass builds its own `allFilePaths` Set, so the three are always different keys. C and C++'s `augmentedFilePaths` was a two-LEVEL memo, and needed no new abstraction: the outer memo's value is a function and a function is an object, so `perFileSet(perFileSet(...))` composes. The two instances stay one per file, and the reason is now in BOTH doc comments rather than only C++'s — cpp delegates to `resolveCImportTarget`, whose `suffixIndex` is keyed on the augmented set, so a shared memo would cross the two languages' indexes. Two sites are deliberately NOT migrated, each with the reason written at the declaration so the next sweep does not re-litigate them: - `configs/swift.ts` is a two-input memo keyed on one. `targets` is not derivable from the key; re-keying on `ctx` would force a banned non-null assertion or an unreachable fallback inside a memo builder. - `rust/qualified-call.ts` `MODULE_SCOPE_CACHE` is three inputs keyed on one, and sits ten lines below a `perFileSet` in the same file — the likeliest thing to be "fixed" by mistake. The other ten remaining `WeakMap`s are different concerns and stay: AST-node caches, worker-pool runtime state, graph metadata, mutable lazily-filled accumulators, and the C++ ADL / inline-namespace indexes, which are reassigned by explicit clear functions and epoch-stamped on read — validity rules beyond key identity that a closure over a private cache cannot express. Net −20 lines of code, +22 of the two "why not" notes. The primitive's own doc is where the cost sits: the Set-identity contract and the two design decisions are written once instead of being twenty-one implicit facts. Pure refactor: 1764 unit tests, 42 guard tests, all sixteen contract-test traversal counts unchanged (c 1, cobol 1, cpp 1, csharp 2, dart 1, go 1, java 2, javascript 2, kotlin 1, php 1, python 1, ruby 1, rust 0, swift 1, typescript 2, vue 2), 647 C/C++ tests, and every bench fingerprint unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B * test(import-target): gate every registered language, not nine of sixteen The bench pinned output fingerprints and scaling for 9 of the 16 languages in `SCOPE_RESOLVERS`. The other seven — c, cpp, javascript, python, rust, swift, typescript, vue — resolve imports in production with nothing pinning their output or their cost. JavaScript was the sharpest case: the 25,972 us/import defect fixed earlier on this branch was gated by unit tests alone. All 16 are now gated, plus the `csharp_csproj` variant: 17 entries. **The nine existing languages are byte-identical** — 234 committed values (9 x 5 arms x 5 fields, plus 9 top-level fingerprints), 0 changed, and no pre-existing budget touched. Measured both before and after the memo consolidation in |
||
|
|
81100e2c74
|
fix(python): resolve calls through __init__.py re-exports (#2864)
* fix(python): resolve calls through `__init__.py` re-exports
A call to a name imported from a package never resolved when the package's
`__init__.py` re-exported it rather than defining it:
pkg/impl.py def target_fn(x): ...
pkg/__init__.py from pkg.impl import target_fn
caller.py from pkg import target_fn
def calls_it(): return target_fn(21) # no CALLS edge
`caller.py` gets no CALLS edge. Both IMPORTS hops are recorded, and all four
functions are extracted as nodes — only the call binding is missing. Because
`__init__.py` re-exports are how Python packages declare a public surface, this
misses a large fraction of real call edges, and the failure is silent: the
defining file looks like dead code with zero callers.
The re-export closure that should carry this already exists and is fully general
(`buildReexportClosures` — SCC over the re-export subgraph, bounded fixpoint for
cycles, transitive `via` chains). Python just never fed it: the subgraph admits
only `kind: 'reexport'` and `kind: 'wildcard'`, and Python emits neither for
`from m import x`.
Python has no dedicated re-export form. A module-level `from pkg.impl import X`
binds X locally AND publishes it as `pkg.X`, so it is both a named import and a
re-export. Emitting `kind: 'reexport'` would be wrong — that form drops the local
binding, which Python's does create. Instead add an optional `reexportsName` flag
to the `named`/`alias` variants, alongside the existing provider-specific
`importedSymbolKind` / `targetIncludesImportedName` flags, and admit flagged
imports into the closure subgraph. Languages with an explicit form keep emitting
`kind: 'reexport'` and leave the flag unset, so nothing changes for them — a
negative-control test asserts a plain named import still does not resolve.
Verified on a fixture covering the three shapes (direct, top-level-via-re-export,
function-local-via-re-export): 1 of 3 CALLS edges resolved before, 3 of 3 after.
On a 12.4k-file Python/Go/TypeScript repository: edges 294,416 -> 301,443
(+7,027) and execution flows 300 -> 813. A previously "100% orphaned" module
(`shared/db/event_writer.py`) now correctly reports its caller.
5 new finalize tests (single hop, 3-hop chain, alias keying, cycle termination,
and the negative control) plus 6 updated Python fixture shapes.
`npx tsc --noEmit` clean in both packages; full unit suite shows no regression
against baseline (remaining failures are pre-existing load-sensitive flakes in
analyzer-identity / evidence-provenance-helper / skip-git-cli / hooks, each
verified passing in isolation).
* fix(python): set reexportsName only for module-level imports
`interpretPythonImport` flagged every `from m import x` as republishing the
name, but only a module-level statement does. A `from m import X` inside a
`def` or `class` body binds locally and puts nothing in the module namespace,
so flagging it fabricates a re-export of a name no importer can reach:
# pkg/__init__.py
def loader():
from pkg.impl import InternalHelper
# caller.py
from pkg import InternalHelper # CPython: ImportError
resolved to `def:pkg.impl.InternalHelper`. Worse, with declaration-order
first-wins in the closure, a scope-blind entry could claim a name ahead of the
real module-level import and give a WRONG def for legal, running code.
`interpretImport` receives a `CaptureMatch`, which is `{name, range, text}`
with no syntax node, so the scope is not recoverable there — and it is not
recoverable downstream either: `pass3CollectImports` applies no scope filter
and `ImportEdgeDraft.fromScope` is hardcoded to the module scope. The decision
therefore moves up to `import-decomposer.ts`, which still holds the live
`import_from_statement` node, and rides down as an `@import.publishes` marker.
Computed once per statement, not once per imported name, with the existing
`findAncestorBeforeBoundary` helper.
Only `function_definition` and `class_definition` suppress publication.
`if` / `try` / `for` / `with` do NOT — Python has no block scope — so the
predicate is an ancestor walk for those two node types and nothing else.
Verified against CPython 3.11 in both directions; both are now pinned by
tests, including the counterpart control that a branch-nested import still
republishes.
Also corrects the docblock in `scope-extractor.ts` that sent this change the
wrong way. It claims pass 3 attaches imports "not to any `Scope` — finalize
reconstructs the owning scope via `provider.importOwningScope` during Phase
2". Finalize does no such thing: `importOwningScope` is declared on
`LanguageProvider` and implemented by a dozen providers, and
`grep -rnE "\.importOwningScope\b" gitnexus/src/` returns exactly one hit —
that doc comment. Nothing invokes it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rzsb6mdGtbu66BG1EaF6Zz
* fix(shared): stop guessing ambiguous and namespace re-exports; bound the via chain
Four changes to the re-export closure, all reachable only now that Python
feeds it.
1. AMBIGUOUS NAMES ARE DROPPED, NOT GUESSED. `populateFileClosure` documented
"declaration order first-wins for duplicates of the same exported name",
which is sound only where a duplicate export is illegal — two
`export { X } from …` is a TypeScript compile error, so the rule never
fires. Python has no such guarantee:
from .v1 import Client # legacy, left behind
from .v2 import Client # the actual public Client
CPython binds v2 (verified on 3.11); first-wins attributed every
`from pkg import Client` in the repo to the DEAD implementation, and
`impact("Client")` pointed at the wrong file. Last-wins is not the fix
either: for the equally common `try:`/`except ImportError:` and
`if sys.version_info` pairs exactly one branch runs, and which one is not
decidable here. Both directions are wrong on real code, so the entry is
dropped — the importer stays unresolved, which is exactly the pre-#2864
answer, and the file-level IMPORTS edge is untouched.
`collectAmbiguousReexports` runs as a PRE-PASS over data phase 0 froze,
so the poisoned set is constant across the fixpoint. That matters: a set
that grew mid-fixpoint would need retraction to propagate to files that
already inherited the name, would make `myClosure.size > before` an
unsound progress signal, and would invalidate the `|SCC| + 1` cap. As a
pre-pass the closure map stays monotone and every existing termination
argument survives unchanged. Only two flagged drafts resolving to two
DIFFERENT in-workspace files count; duplicates of one target are
harmless, and unresolvable targets never entered the closure.
Checked in both loops. Named re-exports take precedence over wildcards,
so suppressing only the named loop would hand the name to a later
`import *` and reinstate an arbitrary winner through the back door.
2. NAMESPACE-RECLASSIFIED DRAFTS ARE EXCLUDED. The admission guards tested
`draft.source.kind` while `tryFinalize` tests the post-reclassification
`draft.base.kind`. Python's `from . import logger` is emitted as `named`,
reclassified to `namespace` by `isNamespaceImport`, and was still
admitted — republishing whatever def shared the module's simple name. For
a `logger.py` holding a module-level `logger = logging.getLogger(...)`,
importers of `from pkg import logger` bound to that Variable instead of
the module. Reproduced end to end. Both predicates now take the draft and
test `base.kind`; this is a no-op for TS/Rust, whose only
`isNamespaceImport` implementation is Python's.
3. `transitiveVia` IS CAPPED AT 32. Each hop copies the inherited path, so
an unbounded chain is Theta(depth^2) in time AND retained memory, and
Theta(|SCC|^2) for a cycle whose chain tracks it. `MAX_REEXPORT_DEPTH =
100` covered this until
|
||
|
|
fa31a7d824
|
fix: close the nine follow-up review findings from #2856 (routes, receiver typing, truncation honesty) (#2899)
* fix(typescript): a type parameter shadows a declared type of the same name (W2-8)
First item of wave 2, promised to the reviewer on #2856.
`export function unwrap<Result>(value: Result): Result` names the PARAMETER, not
the `interface Result` beside it — tsc resolves both annotations to the
parameter. The type-reference capture that makes a contract answerable ("what
breaks if I remove this field?") had no notion of a parameter binding, so every
annotation mentioning `Result` inside `unwrap` minted a `USES` edge into the
interface, at the same confidence as a real consumer and indistinguishable from
one. Measured on the new fixture: `unwrap` produced TWO false edges while the
genuine consumer produced one.
Blast radius is every generic whose parameter name collides with a declared
type, and the colliding names are ordinary choices for both: `Result`, `Key`,
`Value`, `Item`, `Node`, `Options`, `Config`, `Props`, `State`, `Response`.
TWO HALVES, and the first is why upstream's fix could not reach this. #2833
introduced `bindsTypeParameter` for the CALL-receiver path, where a workspace
`class T` was answering for `<T>`. Reusing it here changed nothing at first, and
the reason is its own documented contract: `@declaration.type-parameters` was
captured for class/interface declarations ONLY, so a generic FUNCTION recorded
no parameter list and the predicate correctly returned false — absence is not
evidence. The data was missing, not the logic. So:
- TYPESCRIPT_SCOPE_QUERY now captures type parameters on `function_declaration`,
`generator_function_declaration` and `type_alias_declaration`;
- the graph bridge consults `bindsTypeParameter` before emitting `USES`.
Both are load-bearing — removing either one fails the fixture.
The fixture carries two controls, because the obvious wrong fix is to stop
emitting: a genuine consumer of the interface must still link, and a generic
whose parameter does NOT collide must still link its real reference. Both are
asserted, and the "genuine consumer" case is asserted FIRST so the absences
below it cannot pass vacuously.
SCHEMA_BUMP 53 -> 54: parse-time capture change. A warm cache replays defs with
no parameter list, so the guard reads nothing and the feature is inert while
looking implemented.
Capture fingerprint re-baselined with justification. NO NEW CAPTURE NAME —
diffing the capture-name sets against the wave-1 branch returns empty; the tag
existed and now fires on more declarations. capture_groups_fp 2338 -> 2371,
fixture_count 151 -> 152, scaling 1.06 < 1.5, and JavaScript's fingerprint does
not move at all, which is the check that this is the TS declaration rules rather
than something broader.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(analyze): close the four false-success paths in the graph-write-collapse guard (W2-6)
Second wave-2 item, promised on #2856. All four were reported; all four
reproduced by reading the code they name.
(a) A SAME-COMMIT RE-RUN REPORTED SUCCESS FOREVER. Every other meta-driven
trigger — schema fingerprint, PDG mode, runner identity, CJK segmentation,
embedding dims — has a block that forces a rebuild before the
`alreadyUpToDate` fast path. `graphWriteCollapsed` had none; `grep -rn` found
writes and no reads. So the one state meaning "most of your edges are gone"
was the one state that repaired itself only if the user happened to pass
`--force`. Now forces a full rebuild, and forcing is right rather than merely
re-running: the persisted graph disagrees with what the pipeline produced, so
an incremental pass over unchanged files would write nothing and re-stamp the
same broken index as fresh.
(b) AN INCREMENTAL RE-RUN ERASED THE STAMP. `saveMeta` is a full atomic
overwrite, and the field was spread in only when the CURRENT run had a
verdict. `undefined` meant two different things at that site — "full run, no
collapse" (a positive all-clear) and "incremental write, not comparable" (no
opinion) — so the second case silently dropped `graph-write-collapsed` from
meta.json while the edges were still missing. Now three-way: stamp on
detection, CLEAR on a healthy full run, CARRY FORWARD when there is no
verdict. That is the shape `branch: branchLabel ?? existingMeta?.branch` two
lines away had all along.
(c) THE SERVER PATH NEVER CONSUMED IT. `analyze-worker-ipc.ts` projects the field
"so a server-side caller sees the same degraded outcome the CLI does" — but
nothing read it, so the comment described an intention and every collapsed
run reported `complete` to the UI and to every API consumer. Now reports
`failed` with the counts and the remedy, matching the CLI, which prints
`Repository indexed INCOMPLETELY` and exits non-zero. A consumer that reads
"complete" will query the index and get confident wrong answers.
(d) --pdg ROWS MASKED TOTAL STRUCTURAL LOSS. `expected` counts the in-memory
graph plus the streamed STRUCTURAL manifest; the streamed PDG layers never
enter `graph.relationshipCount`. But `persisted` was `stats.edges`, a count
of EVERY `CodeRelation` row, and PDG writes into that same table. With 1,000
structural edges expected and 4,000 PDG rows persisted, losing every
structural edge still read `persisted = 4000`, cleared the ratio, and stayed
silent — on exactly the large repos `--pdg` is used for.
Worth recording that the OBVIOUS fix does not work. Padding `expected` with
the PDG rows makes the two universes match but leaves the ratio judging a
minority population: 4,000 of 5,000 still clears 0.5. I wrote that first, and
the test I wrote to prove it failed. Only comparing structural against
structural asks the question the check exists to ask, so `getLbugStats` gains
a `structuralEdges` count excluding `PDG_EDGE_TYPES`. `TAINT_PATH` is
deliberately NOT in that set — it is a whole-program Function→Function edge
persisted by the normal emit, so it is structural and stays counted on both
sides.
`index-freshness-graph-collapse.test.ts` had pinned the masking as correct
(`detectGraphWriteCollapse(1000, 4000)` → undefined, "PDG layers write into
the same table, so persisted > expected is normal"). True about the table,
and it licensed the hole. Replaced with the case that matters and a note on
why the fix is at the caller.
The new `structuralEdges` assertion in `lbug-core-adapter` is there because the
failure mode is silent: the query sits in a try/catch that yields `undefined`,
and `undefined` makes the collapse check decline to compare — so a typo in the
Cypher would throw nothing, fail nothing, and switch the guard off. Verified
against a real LadybugDB and mutation-checked by breaking the query.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(processes): make process selection insertion-order invariant (W2-5)
Third wave-2 item. Reproduced before fixing: two equal three-step flows with
`maxProcesses: 1` select `handleAlpha`; inserting the identical nodes and CALLS
edges in reverse select `handleBeta`. Same repository, same commit, a different
persisted graph — so a filesystem that enumerates differently, or an incremental
run that reorders assembly, silently changes what the tool reports.
Four sorts ranked by score or length alone and returned 0 on a tie.
`Array.prototype.sort` is stable, so a 0 preserves INPUT order, which traces
back to `graph.iterNodes()`. Under `maxProcesses` capping that decided which
`Process` and `STEP_IN_PROCESS` nodes were persisted at all. Each now falls
through to a totally-ordered, content-derived key — node id for entry points,
the joined path for traces.
WHAT IS ACTUALLY VERIFIED, stated precisely because "four fixes" would overclaim:
- the ENTRY-POINT sort is individually mutation-verified;
- the two DEDUP sorts are collectively mutation-verified;
- the TRACE-RANK tiebreak is NOT individually observable, and the source says
so. The dedup sorts already impose a total order on the list that reaches
it, so removing it alone fails nothing. Kept as defence in depth: it cannot
misbehave — it only makes an already-deterministic order explicit — and it
is what stops a change to dedup ordering from silently re-opening this.
Finding that out took two fixtures. The first (three chains, three entry points)
is separated by the entry-point sort before trace ranking is reached, so it never
exercises the trace comparator at all; the second gives ONE entry point two
equal-length branches to different terminals, which is the only shape where the
trace comparator decides. Both are kept — they gate different sites.
The invariance tests assert the INVARIANT rather than any single sort, so they
cover all four sites and any future one without needing to know where they are.
Three assertions: same selection under a cap, identical set uncapped, and
identical ORDER — the last because order is what the cap consumes, so a set-only
assertion would pass while the defect persisted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(impact): UNKNOWN dominates a mixed candidate set, instead of reporting the known floor (W2-4)
Fourth wave-2 item. The all-UNKNOWN branch here was reasoned about carefully and
is correct — its comment even names the two ways a set can be all-UNKNOWN. The
MIXED case fell straight through it.
`RISK_ORDER` is `['LOW','MEDIUM','HIGH','CRITICAL']` and has no `UNKNOWN` entry,
so `indexOf('UNKNOWN')` is -1 and an UNKNOWN candidate can never win the reduce.
An ambiguous name with one caller-less candidate (UNKNOWN, per the round-1 fix)
beside one single-caller candidate (LOW) reported `maxRisk: 'LOW'` — a confident
floor over a set containing an interpretation nobody measured. That is the same
false-safe the all-UNKNOWN branch exists to prevent, one case over, and it
surfaced in the UI as "Max blast radius N (LOW risk)".
`maxRisk` answers "how bad could this be?", and an unresolved candidate could be
CRITICAL — so any UNKNOWN in the set makes the aggregate UNKNOWN. Narrowing it
that way would normally cost information, so the measured part travels alongside
as `knownMaxRisk`, present only when the two differ: absent on a fully-resolved
set, where it would duplicate `maxRisk`, and absent on a fully-unknown one, where
there is no measured part. A reader gets "at least LOW among what resolved, and
one interpretation could not be walked at all", which is strictly more than
either value alone. The human-readable message says the same thing.
The seed gained a mixed pair because the existing one could not reach this: both
its twins are caller-less, so it only ever exercises the all-UNKNOWN branch —
which is precisely why the gap survived a round of review. Three assertions,
both halves mutation-verified.
`eval-server.ts` needs no change: it renders `result.maxRisk ?? 'UNKNOWN'`, so it
now shows UNKNOWN where it previously showed the floor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(routes): track ternary polarity in dispatch guards, so a selected verb cannot be inverted (W2-9)
`if ((req.method === 'GET' ? false : true) && pathname === '/api/i')` emitted
`GET /api/i` — the one method that branch guarantees the request does NOT have.
A ternary SELECTS between its arms, so a verb inside one is not reached merely
because the whole condition is truthy, but `findVerbInSubtree` descended into
both arms and returned the first verb it saw. Same inversion `!` produced before
|
||
|
|
78ecce1b92
|
perf(import-target): index the workspace once per run for go/csharp/dart/ruby (#2898)
* perf(import-target): index the workspace once per run for go/csharp/dart/ruby Four import-target resolvers answered their lookups with a full `allFilePaths` scan per import, making resolution O(imports x files): - go (#2877): `findRootPackageFiles` / `findAllFilesInPkgDir`, the latter once per path segment on the GOPATH fallback. Most Go imports are external, so the whole cascade ran to completion before returning null. - csharp (#2878): the no-csproj leg took the raw Set past the memoized index the csproj leg was already using - up to eight passes for a four-segment `using`. - dart (#2879): one scan per candidate path, and for an external package both candidates miss, so both always ran to completion. - ruby (#2880): a complete `buildSuffixIndex` rebuilt and discarded per `require` - every require paid to index every file in the repo. Each now reads an index memoized on the `allFilePaths` Set identity, the shape `getPythonFileIndex` (#1918) and csharp's own `getWorkspaceFileIndex` (#1881) already used. Two shared modules back them: - `workspace-file-index.ts`: normalized list + `SuffixIndex` + a normalized->raw map, for csharp and ruby. - `package-dir-index.ts`: "which files live directly inside a directory ending with <path>", for go and csharp. Candidates are bucketed by the directory's last segment rather than by indexing every directory suffix, which would cost O(files x depth) entries at kernel scale (#2649). Behaviour is unchanged, including the tie-breaks that are expressed only through Set-iteration order and `indexOf` positions: the go root leg stays sorted and its package leg stays unsorted, the first-occurrence rule that excludes a directory nested inside a same-named directory is preserved, csharp's whole-path match still beats an earlier suffix match, and dart still tries `lib/<rel>` fully before bare `<rel>` and matches raw paths. Verified two ways. `import-target-index-parity.test.ts` keeps verbatim copies of the pre-change implementations and diffs against them over a deterministic corpus plus hand-built layouts for each tie-break; six mutations of the new code were confirmed to fail it. Separately, the bench corpus produces byte-identical fingerprints against the pre-change resolvers at both 400 and 1600 files. `bench/import-target/measure.mjs` gates both arms in CI: per-language output fingerprints, a scaling budget (measured 0.98-1.12 here, 3.32-4.10 against the pre-change scans), and the corpus shape, so the corpus cannot be shrunk below the size the scaling arm needs and still print PASS. Closes #2877 Closes #2878 Closes #2879 Closes #2880 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCptYZWRgnnJ821rzebQyj * perf(import-target): cover kotlin, and add depth + absolute-cost arms #2872 landed the same index hoist for Kotlin while this branch was open. Fold it into the shared measures so all five resolvers are gated on one corpus, and adopt the two arms that PR's review proved a scaling ratio alone cannot carry. - `bench/import-target/measure.mjs` gains a kotlin arm: a Gradle-shaped corpus with per-module source roots over one package namespace, `.kt` and `.kts` stems, a nested same-name package directory, and a share of wildcard `.*` imports so the package fan-out tier — the only tier whose output is order-bearing — is inside the fingerprint. - `depth_ratio`: deep arm at a FIXED file count with ~6x the path components. `scaling_ratio` divides the file count out, so it is scale-invariant and structurally cannot see a cost that grows with path depth instead, and `buildSuffixIndex` (C#, Ruby) and Kotlin's `suffixByStem` each emit one entry per component. Measured: go 0.98, dart 0.88 (depth-free indexes), ruby 1.48, kotlin 2.20, csharp 3.45 — which is why the budget is per language. One global budget would have to sit at 5.0 and would let Dart go 0.88 -> 4.9 unnoticed. - `small_ms_ceiling`: an absolute bound at 4x the measured arm, because a constant-factor regression that grows both scale arms equally passes every ratio. - The deep arm must resolve exactly what the small arm resolves. Padding was supposed to change depth and nothing else; a deep arm that stopped resolving would be timing the null path. The five fingerprints are unchanged by this commit - verified against the previous baseline before rewriting it, so adding the kotlin arm and the deep scale did not perturb the four languages' output. Kotlin joins the Set-iteration counter in `import-target-index-parity.test.ts` too. Its own guard (`kotlin-import-index-reuse.test.ts`) counts index BUILDS, which a scan added beside a reused index does not move. That counter is also the only DETERMINISTIC guard against a reintroduced scan, and this commit documents why rather than pretending otherwise: a full workspace scan on 1-in-32 imports was measured to pass every timing arm here (dart, 1.458 scaling against a 1.8 budget, 1.736 ms against a 4 ms ceiling) while the counter reads 14 instead of 1. Tightening the ceilings toward the noise floor to chase that case would only buy flaky CI. `bench/kotlin-import-target/` stays: it fingerprints both file-set iteration orders and probes the four-tier cascade shape by shape, neither of which this corpus does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCptYZWRgnnJ821rzebQyj * perf(import-target): merge matching package dirs in one pass `filesDirectlyInPkgDir` re-spread its accumulator once per matching directory, costing O(files x dirs^2) copies per import. On a Go monorepo where many services carry the same package directory (`svcN/internal/pkg`, which Go's GOPATH cascade queries by two-segment tail) that made the index SLOWER than the scan it replaced: 13.4x at 1600 matching directories. Append into one array and sort once. Measured against a verbatim copy of the pre-change scan, output byte-identical at every k: k=200 1400 files old 0.126 ms was 0.169 ms now 0.042 ms k=800 5600 files old 0.457 ms was 3.002 ms now 0.185 ms k=1600 11200 files old 0.960 ms was 12.890 ms now 0.232 ms The index now beats the scan by 2.5-4.1x on this shape instead of losing to it by up to 13x. Also drop the min-`ord` comparison in `firstFileDirectlyInPkgDir`: the build loop appends a directory to its last-segment bucket the moment it accepts that directory's first file, so bucket order already IS ascending first-file-`ord` order and the first hit is the minimum. Differentially verified at 0 divergences. The invariant, and the build-loop edits that would silently break it, are now recorded at the early return. Type the index containers as deeply readonly so Go's deliberate `[...rootFiles].sort()` copy is compile-enforced rather than comment-enforced, and correct the header's claim that a polyglot repo "never pays" -- only the stored index is per-language. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rLy781K8E3rRHFGEe1khJ * test(import-target): guard index reuse at the adapter boundary `workspace-file-index.ts` documented the hazard as "a defensive `new Set(allFilePaths)` in an ADAPTER" -- the bug #1918 shipped -- and named the unit parity test as the guard. It is not: that test imports the resolvers directly, while production reaches them through `<lang>ScopeResolver.resolveImportTarget`. Inserting the copy at `go/scope-resolver.ts:31`, `csharp:35`, `dart:189` and `ruby:268` left the parity test 28/28 green and `measure.mjs --check` PASS in all four cases. Kotlin and Python already had adapter-level guards; go/csharp/dart/ruby had none. Add `test/integration/<lang>-import-index-reuse.test.ts` for the four, mirroring the Kotlin/Python precedent: resolve through the scope resolver, assert the file set is traversed once (twice for C#, which builds two indexes), and pair every count with a result assertion so a count of 1 cannot be the count of an adapter that resolves nothing. Each was proven to fail under the copy it exists to catch: go expected 600 to be 1 dart expected 600 to be 1 ruby expected 400 to be 1 csharp expected 600 to be 2 `CountingSet` moves to `test/helpers/counting-file-set.ts` and now counts `forEach`, `values`, `keys` and `entries` as well as `[Symbol.iterator]`. It missed a rescan spelled `allFilePaths.forEach(...)` entirely; with the overrides that mutation reads 14 instead of 1. Four fixtures that pinned the guard next door, each now shown to kill its mutation: - the Dart "matched RAW" case used a forward-slash target, so the basename bucket missed before the raw comparison was reached and it asserted `null === null`. A positive twin carrying the backslash in the TARGET catches both half-mutations. - no C# or Ruby target addressed the corpus's `win\dir\thing` file, so deleting the backslash normalization in `workspace-file-index.ts` passed both gates. Now 4 failures. - `normToRaw`'s first-wins rule had no normalization twin in any corpus. - the Go nested-package fixture was decided by the `endsWith` half and never reached the first-occurrence branch its title names; addressing the directory as a single segment makes it reach it. The parity test's own docstring no longer claims the scan count is a complete census -- it names the three materialized arrays it cannot see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rLy781K8E3rRHFGEe1khJ * test(import-target): assert every scale, add collide and retained-heap arms Three holes in the gate this PR ships as its own proof. 1. `--check` computed three fingerprints per language, stored all three, and compared one. `DEEP_PAD = 16 -> 0` deleted the entire depth arm and still printed PASS, because depth padding is count-neutral by design so no asserted number moved. Assert `fingerprint` per scale, and assert `deep.fingerprint !== small.fingerprint` so the padding's EFFECT is pinned, not just its output. 2. The corpus minted per-index directory names (`src/pkg${d}`, `src/Ns${d}`, `lib/feature${d}`), so max last-segment bucket and max matching dirs were both 1 -- and bucket cardinality is the only non-constant term the index has. The `dirCount > 1` merge branch had never executed in any arm. Add a `collide` arm on shared-leaf layouts with identical files/imports/resolved counts; it reaches 9,269 multi-directory merges per run, up to 34 directories at once. Go and C#/Dart legitimately score above the linear budget there and get their own; Ruby and Kotlin stay at 1.8 because their keyed maps are collision-immune and that immunity is the assertion. 3. No arm measured memory, while the C# no-csproj leg newly retains an O(files x depth) suffix index. Add a retained-heap arm on the `bench/cfg` pattern, including its loud failure when `--expose-gc` is missing rather than a silent skip. Measured at 32k files: csharp 73.62 MiB, ruby 55.26 MiB. Ceiling is 1.5x, NOT the 4x the timing arms use -- the measurement is byte-stable to 0.00085% across processes, so 4x would be throwing away the gate. `_arms_note` records why, so nobody harmonises it back. `depth_ratio`, added by this PR, flaked ~1-in-20: go peaked at 1.748 and dart at 2.043 against a 1.6 budget, both ratios of two sub-3 ms minima. Fixed at the estimator, not the threshold -- REPS 5 -> 15, matching `bench/cfg`, `schema-pairs` and `callable-value-flow` (5 was the lowest in the repo; the sibling `kotlin-import-target` uses 7, which was not enough here). 22/22 PASS, every arm now at 70-78% of its budget with a <=1.26x swing. No budget was widened; the distributions are recorded in `_arms_note` so the headroom is visibly earned. Three copies of the same overclaim corrected: the parity test NARROWS the 1-in-32 blind spot, it does not close it -- it watches the Set while the resolvers hold materialized arrays. `_floor` no longer claims its ratios "match" the issues' (different corpora, both quadratic). The step moves to the END of the benchmarks job and runs with `--expose-gc`. A failing step aborts every step after it (#2895), so the newest, least-proven gate must not sit ahead of eight established ones. All five output fingerprints are byte-identical to before this session -- the proof that every change here was behaviour-preserving. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rLy781K8E3rRHFGEe1khJ * docs(import-target): point the reuse contract at the guard that guards it `workspace-file-index.ts` told callers the unit parity test guards the adapter-copy hazard. It does not -- it never crosses the adapter. Name both layers and say which catches what: the per-language `test/integration/<lang>-import-index-reuse.test.ts` files at the adapter boundary, the parity test for a rescan reintroduced inside a resolver. The C# namespace-dir index comment named `findDirectChild`, which this PR deleted; it feeds `firstFileDirectlyInPkgDir` now. Drop `GoResolveContext`, dead since the legacy call-resolution DAG was removed in #942 -- zero importers, and `gitnexus`'s package.json declares no `main`, `exports` or `types`, so it is not a published surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rLy781K8E3rRHFGEe1khJ * refactor(import-target): quality pass over the review-round changes Four cleanup lanes (reuse / simplification / efficiency / altitude) over the previous four commits. No behaviour change anywhere: all 25 bench cells (5 languages x 5 arms) are byte-identical on files, imports, resolved, distinct_outcomes and fingerprint, re-verified after each individual edit. **Restores a fast path the last commit lost.** Fixing the O(k^2) accumulator made the SINGLE-directory case — the overwhelmingly common one — copy the bucket where the original aliased it: measured 1.11x slower at 4 files/dir rising to 1.72x at 128. Holding the first bucket by reference and promoting to an accumulator only when a second directory appears is 0.65-0.97x of the previous code at dirCount=1 and parity at dirCount=64. 176-case differential, 0 divergences. **`sortedRootFiles` accessor.** `rootFiles` was the only index container read directly from outside the module. `readonly` is erased at runtime and `Array.isArray` widens it back, so the copy rule now lives with the code that owns the invariant instead of at the call site. No `Object.freeze`: V8's PACKED_FROZEN_ELEMENTS read cost lands on the hot `matchingDirs` path. **One shared arm for the four reuse guards.** The distinct-file-set test was copy-pasted four ways, 33-38 identical lines each, and this repo's own helpers (`mini-repo.ts`, `scope-model.ts`) document extracting at the SECOND verbatim consumer. `expectDistinctFileSetsGetOwnIndex` takes what actually varies; its `expected` type excludes `null` so the pairing rule cannot be reinstated as a hole. The per-language first and third arms stay duplicated on purpose — corpora and payload shapes genuinely differ. Re-proven: all four still fail under an adapter-inserted `new Set(allFilePaths)`. **Bench.** `dirsFor` shared by the two functions that must agree on directory fan-out (they mint and address the same files). `SCALES` derived from the arm table, so a future arm cannot be measured, printed and silently never asserted. Five timing checks with one shape collapsed to a table — the trailing sentence had already drifted into four wordings. `uniqueTarget`/`collideTarget` as flat functions, mirroring the `uniqueDir`/`collideDir` split rather than nesting a second axis four ternaries deep. One `identityPass` replaces two untimed full resolution passes per cell: -371 ms median. **CI step moved back where it belongs.** It was parked last "until #2895 lands", but that reasoning was backwards twice over: the flake that motivated it was fixed at the estimator in the previous commit, and #2895's own audit measured the last slot as executing zero times in 13 runs. It sits with the other resolver-index guards; #2899 carries the `if: !cancelled()` that fixes step masking for every step at once. Filed rather than fixed here: #2908 (java and cobol still scan the workspace per import, same shape as #2877-#2880, neither memoized), #2909 (make index reuse a contract test over SCOPE_RESOLVERS on one instrument). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Co58j4au9JLf8dmJpwdF9B --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f7a1c28291
|
Merge branch 'main' into fix/skill-evolution-gate | ||
|
|
c6b24162d9
|
perf(kotlin): index import resolution instead of scanning per import (#2872)
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
`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>
|
||
|
|
29929b7488
|
chore(deps): bump docker/login-action from 4.4.0 to 4.6.0 (#2851)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](
|
||
|
|
911fdb1ae1
|
chore(deps): bump ossf/scorecard-action from 2.4.3 to 2.4.4 (#2852)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](
|
||
|
|
665e7bb44a
|
chore(deps): bump release-drafter/release-drafter from 7.6.0 to 7.7.0 (#2853)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](
|
||
|
|
609c28859b
|
Merge branch 'main' into fix/skill-evolution-gate | ||
|
|
74409a37f6
|
perf(cpp): index qualified namespace members once per pipeline run (#2788) (#2794)
* perf(cpp): index qualified namespace members once per pipeline run (#2788)
`resolveCppQualifiedNamespaceMember` walked every parsed file — rebuilding a
per-file `scopesById` map each time — once per qualified `ns::member()` call
site, so the scope-resolution emit phase cost O(callsites x scopes). On a
1,473-file C++ repo that was 25.3 min of a 33-min analyze, with 75% of total
self-time in this one function. Its inner `findMemberInNamespaceTransitive`
compounded it: each recursion step filtered `scopesById.values()` by parent,
O(scopes^2) per file on its own.
This is the same bug #1990 fixed in the sibling ADL path (`pickCppAdlCandidates`
-> `AdlCandidateIndex`), so it gets the same fix: a `QualifiedNsMemberIndex`
(receiver simple name -> member simple name -> callable defs) built lazily once
per `parsedFiles` identity and reset by `clearCppInlineNamespaces`, which runs
from `cppScopeResolver.loadResolutionConfig` at the start of every pass. Per
call site the work drops to two Map lookups.
Ordering is preserved exactly — file-major, `parsed.scopes` declaration order,
a namespace's own `ownedDefs` before its inline-namespace children, depth-first
— because the caller takes `allHits[0]` for the single-hit case and
`narrowOverloadCandidates` is first-wins. Non-inline nested namespaces are
still not descended into, and same-name hits across inline children still
report `'ambiguous'` (#1564).
Measured with `PROF_SCOPE_RESOLUTION=1 analyze --force --index-only` on a
synthetic corpus (`namespace ns_i { inline namespace v1 { ... } }` plus 20
`ns_j::fn()` call sites per file):
| files | emit before | emit after |
|-------|-------------|------------|
| 100 | 153ms | 16ms |
| 200 | 704ms | 24ms |
| 400 | 3,293ms | 42ms |
| 800 | 16,898ms | 78ms |
Before, doubling the file count quadrupled emit; now it doubles. At 800 files
total scope resolution goes 17.2s -> 394ms.
Output is unchanged, verified rather than assumed: a full graph dump (sorted
nodes + relationships) from a baseline build at the parent commit and from this
one are byte-identical on all 134 `cpp-*` fixtures merged into a single repo
(1573 nodes / 1997 relationships) and on the 400-file synthetic corpus.
`test/integration/resolvers/cpp.test.ts` passes 334/334.
#1990 shipped its ADL fix without a scaling gate, which is how the bug class
came straight back here, so this adds one: `bench/cpp-qualified-ns` measures
`(t_large/t_small)/(1600/400)` — 0.93-1.21 indexed versus 3.45 for the old
per-call-site scan — alongside a fingerprint over every
`receiver::member -> outcome` the corpus resolves, and CI runs it with
`--check`. `test/unit/cpp-qualified-ns-index.test.ts` covers the cache
invalidation the index introduces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cpp): address tri-review findings on the qualified-namespace index (#2788)
Multi-engine review of this PR (Claude swarm + ce-code-review, Codex
gpt-5.6-sol swarm + ce + adversarial) returned two P1s and five smaller
findings. All are fixed here.
P1 — the index defeated the pipeline's post-language memory release.
`scope-resolution/pipeline/phase.ts` evicts each language's files and then
calls `forceGc()`, on a stated premise that "This language's ParsedFiles are
now unreachable", sizing C/C++ at ~17-20GB on the Linux kernel. The
module-level `let qualifiedNsIndexSource` falsified that: it pinned the whole
`parsedFiles` array, and the index held defs reaching into those files'
scopes, until the *next* C++ pass cleared it — which in a single analyze never
comes. C++ is 7th of 16 in SCOPE_RESOLVERS, so the set survived nine later
language passes plus emit. Replaced with a
`WeakMap<readonly ParsedFile[], QualifiedNsMemberIndex>`, the pattern already
used by `moduleScopeIndexByPass` in `cpp/file-local-linkage.ts`.
`clearCppInlineNamespaces` still swaps in a fresh WeakMap, because the index
has a second input (`inlineNamespaceScopeIds`) the key cannot observe.
Measured with `--expose-gc`: 61.2MB retained after the caller drops the array
before, 0.1MB after.
The ADL twin (`adl.ts`) has the same pattern, so the hazard predates this PR —
but `pickCppAdlCandidates` returns early before `ensureAdlIndex` on
`noAdlSites`/empty `argInfoBySite`, so it rarely arms, whereas a qualified
`ns::member()` index arms on almost every C++ workspace. Moving the ADL twin
to a WeakMap is left as a follow-up.
P1 — the new bench could not see the regression class it exists to gate.
`callSites()` drew every receiver from `ns_${...}`, so the receiver lookup
never missed; production is the opposite, since Case 1.5 in
`receiver-bound-calls.ts` is reached by every plain-identifier receiver call
and misses on most. A rescan reintroduced only on the receiver-bucket-absent
path scored 1.279 and PASSED the old bench. The corpus now mirrors production
(~1 in 5 receivers name a declared namespace) and adds a namespace reopened
across files, a same-name inline nest, a member declared at both namespace and
inline-child level, and call sites carrying a real `Callsite` so
`narrowOverloadCandidates`/`cppConversionRank`/
`isOverloadAmbiguousAfterNormalization` are inside the fingerprinted surface at
all. That same rescan now measures 4.538 and FAILS; defeating the dedup now
fails the fingerprint arm where it previously passed byte-identical. The
fingerprint moved once, deliberately, for the corpus expansion — recorded in
`_rebaseline_2788_review`, explicitly not precedent.
Also fixed:
- Unbounded recursion aborted analyze. `collectNamespaceMembers` recursed per
inline child with no bound and threw an uncontained `RangeError` at inline
depth 8000 (`phase.ts`'s try has a `finally`, no `catch`), and a receiver
*miss* paid full recursion where the deleted walker skipped on a name
mismatch. An explicit work-stack alone would only have converted that into
an OOM at depth 6000, because the eager table was quadratic in memory too:
for a depth-D chain it legitimately holds D(D+1)/2 entries, since `v2::foo()`
is a valid receiver at every level. Replaced with a lazily-queried node graph
(per-scope own-member buckets plus direct child links, resolved on demand and
memoized per receiver+member). Build is now linear; depth 100000 costs 133ms
where 8000 previously threw.
- "#1990 shipped without a scaling gate" was false. #1990 did ship
`test/integration/cpp-adl-benchmark.test.ts` (
|
||
|
|
a5251d6b08 |
docs(ci): record why the runner box must not restart services mid-job
A run spans ~15h and apt-daily-upgrade.timer fires daily around 06:34, so every scheduled run crosses it. On 2026-08-02 unattended-upgrades upgraded openssl at 07:54:02 and needrestart restarted the Actions runner five seconds later. The job went to Canceled 14s after that, and a cancelled job skips even `if: always()` — so the evidence artifact died with it, which is the one outcome the rest of this workflow's budget nesting exists to prevent. This is a plausible contributor to the unexplained mid-run failures in the July dispatch cluster, none of which left an artifact behind either. The box config itself is applied out-of-band like the rest of the instance setup; the checklist now carries the requirement so a rebuilt box does not silently reintroduce it. |
||
|
|
2fbd5ee515 |
fix(ci): make the task repo resolve the ref its tasks name
Every task in tasks.scenarios.yaml names `ref: main`, and resolving it is
the first thing task binding does. actions/checkout only creates a local
ref for the ref it checked out, so `main^{commit}` resolves on a main run
and dies with "unknown revision" on any other — which is what a
workflow_dispatch from a branch hits, before a single session starts.
The step that points the benchmark at the checkout now also makes that
checkout able to answer for the refs the tasks name. On a main run the
fetch is a no-op.
|
||
|
|
56fc7936d2
|
Merge branch 'main' into fix/skill-evolution-gate | ||
|
|
911151e230
|
fix(resolution): resolve Go pointer-receiver calls, and report the program boundary instead of hedging (#2766) (#2782)
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
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
|
||
|
|
8076b98fcc |
refactor(ci): address the evidence path directly instead of threading it
The upload step needs a path that does not depend on the sweep step
surviving. It did not need shared state to get one: `runner.temp` is
available in a step, only not in a job-level `env:`, so each of the three
consumers can name `${RUNNER_TEMP}/wfevolve` itself. That deletes the
env var and the step that published it — the previous fix swapped one
threading channel for a sturdier one where no channel was required.
Also from the same review pass:
- `announce`/`keep` drop their default-argument capture of `task["id"]`
and `per_arm`. Late binding only bites a closure invoked after the loop
moves on; these are called synchronously inside `sweep_task_cells`,
which blocks until every wave completes. The trick was guarding against
a race that cannot happen here, while implying to the next reader that
it can.
- `_stub_cell_dependencies` returns the list its teardown appends to
rather than taking it as an out-parameter, dropping the boilerplate
from every call site.
- The workflow's `WORKERS` comment points at the `--workers` help text
instead of restating it, so the rationale has one home.
|
||
|
|
d18dbd4143 |
fix(ci): publish the evidence path from a step, not a job-level env
`${{ runner.temp }}` does not exist in a job-level `env:` block — the
runner context is only available to steps — so OUT_ROOT would have
resolved to a bare `/wfevolve` at the filesystem root. The sweep would
have failed writing there, and the upload would have pointed at nothing.
actionlint caught it; this repo lints workflows for exactly this reason.
The property that mattered is kept: the path is fixed before anything can
fail, rather than read from the sweep step's outputs — that being the
step whose death is the reason the upload matters. The first step now
publishes it to GITHUB_ENV, which every later step sees, including the
`if: always()` upload after a killed sweep.
The contract test pins the step's position and its exact line, so the
context cannot creep back into the job block.
|
||
|
|
edb24da1e8 |
feat(ci): expose benchmark cell concurrency to the evolution lane
`--workers` reaches the sweep from evolve.py and from a workflow_dispatch input. Both default to 1, so nothing about the scheduled lane changes: the runner is sized for one cell at a time, and a cell starved of CPU drifts toward its session timeout, which the gate counts as an excluded run and refuses to decide on. generation_timeout_seconds is left alone deliberately — it is a worst-case sum-of-every-timeout bound (843h at current settings), already far looser than any real run, and concurrency only makes it looser. Raising the input is gated on the runner resize; the contract test pins the default so the lane cannot start running 3-way on a 2-vCPU box by accident. |
||
|
|
48a03464de
|
Merge branch 'main' into fix/skill-evolution-gate | ||
|
|
01be282667 |
fix(ci): make the evolution lane survive its own deadlines and remember prior runs
An end-to-end pass over the lane — instance start, job, artifacts, promotion — found three ways it loses work that has already been paid for. **Evidence died with the job.** Three budgets have to nest: EventBridge keeps the box up 24h from ~02:45, the job timeout was also 1440min, and the sweep had no budget of its own. A job-level timeout CANCELS the job, so the upload step never runs; and since the box stops 24h after it starts while a scheduled run can begin well after the cron (the 2026-08-01 run was queued 65min late), the box always won that race — the runner would simply vanish mid-step. The job now gets 21h, the sweep step 19h, so a wedged generation fails the step, keeps the job alive, and still uploads. The nesting is asserted in the contract test. **The upload could be skipped.** Its path came from an output the sweep step wrote — the same step whose death is the reason the upload matters. OUT_ROOT is now a job-level env constant known before anything runs, and the upload is unconditional: results.jsonl and transcripts are appended as the sweep goes, so a killed generation still holds the evidence that explains why it died. **The lane was memoryless.** `--seed-results` is how a run sees what already lost (summarize_gate feeds the prior promotion.json to the proposer), and with the default --generations 1 there is no earlier generation in-process to supply it — the workflow never passed it, so every Saturday proposed from a blank slate and could re-propose the same rejected candidate forever. The lane now seeds from the last successful run's artifact, best-effort: a first run, an expired artifact, a missing gh, or a failed download proceeds without it rather than costing a generation. Also guards the silent-promotion path: `.claude/skills/*` is gitignored with a hand-maintained per-skill allowlist, and `git status --porcelain` — how the workflow detects an applied promotion — is blind to ignored paths. A candidate skill missing from that allowlist would report "No promotion this run" after the gate said promote. A test now asserts every CANDIDATE_SKILLS entry is visible in all three shipped trees. |
||
|
|
565287528d
|
fix(ci): stop CI Report dying silently when the tests job fails (#2728)
* fix(ci): stop CI Report dying silently when the tests job fails
The "Build report" step in ci-report.yml runs under
`bash --noprofile --norc -e -o pipefail`. It located its inputs with
UNIT_SUMMARY=$(find "$DIR/test-reports" -name ... 2>/dev/null | head -1)
`coverage-merge` in ci-tests.yml is `needs: tests` with no `if: always()`,
so any failing shard skips it and the `test-reports` artifact is never
uploaded. `find` then runs against a directory that does not exist and
exits 1; `-o pipefail` carries that status through `| head -1`, the
command substitution hands it to the assignment, and `-e` kills the step.
The death is invisible: `2>/dev/null` discards find's error and the whole
report is built into `$GITHUB_OUTPUT`, so the step logs nothing and just
reports "Process completed with exit code 1". "Comment on PR" is then
skipped, so the CI Report workflow fails and posts nothing on exactly the
PRs whose tests failed — when the report is most useful. The
"Coverage data unavailable" fallback already existed for this case but
was unreachable, because the script died ~160 lines before it.
Route the four lookups through a `find_first` helper that returns empty
when the root is absent. Verified by extracting the step body and running
it against both artifact layouts: with `test-reports` present the output
is byte-identical to the previous script (1335 bytes), and with it absent
the step now exits 0 and emits the coverage-unavailable report instead of
exiting 1 with an empty $GITHUB_OUTPUT.
Observed on 32 of the last 100 failed runs; correlation with the tests
job's conclusion was 6/6 failure and 4/4 success in the sampled runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): let the prebuild assertion report a missing .node
`Build prebuild` deletes `$pkgdir/prebuilds` before running prebuildify,
so a run that emits nothing without failing leaves `find` searching a path
that no longer exists. Under the step's `shell: bash` (`-e -o pipefail`)
that `find` exits 1 and kills the step before the `test -n "$out"` guard
below it — the guard written to explain exactly this case never runs, and
the job dies with a bare "Process completed with exit code 1".
Same shape as the `ci-report.yml` fix in this PR: a lookup that exits
non-zero on an absent root pre-empts the fallback beneath it. `|| true`
hands the empty result to the guard, which still fails the build, now with
`::error::prebuildify produced no .node`.
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>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
|
||
|
|
5a017f2722 |
feat(eval): report evolution progress while the generation is still running
Run 29907431284 printed its whole 14h45m of output at one timestamp (00:02:59.32) as the process exited: stdout is a pipe, so CPython block-buffered it, and there was no way to tell a live run from a wedged one. Three changes make the lane observable in the Actions log: - PYTHONUNBUFFERED for the driver (workflow step) and for the benchmark subprocess (its env is an explicit minimal dict and inherits nothing), so lines reach the log when they are written. - run_managed grows `echo_stdout`, a passthrough that streams a child's stdout to stderr as it arrives while leaving the bounded tail intact. evolve.py enables it for the benchmark sweep — the multi-hour phase, whose per-run lines previously surfaced only as a tail, and only on failure. It stays off everywhere else: a Claude session's stdout is the evidence stream and is written out only after redaction. - The sweep now announces each cell as it starts (`3/18, 47m elapsed`) and reports `took=` and `error_kind=` when it finishes, so an excluded run — the thing that actually blocks promotion — is visible live instead of only in results.jsonl. evolve.py also reports the proposer's duration, turns, and cost once the proposal lands. |
||
|
|
e1df209367
|
Merge branch 'main' into fix/skill-evolution-gate | ||
|
|
bb09ce28e0 |
fix(eval): stop discarding completed benchmark sessions as unverifiable
The evolution loop has not been able to promote anything since it went online. Run 29907431284 (the last green run) reached the gate and threw away 5 of its 18 runs, and the gate requires zero excluded runs in both paired arms — so the generation could never produce a verdict on merit. Two causes, both in the session layer: 1. Claude Code drains background-task bookkeeping after the final result event (`background_tasks_changed`, `task_updated`, `task_notification`, all `type: "system"`). The parent-stream check required the result to be the literal last event, so three sessions that had exited 0 with a complete result and usage payload were recorded as session errors. Trailing `system` events carry no tool_use/tool_result/usage payload and cannot forge skill or cost evidence; anything else after the result still fails closed. 2. The 3600s per-session ceiling killed two `workflow` incumbent runs on inv-bug-pdg-note mid-verification. Successful `workflow` rows in the same run finished in ~1600-2600s across both sessions, so the ceiling moves to 5400s and now lives in one shared constant instead of two argparse defaults that could drift apart. Also marks the activation checklist against reality: the secrets, the Environment, the runner, and the validation dispatch are all in place; the repository variable GITNEXUS_EVOLUTION_ENABLED is the one remaining gap, and until it is set the Saturday cron skips the job in seconds while the EventBridge schedule still starts the runner for the day. |
||
|
|
51095c19f8
|
chore(deps): bump actions/setup-python from 6.3.0 to 7.0.0 (#2757)
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (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
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](
|
||
|
|
e0dc0c2d5e
|
chore(deps): bump release-drafter/release-drafter from 7.5.1 to 7.6.0 (#2756)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.5.1 to 7.6.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](
|
||
|
|
909f2f85b6
|
chore(deps): bump the codeql-action group across 1 directory with 3 updates (#2755)
Bumps the codeql-action group with 3 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.0 to 4.37.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits]( |
||
|
|
de84ad6297
|
feat(spring): index @Bean factories and @Resource injection (#2740)
* feat(spring): index Bean factories and Resource injection * fix(spring): address Bean and Resource review findings * refactor(lbug): keep relation pair parsing in router * test(lbug): preserve schema exports in WAL mocks * test(cache): align schema bump pin --------- Co-authored-by: Shining <xuenning@qiyi.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> |
||
|
|
27ab37c432
|
feat(resolution): type receiver chains from AST structure across all 14 languages (#2708) + epistemic lower-bound (#2744) (#2747) |