chore(reconciliation): promote proven RC19 internal source candidate#98
Conversation
…#2128) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.1 to 25.9.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…igyanpatwari#2097) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/commits) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
… path (abhigyanpatwari#2114) `gitnexus analyze` silently failed to create the LadybugDB VECTOR/HNSW index because `CALL CREATE_VECTOR_INDEX(...)` was run through the prepared `conn.prepare()` path, which rejects multi-statement procedures — degrading semantic search to exact-scan. Route index creation through `conn.query()` via a new adapter-owned `createVectorIndex` (mirrors `createFTSIndex`), make the previously-swallowed error visible (`{ err }` logging), add an in-process idempotency cache, and add real-`@ladybugdb/core` regression coverage. Fixes abhigyanpatwari#2114. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nto the image (abhigyanpatwari#2130) (abhigyanpatwari#2132) * fix(docker): copy hooks/ into Dockerfile.cli runtime stage (abhigyanpatwari#2130) `gitnexus analyze` inside the official image (akonlabs/gitnexus, ghcr.io/abhigyanpatwari/gitnexus) crashed at startup with: Error: Cannot find module '../../hooks/claude/resolve-analyze-cmd.cjs' Require stack: - /app/gitnexus/dist/cli/resolve-invocation.js `dist/cli/resolve-invocation.js` does `createRequire(import.meta.url)('../../hooks/claude/resolve-analyze-cmd.cjs')` at module load (it is the single source of truth for the npm-11 npx-crash invocation decision, abhigyanpatwari#1939), and `analyze.ts` statically imports it. The Dockerfile.cli runtime stage copied dist/node_modules/package.json/the duckdb script/vendor but never `hooks/`, so the require throws before the command does any work. `hooks/` is in package.json `files`, so npm already ships it — Docker was the only distribution dropping it. Fix: copy `hooks/` into the runtime stage, mirroring what npm publishes. Also add `test/unit/dockerfile-runtime-asset-parity.test.ts`: a regression guard that derives every out-of-dist `require()`/`createRequire()` target from source and asserts each is a runtime-stage `COPY`. Scoped to the require family (not `fs.access`/`new URL`), so it locks the abhigyanpatwari#2130 class without false-flagging the intentionally-omitted, gracefully-degrading `web/` and `skills/` assets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(docker): also ship skills/ into the runtime image Follow-up to the hooks/ fix: `skills/` is another published runtime asset (in package.json `files`) the Docker image dropped. The CLI reads the bundled SKILL.md templates from `<pkg>/skills/` for `gitnexus analyze --skills` (ai-context skill generation) and `gitnexus setup`/`uninstall` (installing skills into editor configs). Unlike the hooks/ require(), these reads degrade SILENTLY when the dir is absent — `--skills` writes minimal placeholder content (ai-context.ts), `setup` installs zero skills (setup.ts readdir → []) — so the image looked fine but produced wrong output. Copy `skills/` so the image is fully usable for all CLI tooling. `web/` (also in `files`) is intentionally NOT shipped: this image never builds gitnexus-web (the builder doesn't copy it, build.js logs "skipping web UI"), so it is API-only by design — the UI is the separate Dockerfile.web image / hosted app. The duckdb script is the only runtime asset needed from scripts/, so that stays a single-file copy. Extends the runtime-asset-parity guard with an explicit skills/ assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): correct stale docstring that listed skills/ as not copied The 2nd commit on this branch added a skills/ COPY + an it('copies skills/…') assertion, but the top-of-file docstring still grouped skills/ with web/ as 'intentionally not copied / out of scope'. Drop skills/ from that sentence and note it is shipped (and covered by its own test). web/ remains the sole fs-accessed-but-uncopied example. Documentation-only; assertions unchanged. * fix(test): make runtime-stage detection case-insensitive on AS Docker accepts a lowercase `as runtime`; the parity guard's stage-detection regex was case-sensitive on `AS`, so a future Dockerfile reformat would empty the parsed COPY set and trip the named assertions. Add the /i flag. * fix(test): stop runtime-stage COPY parsing at the next FROM runtimeStageCopiedSources scanned from the runtime FROM to EOF. Bound the scan to the runtime stage (start after its FROM, break on the next FROM) so a build stage added after runtime can't have its COPY lines misattributed. No-op today (runtime is the last stage); the copied set is unchanged. * fix(test): assert at least one runtime COPY is parsed (no vacuous pass) If the runtime FROM or the /app/gitnexus/ source prefix ever stops matching, the copied set goes empty and the parity assertion passes vacuously. Add an explicit copied.length>0 guard so that failure mode is loud and named. * fix(test): strip line comments before require-scanning requiredExternalAssets() regex-scanned raw source, so a future doc-comment such as a commented-out require('../../web/x') in a shallow src file would resolve outside dist/ and spuriously fail the parity guard. Strip // line comments first. Block comments are deliberately not stripped (a naive block strip mangles slash-star inside string/glob literals). Verified the real-tree scanner output is byte-identical with and without the strip, and resolve-invocation.ts's multi-line createRequire is still detected. (Also swaps a stray non-ASCII glyph in the prior commit's comment for ASCII.) * fix(test): account for aliased + computed module-load requires (fail-closed) The parity scanner only matched string-literal require/createRequire, so it missed module-load requires via aliased createRequire bindings and computed paths — and already failed to see community-processor.ts's `_require(leidenPath)` -> vendor/leiden, making the "every out-of-dist asset" claim untrue. Broaden the scan: - Discover per-file createRequire bindings (requireCJS, _require, …) and match their literal-arg calls; keep the createRequire(...)('…') IIFE form. - Detect COMPUTED (non-literal) requires and gate them on MODULE-LOAD position (brace-depth 0), so the four in-function computed requires that target node_modules/package.json (optional-grammars, native-check, capabilities, parse-cache) are correctly out of charter and ignored. A module-load computed require must be vetted in KNOWN_COMPUTED_REQUIRES (seed: community-processor -> vendor/leiden) or the test FAILS CLOSED for manual review. - Allowlist entries are coverage-checked via isCovered, never trusted: a new test removes the `vendor` COPY from a fixture and asserts leiden surfaces as uncovered (so deleting a COPY can't silently pass — the abhigyanpatwari#2130 class). - Exclude `<id>.resolve(...)` (a path lookup, not a load). - Upgrade the comment stripper to a string-aware pass that removes line AND block comments without mangling slash-star inside string/glob literals — the computed branch needs JSDoc requires (e.g. javascript/index.ts) gone, and the literal scan output stays byte-identical. Honest claim wording: the 4th test now says coverage = resolvable + vetted module-load requires, unrecognized computed requires fail for review. Adds unit tests for fail-closed, aliased-literal, and in-function-ignored paths. * fix(test): also scan shipped .cjs/.mjs assets for sibling requires The guard only scanned src/**/*.ts, so hand-written shipped runtime files were invisible — and they DO require siblings: hooks/claude/gitnexus-hook.cjs and hooks/antigravity/gitnexus-antigravity-hook.cjs each require('./hook-lock.cjs'), './hook-db-lock-probe.cjs', './resolve-analyze-cmd.cjs'. Add a second pass over shipped .cjs/.mjs assets (the runtime COPY set minus dep/data roots), resolving each relative require against the asset's OWN package-relative dir and checking COPY coverage — by prefix, NOT on-disk existence: the antigravity hook's './hook-lock.cjs' resolves to hooks/antigravity/hook-lock.cjs (which doesn't physically exist; hook-lock.cjs lives under hooks/claude) yet is covered by the whole-hooks COPY. All 6 shipped sibling requires resolve under the hooks COPY. * fix(docker): move hooks/skills COPYs past the DuckDB FTS RUN The hooks/ and skills/ COPYs sat between the vendor COPY and the DuckDB FTS-extension install RUN, so any edit to hook/skill content invalidated that RUN's cache layer — which performs a one-time network INSTALL of the extension (~tens of seconds per affected build). The COPYs have no input dependency on the DuckDB step; relocate them to after it (before USER node) so stable infrastructure layers are not rebuilt on hook/skill churn. Image contents are unchanged. The runtime-asset-parity guard still detects both (its scan covers the whole runtime stage), and the two are consolidated under one comment. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…abhigyanpatwari#1913) (abhigyanpatwari#2134) * fix(hooks): silence MCP-owned-DB augment skip for strict hook runners The PreToolUse augment-skip path wrote `[GitNexus] augment skipped: MCP server owns DB` to stderr unconditionally on a normal (non-error) skip. Strict hook runners that validate hook output (e.g. Codex `PreToolUse`) treat that as noisy / "invalid pre-tool-use JSON output". Gate the diagnostic behind GITNEXUS_DEBUG via a shared `isDebugEnabled()` helper, so normal skips are silent by default (empty stdout AND stderr, exit 0) and the reason stays recoverable with `GITNEXUS_DEBUG=1`. Applied consistently to all three hand-maintained hook copies (claude, antigravity, claude-plugin). Tests: - Unit (claude CJS + plugin): assert default-silent and debug-on behavior for the MCP-owned-DB skip and for the fail-closed (lsof ETIMEDOUT) skip that routes through the same gated line; the owner-detection tests run with GITNEXUS_DEBUG=1 so the skip discriminator stays observable. - e2e (antigravity): the antigravity adapter shares the identical gated skip but only runs from its install dir, so cover it through the install pipeline with a faked DB-owner probe (strict empty-stdout/stderr + debug-on). Promote the fake-probe helpers (createHookToolDir / hookEnv, plus a module-private writeExecutable) into shared hook-test-helpers so unit + e2e reuse them. Fixes abhigyanpatwari#1913 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hooks): unify GITNEXUS_DEBUG gating in main() catch handlers The main() catch-handler in all three hook copies still gated its crash log on truthy `if (process.env.GITNEXUS_DEBUG)`, while the skip diagnostic the abhigyanpatwari#1913 fix added is gated on the strict `isDebugEnabled()` helper (=== '1' || === 'true'). That split meant GITNEXUS_DEBUG=0 or =false suppressed the skip line yet still enabled crash logging — two conflicting contract signals in the same file. Switch the three catch handlers to isDebugEnabled() so GITNEXUS_DEBUG has one strict meaning everywhere: exactly '1' or 'true' enables all diagnostics; everything else (incl. '0', 'false', empty, unset) is silent. Add boundary tests asserting the MCP-owner skip stays silent with GITNEXUS_DEBUG='0' and 'false' (CJS + Plugin), pinning the strict contract. Refs abhigyanpatwari#1913 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hooks): gate antigravity stale-index hint stderr behind GITNEXUS_DEBUG The antigravity AfterTool handler mirrored the stale-index hint to stderr unconditionally on a normal (non-error) success path — the last ungated stderr write of the class issue abhigyanpatwari#1913 targets, and a divergence from the claude hook, which never mirrors this hint to stderr. Gate the stderr mirror behind isDebugEnabled(). The hint still reaches the agent via additionalContext (stdout JSON) — parts.push(hint) stays unconditional — so there is no functional loss; only the by-default terminal mirror moves behind GITNEXUS_DEBUG=1. This knowingly changes the abhigyanpatwari#1730 terminal-mirror behavior in favor of strict-runner cleanliness and parity with the claude adapter. Split the e2e assertion into a default-silent test (hint in additionalContext, absent from stderr) and a GITNEXUS_DEBUG=1 test (hint mirrored to stderr). Refs abhigyanpatwari#1913 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(hooks): document GITNEXUS_DEBUG=1 for hook diagnostics GITNEXUS_DEBUG was documented only in the cursor integration README, so the diagnostic escape hatch for the Claude Code / Antigravity hooks was undiscoverable. Operators hitting a silent hook skip (MCP server owns the DB, fail-closed probe timeout, or an already-current index) had no documented way to surface the reason. Add a Troubleshooting subsection explaining that the hooks stay silent on normal skip paths for strict runners, that GITNEXUS_DEBUG=1 surfaces the reason on stderr, and that only '1'/'true' enable diagnostics (stdout JSON the agent consumes is unaffected). Refs abhigyanpatwari#1913 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(hooks): update setup-antigravity unit test for gated stale-index hint U2 (7995e92) gated the antigravity stale-index hint stderr mirror behind GITNEXUS_DEBUG, but a second test — setup-antigravity.test.ts's "AfterTool emits stale-index hint" — also asserted the hint on stderr by default and was missed (it lives outside the two files validated locally; the full CI matrix caught it). Update it to the U2 contract: assert the hint via additionalContext with stderr silent by default, plus a GITNEXUS_DEBUG=1 run asserting the terminal mirror reappears. Refs abhigyanpatwari#1913 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…abhigyanpatwari#2078) * feat(ingestion): add Java Spring route annotation → Route node extraction Previously, GitNexus only supported Route node generation for JS/TS ecosystems (Express, Next.js, Fastify, etc.) and Python (FastAPI, Flask). Java Spring's annotation-based routing (@RequestMapping, @GetMapping, @PostMapping, etc.) was only supported at the group contract layer (http-patterns/java.ts) for cross-repo matching, but NOT at the ingestion layer for generating graph Route nodes. This commit adds ingestion-layer support: 1. JAVA_QUERIES (tree-sitter-queries.ts): - Added method-level annotation captures (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping) → @decorator captures - Added class-level @RequestMapping → @decorator capture (prefix) - Supports both positional ("/path") and named (path="/path", value="/path") annotation argument forms 2. parse-worker.ts: - Java class-level @RequestMapping is detected and stored as a prefix (not pushed as a standalone Route) - After per-file capture processing, the prefix is applied to all method-level routes in the same file via the existing ExtractedDecoratorRoute.prefix field - The routes phase (normalizeExtractedRoutePath) handles the prefix joining, producing final URLs like /api/users/list 3. Tests: - Unit test (worker-backed): 4 cases covering prefix joining, bare routes, class-level exclusion, multi-file isolation - Integration test (full pipeline): 6 cases covering end-to-end Route node + HANDLES_ROUTE edge generation Closes the feature gap where `route_map`, `shape_check`, and `api_impact` MCP tools returned empty results for Java Spring projects. * chore(autofix): apply prettier + eslint fixes via /autofix command * fix: address review findings — extract spring.ts module, fix PatchMapping, multi-class support Addresses all P2 findings from tri-review: 1. **Architecture**: Extracted Spring route logic from parse-worker.ts into a dedicated `route-extractors/spring.ts` module (matching the pattern of `laravel.ts` and `fastapi-router-bindings.ts`). parse-worker now has a single dispatch line — no language-specific logic inline. 2. **PatchMapping bug**: Added `'PatchMapping'` to `ROUTE_DECORATOR_NAMES` (was silently dropped before). 3. **Multi-class bug**: The new `extractSpringRoutes` walks each class declaration independently with its own prefix — no more single-scalar `javaClassPrefix` last-wins issue. 4. **Test hygiene**: Unit tests now import `extractSpringRoutes` directly (no dist build / worker pool dependency). Tests run in all tiers. 5. **Removed JAVA_QUERIES decorator patterns**: The Spring extractor does its own AST walk, so the tree-sitter query captures for Java annotations are no longer needed (avoids duplicate route emission). Additional test coverage: - Multi-class in one file with independent prefixes - @PatchMapping support - Named annotation args (path= and value=) on class-level @RequestMapping * refactor: move Spring route extraction to LanguageProvider hook Addresses the second review comment: instead of an inline `if (language === SupportedLanguages.Java)` dispatch in parse-worker, the Spring route extraction is now wired through a new optional `extractDecoratorRoutes` hook on LanguageProviderConfig. - Added `extractDecoratorRoutes` to LanguageProviderConfig interface - Java provider registers `extractSpringRoutes` as its implementation - parse-worker calls `provider.extractDecoratorRoutes?.()` generically - Removed direct import of spring.ts from parse-worker This keeps parse-worker fully language-agnostic — no language names appear in the dispatch path for route extraction. * refactor: rewrite spring.ts with tree-sitter captures, fix inline imports Addresses all 4 inline review comments: 1. Rewrote spring.ts to use a single predicate-free Parser.Query (same pattern as group-layer JAVA_ROUTE_ANNOTATION_PATTERNS). Two-phase loop: first pass collects class prefixes by node.id, second pass resolves method routes via findEnclosingClass. No more manual DFS / recursion. 2-3. Moved inline import(...) type references in language-provider.ts to proper top-level imports (Parser, ExtractedDecoratorRoute). 4. Covered by #1 — recursive helpers removed entirely. Added 3 extra test cases: non-route named args filtering, prefix isolation across mixed classes, line number accuracy. * refactor: extract shared Spring route primitives + add parity test Addresses review follow-up on abhigyanpatwari#2078: - Extract the primitives shared by the ingestion (route-extractors/spring.ts) and group (http-patterns/java.ts) Spring extractors into a new route-extractors/spring-shared.ts: METHOD_ANNOTATION_TO_HTTP, findEnclosingClass, isRouteMemberKey, and a safe unquoteSpringLiteral. Both extractors now import from it (group -> ingestion, the layer-correct direction) so the shared semantics can't drift apart. - Replace spring.ts's local unquote() with the safer unquoteSpringLiteral (returns null for non-string nodes instead of assuming a quoted string). - Add test/unit/spring-route-extractor-parity.test.ts: runs one shared Spring fixture through both extractors and asserts they surface the same provider method/path combinations. The broader HttpRouteExtractor source-scan optimization is tracked in abhigyanpatwari#2138. --------- Co-authored-by: henry <zhangwei2017@unipus.cn> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
…ri#2106) (abhigyanpatwari#2137) * feat(git): add getCurrentBranch + resolveRefToCommit helpers (abhigyanpatwari#2106) * feat(storage): branch-scoped getStoragePaths + branchSlug + resolveBranchPlacement (abhigyanpatwari#2106) * feat(analyze): branch-aware indexing — per-branch slot, no overwrite (abhigyanpatwari#2106) * feat(registry): nest non-primary branches under one path entry (abhigyanpatwari#2106) * feat(mcp): optional branch scope on query tools + list_repos branches (abhigyanpatwari#2106) * feat(cli): --branch on analyze + query/context/impact/cypher/detect-changes (abhigyanpatwari#2106) * feat(cli): branch-aware list/status + per-branch staleness meta (abhigyanpatwari#2106) * fix(review): apply autofix feedback - guard analyze against --branch != checked-out branch (prevents writing one branch's working tree into another branch's index slot) - fix branch-handle pool reinit thrash (track observed indexedAt by lbugPath, since applyBranchScope returns fresh handles) - remove dead resolveRefToCommit helper (staleness uses HEAD vs branch meta) - RepoListing.branches -> Omit<BranchSummary,'stats'> for type cohesion - add tests: branchSlug traversal containment, --branch mismatch reject, callTool branch threading, legacy-entry branch routing, status detached/stale * fix(review): address tri-review findings (abhigyanpatwari#2106) - P1 data-loss: a detached-HEAD re-analyze (CI's actions/checkout default) no longer strips the primary's meta.branch stamp; preserve it so a later branch analyze cannot claim & overwrite the flat/primary index. +cascade integration test - P2: capture validateBranchName's trimmed return for --branch so a whitespace-padded value no longer false-rejects on-branch or ghosts an index - F1: on a lost/rebuilt registry, a branch run reconstructs the primary top-level entry from the flat meta, not the feature branch's meta * fix(storage): only trust a non-empty-string flatMeta.branch (abhigyanpatwari#2106 R5) * fix(analyze): warn when the default branch is not the primary index (abhigyanpatwari#2106 R8) * fix(mcp): resolve --branch <primary> on a legacy unstamped flat index (abhigyanpatwari#2106 R4) * feat(cli): gitnexus clean --branch to remove a single branch index (abhigyanpatwari#2106 R7) * fix(mcp): evict orphaned branch pools on unregister/clean (abhigyanpatwari#2106 R3) * fix(analyze): union per-branch cache keys so a branch switch keeps shards (abhigyanpatwari#2106 R6) * fix(analyze): normalize the auto-detected branch label via sanitizeDetectedBranch (abhigyanpatwari#2106 R1) * fix(cli): skip AGENTS.md base_ref refresh for a non-primary branch fast path (abhigyanpatwari#2106 R2) * fix(storage): atomic writeRegistry + re-read-before-write to narrow the registry race (abhigyanpatwari#2106 R9) * refactor(storage): extract branch primitives to branch-index.ts (abhigyanpatwari#2106 R10)
…twari#2129, abhigyanpatwari#1858, abhigyanpatwari#1589/abhigyanpatwari#1852) (abhigyanpatwari#2136) * fix(query): stop impact()/context() under-reporting blast radius (abhigyanpatwari#2129, abhigyanpatwari#1858) Two read-side fixes to the "run impact before editing" safety workflow, both about the tools rendering "I could not give a single confident answer" as "no impact" — the most dangerous failure mode for a refactor-safety tool. abhigyanpatwari#2129 — ambiguous resolution no longer hides a real caller behind a bare `impactedCount: 0`. When a bare name collides with several symbols, the resolver returns `ambiguous`; previously the payload carried a flat `impactedCount: 0`, so the real caller (which calls a *different* same-name node) was invisible unless the user already knew to disambiguate. The ambiguous branch now runs a bounded, summary-only BFS per candidate (capped at 6) and surfaces each candidate's true count plus the top-level `maxImpactedCount` / `maxRisk`, ranked most-impactful-first. `risk` stays `UNKNOWN` (ambiguity must not read as "safe"), `impactedCount` stays 0 (no single resolved symbol). The BFS and edge storage are unchanged — an empirical repro confirmed they are correct; the bug was purely in how the ambiguous case reported. Disambiguation by uid still returns the exact result. abhigyanpatwari#1858 — impact()/context() now carry an additive `epistemic` field. When the queried symbol sits on an interface / indirection boundary (it implements or extends an interface, or is one) whose consumers bind via a DI container or dynamic dispatch, those callers are not traced to the concrete symbol, so the count is a lower bound. The result is annotated `epistemic: 'lower-bound'` with a human-readable `boundaries[]` note; a fully resolved leaf stays `epistemic: 'exact'`. Aligned to the surviving numeric confidence model (the 0.85 IMPACT_RELATION_CONFIDENCE heritage floor), not the long-deleted TIER_CONFIDENCE enum. Purely additive — no existing field or count changes. Tests: impact-ambiguous-blast-radius (per-candidate surfacing + uid disambiguation) and impact-epistemic-lower-bound (interface boundary → lower-bound, resolved leaf → exact, context parity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(routes): configurable fetch wrappers + faster consumer scan (abhigyanpatwari#1589/abhigyanpatwari#1852) Closes the residual gap behind the now-merged abhigyanpatwari#1852 (which fixed abhigyanpatwari#1589): the fetch-wrapper consumer scan only traced wrappers the parse phase auto-detected as calling the bare global `fetch()`. A wrapper built on axios / a custom client, or one named outside the built-in convention, was invisible — route_map silently returned `consumers: []` (the exact "named outside convention → silent zero" hole abhigyanpatwari#1858 calls out as needing a backstop). - Configurable wrappers: `.gitnexusrc` gains a `fetchWrappers: [...]` list (validated as identifier/member names, de-duped, capped, regex-safe), threaded AnalyzeOptions → PipelineOptions → routes phase. Configured names are unioned with the auto-detected ones; configured names alone now trigger the scan even when nothing was auto-detected. - Perf (F3 from abhigyanpatwari#1852's review): the cross-file scan built one RegExp per (file × wrapper) — O(files × wrappers). It now builds a single alternation regex per file (O(files)) and reuses file contents already read for handler extraction instead of re-reading them. Tests: configurable-fetch-wrapper (axios-based `doRequest` wrapper — invisible without config, traced with it) + .gitnexusrc `fetchWrappers` validation cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): harden the under-reporting fixes after adversarial review Addresses findings from a reviewer-swarm pass over the two prior commits: - CLI text false-safe (major): `formatImpactResult` (eval-server.ts) had no ambiguous branch, so `gitnexus impact <colliding-name>` printed "No dependencies found. This symbol appears isolated." for an ambiguous target — the exact false-safe abhigyanpatwari#2129 exists to kill, defeating the JSON-layer fix at the text surface. Added an ambiguous branch (per-candidate blast radius + maxImpactedCount/maxRisk) and a lower-bound branch for both the zero-count and non-zero paths, mirroring the context formatter. Covered by new unit tests. - Group fan-out dead work (major): impactByUid now passes skipEpistemic:true — the group cross-impact fan-out consumes only byDepth, so computing the abhigyanpatwari#1858 boundary per neighbor was wasted round-trips on the highest-volume path. - Ambiguous all-UNKNOWN risk (minor): if every per-candidate probe fails, maxRisk now reports 'UNKNOWN' instead of falling to the 'LOW' seed (which would read as "safe"). - Candidate-probe cost (minor): the per-candidate summary BFS now sets skipEnrichment:true, bypassing the process/module aggregation passes it does not use. - Epistemic latency (minor): computeEpistemicBoundary now runs concurrently with the impact BFS instead of as a trailing serial round-trip. - Wrapper over-match (minor): the consumer-scan regex uses a `(?<![.\w$])` lookbehind instead of `\b`, so a bare configured name like `get` matches the free call `get('/x')` but not a member access `client.get(` (and `apiFetch` no longer matches `myApiFetch`). - Boundary wording (nit): correct article ("a class" vs "an interface") and singular/plural ("1 implementation"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lint): drop unused describe import in new impact tests The withTestLbugDB harness wraps describe internally, so the explicit describe import was unused — unused-imports/no-unused-imports is an error (not a warning) in the root eslint config, failing quality/lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query): flag partialProbe when an ambiguous candidate probe fails (abhigyanpatwari#2129 review F1) The ambiguous-impact branch hoists maxRisk/maxImpactedCount so a colliding name can't read as "isolated". But if a per-candidate BFS throws (e.g. DB pool contention during the ≤6-way fan-out), it was recorded as risk:'UNKNOWN', impactedCount:0 and silently masked by any benign sibling success — maxRisk reduced to the benign tier and maxImpactedCount reflected only successful probes. Track probeFailed and surface partialProbe:true (additive, intentionally distinct from the traversal-interrupted `partial` flag); formatImpactResult prints a lower-bound warning. Covered by a formatter unit test (a natural in-harness probe throw is unreachable — _runImpactBFS is fully self-catching under summaryOnly+skipEpistemic+ skipEnrichment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query): report the full match count when ambiguous candidates are truncated (abhigyanpatwari#2129 review F11) The ambiguous candidate list is capped at AMBIGUOUS_MAX_CANDIDATES (6), but the CLI headline read the truncated `candidates[]` length — so a name matching 9 symbols printed "6 symbols share this name" while the JSON message stated the true count. Add an additive `totalCandidates` field carrying the full match count, include a "showing N of M" clause in the message when truncated, and have formatImpactResult report the full count. Covered by formatter unit tests for the truncated and non-truncated cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(query): run context() epistemic probe concurrently with methodMetadata (abhigyanpatwari#1858 review F2) impact() overlaps the abhigyanpatwari#1858 boundary probe with its BFS, but _contextImpl awaited computeEpistemicBoundary serially after every other query. Start the probe right after `symKind` is known (the earliest point it can — symKind depends on the incoming/outgoing round-trips) so it runs concurrently with the methodMetadata fetch, and await it at result assembly. Output is unchanged (covered by the existing epistemic context() tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query): flag a leaf interface as lower-bound in context() (abhigyanpatwari#1858 review F3) context() passed `symKind` to computeEpistemicBoundary, but symKind collapses a single-resolved Interface to 'Class' (resolvedLabel is '' on the single-candidate path), so the `symType === 'Interface'` self-boundary branch never fired and a directly-queried leaf interface (implements nothing, but consumed) was under-reported as 'exact'. Pass an interface-preserving type (`resolvedLabel || sym.type || symKind`) instead — enrichCandidateLabels runs before the single-candidate early return and patches sym.type to 'Interface', mirroring impact()'s derivation. impact() was already unaffected. Covered by a new context()-on-a-leaf-interface test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(query): hoist epistemic relation-type lists + add USES to the allowlist (abhigyanpatwari#1858/abhigyanpatwari#2129 review F4, F5) F4: promote computeEpistemicBoundary's function-local heritage/consumer relation-type lists to module-level readonly constants (EPISTEMIC_HERITAGE_RELATION_TYPES / EPISTEMIC_CONSUMER_RELATION_TYPES) next to VALID_RELATION_TYPES / IMPACT_RELATION_CONFIDENCE, so a future heritage edge type is visible to the probe. Kept as arrays (not Sets) because they bind as Cypher params. F5 (latent bug): USES is emitted (emit-references.ts) and already in the default impact relTypes + context() queries, but was missing from VALID_RELATION_TYPES — so impact({relationTypes:['USES']}) filtered to [] and silently ran the full default traversal. Add it (0.5 confidence fallback, matching FETCHES/WRAPS). Updates the security.test.ts allowlist assertions (size 15→16, USES now valid). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(query): document the _runImpactBFS enrichment skip-flag composition (abhigyanpatwari#1858/abhigyanpatwari#2129 review F6) The three skip-flags (skipPerSymbolEnrichment / skipEpistemic / skipEnrichment) suppress distinct sub-phases and compose implicitly. Add a JSDoc block at the opts type listing what each suppresses, the three real call patterns, and the key interaction (skipEnrichment makes skipPerSymbolEnrichment a no-op). Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): genericize the shared string-array validation messages (abhigyanpatwari#1589/abhigyanpatwari#1852 review F7) The shared `string-array` ValueKind hardcoded fetch-wrapper phrasing in three messages (non-array, identifier-shape, empty-list). Since `source` already names the config key, genericize all three so the shared normalizer carries no fetchWrappers coupling — a future string-array config key gets sensible errors. Test assertions updated to the new wording. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(query): type the ambiguous candidate summary + epistemicPromise (abhigyanpatwari#1858/abhigyanpatwari#2129 review F8) The ambiguous per-candidate summary was read through `any`, so a rename of _runImpactBFS's return fields would silently zero candidate counts. Name the read shape ({impactedCount, risk, summary?.direct}) at the narrowing site, and type epistemicPromise as the optional-epistemic union (the skip case's `{}` subtype) — keeping computeEpistemicBoundary's own return precise (epistemic required). Type-only; no runtime change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(routes): trust validated fetchWrappers config, drop redundant re-filter (abhigyanpatwari#1589/abhigyanpatwari#1852 review F9) `ctx.options.fetchWrappers` is already trimmed/shape-validated/de-duped/capped in analyze-config.ts, so the routes-phase re-trim/re-typeof pre-pass was redundant. Pass it straight through; the single Set-construction filter remains to guard the auto-detected functionName values (which don't pass through analyze-config). No behavior change — covered by the existing fetch-wrapper route suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(routes): make the wrapper-call boundary Unicode-aware (abhigyanpatwari#1852 review F10) The consumer-scan lookbehind used ASCII `\w`, so a configured bare wrapper name preceded by a non-ASCII identifier character (`caféget('/x')`) satisfied the boundary and produced a spurious FETCHES edge. Switch to the `u` flag with Unicode property classes (`(?<![.\p{L}\p{N}_$])`). Covered by a fixture consumer (`cafédoRequest('/api/things')`) asserting no spurious edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(routes): count wrapper-scan line numbers incrementally (abhigyanpatwari#1852 review F12) The wrapper consumer scan computed each match's line number via content.substring(0, match.index).split('\n').length — an O(matchIndex) allocation per match. Matches arrive in ascending index, so accumulate newlines with a running counter instead. 1-based line numbers are byte-identical (covered by the existing fetch-wrapper route suites). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): keep the abhigyanpatwari#1858 epistemic probe from skewing the impact-pagination mock The impact-pagination mock counts every query containing `r.type IN` as a BFS depth level. Once the abhigyanpatwari#1858 epistemic boundary probe was parallelized with the BFS (it fires `MATCH (x)-[r]->(iface) ... r.type IN $heritage` before the frontier loop), that query was miscounted as depth-1, shifting the real depths so multi-depth impactedCount read 50 instead of 200. Short-circuit the epistemic queries (uniquely aliased `iface`) to empty in both mock setups so only frontier queries count. Test-only; production is unaffected (the epistemic query is a separate real query there). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dlx (abhigyanpatwari#307) (abhigyanpatwari#2139) * fix(embeddings): resolve onnxruntime-common under pnpm-strict / pnpm dlx (abhigyanpatwari#307) `@huggingface/transformers` does a bare `import 'onnxruntime-common'` from its shipped `dist/transformers.node.mjs`, but never declares onnxruntime-common in its own `dependencies`. npm's flat node_modules (and pnpm with hoisting) place it on transformers' resolution path by accident; pnpm's isolated store only links a package's declared deps into its scope, so under pnpm-strict / `pnpm dlx` / `pnpx` the import dies with ERR_MODULE_NOT_FOUND before `analyze --embeddings` can run. Declaring onnxruntime-common in gitnexus' own deps (abhigyanpatwari#2074) does not fix this under pnpm: Node resolves the bare specifier from transformers' module scope, not ours, and overrides/resolutions can only re-version an existing edge, never add the missing one (verified against a real `hoist=false` install — the declaration only changes which version wins the hoist, never whether the import resolves). Fix: install a synchronous, in-thread ESM resolution hook (`module.registerHooks`) right before the lazy transformers import that redirects `onnxruntime-common` to the copy gitnexus depends on — but only when the default resolver fails. On npm / hoisted layouts the default resolver succeeds first and the hook never fires, so working setups are unchanged. The hook only intercepts the exact `onnxruntime-common` specifier on failure, so it can never mask an unrelated resolution error; onnxruntime-node's native binding still loads normally from transformers' own scope. `registerHooks` (sync, in-thread, single inline closure) is preferred over the older `module.register` (async, off-thread, now deprecated — DEP0205, removed in Node 26): the redirect is a one-line conditional that needs no worker thread, no separate hook module, and no `data` marshalling. It is available on Node >= 22.15; on older runtimes the helper is a graceful no-op (the gitnexus engines floor is >= 22.0.0, and the import still resolves on hoisted layouts there). Chosen over bundling transformers (the build is tsc-only, and transformers carries native onnxruntime-node + WASM onnxruntime-web assets that bundle poorly). Installation is idempotent, best-effort, and lazy — only on the local-embedding path, so it never affects analysis, the parse workers, or HTTP embedding mode. Validated end-to-end: the compiled resolver fixes a real pnpm `hoist=false` transformers install (ERR_MODULE_NOT_FOUND -> resolved). The separate `@ladybugdb/core` native-binary path under pure `pnpm dlx` is unchanged (abhigyanpatwari#1967 handles that gracefully). Refs abhigyanpatwari#307, abhigyanpatwari#2069 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(embeddings): version-match the onnxruntime-common redirect target (abhigyanpatwari#307) Prefer the onnxruntime-common that onnxruntime-node (the native binding transformers actually loads) depends on, so the redirected copy is version- matched to that binding even under `pnpm dlx` — where gitnexus' npm-style `overrides` block does not apply, because it is honoured only from a root manifest and gitnexus is a transitive dependency there. The walk resolves transformers' main entry (not its `exports`-blocked package.json) -> onnxruntime-node -> its onnxruntime-common, and falls back to gitnexus' own direct dependency when the chain can't be walked. Also corrects the doc comment that claimed the gitnexus copy was already "version-aligned". Addresses a PR abhigyanpatwari#2139 tri-review finding (P2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(embeddings): narrow the onnxruntime-common resolve fallback to absence errors (abhigyanpatwari#307) The resolve closure's `catch` swallowed every error from `nextResolve` and redirected, which would silently paper over a genuinely present-but-broken onnxruntime-common install. Only substitute gitnexus' copy when the specifier is actually absent (ERR_MODULE_NOT_FOUND, or ERR_PACKAGE_PATH_NOT_EXPORTED for an exports-broken copy); rethrow anything else. Adds a test that an unrelated error code rethrows. Addresses a PR abhigyanpatwari#2139 tri-review finding (P3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(embeddings): cover the onnxruntime-common resolver best-effort swallow path (abhigyanpatwari#307) The outer try/catch in ensureOnnxRuntimeCommonResolvable() was untested. A throwing registerHooks spy drives it; the call must not throw (initEmbedder does not guard the return, so a throw would break `analyze --embeddings`). The vitest quirk that surfaced an earlier attempt applies to throwing mock factories, not a throwing spy implementation, so this is testable cleanly. Addresses a PR abhigyanpatwari#2139 tri-review finding (P2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(embeddings): tighten the onnxruntime-common redirect-URL assertion (abhigyanpatwari#307) `/^file:\/\/.*onnxruntime-common/` matched the substring anywhere, so a lookalike path (e.g. `/x/onnxruntime-common-fake/`) would pass. Require an actual `/node_modules/onnxruntime-common/...js` segment so the assertion proves the redirect resolves to the real package, not just a string match. Addresses a PR abhigyanpatwari#2139 tri-review finding (P3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(embeddings): drop the no-op __resetOnnxRuntimeCommonResolverForTests seam (abhigyanpatwari#307) The test helper reloads the resolver via vi.resetModules() + a fresh import(), which already re-initialises the module-level one-shot `attempted` flag to false. The __reset export it then called was therefore a no-op. Remove the test-only export and its call; isolation now rests solely on vi.resetModules(). Addresses a PR abhigyanpatwari#2139 tri-review finding (P3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(embeddings): correct the onnxruntime-common resolver isolation comment (abhigyanpatwari#307) The doc comment claimed the hook "never affects other tools' resolution". Once installed, `module.registerHooks` is process-global and its resolve closure runs for every subsequent resolution — it passes them all through untouched and only substitutes the exact `onnxruntime-common` specifier on genuine absence, at a cost of one string comparison. Also note `registerHooks` is @experimental and requires Node >= 22.15 (graceful no-op below that). Comment-only. Addresses a PR abhigyanpatwari#2139 tri-review finding (P3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e doesn't crash (abhigyanpatwari#2112) (abhigyanpatwari#2135) * fix(parse): survive non-cloneable worker results so large-repo analyze doesn't crash (abhigyanpatwari#2112) A parse worker delivers its accumulated result to the main thread via postMessage, which structured-clones the payload synchronously on the worker thread and throws a DataCloneError on the first value it can't serialize. The reporter's case was a node `properties` value pointing at a native `toString`. The worker re-posted the throw as {type:'error'}, the pool counted it as a worker death, and under GITNEXUS_WORKER_POOL_SIZE=1 the same graph re-threw on every respawn until the slot's budget was exhausted and the whole parse phase aborted -- defeating even the conservative single-worker workaround. Add a clone-safety net at the worker result boundary. On a clone failure the worker isolates the offending file, strips the non-cloneable value from a plain extraction record (keeping the record -- strictly-missing data, never wrong) or drops a whole ParsedFile so scope-resolution re-derives it on the main thread with intact edge data, records the affected paths on the result, warns naming the field + file so the leak is diagnosable, and re-posts. Healthy runs are byte-identical: the net runs only after a real DataCloneError, so there is zero overhead on the fast path. Skipped paths surface via the parsing processor alongside the skipped-language telemetry. The strip drops the same values the store path's JSON.stringify already silently removes, so store/no-store runs converge. Scope: PR-1 -- failure mode C, the deterministic POOL_SIZE=1 killer. The timeout/native-abort graceful-degradation cascade (failure modes A & B) is coupled to downstream-exclusion + a hard worker watchdog and is tracked as follow-up work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): fail-closed clone-safety recovery + bound recursion depth (abhigyanpatwari#2135 review) The clone-safety recovery path could re-arm the abhigyanpatwari#2112 worker-death cascade it was built to prevent: in postResultCloneSafe the sanitizer call and the re-post sat outside the try/catch, and containsNonCloneable/stripNonCloneable recursed with a cycle guard but no depth bound. A throw inside the sanitizer (a RangeError from a deeply-nested record, reproduced at depth >=3000) escaped to the message handler's {type:'error'}, which under GITNEXUS_WORKER_POOL_SIZE=1 is the respawn-budget-exhaustion abort. Wrap the sanitizer + re-post in their own try/catch so any throw fails closed to a primitive-only {type:'error'} deliberately, and thread a MAX_CLONE_DEPTH bound through both scan/strip functions so an over-deep subtree is treated as non-cloneable (dropped/undefined) instead of overflowing the stack. The isStructuredCloneable catch-all is left broad on purpose — it bounds structuredClone's own internal recursion in the non-plain-object probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): harden clone-safety against throwing getters and detached buffers (abhigyanpatwari#2135 review) Two sanitizer-defeat vectors let the re-post throw a DataCloneError again: - A throwing getter on a record: containsNonCloneable/stripNonCloneable read obj[key], so a getter that throws escaped the scan/strip pass. Read defensively — a throwing property read is treated as non-cloneable (scan returns true, strip drops the property). - A detached ArrayBuffer/TypedArray: both passed buffers/views through unconditionally, but structuredClone rejects a detached one, so the re-post threw. Route buffers/views through the authoritative isStructuredCloneable probe instead. No byteLength heuristic — a legitimately empty new Uint8Array(0) also has byteLength 0 yet clones fine, so a length check would false-positive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): memoize stripped copies so DAG-aliased records aren't over-dropped (abhigyanpatwari#2135 review) stripNonCloneable carried a shared `seen` WeakSet and returned the ORIGINAL (un-stripped) value on revisit. When a non-cloneable was reachable via two paths (a DAG), the second path spliced the original function-bearing object back into the output, so the rebuilt element failed the last-resort isStructuredCloneable guard and the whole record was dropped as "unsalvageable" — contradicting the "record kept, value stripped" contract. Replace the WeakSet with a Map<object, stripped-copy>: allocate the empty copy, memoize it before recursing into children (so cycles return the in-progress copy), and return the memoized copy on revisit. DAG-aliased subtrees now collapse to one shared stripped copy and are kept-and-stripped, not dropped. The array branch moves from .map() to allocate-then-push so its identity can be pre-inserted. Object Map/Set keys aren't identity-preserved across stripping — acceptable because parse-result Maps are primitive-keyed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(parse): single-pass clone-safety scan preserving array identity (abhigyanpatwari#2135 review) makeWorkerResultCloneSafe scanned each dirty array twice — a field-level whole-array containsNonCloneable probe, then a per-element pass — and always reassigned the field. Fold into one per-element pass that builds the output array lazily (copying the clean prefix only once the first dirty element appears) and reassigns the field only when something changed. A fully-clean array is now scanned once and keeps its referential identity; the clean prefix of a dirty array is copied by reference. Behavior is otherwise identical (failure-path-only code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(parse): drop unused generic + pin clone-safe field names to keyof (abhigyanpatwari#2135 review) makeWorkerResultCloneSafe carried a generic `<T extends Record<string,unknown>>` that was never load-bearing (it mutates in place and returns {skipped}), and the call site passed untyped string-literal option sets — so renaming `parsedFiles` or `skippedPaths` would silently disable the drop-whole / skip protection. Drop the generic (plain `Record<string,unknown>` param) and type the option sets at the call site as `Set<keyof ParseWorkerResult>`, so a field rename is now a compile error. The `as unknown as Record<string,unknown>` widening stays — it's the standard cast for a no-index-signature interface (TS rejects a single-step `as`); the function genuinely operates structurally on the result's arrays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): keep the per-file reason in the clone-safety skip log (abhigyanpatwari#2135 review) The processor's skipped-file warning logged only the paths, dropping the per-file reason the worker already attached — losing the distinction between a recoverable "stripped N value(s)" and a whole-record "dropped" entry. Format each entry as `path (reason)` so the aggregate line carries the diagnostic detail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): deterministic findFilePath attribution for ParsedNode (abhigyanpatwari#2135 review) findFilePath swept all child objects one level deep in Object.keys order, so a ParsedNode could be attributed to a sibling child's path-like key instead of its real path at properties.filePath. Check the known `properties` child first, then fall back to the generic sweep, so node attribution is deterministic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): zero skippedPaths in the slim cache result (abhigyanpatwari#2135 review) slimParseWorkerResultsForCache spread the worker result without clearing the clone-safety skippedPaths telemetry, so a sanitized result persisted its skip list into the on-disk parse-cache shard. Replay already ignores the field; zero it (like calls/assignments/parsedFiles) to keep shards lean and the intent explicit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): exercise real postResultCloneSafe wiring + tighten RED control (abhigyanpatwari#2135 review) The integration GREEN worker re-implemented postResultCloneSafe inline, so the production wiring (the {type:'warning'} post + the skippedPaths append) had no coverage, and the RED control asserted a bare .rejects.toThrow() that any failure would satisfy. Extract postResultCloneSafe into a side-effect-free module (post-result.ts) — importing it from the parse-worker entry module would construct the parser, post ready, and attach the real handler — and have the GREEN test worker import and call the real one. Tighten the RED matcher to the actual abort contract (/circuit breaker|consecutive failures|respawn budget|could not be cloned/), which also documents that the raw poison result aborts via the pool's consecutive-failure circuit breaker under POOL_SIZE=1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): recover the clone-safety net from any post failure, not only DataCloneError (abhigyanpatwari#2135 review) The V8 structured-clone research surfaced the net's one real correctness hole: structuredClone invokes getters, and a getter that THROWS surfaces its own error (a RangeError, etc.) — NOT a DataCloneError (confirmed against a real MessageChannel). postResultCloneSafe gated recovery on isDataCloneError, so such a throw re-threw past the sanitizer and re-armed, under POOL_SIZE=1, the worker-death cascade the net exists to prevent. Attempt the sanitize + re-post recovery for ANY first-post failure (the sanitizer already reads properties defensively, so a throwing getter is dropped), falling closed to a primitive-only {type:'error'} only if the re-post still fails. Adds an integration case: a node with a throwing getter is recovered and delivered, not re-thrown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(parse): name the exact offending key path in the clone-skip diagnostic (abhigyanpatwari#2135 review) The clone-safety net's skip reason named only the array field + file ("stripped 1 value from nodes"), not the offending property key — which is precisely why the original abhigyanpatwari#2112 leak stayed unpinned. Thread a dotted key path through stripNonCloneable (recording each stripped value's path: properties.toString, meta.data[3], …) and surface the first few in the reason ("from nodes: properties.toString"). Now a single log line — or the contract/strict checks — names the leaking property, so a residual runtime escape can be fixed at source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): clone contract — a representative ParseWorkerResult is structured-cloneable (abhigyanpatwari#2135 review) Shape-regression guard: builds a representative ParseWorkerResult (typed as the real interface) and asserts isStructuredCloneable. Typing it as ParseWorkerResult makes adding a new boundary field a compile error here until the test is updated, and the runtime assert catches a field whose type regresses to a non-cloneable shape — independent of language input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(parse): strict-mode clone gate (GITNEXUS_STRICT_CLONE) — fail loudly instead of silent sanitize (abhigyanpatwari#2135 review) The runtime net's silent recovery in production is exactly what let the original abhigyanpatwari#2112 leak stay unpinned. Add an opt-in strict mode (GITNEXUS_STRICT_CLONE=1, inherited by workers): on a clone failure, postResultCloneSafe THROWS with the exact offending key path instead of sanitizing + delivering, so a leak introduced by a future provider/extractor change fails loudly at its origin (CI/dev) rather than being quietly stripped. Off in production, where the net keeps the run alive. Adds a self-contained integration case (sets the flag, asserts the poison run rejects with the key path) and skips the synthetic-poison suite under a global strict run (its value there is running the REAL-extractor integration tests under strict). Wiring a strict CI lane (GITNEXUS_STRICT_CLONE=1 on a vitest integration step) is left to the maintainer — it needs a green full-suite verification and touches the protected workflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): don't ship pipelineResult across the analyze-worker IPC boundary (abhigyanpatwari#2112) The forked analyze worker reports completion to the parent over child_process IPC, which uses Node's DEFAULT 'json' serialization (api.ts forks with no `serialization:` option). `AnalyzeResult.pipelineResult` is populated on every successful analysis and carries `pipelineResult.graph` — the live KnowledgeGraph closure object. Sending the raw result is wrong three ways: (1) the graph's nodes/relationships getters force-materialize the entire graph into two arrays and JSON-stringify them on every analyze, discarded immediately (a multi-hundred-MB no-op on a large repo — the abhigyanpatwari#2112 scenario); (2) the graph's methods are own function properties that JSON drops silently, so a surviving graph is a husk whose forEachNode() throws far from the cause; (3) a BigInt/circular value anywhere in the payload makes process.send throw TypeError synchronously — caught and re-sent as {type:'error'}, mis-reporting a SUCCESSFUL analysis (DB already written) as a FAILURE. This is the abhigyanpatwari#2112 failure family on the server path, and unlike the parse-worker result boundary it has no clone-safety net. The parent (api.ts) reads only result.repoName; pipelineResult's real consumers (CLI skill generation, cli/analyze.ts) call runFullAnalysis in-process and never cross this fork. So project the result down to an explicit JSON-safe allowlist of scalar fields. Typed as Omit<AnalyzeResult,'pipelineResult'> so a future non-serializable field added to AnalyzeResult fails to compile until handled here deliberately. Found by the abhigyanpatwari#2112 cross-process serialization-boundary audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): Cloneable<T> + assertCloneable() compile-time clone-boundary guard (abhigyanpatwari#2143) The runtime clone-safety net is the production backstop; this is its compile-time complement. The worker result is plain data except a few `unknown`-typed sinks (a node's `properties` bag, the provider `extractTemplateConstraints` / `collectCaptureSideChannel` hook returns) — `unknown` lets a non-serializable value (a function, a leaked tree-sitter SyntaxNode, …) cross the structured-clone boundary with no compile-time guard. That is the structural hole abhigyanpatwari#2112 leaked through. `Cloneable<T>` is a homomorphic recursive mapped type that maps a function or symbol member to `never`, so a struct carrying one is no longer assignable to its own `Cloneable<T>`. `assertCloneable(value)` is a runtime identity (zero cost) whose parameter is `T extends Cloneable<T> ? T : Cloneable<T>`, so a clone-unsafe argument fails to compile, naming the offending key. Because it is a homomorphic mapped type it preserves `interface` shapes and `readonly` modifiers and needs NO index signature on the payload types — this sidesteps the "closed interface is not assignable to a recursive index-signature type" wall that blocked the original value-typed-`Cloneable` attempt (the reason abhigyanpatwari#2143 was deferred from PR abhigyanpatwari#2135). The conditional parameter type avoids the `T extends Cloneable<T>` circular-constraint error. Tests: runtime identity contract, plus type-level @ts-expect-error assertions (enforced by tsconfig.test.json) that a function/symbol member is rejected and clean interface payloads are accepted. Applied to the real provider hooks in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): guard provider clone-boundary hooks with assertCloneable (abhigyanpatwari#2143) Apply the compile-time guard to the provider hooks that feed the `unknown`-typed worker-result sinks, so a future non-serializable value in their payloads is a compile error at the source site rather than a runtime DataCloneError at the worker post: - C++ extractTemplateConstraints (CppConstraintPayload) - C++ collectCaptureSideChannel (CppCaptureSideChannel) - C collectCaptureSideChannel (CCaptureSideChannel) - Kotlin collectCaptureSideChannel (KotlinCaptureSideChannel) The C++ template-constraint adapter previously returned `unknown`; it now returns the concrete `CppConstraintPayload | undefined` and routes its payload through `assertCloneable`. The side-channel hooks are wrapped at their provider wiring sites. `assertCloneable` is a runtime identity, so behavior is unchanged (C static-linkage + C++ constraint suites stay green); the guarantee is the type-check — src tsc now proves every nested member of those real payload trees is structured-clone safe. Test: type-level assertions (enforced by tsconfig.test.json) that each concrete payload type is `Cloneable<T>`, INDEPENDENT of the provider wiring — so the regression is caught even if the assertCloneable wrapper is later removed. Proven non-vacuous (a function-bearing type fails the same assertion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): scan an array's non-index own properties in the clone sanitizer (abhigyanpatwari#2135 review) structuredClone serializes an array's NON-index own-enumerable properties (e.g. `arr.meta = fn`) and throws DataCloneError on a non-cloneable one. The clone sanitizer's array branches iterated numeric indices only, so such an array was waved through (containsNonCloneable returned false, makeWorkerResultCloneSafe left the field unrewritten with skipped:[]) — the re-post then threw, fell through to the fail-closed {type:'error'}, and re-armed the POOL_SIZE=1 cascade the net exists to prevent. Add isArrayIndexKey() and, in BOTH containsNonCloneable and stripNonCloneable array branches (kept in lockstep), scan/strip the non-index own-enumerable keys after the index loop. A cloneable non-index prop is carried onto the stripped copy; a non-cloneable one is stripped and recorded. Not reachable from current parse output (no extractor attaches non-index array props) — a defense-in-depth hole closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): contain a throw inside the clone sanitizer instead of escaping to fail-closed (abhigyanpatwari#2135 review) findFilePath was documented "never throws" but read element properties unguarded in its generic sweep — a throwing getter at a non-path key (or a Proxy with a throwing ownKeys trap) threw out of makeWorkerResultCloneSafe, past postResultCloneSafe's recovery, to the fail-closed {type:'error'} that under POOL_SIZE=1 re-arms the cascade the net prevents. Likewise a Proxy with a throwing getPrototypeOf trap throws inside containsNonCloneable's instanceof checks. - findFilePath/pathFromChild now read via safeGet (try/catch) and guard Object.keys, honoring the "never throws" contract. - Each element's sanitize in makeWorkerResultCloneSafe is wrapped: a throw during scan/strip drops that one element (recorded as "sanitizer error") rather than sinking the whole result — so one pathological element can't fail-close the run. - Corrected the makeWorkerResultCloneSafe JSDoc ("ONLY after a DataCloneError" → after ANY post failure, matching the caller) and documented the deliberate failure-path double-traversal (the non-allocating pre-scan is what preserves clean-element referential identity). Tests: a throwing getter on a path-less element is stripped & delivered (not escaped); a Proxy structural-trap element is dropped, clean siblings survive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse): add a final cloneable postcondition gate to the clone sanitizer (abhigyanpatwari#2135 review) makeWorkerResultCloneSafe rewrote only ARRAY result fields, so a future non-array sink (a nested object / Map result field) carrying a non-cloneable value — or an array field whose own non-index property the element loop didn't reach — would survive the sanitizer and throw on the re-post. Add a final `if (!isStructuredCloneable(result))` gate that strips any remaining offending field in place, making "the returned result is structured-cloneable" a hard postcondition independent of future ParseWorkerResult shape. Failure-path-only and a no-op once the array loop already made the result clean (the per-field probe short-circuits every clean field, so it adds no work or skip entries then). Tests: a function on a non-array result field is stripped & the result becomes cloneable; the gate adds no skip entry when the array loop already cleaned up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(parse): reject an `any`-typed member in the Cloneable<T> compile-time guard (abhigyanpatwari#2135 review) `Cloneable<any>` previously resolved to `any` (not `never`), so a payload with an `any`-typed member — the most likely escape hatch, since `unknown` is already blocked — passed `assertCloneable` with no compile error. Add an `IsAny<T>` branch (the canonical `0 extends 1 & T` probe) as the FIRST arm so `any` resolves to `never`, matching how `unknown` is already rejected. It must precede the primitive arm: `any extends CloneablePrimitive` would otherwise resolve to `any` and re-admit it. The IsAny-first arm perturbs inference for a bare `undefined` literal argument (T infers as `unknown` → never); real consumers pass `X | undefined` unions (the provider hooks), which are unaffected (src tsc clean), so the runtime identity test now uses a `string | undefined` value — the realistic shape. Tests: an `any` member fails `assertCloneable` (@ts-expect-error, enforced by tsconfig.test.json) and `Cloneable<any>` resolves to `never` at the type level. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): type the analyze-worker IPC projection as a Pick allowlist, not Omit (abhigyanpatwari#2135 review) `AnalyzeResultIpc = Omit<AnalyzeResult,'pipelineResult'>` kept every other field in the type — including optional ones like `isPrimaryBranch?` — so the type advertised a field the runtime allowlist never sends, and the doc-comment's "a future field fails to compile until handled here" only held for REQUIRED fields. Switch to `Pick<AnalyzeResult, …the six scalar fields…>`: the allowlist IS the type, so the projection return literal is exhaustive by construction (omitting a key is a compile error) and a new `AnalyzeResult` field is simply absent from the wire until deliberately added here. `isPrimaryBranch` is intentionally excluded (nothing consumes it server-side over this fork; the parent reads only `repoName`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(parse): remove the now-dead isDataCloneError export (abhigyanpatwari#2135 review) postResultCloneSafe recovers on ANY fast-path post failure and never inspects the error type (a throwing getter surfaces a RangeError, not a DataCloneError — gating on the type was the original net-gap bug). isDataCloneError has no production caller; it was only exercised by its own unit test. Remove the function and that test block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(parse): use the exported SkippedPath type in parsing-processor (abhigyanpatwari#2135 review) The clone-safety telemetry accumulator inlined `Array<{path,reason}>` — a structural duplicate of the exported `SkippedPath`. Import and use the canonical type so a future rename of its fields is a compile error here instead of a silent structural drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(parse): document the cloneable-return contract on the worker-boundary hooks (abhigyanpatwari#2135 review) extractTemplateConstraints and collectCaptureSideChannel return `unknown` and feed values across the worker structured-clone boundary, but the hook contracts didn't state the cloneability requirement — a future language implementing them without care could leak a non-serializable value. Document that the return MUST be structured-clone-safe and should be wrapped with assertCloneable, so the guarantee is a compile error at the source (abhigyanpatwari#2143). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): assert the clone-skip telemetry surfaces in the GREEN integration case (abhigyanpatwari#2135 review) The GREEN clone-safety integration test asserted only graph content (all files present), not that the skippedPaths / {type:'warning'} wiring its docstring claims to cover actually fired. Capture the production logger via _captureLogger and assert the sanitize telemetry names the offending file (poison.ts) AND the exact stripped key path (properties.toString) — proving the worker's skippedPaths append + the parsing-processor warning surfaced end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(server): cover the IPC projection against a real KnowledgeGraph (abhigyanpatwari#2135 review) The IPC projection tests used a hand-built hostile object. Add a case that puts a real createKnowledgeGraph (whose nodes/relationships getters would materialize the whole graph under JSON.stringify) in pipelineResult and asserts the projection drops it entirely — the serialized payload stays under 300 bytes (a materialized 50-node graph would be thousands), with the scalar fields intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): cover the unsalvageable-drop branch and the skippedPaths merge union (abhigyanpatwari#2135 review) Two untested clone-safety branches from the tri-review: - "dropped unsalvageable": a dirty element whose stripped copy is STILL not structured-cloneable must be dropped, not delivered (else the re-post throws). Add a deterministic test (a non-plain member with a stateful getter that the strip-time probe sees clean but that turns into a function on the post-strip verification) asserting the element is dropped and the run survives. - mergeResult skippedPaths union across sub-batches. mergeResult (and its appendAll helper) was module-private in the parse-worker ENTRY module, which a main-thread test can't import (it runs MessagePort setup). Extract it to a side-effect-free result-merge.ts (mirroring post-result.ts) and unit-test the union (including the `??=` target-init path), the skippedLanguages sum, and array append. parse-worker imports it back; verified the built worker still parses + merges via the real-worker integration path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(parse): root-prettier format the clone-safety review-fix files (abhigyanpatwari#2135 review) Clears the failing `quality / format` CI gate (root prettier, not the gitnexus-local config). Reformats the pre-existing abhigyanpatwari#2143 wrapping lines in c-cpp.ts + kotlin.ts plus the clone-safety review-fix files touched in this PR-update (clone-safety.ts and the new/updated tests). Formatting-only — no behavior change; tsc, the type-level assertions (tsconfig.test.json), and the unit + integration suites stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parse): avoid js/trivial-conditional in the type-level clone assertions (abhigyanpatwari#2135 review) CodeQL flagged the `expect(a && b && c).toBe(true)` lines in the type-level test assertions as js/trivial-conditional: after type erasure the operands are constant `true`, so the `&&` chain always evaluates the same. Replace the `&&` chain with array equality (`expect([...]).toEqual([true, ...])`) — no conditional, and the real assertions remain the `const x: …IsNever = true` / `: IsCloneable<…> = true` annotations (enforced by tsconfig.test.json, which fail to compile if a guard regresses). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olute path (abhigyanpatwari#2111) (abhigyanpatwari#2144) * fix(grammars): load vendored tree-sitter grammars from vendor/ by absolute path (abhigyanpatwari#2111) The recurring Windows `EPERM: operation not permitted, symlink` (errno -4048) when adding the MCP server to Antigravity is NOT the abhigyanpatwari#2101/abhigyanpatwari#2110 module-load crash — it is an install-time arborist failure during the `_npx` reify that the MCP client triggers on every `npx gitnexus` launch. Root cause: the `postinstall` materialize step copied each vendored grammar (`vendor/tree-sitter-{c,dart,proto,swift,kotlin}`) into `node_modules/gitnexus/node_modules/tree-sitter-*` as a real package so runtime `require('tree-sitter-dart')` would resolve. Those packages are in no dependency graph, so every subsequent npm/npx reify treats them as **extraneous** and prunes/relocates them — on Windows the relocation goes through `@npmcli/move-file`'s symlink path and throws EPERM (symlinks need Developer Mode/admin), and on every OS the 2nd run silently deletes the grammars. This is the same class as abhigyanpatwari#1728, which the materialize step itself claimed to have fixed. Fix (the prebuildify + node-gyp-build ecosystem pattern): never copy grammars into node_modules. Load each by absolute path from `vendor/<name>` via the new `requireVendoredGrammar` helper — the grammar's own `bindings/node` runs `node-gyp-build(<dir>)` and loads the committed `vendor/<name>/prebuilds/ <platform>-<arch>/…` directly (all 5 ship all 6 tuples). vendor/ is inside the package but not a node_modules subtree, so arborist never sees the grammars and the reify is idempotent — no EPERM, no silent deletion. - new src/core/tree-sitter/vendored-grammars.ts (requireVendoredGrammar / vendoredGrammarDir / VENDORED_GRAMMAR_PACKAGES; VENDOR_ROOT stable in dev+dist) - route all consumers through it: parser-loader, parse-worker, grpc proto, include-extractor (C), http-patterns kotlin, cli optional-grammars probe - postinstall drops the materialize step; build-tree-sitter-grammars.cjs builds in-place under vendor/ (gitignored) and deletes materialize-vendor-grammars.cjs - tests + grammar-introspection helper load grammars from vendor/ too (single source of truth); new vendored-grammars.test.ts guards against reintroducing a bare `require('tree-sitter-<vendored>')` Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(grammars): throw on a non-vendored name in requireVendoredGrammar Drift guard (PR abhigyanpatwari#2144 review, P3): validate the argument against VENDORED_GRAMMAR_PACKAGES and fail loudly on an unknown name, so the three grammar lists (package set / CLI probe / build registry) drifting out of sync surfaces as a clear error instead of a confusing absolute-path require miss. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(grammars): prepack guard against stray vendor/<g>/build/ shadowing prebuilds Publish hygiene (PR abhigyanpatwari#2144 review, P2). Now that build-tree-sitter-grammars.cjs source-builds into vendor/<name>/build/, a stray build dir would ship in the tarball (files:["vendor"] overrides .gitignore/.npmignore) AND shadow the committed prebuild — node-gyp-build resolves build/Release before prebuilds/. assert-publish-grammar-coverage.cjs (prepack) now fails `npm pack` if any vendor/*/build exists (findStrayBuildArtifacts), with a clear `rm -rf` fix hint. Adds unit coverage for the new pure function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(grammars): harden the abhigyanpatwari#2111 no-bare-require regression guard PR abhigyanpatwari#2144 review (P2). The guard regex missed dynamic import(), side-effect `import 'x'`, /subpath, and backtick loads, and only scanned src/. It now covers every node_modules-forcing form (single/double/backtick quotes, optional subpath), scans test/ too (excluding fixtures and the guard file itself), drops the `//`-substring false-negative (leading-comment-only heuristic), and adds a self-test asserting every load form is caught while prose mentions and tree-sitter-cpp are ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(grammars): correct stale vendored-grammar comments PR abhigyanpatwari#2144 review (P3). kotlin/query.ts called tree-sitter-kotlin an "optionalDependency" — it is vendored and loaded from vendor/ by absolute path (abhigyanpatwari#2111). proto.ts now states its remaining `_require` is only for the real `tree-sitter` dependency, not a vendored grammar (which goes through requireVendoredGrammar). Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…patwari#2124) * fix(storage): prevent registry wipe on transient I/O errors listRegisteredRepos({ validate: true }) used a bare catch {} that treated ALL fs.access() errors as 'index gone.' Under swap pressure or I/O storms, EIO/EAGAIN/EBUSY/EACCES errors caused ALL entries to be pruned and writeRegistry([]) was called — permanently wiping the registry. Fix: only prune on ENOENT (file genuinely gone) or ENOTDIR (structural removal). Transient errors keep the entry alive. Includes 5 regression tests covering ENOENT, ENOTDIR, EACCES, EIO, and EAGAIN. * test(storage): point registry transient-error test at the right PR (abhigyanpatwari#2124) The describe() title cited abhigyanpatwari#2121, which is the unrelated prebuildify CI fix (drop broken -t 22 from prebuildify), not the registry-wipe bug. No dedicated issue exists for this fix, so reference PR abhigyanpatwari#2124 instead so git blame / bisect readers land on the actual change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(storage): remove unused os import (CodeQL alert 693) The os import was never referenced. Removes the code-scanning unused-import alert and the PR autofix finding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(storage): cover partial prune, on-disk persistence, and EBUSY The original bug was about *persisting* the wrong registry list, but the tests only checked the in-memory return value of a single-entry registry. Add coverage for the paths that actually exercise persistence: - mixed-batch partial prune: register two repos, fail one with ENOENT and the other with EIO in the same validation call, then read registry.json off disk and assert exactly the EIO survivor was persisted (not [] from over-prune, not both from a no-op). This is the off-by-one path. - assert the on-disk registry is unchanged in the EACCES/EIO/EAGAIN keep tests (the keep path must not rewrite/shrink the file). - assert the ENOENT prune is persisted ([] written) as a regression guard. - add the EBUSY keep case named in the source comment but previously untested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(storage): clarify the keep-branch comment (EACCES may be permanent) The previous comment called EACCES "transient," but EACCES is often permanent (e.g. a chmod'd directory). Reframe the comment around the actual decision rule — prune only when the index is provably gone (ENOENT/ENOTDIR), keep on everything else — and note that keeping a possibly-permanent error is still the correct conservative choice (a stale entry is harmless and removable; an over-prune destroys data). Comment-only; behavior unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(storage): warn when keeping a registry entry on a non-fatal fs error The keep branch was silent, so an I/O storm that keeps entries alive (the whole point of the fix) was invisible in logs. Emit a structured logger.warn naming the entry and the fs.access error code on the keep path only. Observability-only: the keep/prune decision is unchanged and the warn cannot throw. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(storage): describe listRegisteredRepos validate semantics accurately The doc comment said validation checks each entry's .gitnexus/ "still exists," which no longer matches the keep-on-transient behavior. Spell out that validation prunes only provably-gone indexes (ENOENT/ENOTDIR) and keeps entries that are merely not provably absent — so a kept entry is "not confirmed present," not "confirmed present." Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(storage): prettier-format the transient-error test imports Collapse the multi-line repo-manager import to a single line per Prettier, clearing the PR autofix formatting finding. Formatting-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: buihongduc132 <buihongduc132@gmail.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cpp): suppress deleted overload winners * test(cpp): update scope capture fingerprint --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
…ari#2081) (abhigyanpatwari#2099) * feat(cfg): language-agnostic CFG construction core (abhigyanpatwari#2081) U1 of M1 (CFG layer). Plain JSON-serializable CFG data model (BasicBlockData/ CfgEdgeData/FunctionCfg — must survive the worker→main boundary + ParsedFile store), a CfgBuilder accumulator (leaders→blocks→edges, synthetic ENTRY/EXIT, idempotent edges), a ControlFlowContext (break/continue/switch + labeled-jump target stacks), and a TraversalResult ({entry, dangling exits}). AST-agnostic and unit-tested on the classic control-flow topologies (if/else, while back-edge, mid-block return, labeled break/continue) the S2 spike validated; reachability helper backs the R9 property test. * feat(ingestion): U2 — TS/JS CFG visitor over tree-sitter AST (abhigyanpatwari#2081) Add the TS/JS CfgVisitor that walks a function's tree-sitter AST and drives the U1 CfgBuilder to produce a serializable FunctionCfg. One visitor covers both languages (shared grammar family). Handles the classic CFG hazards explicitly (R2, R10): - loops allocate a dedicated loop-exit block so `break` has a concrete target before the loop's successor is known; `continue`/back-edge close the loop (while, do-while, C-for with init-once + increment-as-continue-target, for-in, for-of) - switch fallthrough falls out naturally: a non-breaking case yields exits we wire to the next case as `fallthrough`; a breaking case wires to the switch exit via ControlFlowContext - try/catch/finally: normal completion AND exceptional flow both route through finally (post-domination); a conservative exceptional edge models that the protected region may raise to its handler (not just explicit `throw`) - labeled break/continue resolve against the labeled loop's frame - early return/throw wire to EXIT/handler and terminate their block 19 hazard tests (one per construct) + AC1 10-function fixture; all green. No change to the committed U1 core or ControlFlowContext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): U3 — worker CFG build + cfgSideChannel + cache coherence (abhigyanpatwari#2081) Run the CFG visitor in the parse worker (where the AST lives), serialize the per-function CFG onto a new ParsedFile.cfgSideChannel, and keep it coherent across the disk-backed store and the warm/durable parse cache (R3, R4). - gitnexus-shared parsed-file.ts: add `cfgSideChannel?: unknown` as a DISTINCT field from captureSideChannel (different producer/consumer/lifecycle; plain JSON data — blocks/edges deliberately lack the `nodeId` the store's interning reviver keys on, so no mis-interning). - cfg/types.ts + visitors/typescript.ts: add CfgVisitor.isFunction so the worker enumerates functions (and applies the line budget) by a cheap node-type test. - cfg/collect.ts (new): collectFunctionCfgs walks the tree, builds one CFG per function (nested included), applies maxFunctionLines (over-cap = skipped). - language-provider.ts: add `cfgVisitor?: CfgVisitor<SyntaxNode>` hook; typescript.ts attaches it to both the TS and JS providers (shared grammar). - parse-worker.ts: read pdg + pdgMaxFunctionLines from workerData (read once at init — the worker never sees PipelineOptions), gate the build, attach cfgSideChannel alongside captureSideChannel. - parse-cache.ts: bump SCHEMA_BUMP 4→5 (ParsedFile shape changed) and fold the pdg flag into computeChunkHash so a pdg-off cached chunk is NOT reused on a --pdg run (the abhigyanpatwari#2038-class warm-cache trap). Default path keeps its keys. - worker-pool.ts + parse-impl.ts + pipeline.ts: thread pdg/pdgMaxFunctionLines PipelineOptions → WorkerPoolOptions → workerData, and into the chunk-hash key. 9 boundary tests: collect contract, JSON round-trip identity (no AST leakage), the pdg cache-key guard, the line-cap skip, and the no-visitor gate. Full CFG suite (U1+U2+U3) green; build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): U4 — emit BasicBlock + CFG within scope-resolution (abhigyanpatwari#2081) Emit persisted BasicBlock nodes + CFG edges from each ParsedFile's worker-built cfgSideChannel, INSIDE scope-resolution's Phase-4 graph emission — the last point where the worker-built CFGs are loaded (emitParsedFiles carries the channel; the disk store is cleared right after the orchestrator returns). This is the architecture the doc-review corrected to: a standalone post-`mro` phase (the issue's literal subtask) provably reads empty data (KTD1). - cfg/emit.ts (new): pure emitFileCfgs(graph, cfgs, maxEdgesPerFunction, onWarn). BasicBlock id = `BasicBlock:<filePath>:<functionStartLine>:<blockIndex>` (KTD3 — funcStart disambiguates blocks across functions in one file; no `name` column). CFG edge = CodeRelation type 'CFG' with the edge KIND (seq/cond-true/…) in `reason` (kinds can't be their own edge type). Per- function edge cap stops at the cap and warns with the dropped count — no silent truncation (R6/KTD6). - run.ts: pdg-gated emit pass over emitParsedFiles after emitPostResolutionEdges (store still live); RunScopeResolutionInput gains pdg + pdgMaxEdgesPerFunction. - phase.ts: thread ctx.options.pdg / pdgMaxEdgesPerFunction into the call. - pipeline.ts: PipelineOptions.pdgMaxEdgesPerFunction. 6 tests: node/edge shape (KTD3 id, no name, type='CFG', kind in reason), cross-function id uniqueness, AC2 reachability-from-ENTRY property, the edge cap's no-silent-truncation contract, and empty-input no-op. Flag-off byte-identity + full runPipelineFromRepo round-trip land in U7. Build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): U5 — `--pdg` opt-in plumbing (CLI + .gitnexusrc → both sinks) (abhigyanpatwari#2081) Expose the CFG/PDG substrate as an opt-in and thread it from CLI/.gitnexusrc to the single source of truth (PipelineOptions.pdg), which fans out to BOTH sinks already wired in U3/U4: the worker build gate (workerData.pdg) and the scope-resolution emit gate. Off by default (R7). - cli/index.ts: `--pdg` commander flag. - cli/analyze.ts: AnalyzeOptions.pdg + pass `pdg` into runFullAnalysis options. - cli/analyze-config.ts: KEY_SPECS `pdg` (boolean) so `.gitnexusrc { "pdg": true }` normalizes and a non-boolean value fails closed with GitNexusRcError. - core/run-analyze.ts: AnalyzeOptions.pdg → runPipelineFromRepo({ pdg }). (The internal PipelineOptions/WorkerPoolOptions/workerData fields + the parse-cache key fold landed in U3/U4; this unit adds the user-facing surface. The budget knobs stay at internal defaults for M1.) Tests: analyze-config pdg normalization + non-boolean rejection; opt-in.test.ts covers the CLI/file merge precedence and that pdg perturbs the chunk-dispatch key. The full worker-build + main-emit round-trip is the U7 integration test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): U7 — CFG acceptance fixtures, parity, end-to-end + docs (abhigyanpatwari#2081) Acceptance criteria for the M1 CFG layer: - AC1: a 10-function TS fixture's CFG node/edge set matches a committed snapshot (cfg-snapshot.test.ts). - AC2: every BasicBlock is reachable from its function ENTRY (property test over the emitted graph; the fixture has no dead code). - AC3: hazard fixtures lock the classic-bug coverage — try/throw/finally post-domination + labeled break/continue resolution. - AC4: the existing pipeline-graph-golden test stays byte-identical with --pdg off (verified; no UPDATE_GOLDEN), proving the opt-in adds zero default-run drift. - End-to-end (pipeline-pdg.test.ts): runPipelineFromRepo({ pdg: true }) on a tiny repo emits BasicBlock nodes + CFG edges with both endpoints present — the true both-sinks proof (worker builds → store → scope-resolution emits); the default run emits zero. Docs: CHANGELOG M1 entry, ARCHITECTURE "Optional CFG/PDG emission" subsection (why emit is in-phase, not post-mro), README CFG language-support note. Full CFG suite (U1–U7): 56 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): drop unused helper in cfg-snapshot test (abhigyanpatwari#2081) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): apply ce-code-review autofix feedback (abhigyanpatwari#2081) Review (10 reviewers) confirmed OFF-path byte-identity (adversarial + golden) and found defects all within the --pdg path. Fixes: - P1 same-line BasicBlock id collision: add a start-column disambiguator to FunctionCfg + the id (`BasicBlock:<file>:<line>:<col>:<idx>`) so two functions sharing a start line no longer collide under first-writer-wins addNode. - P1 worker crash-cascade: per-file try/catch around collectFunctionCfgs so a CFG-build throw cannot escape to the language-group catch and silently drop every remaining file in the group. - P2 edge-cap drop now logs unconditionally (input.onWarn is validator-gated/ silent in prod) — upholds the no-silent-truncation guarantee. - P2 Array.isArray guard before the cfgSideChannel cast in run.ts. - P2 maxFunctionLines default: worker applies DEFAULT_PDG_MAX_FUNCTION_LINES=2000 when unset; caps forwarded through run-analyze AnalyzeOptions (closes the server-path drop). - P3 README duplicate paragraph removed; `0`-vs-default docstrings corrected; CLI --pdg flag made language-neutral; reachableBlocks JSDoc corrected. - Documented the break-through-finally + stacked-label CFG limitations. - Tests: same-line id-collision regression, standalone throw→EXIT, dead-code- after-return, async/generator/method coverage, strengthened labeled-continue. Refuted: the HTTP-500 getNodeQuery finding — M0 already shipped the BasicBlock branch + name-floor (R12/web-safety handled). CFG + analyze-config suites: 95 tests green; golden parity (AC4) byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(ingestion): benchmark CFG construction + O(n) block-text accumulation (abhigyanpatwari#2081) Closes the M1 review's requires_verification perf gap ("no benchmark for collectFunctionCfgs; a wall-time + cfgSideChannel byte-size regression gate would catch the extendBlock concatenation before kernel scale"). - bench/cfg/measure.mjs (new): build-free tsx harness timing collectFunctionCfgs (parse once, reuse the tree) across three scaling scenarios — straight-line (extendBlock path), many-functions (collect walk), branchy (block/edge growth) — at 500→2000. Reports a wall-time scaling ratio AND a cfgSideChannel byte-size ratio, plus an order-independent sha256 over the emitted blocks/edges as the behavior gate. `--check` compares both ratios + the fingerprint against bench/cfg/baselines.json; mirrors the scope-capture / python-scope harnesses. - .github/workflows/ci-tests.yml: run the gate on every test job (build-free, alongside the existing scope-capture guards) so an O(n^2) re-regression fails CI. - cfg-builder.ts: structural fix for the one real hotspot the bench surfaced — accumulate basic-block text as fragments joined once in finish(), instead of concatenating onto a growing string per coalesced statement (O(n^2) → O(n)). Behavior-identical (the CFG fingerprint + the AC1 snapshot are unchanged). Measured (post-fix): time ratios straight-line ~1.3, many-functions ~1.0, branchy ~1.1 (all sub-quadratic; a true O(n^2) would be ~4.0). cfgSideChannel bytes scale linearly (~1.0-1.04). 60 CFG tests green; build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(ingestion): add memory + disk growth gates to the CFG benchmark (abhigyanpatwari#2081) Extend bench/cfg/measure.mjs beyond wall-time to the two other scalability dimensions that matter at kernel scale: - DISK growth: utf8 byte size of the serialized cfgSideChannel — exactly what a --pdg run writes onto every ParsedFile shard (durable store + parse cache). - MEMORY growth: retained JS heap of the cfgSideChannel payload, measured by the release-delta method (heap held minus heap after dropping it) — robust to pre-existing garbage and dead-stable run-to-run. Needs `node --expose-gc`; without it the heap metric is null and its gate is skipped (local runs still work). ci-tests.yml now passes --expose-gc so the heap gate runs in CI. Both gated on linear scaling in baselines.json (disk_bytes_budget / heap_budget 1.2-1.3). Measured: disk ~1.0-1.04, retained heap ~0.87-1.0 — both linear (~1KB/function each; ~2MB heap / 1.6MB disk at 2000 functions, --pdg only). Bumped REPS 7->15 to stabilize the noisier time signal and widened the coarse time tripwire budgets (the disk/heap gates carry the tight regression detection). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): address tri-review + CFG-expert findings (abhigyanpatwari#2081) Corroborated findings from the tri-review (Codex + CE personas + GitNexus swarm + a CFG/program-analysis domain-expert lane). The OFF-path stays byte-identical; all fixes are within the --pdg path or the benchmark. - [Codex+CFG-expert] Exceptional `throw` edges now wire EVERY block in a try's protected region to the handler, not just the body ENTRY. A branched try body (`try { if (x) { use(t); } } catch`) previously left interior blocks with no path to `catch` — a taint false-negative into the handler for the M2 PDG pass. - [Codex+CFG-expert] An unresolved labeled jump (a stacked outer label or a labeled non-loop block) now routes to the function EXIT instead of leaving a dangling sink — restores the single-exit invariant post-dominator/PDG computation needs. - [Codex] computeChunkHash now folds pdgMaxFunctionLines/pdgMaxEdgesPerFunction into the chunk key (not just the pdg boolean), so a warm cache built under one cap is never served to a run with a different cap (abhigyanpatwari#2038 class, extended to the budgets). Adds PdgCacheKey; boolean form kept for back-compat. - [perf] visitTry resolves catch/finally in a single namedChild pass (the double `namedChildren.find` allocated two throwaway arrays). - [adversarial] The bench `straight-line` scenario now runs at 2000->8000: output is a constant 4 blocks so disk/heap can't see the concat path, and at the old N a genuine O(n²) was masked by V8 cons-strings. Verified at the new N: the array-join impl ~1.0, a rope-optimized `+=` ~1.0 (correctly not flagged), a real O(n²) (re-join-every-append) ~3.8 — budget tightened 2.0->1.5. - [adversarial+Codex] The bench `--check` now FAILS LOUDLY when run without `--expose-gc` instead of silently skipping the retained-heap gate. - Doc: re-labeled the finally-bypass as a SOUNDNESS (false-negative) limitation tracked for M2, not mere "precision." 3 new regression tests (branched-try interior→handler, stacked-label→EXIT, cap-fold key). 99 CFG tests pass; build clean; bench gate green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(parse-cache): clarify that SCHEMA_BUMP still invalidates caches once (abhigyanpatwari#2099 F6) The computeChunkHash comment claimed pdg-off warm caches "survive this change untouched" — true for the key FORMAT, but misleading as an upgrade-behavior promise: SCHEMA_BUMP 4→5 changes PARSE_CACHE_VERSION and both stores hard-invalidate on it. Separate the two facts so the next cache change isn't reasoned about from a false premise. Review finding F6 (P3) of PR abhigyanpatwari#2099 tri-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cfg): correct for-loop back-edge kinds when no increment clause (abhigyanpatwari#2099 F5) A for with a body but no increment emitted an unconditional header→header 'loop-back' self-edge (a path that never executes the body) while the real back-edge body→header was labeled 'seq'. Any consumer identifying loops via reason='loop-back' picked the phantom edge and excluded the body from the natural loop. Gate the self-edge on the body being absent (the one case where the header genuinely re-tests itself) and carry 'loop-back' on the body's exits when they ARE the back-edge, matching visitWhile/visitForIn. Review finding F5 (P3) of PR abhigyanpatwari#2099 tri-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cfg): treat an empty catch clause as a real handler (abhigyanpatwari#2099 F2) visitTry keyed handler semantics off the traversal result — null for an empty body, since visitSeq([]) returns null — instead of the syntactic clause. An empty `catch {}` was therefore treated as NO catch: the swallowed exception escaped to the outer handler/EXIT, the no-catch re-propagation misfired past finally, and code after a try whose body always throws became unreachable from ENTRY — a hard false-negative source for the M2 taint pass, on an extremely common pattern. Synthesize one empty block spanning the clause (entry == sole exit) when the catch body traverses to null, before the protected region is walked. Exception flow lands in it and rejoins the normal continuation; all downstream wiring (handler selection, finally routing, the !catchRes re-propagation gate) operates on the syntactically-correct shape. Review finding F2 (P2, reproduced) of PR abhigyanpatwari#2099 tri-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cfg): guard CFG emission per element, not just per outer array (abhigyanpatwari#2099 F4) The cfgSideChannel guard checked only Array.isArray before casting to FunctionCfg[] — its own comment promised a wrong-shape value would 'skip emission, not throw a TypeError mid-graph-build', but a malformed ELEMENT sailed through. Worse, the obvious-looking failure shape never throws at all: emitFileCfgs string-templates any edge endpoint into the BasicBlock id and graph inserts are no-throw, so a non-integer endpoint silently became a dangling 'BasicBlock:…:undefined' edge that degrades the DB rel-pair COPY to row-by-row fallback inserts much later. Layered fix matching house precedents (parsedfile-store reviver, worker-side per-file catch): a per-element shape+content predicate (arrays + integer edge endpoints) that warns and skips malformed elements while valid siblings still emit, plus a per-file try/catch backstop for shapes that genuinely throw (e.g. a null inside blocks). Review finding F4 (P3) of PR abhigyanpatwari#2099 tri-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(parse-cache): drop emit-time edge cap from the pdg chunk key (abhigyanpatwari#2099 F3) pdgMaxEdgesPerFunction is applied exclusively in emitFileCfgs during scope-resolution on the main thread — the worker never receives it (workerData carries only pdg + pdgMaxFunctionLines), so the cached worker output is byte-identical across cap values. Folding it into the chunk key (added by a prior review round) only converted a free knob into a repo-sized cost: every cap change forced a full re-parse and a durable-store rewrite of unchanged data. Keep pdg + maxFunctionLines (genuinely worker-visible, shape the cached cfgSideChannel) and document the classification test in the PdgCacheKey doc comment so the next option gets sorted deliberately: worker-shard inputs go in this key; persisted-graph-only inputs belong in the RepoMeta pdg stamp (F1). Chunks written under the old ns string miss once and prune — no migration needed. Review finding F3 (P2) of PR abhigyanpatwari#2099 tri-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(analyze): record pdg config in RepoMeta; force full writeback on mode flip (abhigyanpatwari#2099 F1) Running --pdg against an already-indexed repo silently persisted ~zero CFG: incremental eligibility had no pdg term, RepoMeta recorded no mode, and extractChangedSubgraph keeps only changed-file nodes — on a no-change --pdg re-run every freshly built BasicBlock was dropped from the written subgraph ('Incremental: changed=0', run succeeds, zero rows). The converse flip left zombie mixed-coverage blocks only --force could clean. Worse, a clean-tree flip hit the alreadyUpToDate fast path and never ran the pipeline at all. - RepoMeta gains an additive-optional pdg stamp ({maxFunctionLines, maxEdgesPerFunction}, resolved values; absent ≡ pdg-off, which covers every legacy meta). No INCREMENTAL_SCHEMA_VERSION bump — that would force a one-time full rebuild for everyone. The end-of-run meta is a fresh literal, so omitting the field on a pdg-off run is what clears the stamp after an on→off flip. - pdgModeMismatch (pure, exported) compares the resolved triple; the flip check sits before the fast path and always logs its notice (not gated on options.force — --skills implies force with no message of its own), naming the .gitnexusrc pdg key that pins the mode. - The full-rebuild branch now writes the incrementalInProgress dirty flag (toWriteCount: 0 sentinel) before the wipe whenever a prior meta exists, mirroring the incremental branch. This closes the crash window where a rebuild dying between the bulk load and saveMeta left meta/DB inconsistent and the fast path certified zombie (or missing) CFG rows indefinitely — and incidentally closes the same pre-existing hole for user --force runs. Recovery log reworded accordingly. Tests: pdg-mode-flip.test.ts (real git + LadybugDB; primary assertion is a direct BasicBlock table count — meta.stats aggregates nondeterministic Community/Process rows) covering off→on, steady-state fast path, on→off zombie cleanup, cap-change rebuild, and dirty-flag + flip composition; pure-helper tests for default resolution and the 0=unlimited carve-out. Review finding F1 (P1) of PR abhigyanpatwari#2099 tri-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er (abhigyanpatwari#1850) * fix(web): replace broken Browse-for-folder with server-side directory picker The "Browse for folder" button used `<input type="file" webkitdirectory>` which only exposes relative paths via `webkitRelativePath`. The code extracted just the folder name (e.g. `myproject`), causing the server to reject it with "path must be an absolute path". No browser API can expose absolute filesystem paths, so the approach was fundamentally broken on all platforms. - Add `GET /api/fs/list` endpoint that lists subdirectories at a given absolute server-side path (rate-limited, validated) - Add `listDirectories()` client function in backend-client.ts - Add `DirectoryPicker` modal component with breadcrumb navigation - Replace broken `webkitdirectory` input in RepoAnalyzer with the new server-side directory picker - Update i18n strings (en + zh-CN) - Add unit tests for the new endpoint (9 tests) Docker users can now browse `/workspace/` and other container paths directly from the UI. Manual path entry continues to work unchanged. Closes abhigyanpatwari#1518 * test(e2e): add Playwright tests for server-side directory picker 13 Playwright e2e tests covering the full DirectoryPicker flow: - Open/display: modal opens, shows root dirs, displays current path - Navigation: click into dirs, breadcrumb back-nav, home button - Selection: populates path input, returns absolute path, close without selecting - Edge cases: empty dir, API error, manual typing still works Also updates existing onboarding.spec.ts to match the renamed "Browse server directories" button, and adds data-testid attributes to DirectoryPicker and RepoAnalyzer for reliable e2e targeting. * fix(a11y): add accessibility and UX polish to DirectoryPicker - Add role="dialog", aria-modal, aria-label to the modal panel - Add aria-label to close button, home button - Add aria-hidden to decorative icons (chevrons, backdrop) - Add role="status" to loading spinner with sr-only label - Add role="alert" to error state - Add aria-current="location" to active breadcrumb segment - Wrap breadcrumb in nav landmark with aria-label - Add Escape key handler to dismiss the modal - Auto-focus the modal panel on open - Add focus-visible ring styles to all interactive elements (matches existing focus-visible:ring-2 ring-accent/40 pattern) - Increase breadcrumb button padding (px-1.5 py-1) for better touch targets - Increase directory entry padding (py-2.5) for touch comfort - Add active:bg-hover/70 pressed state on directory entries - Add active:bg-accent/80 pressed state on select button * chore(autofix): apply prettier + eslint fixes via /autofix command * fix: skip traversal guard for bare root paths in /api/fs/list (abhigyanpatwari#2109) * fix(web): replace server-side directory picker with secure folder upload PR abhigyanpatwari#1850 review found the new GET /api/fs/list directory-browsing endpoint enumerated any absolute server path (CodeQL js/path-injection, plus a DoS and cross-origin enumeration via the CORS/PNA allow-list). Browsers can't hand the server an absolute path, so rather than harden the endpoint, remove it and upload the folder instead — webkitdirectory exposes the file contents. - Add POST /api/analyze/upload: busboy-streamed multipart ingest into an mkdtemp sandbox under UPLOAD_ROOT with resolve-then-contain write sanitization, hard size/count/dir caps, manifest-first ordering, and guaranteed cleanup; promote (atomic same-filesystem rename, no EXDEV) and analyze via the shared job/worker machinery, never returning a server path. - Frontend: <input webkitdirectory> upload flow with client-side filtering (.git/node_modules/build), XHR progress, accessibility, en/zh-CN i18n. - Remove /api/fs/list + handleFsListRequest, DirectoryPicker, listDirectories and their tests. - Harden the adjacent /api/analyze {path} route: localhost-only CORS on write routes + realpath/exists/isDir validation replacing the inert normalize!==resolve guard. - Extend DELETE /api/repo cleanup to upload dirs (by entry.path) and add a startup sweep for orphaned staging dirs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): resolve CodeQL path-injection + CSRF introduced by the upload change The first push surfaced two new CodeQL alerts in the newly-added code (the upload sandbox itself passed — its resolve-then-contain sanitizer is recognized): - HIGH js/path-injection at the analyze route: the KTD11 in-route `fs.realpath(repoLocalPath)` / `fs.stat` was a user-controlled filesystem read with no security gain (the worker already reads the path; cross-origin reach is closed by requireLocalhostOrigin). Drop the in-route fs calls; keep only the absolute-path check + the localhost-origin guard. - MEDIUM js/client-side-request-forgery: the new raw `xhr.open` was a fresh request sink. Route the upload through the shared, origin-validated fetchWithTimeout instead (the centralized sink all other calls use). Trades the upload-progress percentage for an indeterminate "Uploading…" state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): resolve tri-review findings on the upload flow A multi-agent review of the upload implementation surfaced a P0 plus several P2/P3s; all are addressed here. - P0: the upload handler took the single analysis slot (createJob) before validating/promoting, so any failure in that window left a queued job that was never failed — wedging ALL analysis until restart (trivially triggered by a single-segment manifest). Now: validate the folder before taking the slot, release it via failJob on any pre-launch error, and reject single-segment / multi-top manifests during ingest (also fixes a silent file-drop). - CI: rate-limit.test's source-regex broke when Prettier wrapped the /api/analyze registration; made it wrapping-tolerant. - Resource: the startup sweep now also removes stale promoted upload dirs with no .gitnexus index (orphans from analyses that failed before registering). - Frontend: guard against post-unmount SSE opening, reset upload state on cancel/mode-change, guard concurrent uploads, fall back to the folder name, add aria-busy, and fix the {{count}} plural ("1 files"). - Maintainability: extract launchAnalysisWorker into analyze-launch.ts (DI + typed WorkerMessage IPC), move requireLocalhostOrigin to middleware.ts, share REPO_NAME_PATTERN, tighten UploadJobRef, name the collision-retry constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): reset isMountedRef on mount (StrictMode double-invoke) The mount effect set isMountedRef=false on cleanup but never back to true on re-mount, so under React StrictMode's mount->unmount->mount the ref stayed false for the component's lifetime — trackJob then always early-returned and the upload never advanced past 'starting' (caught by the folder-upload e2e). Set it true at the start of the effect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): de-flake upload-ingest cleanup test via injectable staging root ingestUpload gains an IngestOptions.root override (mirroring SweepOptions.root) so the test asserts cleanup against a per-test mkdtemp root instead of counting global ~/.gitnexus/uploads/.staging-* entries, which raced parallel forks. Production default stays UPLOAD_ROOT (promote rename same-filesystem invariant). * fix(web): make stale analyze/upload requests inert after mode switch, cancel, or unmount A folder upload (or URL analyze) still in flight when the user switched modes could resolve later, call trackJob(), and drive the old job's SSE stream under the new mode's form. The only guard was isMountedRef — mode change and cancel never unmount the component. - requestControllerRef: per-request AbortController doubling as the staleness token (captured per closure, checked after the await; the abort error is matched via signal.aborted, never error identity, since it surfaces both as BackendError('Request aborted') and as a raw AbortError from response.json()) - uploadFolder() now takes an optional AbortSignal; fetchWithTimeout already merges caller signals via AbortSignal.any - a stale-but-created job gets a fire-and-forget cancelAnalyze(jobId) (skipped when a live tracking session owns the id) so the single analyze slot is freed - handleModeChange early-returns on same-tab clicks and resets phase to input so an aborted request can't strand the form at 'starting' - fixed the stale breaker comment: resilientFetch records AbortError as breaker-neutral (recordNeutral), not as a retryable-network penalty * refactor(web): consolidate stale-request guard plumbing - single invalidateRequest() helper for the abort+null pattern (4 sites) - drop isMountedRef checks subsumed by the aborted-controller token (unmount aborts the controller, and unlike isMountedRef the token stays correct across a StrictMode unmount/remount) - dedup the component test's render/mock scaffolding - countStaging filters on the exported STAGING_PREFIX, not a magic string * fix(web): scope stale-job cancellation to the upload path Code review caught a regression in the first cut: URL analyzes dedup-alias by repo (createJob returns the existing active job's id), so a stale resolution's fire-and-forget cancel could kill a job another session — or the user's own fresh resubmit — is actively watching; the jobIdRef ownership guard was order-dependent and instance-local. Uploads always own a fresh, never-deduped job, so the cancel is kept (unconditionally) there and dropped on the URL path, where a same-URL resubmit re-attaches via dedup and the server's job timeout / TTL sweep bounds the slot occupancy. Also: remove the isMountedRef machinery outright (zero readers remain — the aborted-controller token subsumes it and stays correct across StrictMode remounts), make the e2e abort check ERR_ABORTED-specific, and let a broken test root fail loudly instead of passing vacuously. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sparsh <73558748+prajapatisparsh@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
abhigyanpatwari#2149) Bumps [langchain](https://github.com/langchain-ai/langchainjs) from 1.4.2 to 1.4.4. - [Release notes](https://github.com/langchain-ai/langchainjs/releases) - [Commits](https://github.com/langchain-ai/langchainjs/compare/langchain@1.4.2...@langchain/openai@1.4.4) --- updated-dependencies: - dependency-name: langchain dependency-version: 1.4.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
abhigyanpatwari#2150) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.7 to 3.4.8. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.7...3.4.8) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…bhigyanpatwari#2151) Bumps [sigma](https://github.com/jacomyal/sigma.js) from 3.0.2 to 3.0.3. - [Release notes](https://github.com/jacomyal/sigma.js/releases) - [Changelog](https://github.com/jacomyal/sigma.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/jacomyal/sigma.js/compare/sigma@3.0.2...sigma@3.0.3) --- updated-dependencies: - dependency-name: sigma dependency-version: 3.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…igyanpatwari#2153) Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.5 to 4.1.8. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/coverage-v8) --- updated-dependencies: - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.8 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…atwari#2155) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 6.2.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v5.6.0...a309ff8) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…twari#2156) Bumps [@vercel/node](https://github.com/vercel/vercel/tree/HEAD/packages/node) from 5.8.8 to 5.8.12. - [Release notes](https://github.com/vercel/vercel/releases) - [Changelog](https://github.com/vercel/vercel/blob/main/packages/node/CHANGELOG.md) - [Commits](https://github.com/vercel/vercel/commits/@vercel/node@5.8.12/packages/node) --- updated-dependencies: - dependency-name: "@vercel/node" dependency-version: 5.8.12 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…abhigyanpatwari#2157) Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.3.0 to 7.3.1. - [Release notes](https://github.com/release-drafter/release-drafter/releases) - [Commits](release-drafter/release-drafter@c2e2804...693d20e) --- updated-dependencies: - dependency-name: release-drafter/release-drafter dependency-version: 7.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…yanpatwari#2159) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](docker/setup-qemu-action@ce36039...0611638) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…abhigyanpatwari#2158) Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.4.0 to 4.1.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](actions/attest-build-provenance@v2.4.0...a2bbfa2) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ri#2152) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@de0fac2...df4cb1c) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
…yanpatwari#2082) (abhigyanpatwari#2160) * fix(cfg): route early exits through finally with target-relative threading (abhigyanpatwari#2082 U2) * feat(cfg): harvest per-statement def/use facts into the side channel (abhigyanpatwari#2082 U1) * feat(cfg): add reaching-definitions solver with GEN/KILL fixpoint + statement sweep (abhigyanpatwari#2082 U3) * feat(cfg): persist budgeted REACHING_DEF projection with RepoMeta coherence (abhigyanpatwari#2082 U4) * test(cfg): REACHING_DEF snapshot, pipeline both-sinks, and cache-seam coverage (abhigyanpatwari#2082 U5) * bench(cfg): reaching-defs scaling gates — dense-bindings + fact-fanout scenarios (abhigyanpatwari#2082 U6) * fix(mcp): exclude BasicBlock pseudo-symbols from detect_changes on pdg indexes (abhigyanpatwari#2082 U7) * style: prettier pass over M2 files * fix(cfg): review-pass fixes — defKey overflow guard, catch-param block, class defs, intra-statement reads, graceful fact degradation (abhigyanpatwari#2082) - reaching-defs: STMT_STRIDE 2^16→2^21 + upfront aliasing bail-out; a use that shares its statement with a def now also sees the same-statement def (assign-and-test idiom was a taint false negative); drop dead posInOrder - visitor: catch-param def gets its own once-executed block (prepending into a loop-header entry re-genned per iteration and killed loop-carried redefs); unresolved-label jumps now thread all active finallys; the finalizer-threading protocol moved to control-flow-context as shared helpers for future language visitors - harvest: class declarations def their name (was a bogus use in JS, silent skip in TS); class-expression names stay internal - emit: isEmitSafeCfg adds index==position contiguity; fact validation split into hasEmitSafeFacts so malformed facts degrade to CFG-only instead of dropping the function's whole CFG layer; facts-per-edge multiplier single source; lazy top-binding tally; dead solveMs removed - run-analyze: pdgModeMismatch compares the key union structurally — new resolved knobs join the comparison automatically - mcp: BasicBlock exclusion via id prefix (NULL-name rows of real symbols are no longer dropped) + same filter on the BM25 filePath fallback - bench: rd ratio denominator clamped (gate no longer self-disables at fast small-N); PROF-gated pdg timing in run.ts * test(run-analyze): model the M2 RepoMeta.pdg stamp in resolvePdgConfig defaults The DEFAULTS constant lacked the maxReachingDefEdgesPerFunction field that resolvePdgConfig resolves since the M2 stamp landed, failing two strict toEqual expectations (the CI 'tests' job failures). Models M2 steady-state equality; the M1-era-stamp upgrade path stays pinned in pdg-mode-flip.test.ts. Finding P1-4 of review 4471987625 (abhigyanpatwari#2160). * test(cfg): reassign the shadowing fixture's bindings — fixes prefer-const CI errors Both withShadowing let bindings now genuinely reassign (s = s + 1 per scope), clearing the two prefer-const errors that failed quality/lint. Plain const would change the binding kind the harvest test exercises; reassignment keeps the let semantics and enriches the reaching-defs facts the snapshot pins (snapshot + per-binding assertion updated accordingly). Finding P2-6 of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): validate entry/exit indices in the emit-safety guard A corrupted side-channel element with an out-of-range entryIndex passed isEmitSafeCfg and threw inside the reaching-defs RPO walk — caught by the per-FILE try/catch, costing every sibling function's REACHING_DEF projection instead of the one element (and logging a misleading message). entry/exit join the guard's id-anchor checks. Finding P3 (entryIndex) of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): report the def-key stride bail-out as a distinct 'overflow' status The STMT_STRIDE aliasing guard reused status 'truncated', so the emit warn misnamed it as the fact-materialization limit (printing an unrelated maxFacts value, including '(0)' when unlimited) and telemetry conflated the two. A distinct 'overflow' status gets its own warn naming the actual cause; the function's CFG layer is explicitly unaffected. Finding P3 (stride-bail diagnosis) of review 4471987625 (abhigyanpatwari#2160). * perf(cfg): cache the nearest enclosing scope per node during the prescan resolve() walked the AST parent chain per identifier — O(expression nesting depth), quadratic on deeply-chained single-statement expressions in generated code (not caught by any bench scenario, which scale blocks/bindings, not expression depth). The prescan already visits every node once, so caching its innermost scope makes phase-2 resolution O(scope-chain). Behavior-identical; the parent-chain walk survives as fallback for prescan-unvisited nodes. Finding P2 (resolve depth walk) of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): stop harvesting initializer-less var declarators as defs A bare `var x;` mid-function is hoisted and writes nothing at runtime, but the harvester recorded a def — fabricating a kill of the live def in the same block: `x = source(); var x; sink(x)` lost the source→sink fact (a reaching-defs false negative). Defs now require an initializer for variable_declaration declarators; let/const genuinely initialize and keep their def. Finding P2-5 of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): unwrap parenthesized/non-null lvalue wrappers before def detection `(x) += 1` and `(x)++` gated the def on the node type being exactly 'identifier', so the parenthesized form fell to the uses-only branch — the def (and its kill) silently vanished. Wrappers that don't change the lvalue (parenthesized_expression, TS non_null_expression) now unwrap at all three lvalue sites. Finding P3 (parenthesized lvalues) of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): conditionally-evaluated defs are MAY-defs — gen without kill A def inside a short-circuit right operand, ternary arm, logical assignment, or switch case test was harvested as a must-def; the solver's total kill then erased the prior def on the not-taken path — a taint false negative on core idioms (`if (a && (x = clean())) {} sink(x)` lost source→sink; `cached ?? (cached = load())` likewise). StatementFacts gains an optional mayDefs field (conditional-context tracking in the harvester); the solver's per-block GEN carries {set, kills} so a may-def UNIONS into the binding's set instead of replacing it, in both the transfer and the statement sweep; the emit fact-guard validates mayDefs indices; switch case tests harvest via the conditional path. Finding P1-1 of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): model labeled statements generically — break keeps its real continuation A break to a label the visitor didn't model (labeled non-loop block, the OUTER label of a doubly-labeled construct) routed to EXIT, REMOVING the only path that kept the pre-jump def live — a reaching-defs false kill the in-code comment wrongly called sound. Loop/switch frames now carry their full label LIST (`outer: inner: for` resolves both); a labeled non-loop statement gets a break-target frame whose target is a synthesized join after the body; an unlabeled break never matches a block frame; labels compose with finalizer threading (a labeled break crossing a finally still threads it). Finding P1-2 of review 4471987625 (abhigyanpatwari#2160). * fix(cfg): throw edges deliver ALL of a block's defs to the handler The throw contribution was IN ∪ OUT — entry and final states only. The intermediate defs of a multi-def coalesced block were invisible to the handler, though they are exactly what the catch observes when a later statement throws: `try { x = parse(a); x = normalize(x); } catch { sink(x) }` lost the parse→sink fact (normalize throwing delivers parse's value). Throw predecessors now contribute IN(from) ∪ allDefs(from) — a static per-block all-def-sites map — which subsumes OUT; monotone and deterministic. Finding P1-3 of review 4471987625 (abhigyanpatwari#2160).
…ok slot (abhigyanpatwari#2163) (abhigyanpatwari#2165) * fix(hooks): bound db-lock probe subprocesses and gate probe behind hook slot (abhigyanpatwari#2163) The Claude PreToolUse db-lock probe leaks orphaned lsof processes when the hook process is hard-killed mid-probe (e.g. Claude Code's 10s hook timeout under load). Orphans accumulate, raise load, slow the next probe, and snowball to sustained 100% CPU. - Wrap the unix lsof/ps fallback in coreutils timeout (-k 1 2 / -k 1 1), resolved via a lazy self-test, so probe children self-destruct within ~3s even if the hook is SIGKILLed. GITNEXUS_HOOK_TIMEOUT_PATH overrides the guard binary; the sentinel value 'disabled' turns the guard off; hosts without a usable guard keep the previous behavior. - Acquire the per-repo hook slot before probing (all three adapters), bounding concurrent probes to 3 per .gitnexus, with probe and augment inside try/finally so the slot is always released. - Tests: source-order contract, slot-gating behavior, orphan reaping with a SIGTERM-immune fake lsof and a SIGKILLed parent (red on base), probe-copy byte parity, no-guard equivalence, broken-guard rejection. Note: pre-commit typecheck skipped; the 62 tsc errors are pre-existing on main (all in src/core/** and src/server/, none in files touched here; base==head invariant verified). * fix(hooks): address tri-review P3 findings (abhigyanpatwari#2165) - Map guard signal-death (status null + signal, no spawnSync error) to fail-closed at both the lsof and ps call sites, closing the freeze window (SIGSTOP / laptop sleep > 2s) that previously landed fail-open. Rewrite the exit-code comments: coreutils surfaces the -k kill as signal death, 124 is budget expiry (live arm), 137 covers only exit-code-propagating wrappers or an externally SIGKILLed child. - Add a debug-gated 'augment skipped: hook slots saturated' stderr line on the slot-starved early return in all three adapters, restoring observability under GITNEXUS_DEBUG=1. - GITNEXUS_HOOK_TIMEOUT_PATH now participates in candidate fall-through: the env candidate is tried first, then the built-ins, each behind the lazy self-test — an existing-but-unusable env path (directory, non-executable) can no longer silently disable orphan containment. - Tests: +6 — guard exit 124 pins the live arm (CJS+Plugin), guard signal-death pins the new mapping (CJS+Plugin, red before the fix), antigravity behavioral slot-gate, env-dir fall-through still reaps a SIGTERM-immune orphan via a built-in guard. Note: pre-commit typecheck skipped; the 62 tsc errors are pre-existing on main (none in files touched here).
…oving version args for AI CLIs (abhigyanpatwari#2174)
…bhigyanpatwari#2164) * feat(taint): harvest occurrence-tagged call/member sites on StatementFacts (abhigyanpatwari#2083 U1) Worker-side site harvest in TsHarvester: call/new/member-read records with dotted callee paths, receiver slots, per-argument occurrence tagging with nested-site links, per-declarator resultDefs, spread/template/require-literal markers. hasTaintSafeSites validation seam. The pdg parse-cache chunk-key namespace is versioned (pdg:1 -> pdg:2) instead of a global SCHEMA_BUMP so flag-off users keep warm caches; bench fingerprints re-baselined for the three call-bearing scenarios (straight-line/dense-bindings byte-unchanged). * feat(taint): built-in TS/JS source/sink/sanitizer model + site matcher (abhigyanpatwari#2083 U2) Typed spec (kind taxonomy; sanitizers carry neutralizes-kinds), the canonical Express/Node model, and matchFunctionSites: ESM alias/namespace + require- literal callee resolution, bare-name fallback restricted to true globals, sanitizers module-or-global only (never user-shadowable by name), spread/ template arg-position rules, deterministic taintModelVersion. * feat(taint): pure intra-procedural taint propagation engine (abhigyanpatwari#2083 U3) Two-rule model (statement-local + du-fact worklist) with per-taint neutralized-kind exclusion sets: sanitizers exclude only the sink kinds they neutralize (escape(req.body) suppresses res.send but still fires db.query; exec(path.basename(t)) fires), intersection-over-paths so a bypass occurrence keeps the taint live, kill locality on resultDefs, propagate-through args+receiver with viaCall hops, one path per finding, deterministic caps, coverage-gap statuses. Test-first: 38 scenarios on real harvested CFGs. * feat(taint): thread taint caps + model version through pdg config/meta (abhigyanpatwari#2083 U5) resolvePdgConfig gains maxTaintFindingsPerFunction (200), maxTaintHops (32), and the taintModelVersion digest; RepoMeta.pdg + RunScopeResolutionInput surfaces added. The key-union comparator trips full writeback on M2->M3 upgrade and on model-version change without --force (mode-flip tested). No CLI flags or rc keys (programmatic parity with the other caps). * feat(taint): in-phase taint emit with sparse TAINTED/SANITIZES edges (abhigyanpatwari#2083 U4) run.ts pdg window: match-first fast path (solver only when a function has both a matched source and sink) -> computeReachingDefs with the shared RD fact derivation -> computeTaintFlows -> per-finding TAINTED (versioned hop-encoded reason via the shared path codec, statement-level occurrence identity) + per-kill SANITIZES, dedup-before-budget, truncate-and-warn. All emit counters surfaced (aggregate warn for gaps/drops, debug for volume); PROF gains taint=. Flag-off golden untouched. * feat(mcp): explain tool for persisted taint findings (abhigyanpatwari#2083 U6) Anchorless calls enumerate the sparse TAINTED table (bounded, deterministic, limit-clamped); anchored calls (file or symbol via resolveSymbolCandidates) return full decoded hop detail. sinkKind rides a version-1 codec header (1;<kind>|hops — no other persisted channel exists; U4/U6 ship together). RepoMeta.pdg probe yields a no-taint-layer note instead of an error. TAINTED/SANITIZES pinned OUT of VALID_RELATION_TYPES (KTD9a negative- membership tests); generators + canonical skill docs + mirrors updated. * test(taint): acceptance fixture battery, snapshots, and bench gates (abhigyanpatwari#2083 U7) pdg-repo taint-cases fixtures complete the six plan shapes; committed findings/kills snapshot via a shared pure-path harness that also feeds the AE2 exact-equality assertion (stored TAINTED == pure-path findings, the no-explosion gate). New taint-dense bench scenario with four --check gates: per-function findings pinned AT the cap, absolute reason-byte + site-bytes disk ceilings (the load-bearing R10 gate), zero-match pass < 0.5x match- dense, N-linearity. Pre-existing scenario baselines untouched. * refactor(taint): share one pointKey helper across propagate + emit (abhigyanpatwari#2083 review) Extract pointKey(ProgramPoint) to cfg/reaching-defs.ts (colon-separated, matching the codebase block:stmt id convention) and import it in both propagate.ts and emit.ts, replacing the two divergent locals (':' vs '.'). Edge-id material now uses the colon form; ids are in-memory only and no test asserts the pointKey segment shape. * fix(taint): discriminate taint state by source occurrence (abhigyanpatwari#2083 review) Two distinct sources flowing into one variable at one def point no longer collapse to a single TAINTED edge: the taint-state key gains a root source-occurrence discriminator ({point, siteIndex} — the same fields recordFinding's identity uses, excluding kind). Def->use fact lookup keys on the source-independent (binding, def-point) portion. Same-source multi-path flows still share one state so their exclusion sets intersect (the raw arm soundly wins); termination holds (finite keys, monotone shrink, no cross-source ping-pong). Restores the KTD6 identity contract. * fix(mcp): route dotted symbol names in explain to symbol resolution (abhigyanpatwari#2083 review) The fileish classifier matched any dotted name (UserController.create) as a file via its extension-like suffix, so symbol resolution never ran and the tool returned a silent empty file-anchored result. Tighten the classifier to require a path separator or a real source extension (derived from the resolver's EXTENSIONS list, multi-language), so dotted/bare names route to resolveSymbolCandidates (found / ambiguous / not-found). * fix(mcp): gate explain no-taint-layer note on taintModelVersion (abhigyanpatwari#2083 review) An M1/M2-era --pdg index has meta.pdg defined (BasicBlock/REACHING_DEF recorded) but no taintModelVersion and zero TAINTED rows. The probe keyed on generic meta.pdg presence, so explain returned the generic empty note instead of the actionable 'no taint layer — run analyze' hint. Gate on meta.pdg?.taintModelVersion (the field M3 stamps) so an M2-era index gets the layer hint; a taint-stamped index with no findings still gets the generic note. * fix(taint): sequence-expression value flows only the final operand (abhigyanpatwari#2083 review) A comma expression in value position (exec((log(x), 'safe'))) default- descended, fanning every operand's occurrences into the enclosing sink argument — over-tainting exec's arg 0 with x. Add an explicit walkValue case that records earlier operands' uses with occurrence fan-out suppressed (new FactAccumulator.suppressOccurrences) and routes only the last operand through the value path. Sites-layer only; defs/uses/mayDefs byte-identical (cfg + reaching-defs snapshots unchanged). * perf(taint): FIFO head-cursor worklist + dedup before chainHops (abhigyanpatwari#2083 review) Replace queue.shift() (O(N) dequeue) with a strict-FIFO head cursor plus order-preserving prefix reclamation; FIFO is load-bearing because chainHops reads the live taints map whose parent/source/viaCall are rewritten order-sensitively on monotone shrink, so hop determinism is dequeue-order contingent. Extract findingKey() and dedup-check before chainHops in the justify branch — already-recorded identities discard their hop chain (first write wins), so the ancestry walk was pure waste. The else kill branch is untouched. Findings + hops byte-identical (snapshot unchanged). * perf(taint): O(1) member-read dedup via composite-key set (abhigyanpatwari#2083 review) addMemberRead rescanned the whole per-statement sites array per call to dedup by (object, property, parent) — O(n^2) on member-read-dense statements. Track a composite-key Set alongside sites for O(1) dedup. (The require-literal join is already O(sites) with a no-op body on non-require sites, so no early-exit is needed there.) Behavior identical: harvest + model-match + taint snapshots unchanged. * refactor(taint): drop test-only export; source taint caps via emit.ts (abhigyanpatwari#2083 review) Remove the sanitizerNeutralizes export (its only consumers were two test assertions — inlined to entry.neutralizes membership). Re-export the DEFAULT_PDG_MAX_TAINT_* caps from emit.ts and point run.ts at emit.ts, so the pipeline's taint dependency surface is the single orchestration module rather than reaching into propagate.ts. * test(taint): extract the shared TS CFG/taint test harness (abhigyanpatwari#2083 review) The parse/collectFunctions/cfgOf/cfgsOf/importsFor harness was copied byte-for-byte across four suites (harvest, model-match, propagate, taint-emit). Promote it to test/helpers/ts-cfg-harness.ts and import it. site-safety/reaching-defs carry a structurally different inlined builder and are left as-is. Pure extraction, no assertion changes. * test(mcp): harden explain limit-rejection battery (abhigyanpatwari#2083 review) Add NaN, Infinity, -Infinity, and a numeric string to the out-of-bounds limit cases — a regression fence over the interpolated LIMIT, confirming the Number.isInteger guard rejects every non-integer/non-finite/string input before it reaches the query.
ci: run gitleaks without licensed action wrapper
evaOS review status: stale headPR: #98 - chore(reconciliation): promote proven RC19 internal source candidate evaOS review stopped because this queued head is no longer the live PR head. Automation note: agents should wait for this comment to reach PR URL: #98 Details: Superseded by a newer PR head. |
…ary-race fix(canary): hash extension files from one read
evaOS review status: stale headPR: #98 - chore(reconciliation): promote proven RC19 internal source candidate evaOS review stopped because this queued head is no longer the live PR head. Automation note: agents should wait for this comment to reach PR URL: #98 Details: Superseded by a newer PR head. |
…ardening fix(cli): sanitize wiki error output
|
@coderabbitai review @codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
✅ Action performedReview finished.
|
evaOS review status: failedPR: #98 - chore(reconciliation): promote proven RC19 internal source candidate evaOS review failed for this head and needs retry or operator attention. Automation note: agents should wait for this comment to reach PR URL: #98 Details: Review failed; see bot evidence for operator-only details. |
…image fix(container): repin patched Node base image
…-sanitizer fix(cli): use CodeQL-recognized log sanitizer
evaOS review status: failedPR: #98 - chore(reconciliation): promote proven RC19 internal source candidate evaOS review failed for this head and needs retry or operator attention. Automation note: agents should wait for this comment to reach PR URL: #98 Details: Review failed; see bot evidence for operator-only details. |
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 14655 tests passed 62 test(s) skipped — expand for details
Code CoverageTests
📋 View full run · Generated by CI |
Summary
mainwith the proven upstreamv1.6.10-rc.19source floorThis is a source-promotion PR for the internal Electric Sheep fork. It does not
publish a package, create a tag, approve a protected environment, deploy a
runtime, modify OpenClaw, or mutate a live GitNexus index.
Provenance
59ad8d906fea9858d6f5e31fbd2d5c90bbc20b8a8fb88c3ada204ab1372874b4c672a909beba828b2ec953bda2dbccfa2605cea2992879925d277e5493178bc50aada0dc4d353f24922a8836ef93278emainbefore promotion:86b3d3529092fcb74983a0357dd82a1db3c4a5b5v1.6.10-rc.19atd43c479ac58c18d397958f48a99465d6c3110b1dmainat the latest pre-promotion check:1482c0bc89d2e458df4e846a3db09753f72718cdv1.6.10: absent at final pre-promotion checkThe missing stable tag is an explicit R20 product disposition, not an unreported
rebase. A later stable release is future reconciliation input and does not block
this internal RC19-based source candidate.
The promotion ancestry bridge is an
oursmerge with parents59ad8d90andfork
main86b3d352. It records both preserved histories without resolvingfiles or changing the validated runtime tree. Later changes are narrowly scoped:
the workflow-only Gitleaks repair in #100, single-read diagnostic manifest
hardening in #102, wiki terminal-error sanitization in #104/#108, and an
immutable Node base-image repin in #106. None changes indexing, embeddings,
graph semantics, provider routing, or package publication behavior.
Preserved State
/Volumes/LEXAR/repos/GitNexuswas not used for integration7171cf3d3cb7dbe19499ba6c95f0e5c2820abc19remains preservedr02-commit-disposition.jsonCapability Ledger
liblzma5Validation
29310661786: success; 39 checks, 35 success and 4 intentional skips29310717744: success; 39 checks, 35 success and 4 intentional skips8097d86fand59ad8d90with zero validated findings29315906591: success with an offline pinned CLI29315906713: success, includingCI Gate29315906534attempt 2: both language analyzers success29315906578: both image scans success29315906565: CLI and web builds success29315906548and Workflow Lint29315906579: successGitHub retains one non-required CodeQL policy snapshot from before three
alert-specific false-positive dismissals. It also attributes an old fork-main
quality finding to the large reconciliation diff. All required contexts are
green, the required CodeQL analyzers pass, and current instances for the patched
container CVE and five wiki log-injection findings are fixed. See
r24-current-head-security-disposition.md.GitHub may still report Dependabot alerts for the old default branch until this
source candidate is merged. Those default-branch alerts are not evidence about
the candidate lockfiles;
r21a-dependency-security-evidence.mdretains thecandidate's clean audit results.
Disposable Release Canaries and OpenClaw Scale Evidence
r19/fast-canary-59ad8d90/run-1.logr19/fast-canary-59ad8d90/run-2.logr19/final-run-1-59ad8d90/Each blocking canary creates a fresh 61-file/60-importer repository and isolated
home, exercises FTS deletion, add/edit/rename/delete, a 50-plus-file escalation,
all four durable interruption boundaries, forced-rebuild equivalence, embedding
row identity/dimensions, dirty-marker cleanup, sidecars, and process exit status.
Embeddings use a credential-free localhost deterministic server; no external
provider, model download, or API billing is involved.
The earlier full-corpus diagnostic exposed the graph-order defects corrected by
#94 and #96. Repeating complete OpenClaw graph construction and roughly 250,000
local deterministic embeddings for every candidate is now a nonblocking soak,
not a source-promotion gate. The decision and exact proof contract are retained
in
r19/fast-canary-gate-decision.md. The live index and its only copy remainexcluded.
Release Governance
mainrejects force pushes and deletioninternal-releaserequires administrator approvalmainpushes do not publishThe independent approval is a merge gate, not an engineering or PR-opening
blocker. This PR must remain unmerged until an eligible reviewer approves it or
the product owner separately authorizes a narrowly scoped governance decision.
Upstream Contributions
Fork-proven, sanitized contributions are tracked through upstream issues and PRs
abhigyanpatwari#2454-abhigyanpatwari#2482 as linked from fork issue #62. Upstream review, merge, or release is
non-blocking contribution follow-through and is not part of this promotion gate.
Rollback
mainis unchangedCloses #63
Parent: #39
Related: #58, #60, #66, #67, #68, #71, #72, #73, #74, #75, #76, #77, #78, #79, #80, #82, #83, #85, #87, #89, #91, #92, #94, #96, #97