mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
25 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0261982d9a
|
fix(analyze): make --memory-budget set the real heap and report rebuild reasons once (#3386)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(analyze): --memory-budget flag with heap-limit override and worker-pool degradation (#3137) Adds an explicit `--memory-budget <mb>` CLI flag that overrides the RAM/cgroup auto-sized main-thread heap ceiling for the parse phase: - CLI validation (integer >= 200 MB) before bar.start(), matching the --workers pattern - Threaded CLI → runFullAnalysis → PipelineOptions → parse-impl as memoryBudgetBytes - parse-impl resolves the heap limit as budget ?? v8.heap_size_limit, so both the preflight projection warning and the #2649 mid-loop abort probe honor the budget - Graceful degradation: when the projected heap need exceeds the budget at the computed pool size, the pool shrinks (never below 1, never above the operator's --workers) before sub-batch math and pool construction, so all downstream consumers see the degraded size Omitting the flag keeps the auto-sizer path byte-identical. Refs #3137 * feat(analyze): collapse rebuild-gate log into one summary + persist needsFullRebuild verdict (#3137) The nine meta-mismatch rebuild gates (pdg mode, content retention, schema fingerprint, graph-write collapse, analysis features, Spring vendor prefixes, runner identity, FTS CJK mode, embedding dims) each logged individually and set force:true independently. An upgrade that trips several at once printed a scattered wall of near-identical warnings. - Gates now collect into rebuildReasons[]; a single summary block prints them (inline for one, numbered for many) and sets force once. Per-gate Tip text is preserved verbatim inside the entries. - The verdict persists to meta (needsFullRebuild: {reasons, recordedAt}) BEFORE the rebuild starts. If the rebuild is interrupted, the next run announces the recorded reasons up front instead of quietly attempting an incremental write on a half-rebuilt index — the gates may not all re-fire against a wiped DB. - The verdict is cleared on the next successful completion (the final meta does not carry the field forward). Semantics unchanged: every gate was already evaluated (none early-returns), force is idempotent, and a rebuild happens iff at least one reason fired. Refs #3137 * refactor(cli): share one integer flag parser across analyze, watch, and wiki Replace the duplicated Number.isInteger checks for --workers, --embeddings, the positive env-backed analyze flags, the watch interval flags, and wiki's --timeout/--retries with parseIntegerOption (per-flag minimum, optional scale for the safe-integer bound). User-facing messages are unchanged. * fix(analyze): make --memory-budget set the real V8 heap through the respawn The budget now drives ensureHeap's existing respawn instead of a parse-phase override, so the #2649 preflight, mid-loop abort, remedy text, and GC pacing all see one heap limit. The respawn sizes old space plus three semi-spaces to equal the budget, the child resolves as already at the budget (no second respawn), and GITNEXUS_HEAP_LIMIT_SOURCE drives budget-aware OOM advice. Budget validation moves to the preAction hook so analyze and watch reject a bad value before any respawn. Removes the pool-shrink block and the memoryBudgetBytes plumbing through PipelineOptions and run-analyze. * docs(analyze): describe --memory-budget accurately and translate its help The help text claimed graceful worker-pool degradation, which no longer exists; it now says the flag sets the main-thread V8 heap and that parse workers keep their own caps. Wires the option through the help i18n map with en and zh-CN strings, and documents it in both READMEs and the out-of-memory troubleshooting section. * feat(analyze): add a pure rebuild-reason collector One collector per run holds keyed rebuild reasons, merges by key, flattens reasons stored by an interrupted rebuild into one recovery entry, validates stored reasons on read, and formats the single up-front summary plus one follow-up line for reasons added after the pipeline. * fix(analyze): route every forced rebuild through one reason collector Every path that forces a full rebuild (the nine meta gates, --force, --skills, --no-parse-cache, --drop-embeddings, --repair-fts retention, Spring Actuator, AsyncAPI, shared-store graph gaps, dirty-flag recovery, the post-pipeline capability gate, and the #2409 escalation) now adds a keyed reason to one collector. The rebuild decision is applied from the collector at fixed checkpoints, one summary prints right before the pipeline, and late reasons print one follow-up line. The escalation stays non-forcing. runFullAnalysis returns the collected keys, which replaces the runner-identity source-regex test with a behavior test. Removes the separate needsFullRebuild field and its announcement, and stops folding --skills and --no-parse-cache into --force. * fix(analyze): persist rebuild reasons on the existing crash marker Every incrementalInProgress writer (the full-rebuild stamp before the wipe, the incremental pre-write, saveIncrementalDirtyState including the #2409 escalation, and buildFtsDirtyStamp) now carries the collected reasons into the active slot's metaDir, so an interrupted rebuild explains itself on the next run through one merged recovery entry. A successful run still clears the marker and its reasons; the FTS-park recovery clears them without forcing. * test(analyze): cover every rebuild-reason key through runFullAnalysis Add a coverage table that the typechecker keeps complete: every RebuildReasonKey maps to a test file that drives it through runFullAnalysis and asserts the returned key. Adds the missing graph-write-collapse and drop-embeddings drivers, asserts the key in the existing pdg-mode, spring-vendor-prefixes, cjk-segmentation, and embedding-dims tests, and removes plan-local IDs from test names and comments. * fix(review): apply review findings - A --max-old-space-size pin equal to --memory-budget no longer counts as the exact budget heap (V8 adds the young generation on top); only the budget-respawned child skips the respawn, so the limit really equals the budget. - Snapshot the analyze env before ensureHeap and restore GITNEXUS_HEAP_LIMIT_SOURCE, so a kept process does not leak its heap source into a later programmatic analyzeCommand call. - --skills and --no-parse-cache keep the forced storage requirements they had before force stopped being folded from them. - Merge the duplicated follow-up announcement into one helper and fix a stale --drop-embeddings comment. - The rebuild-reason coverage table no longer greps driver files for the key string; add tests for a programmatic invalid budget and the multi-cause interrupted-rebuild text. * fix(review): don't announce the escalated write as a full rebuild The #2409 escalation is a non-forcing reason, but its follow-up line used the 'Full rebuild also required' lead. A follow-up that carries only non-forcing reasons now leads with 'Write plan changed'. * docs(analyze): document GITNEXUS_HEAP_LIMIT_SOURCE in the env table CONTRIBUTING requires every new GITNEXUS_* variable to have a row; this one is internal (set by analyze itself) and exists so OOM advice points at --memory-budget. * fix(review): address GitNexus review threads on #3386 - heapPressureRemedy measures pressure against the real auto-sized cap (heapCapMbFor) instead of a flat 0.75 x RAM, and no longer tells a GITNEXUS_MEMORY=off run with no pin to drop a pin that does not exist. - toStored() persists the interrupted rebuild's reasons first, as documented. - ensureHeap's doc names which paths leave GITNEXUS_HEAP_LIMIT_SOURCE unset. - The heap-respawn suite restores the caller's GITNEXUS_MEMORY. - The non-forcing follow-up test rejects any 'full rebuild' wording. * fix(review): require both budget flags and check key coverage at runtime - A budget-respawned child is recognized only when the inherited heap-source marker comes with both the budget's old-space and semi-space flags; the marker alone is an inherited env var, not proof. The old-space parser is generalized to any V8 size flag instead of copying its regex. - REBUILD_REASON_KEYS is exported and RebuildReasonKey derives from it, so the coverage table is checked at runtime (CI does not type-check test files). * fix(review): don't claim a full rebuild in a non-forcing summary formatSummary and formatFollowUp now share one leadFor helper, so a block of only non-forcing reasons reads 'Write plan changed' in both. --------- Co-authored-by: ChunxueLi <mecoloud@users.noreply.gitee.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> |
||
|
|
5fc518d2cb
|
fix(dart): resolve package imports by pubspec identity (#3369)
* fix(dart): resolve package imports by pubspec identity * fix(dart): keep package-identity edges out of the cycle check Pubspec identity edges invalidate importers when a manifest changes. They cannot form an init cycle, so the cycle query excludes them before the row cap. Discovery reads each manifest once, with a size bound, and resolution shares one package-URI parser with those edges. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3369) - Reject package URIs with an empty library path so they do not emit identity edges - Skip the pubspec permission test where chmod cannot deny reads - Document that the Dart heap probe is not a uniqueTarget spelling Note: pre-existing failure in test/unit/incremental-index-extension-dml-gate.test.ts not addressed by this PR. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dart): list package directories through a no-follow descriptor A directory replaced by a symlink between the parent listing and the next visit must not be traversed. The walk opens it with O_DIRECTORY|O_NOFOLLOW and lists that inode. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(mcp): keep the Dart identity reason out of MCP startup The cycle query still excludes the same reason string. The constant now lives with the other non-initializing import reasons, so MCP startup does not load a language provider. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3369) Open discovered pubspecs and child directories through the parent directory inode on Linux, so replacing that directory with a symlink cannot redirect the walk. Note: pre-existing failure in test/unit/incremental-index-extension-dml-gate.test.ts (worker pool startup timeout) not addressed by this PR. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dart): reject Windows junctions during pubspec walk * Address PR review feedback (#3369) Refuse pubspec discovery that cannot set O_NOFOLLOW, and verify macOS child opens against the pinned directory chain instead of reopening a mutable path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dart): bound live pubspec descriptors and close the macOS check-then-open A deep directory chain held one descriptor per level until open failed with EMFILE, and macOS child opens statted the path before using it. Refuse the next directory at 64 live handles, and stat only the descriptor opened with O_NOFOLLOW. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dart): open pubspecs non-blocking so a FIFO cannot hang discovery A listed pubspec can be replaced by a FIFO before open. O_RDONLY alone waits inside open for a writer, so the file-type check never runs. O_NONBLOCK returns immediately and the walk rejects the non-regular file. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dart): cap names read from each pubspec directory readdir kept every entry before the visit budget could run, so one huge directory could allocate without bound. Read the listing one name at a time and fail closed past 100,000 entries. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dart): reject a pubspec that grows while its descriptor is read The size cap was taken from the stat before the read, so a file that grew in that window could be parsed from a short prefix. Re-stat the same descriptor afterward and fail closed when the size no longer matches the bytes captured. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): rebaseline the Dart scope-capture fingerprint for package-import fixtures The benchmark hashes every dart-* fixture. The new package-import corpus adds six Dart files and 33 capture groups. Parking that directory restores the previous fingerprint, so this is corpus growth, not a capture change. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
47c2799ba7
|
fix(analyze): add cold parser rebuild option (#3153) | ||
|
|
b15ff2d888
|
feat(ingestion): mint Destination nodes from AsyncAPI 3.x documents (#3140)
* feat(ingestion): read AsyncAPI 3.x documents into broker addresses
Adds a format-driven reader that turns the `operations[]` entries of an
AsyncAPI 3.x document into (broker, address, direction) triples, plus the
protocol-to-broker map behind it. Nothing consumes it yet.
The reader lives outside `frameworks/spring/` on purpose, like
`destination-key.ts` and for the same reason: an AsyncAPI document is a
published artifact emitted by generators across several language toolchains
and written by hand as often as generated. The entry criterion is therefore
the document format -- a root `asyncapi` key -- and never the generator.
AsyncAPI 2.x is refused under its own countable reason rather than mapped.
Its `publish`/`subscribe` are inverted relative to 3.x `send`/`receive`, so
a naive mapping reverses every direction in the async graph while leaving it
connected: nothing fails, the arrows simply point the wrong way. A silent
skip would be indistinguishable from "this service publishes no document",
which is the one thing the refusal count has to be able to tell us.
The broker is read twice over -- from the operation's bindings and from its
channel's server protocol -- and the two readings must agree. A destination
keyed on the wrong broker joins a stranger, and with the document
contradicting itself there is no way to tell which reading is right, so the
operation is refused rather than decided by a coin flip.
An unmapped protocol passes through as its own literal instead of being
dropped, because `destinationNodeKey` takes a plain string precisely so a
non-Spring caller can attest to a broker Spring has no member for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cli): add --asyncapi-spec, an explicit path to AsyncAPI documents
Threads an `asyncApiSpecPath` option from the CLI, the server analyze
endpoint, and the programmatic entry through to `PipelineOptions`. Nothing
reads it yet; the reader added in the previous commit is still unwired.
Shaped deliberately after `springActuatorPath`, the existing option for an
out-of-band artifact: an explicit local path, accepting a directory or a
single file, resolved against the repository root so a committed
`docs/asyncapi` and an absolute cache populated by something else are both
natural, and `undefined` keeping the feature entirely off. Mirroring that
option rather than inventing a mechanism is what lets a downstream consumer
point the reader at documents fetched out of band without patching a file
here.
`analyze --watch` REJECTS the flag, exactly as it rejects --spring-actuator.
The watcher reacts to source changes and nothing watches a document
directory, so honouring it there would read the documents once and then
serve a stale answer for the rest of the session -- worse than refusing,
because it looks like it worked.
Additive only: 49 inserted lines, no deletions and no modified lines. Every
new interface member is optional and every forward is an object-literal
spread of an undefined value, so with the option unset the analyzer takes
byte-identical paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(ingestion): mint Destination nodes from AsyncAPI documents
Wires the reader into the destinations phase. With `--asyncapi-spec` set,
every `send` operation emits PUBLISHES_TO and every `receive` emits
CONSUMES_FROM against the ordinary resolved `Destination` node -- same key,
same `address` property -- so a document and a source site that name one
address on one broker land on ONE node and the two halves of a conversation
meet. Verified end to end: an address named only in a document is minted
with the right broker and direction, and an address a source site already
resolved stays a single node with its own `literal` provenance while the
same address on a different broker stays separate.
This claims only what a document states -- that the service talks to that
address, on that broker, in that direction -- and never which method does
it. The addresses in one document partition by (broker, action) into buckets
that usually hold more than one operation, so any assignment past a bucket
of size one is a heuristic, and a wrong one attaches a real address to the
wrong handler: a false connection wearing the clothes of a resolved one. The
edge therefore starts at the document, not at a callable. That is weaker
than a source-derived edge and worth having anyway, because it is available
where the source supplies nothing at all -- a programmatically registered
listener, a broker with no patterns here, a language whose messaging idiom
nobody has taught this codebase yet.
Documents are read even when the source pass found no messaging, which is
why the early return had to move: a repository whose brokers are invisible
to the patterns is precisely the case a published document covers, and an
early return keyed on source sites skipped the documents exactly there.
Their counters are kept in their own block rather than folded into the
existing ones. `refusalsByReason` is the denominator of the SOURCE
unresolved fraction, and a mistyped specification directory must not be able
to make the source look worse than it is. The block is absent -- not zeroed
-- when no path was configured, so "not asked for" stays distinguishable
from "asked for and found nothing"; those need different answers from an
operator and one zero cannot say which happened.
The direction assertion is the one that matters and it is pinned by type,
not by existence: inverting the mapping in the source tree fails exactly one
test, because every other assertion passes identically under both readings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: document --asyncapi-spec and why step 4 of the cascade stays empty
Adds the flag to both READMEs and to the three byte-identical copies of the
CLI skill, which a sync test pins together.
Also rewrites the note on the `specification` seam in the address cascade.
It said "nothing supplies it today", which was true and is now misleading:
a reader exists, and the hook is still unsupplied because of a decision
rather than for want of one.
A document names addresses; it does not name the method that uses one. To
hand an address to a particular candidate something must choose which of the
document's operations belongs to it, and the only division both sides agree
on -- (broker, action) -- leaves buckets that usually hold more than one
operation. On a real generated document exactly one bucket of four was
unambiguous. Every assignment past a bucket of size one is a heuristic, and
a wrong one puts a REAL address on a joining node under the wrong site: a
false connection wearing the clothes of a resolved one, which is the outcome
the keying rule exists to prevent.
The note also records the two things that would change that and are not
heuristics -- a document carrying the implementing symbol, or a
configuration source answering the `${key}` the candidate already recorded
-- and that the second wants its own resolver, since what it needs is the
placeholder key rather than the candidate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ingestion): refuse the document shapes that would forge a join
Four ways a conformant AsyncAPI document could mint a Destination that
connects two services which have said nothing about each other. Every one is
reachable from ordinary 3.x vocabulary, not from malformed input, and each is
now a countable refusal.
A PARAMETERIZED ADDRESS is a pattern, not a place. Two services that publish
`{env}.orders` share a template; one deploys with env=prod and the other with
env=staging, and keying on the template text merges them into a single node
with a publisher on one side and a subscriber on the other. This is the
document-side twin of `overridable-config-default`, which argues the same
thing about `${key:default}` in source. A channel declaring `parameters` is
the specification's own statement that its address is a template, so the
detector is a reading rather than a guess; the `{` test catches generators
that template without declaring.
ANY BINDINGS KEY WAS TAKEN AS A PROTOCOL. AsyncAPI allows `bindings` to be a
Reference Object, so the map's own key can be `$ref` -- and passed through,
that becomes half of a join key carrying no broker information at all. Two
services that both reference shared bindings and both name `orders` then land
on one node, defeating the broker-in-key rule that keeps `kafka orders` and
`rabbit orders` apart. A broker must now be spelled like a protocol name.
A BROKER CONTAINING A SPACE COLLIDES, because the node key joins with one:
("kafka orders", "x") and ("kafka", "orders x") are the same key. That was
latent while every broker came from Spring's closed union. This module is the
first caller to feed the shared helper text that a document wrote, which is
exactly the condition under which it stops being latent, so it is closed here
-- at the producer -- rather than by changing an encoding that `routeNodeKey`
shares.
THE ADDRESS WAS TRIMMED, while the source cascade keeps an address exactly as
written so `" orders "` stays its own node. Two producers of one key held
opposite whitespace policies and the document side erred toward joining.
Also: fold the transport-security protocol variants (`kafka-secure`,
`secure-mqtt`, `wss`, `stomps`, `https`) onto their base protocol. The
`amqp`->`rabbit` argument already in this file demands it -- AsyncAPI's server
vocabulary distinguishes them and its bindings vocabulary does not, so a
secured cluster's own document was being read as self-contradictory. Treat a
channel with no `servers` as available on all of the document's servers, which
is the specification's default and was costing every single-server document
its destinations. Bound the address and operation-id lengths, because
`generateId` concatenates rather than hashes, and bound total operations
across the run rather than only per document.
Read each file through ONE handle for both the size gate and the read, as
`actuator-runtime.ts` does and for the reason its comment gives (CodeQL
js/file-system-race): re-resolving the path lets a swapped file bypass the
cap, and the out-of-band cache this option reads is written by other tooling
by definition. Open it with O_NONBLOCK: the type check that rejects a FIFO is
unreachable without it, because opening a FIFO for reading blocks in open(2)
until a writer appears -- found by writing the test first and watching it time
out rather than fail.
Count symlinked entries and walk truncation instead of dropping them in
silence. A symlinked cache and a wrong path were producing identical results.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(analyze): rebuild when documents are configured, and report what was read
An AsyncAPI document is external to git freshness in exactly the way an
Actuator snapshot is: replacing one moves no commit and dirties no file. The
option's own README paragraph advertises an absolute cache written by other
tooling, and on the second run of that workflow the already-up-to-date fast
path fired, no document was ever opened, and the previous run's addresses were
served as current. Measured, not reasoned: editing a document and re-running
printed "Already up to date" and left the old address in the graph; with this
change the same probe re-reads and the new address replaces it.
So an enabled run forces a rebuild and dropping the option forces one more, to
clear document-derived evidence -- the treatment `springActuatorPath` already
gets, for the same reason. Only the FLAG is recorded in index metadata, not
the path: Actuator retains its inputs so future scans keep excluding them,
whereas a committed document is deliberately NOT excluded (it wants its real
`File` node), so there is nothing to retain and recording the path would put
an operator's directory layout into metadata for no consumer.
That also settles a defect it would have been tempting to patch separately. A
synthetic `File` node for an out-of-tree document carries a path that is in no
write set and is not covered by `isGraphWideNode`, so an incremental writeback
dropped the node while keeping its edges -- which then COPY against a row that
was never written, and fail into an IGNORE_ERRORS retry that reports success.
A forced rebuild has no incremental subgraph to get that wrong.
Distinguish the two meanings of `resolution: 'specification'`. That value
belongs to the address cascade and means a CODE candidate was resolved through
the step-4 hook; a node minted from a document has no code site and now says
`asyncapi-document`. Reusing one string would leave a query that groups by
provenance unable to separate an address a document states from one a document
was used to resolve, and only the second is a claim about source.
Report what was read. The stats block was justified on the grounds that an
operator must be able to tell a mistyped directory from a repository with no
documents -- and nothing surfaced it, so the justification was aspirational. A
configured path that yields nothing, or a walk that hit a bound, now warns
unconditionally, as `spring-auto-configuration.ts` does for the same class of
input. The phase summary carries the refusal breakdown rather than only the
totals, because the unresolved fraction is the number this work is judged on
and a bare count says how big the gap is without saying what would close it.
Tests for the three wiring lines that were individually deletable with a green
suite, following the templates already in the repository: a row in the
`--watch` rejection table, the CLI-threading assertion beside the Actuator
one, and the shipped-skill fragment that pins the flag in all three copies.
Also pin `filePath: ''` on a spec-minted destination -- the half of the keying
rule that stops a shared node becoming collateral damage of one document's
next change -- and the in-repo `File` branch, which was dead-code-able.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ingestion): close the join-forging paths a second review round found
The `$ref` exclusion added last round fixed one instance of a class and left
the class open. A `bindings` map key of `x-scs-function` -- an ordinary
Specification Extension, which generators emit -- still became the broker, so
two unrelated services carrying one vendor annotation and one address landed
on ONE node whose broker half said nothing about any broker. And
`{ kafka: {}, x-internal: {} }` read as two brokers, losing a conformant
document and reporting it as self-contradictory, which also made any document
author a one-line saboteur of their own cross-service links.
So the two readers are now separate functions with opposite defaults, and the
header says why they must be. `servers[].protocol` is a FIELD DECLARED to hold
a protocol: an unrecognized value there is the document's own claim and passes
through, because refusing it would lose a destination the document states
plainly. A `bindings` MAP KEY is not that -- the specification puts `$ref` and
`x-` in the same namespace -- so a non-protocol key is the EXPECTED case and
only AsyncAPI's binding vocabulary may answer. The syntactic test that was
applied to both was the right rule for one of them.
The walk fix from last round introduced something worse than it reported. A
shared abort flag meant depth exhaustion in ONE branch terminated the whole
traversal, so ten good documents beside a twelve-deep unrelated subtree were
kept or lost depending on whether that subtree sorted before or after them.
Truncation and budget-exhaustion are now separate: depth returns from its own
branch, and only a genuinely global bound stops the walk.
Rewriting `protocol.ts` dropped the whitespace check from the server-protocol
path and made the node-key collision reachable again. The test written for
that collision last round caught it within the minute; the comment now records
that it was learned twice.
Everything else measured this round:
- The broker is the THIRD string that reaches a graph identifier, and it was
unbounded while the header claimed there were two. A one-megabyte protocol in
a document satisfying every other cap was measured producing a gigabyte of
resident identifier strings, because `generateId` concatenates rather than
hashes and the phase mints one id per node and per edge.
- The run-wide operation budget counted ACCEPTED operations, reproducing at the
run level the exact defect the per-document cap was corrected for last round:
a run whose every operation is refused never decrements it. Both now count
operations EXAMINED, and `operation-cap` sets `truncated` -- it is a bound
that stopped the operation count, which is what that flag is documented to
mean.
- The channel-inherits-all-servers rule ran per operation. Hoisted: it depends
only on the servers.
- A subdirectory that cannot be listed is counted rather than dropped, so a
mixed-permission cache cannot report a clean, complete read.
- The read LOOPS, like the Actuator reader this claims to follow. A single read
was never short across seven hundred probes on APFS, but POSIX permits it and
FUSE mounts with `direct_io` -- the deployment this option targets -- return
short counts. A document truncated at a line boundary still parses, so the
failure is silent: operations vanish with `refusals: {}`.
- `parameters: {}` no longer refuses a literal address; an empty container
states nothing and generators emit them.
- A channel that is itself a Reference Object gets its own reason instead of
`no-address`, which was telling operators their documents omit addresses when
the reader simply stops one hop short.
- A multi-protocol document resolves from its operation's own bindings; only
when those are silent does an inherited multi-protocol server set refuse, and
under `ambiguous-server-default` rather than a reason that says the document
contradicts itself. It does not.
- HTTP and WebSocket are refused for destination minting. For a broker the
topic is the namespace; for HTTP the host is, so keying on the path alone
would make every service exposing `/events` one node. A `Route` already
models an HTTP endpoint, with its method in the key.
- The sniff window is a parse gate, not a read gate, and four kilobytes refused
a good document behind a licence header.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(ingestion): pin the wiring and the reporting that were deletable
Three lines could be deleted with the whole suite green, and each of them
makes the feature partly or wholly inert: the forward from `run-analyze` into
`PipelineOptions`, the forced rebuild while documents are configured, and the
cleanup rebuild when the option is dropped. None is visible one layer up,
where the CLI test asserts on a mock's arguments.
One integration test closes all three. It drives the real `runFullAnalysis`
against a real repository and asserts, in order: the enabled run does not take
the up-to-date fast path and logs the rebuild; the destination reaches the
graph, which only happens if the option is forwarded; a document edited with
the tree clean and the commit unchanged is re-read; dropping the option
rebuilds once and removes the document-derived evidence; and the run after
that is up to date again -- the "rebuilds once" half, which rests on the
metadata being written as a fresh literal rather than merged, and which
nothing pinned.
The document lives OUTSIDE the repository on purpose. That is the workflow the
option is documented for, and it is the only one where the hazard exists:
editing a tracked file dirties the tree and forces a rebuild anyway, so an
in-repo fixture would pass with the freshness fix reverted. Verified by
reverting both: deleting the forward fails on the empty destination list,
deleting the forced rebuild fails on the missing log line.
Also pinned, each because deleting the code it covers left the suite green:
the `parameters` half of the templated-address refusal (its old test supplied
a braced address too, so the `{` half alone satisfied it); the phase actually
forwarding `symlinksSkipped`; the unconditional warning, whose whole argument
is that a tally nobody can see is not a tally -- captured through the
repository's own `_captureLogger`; a `.yml` document; a character device,
which is the case the `isFile` check exists for and which the FIFO test does
not reach; and the bound that stops a walk.
Reject an empty `--asyncapi-spec` at the CLI. It resolved to the repository
root and walked the whole tree, defeating this module's own rule that there is
no glob-based auto-discovery -- and the HTTP entry point already rejected the
identical value. Two doors onto one option must not hold different rules.
Surface the flag in the MCP context resource beside `spring_actuator`. It
matters more there than for its neighbour: Actuator annotates nodes the source
pass already found, while document reading mints destinations and edges with
no code site, and nothing said where they came from.
Log the configured path relative to the repository. The same change refuses to
persist that path to index metadata because it would record an operator's
directory layout; holding that rule for metadata and not for logs was holding
it in one place.
Both test files now clean up their temporary directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* style(ingestion): apply the repository's prettier contract
`quality / format` runs `npx prettier --check .`, and three files added by this
branch were not formatted to it. No behaviour changes: the reader's line
breaks and two test literals move, nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(ingestion): release the mini-repo handle the document test allocated
`setupMiniRepo` documents that the caller owns cleanup, and every other test
in this file calls `repo.cleanup()` in its `finally`. The AsyncAPI document
test removed only the document directory, so each run left a temporary
repository behind.
The two owners are separate on purpose: the document directory is a SIBLING of
the repository, placed outside the working tree so that editing it cannot
dirty the tree and force a rebuild on its own. The repo's cleanup therefore
does not reach it, and both calls belong in the same block.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ingestion): stop partial server evidence from reading as unanimous
Six review findings, every one a way this reader could name a broker the
document does not name. They share a shape: something is DROPPED rather than
refused, the remaining evidence agrees with itself, and an operation is
attributed with confidence to a broker its document never settled on. A wrong
broker is half a join key, so it does not produce a missing edge -- it produces
an edge to a stranger, reported as a fact.
CAPPED SERVER MAPS. A channel with no `servers` inherits all of them, and that
map is capped at 1,000. A document whose first thousand servers are Kafka and
whose thousand-and-first is JMS read as unanimously Kafka, because unanimity
was tested on the slice. Counted `server-cap` and set `truncated`, but neither
stopped the attribution. The inherited path now refuses under
`capped-server-default` -- checked BEFORE agreement, since a subset agrees with
itself for free.
ROOT SERVER REFERENCE OBJECTS. The Servers Object patterned field is
`Server Object | Reference Object`, so `{ $ref: '#/components/servers/prod' }`
is conformant. Reading `protocol` off the raw value dropped every one: an
all-reference document had no protocol at all, and -- worse -- a MIXED set lost
its disagreeing half and became unanimous. One hop is now followed, through
`#/servers` and `#/components/servers`; anything else is refused under
`unresolved-server-reference` rather than skipped.
CHANNEL BINDINGS. Only the operation's bindings were read. A conformant channel
carrying `bindings: { kafka: {} }` with no operation binding was dropped as
`protocol-unknown` while the document said plainly which broker it meant, and a
disagreement between the two levels was invisible. Both are read; a conflict is
`protocol-disagreement`.
EMPTY `servers`. "If `servers` is absent or empty, this channel MUST be
available on all the servers defined in the Servers Object" -- one sentence,
both cases. A zero-iteration loop returned `explicit: true`, which blocked the
inherited fallback and dropped valid operations.
POINTER DECODING ORDER. RFC 6901 percent-decodes the fragment BEFORE splitting
on `/`. The raw token was tested for a separator first, so `#/channels/orders%2Fv1`
passed a check it should have failed and then decoded into two segments -- a
pointer addressing `channels.orders.v1` was read as a channel named `orders/v1`,
inventing a channel the document never declared. A malformed escape is now
refused rather than resolved against its undecoded text. `~1` still resolves; it
is the pointer's own escape and belongs after segmentation.
THE SNIFF WINDOW. A fixed window decides by where the root key sits rather than
whether it is there, so every window is a false negative waiting for a longer
preamble -- 4 KiB was replaced by 64 KiB for that reason and inherited the same
defect. The whole text is scanned; it is already bounded and already in memory,
and the gate exists to skip the PARSE, which is the expensive half. A leading
UTF-8 BOM is stripped before both sniff and parse.
Twelve of the fourteen new tests were run against the unfixed reader and all
twelve failed; the other two are controls that must pass either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(ingestion): simplify AsyncAPI pointer and binding resolution
Decode each $ref once, union binding evidence, and stop walking capped
server maps whose brokers are unused on the inherit path.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
131d9f93fd
|
feat(route): resolve vendor-derived Spring mapping annotations by suffix (#2883)
* feat(route): resolve vendor-derived Spring mapping annotations by suffix Frameworks commonly wrap Spring's built-in annotations with company-specific variants (e.g. Winning Health's @WinPostMapping wraps @PostMapping). The annotation definition lives in a binary JAR — not in source — so the meta-annotation cannot be read statically. Add resolveSpringAnnotationAlias(): resolves custom annotations by naming suffix (WinPostMapping → PostMapping → POST). This matches the universal Java convention of naming derived annotations with the base name as a suffix. Works for any vendor prefix, not just one company. The fix is in springAnnotationHttpMethods() (spring-shared.ts), which both the ingestion extractor (spring.ts) and the group extractor (java.ts) call. A single-function change propagates to both layers automatically. Zero configuration: no .gitnexusrc, no annotation allowlist. If an annotation name ends with a known Spring mapping suffix, it inherits that annotation's HTTP semantics. False-positive risk is negligible. Tests: 16 new unit tests covering resolveSpringAnnotationAlias directly, springAnnotationHttpMethods with aliased annotations, end-to-end extractSpringRoutes with vendor annotations, and ingestion/group parity. Existing route tests (260) continue to pass. * fix(route): address review findings — class-level aliases, registered prefixes P1: class-level @WinRequestMapping now gets the same prefix/constraint semantics as @RequestMapping — all five class-level exact-match sites (spring.ts phase-1 collect, typeRequestMethods, typeClassPrefixes; group http-patterns java.ts typeRequestMethods + type-level branch) route through the new shared isClassLevelMappingAnnotation predicate, and the hard-coded 'RequestMapping' argument in springAnnotationHttpMethods calls is replaced with the actual annotation name so alias resolution applies. P2: suffix-only alias matching accepted unrelated annotations (@AuditPostMapping emitted a phantom POST /audit). Alias resolution now requires a REGISTERED vendor prefix — 'Win' by default, extendable via GITNEXUS_SPRING_VENDOR_PREFIXES=Win,Acme without a rebuild. Tests: negative e2e for the unregistered-suffix phantom route, vendor class-prefix parity e2e, predicate unit matrix, env-registration test. * style: prettier * chore: drop accidental gitnexus-shared/dist worktree symlink from prettier commit Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#2883) Wire vendor Spring mapping aliases into Kotlin ingestion and group extraction, restore GITNEXUS_SPRING_VENDOR_PREFIXES after the env test, and stamp spring.route-bindings so existing indexes rebuild. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(route): honor Kotlin vendor aliases and prefix freshness Parse Kotlin RequestMapping method arrays in the shared Spring helper, bump spring.route-bindings, and rebuild when registered vendor prefixes change. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ChunxueLi <mecoloud@users.noreply.gitee.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
e1e2464960
|
feat(kotlin): bind Spring config consumers on Kotlin sources (#3126)
* feat(kotlin): bind Spring @Value and ConfigurationProperties consumers Kotlin sources were skipping the Java-only config-binding attach path, so mixed JVM apps under-reported blast radius for Kotlin placeholders. Capture from the live AST, serialize on the existing side channel, and reuse the shared binder. (U1-U4) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(review): require exact Spring annotation FQNs and skip raw-string escapes Reject similarly named third-party imports and leave Kotlin triple-quoted bodies undecoded so capture stays fail-closed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): use only grammar-valid Kotlin string and class-name nodes The coverage shard failed the #1920 literal gate on invented string node types and a Java-style name field. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): fail closed on non-literal prefixes and persist unresolved config markers Reject constant and boolean @ConfigurationProperties arguments, scope nested Value shadows to their owner, and rewrite drifted consumer files when a config key is deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * test(kotlin): add config-consumer capture benchmark and fix JVM feature stamps The Kotlin capture path had no performance or behavior guard: a file-wide lexical shadow regression silently dropped two of every three facts. The new bench arm fingerprints @Value / @ConfigurationProperties facts from an explicit-import control against a wildcard-import corpus whose files each declare a sibling nested `Value` type, so parity between the arms is the regression gate, and CI runs it with scaling + widening budgets. Broadening spring.config-bindings to .kt also means Kotlin-only JVM repos now stamp the feature, which the two exact-map orchestration expectations still denied. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(kotlin): let an explicit import shadow the Spring wildcard import importedAs accepted a star import of the Spring package even when the same simple name was explicitly bound to another type, so a file importing com.example.Value alongside the Spring annotation package emitted a false @Value consumer fact. Kotlin resolves the explicit import first, so the wildcard branch now only applies when the name is otherwise unbound. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c217a0f257
|
feat(spring): import optional Actuator runtime data with Kotlin JVM mapping (#3107) | ||
|
|
9718e1247a
|
fix(parse): stabilize parse-cache chunks and cheapen ParsedFile loads (#3093)
* fix(parse): stabilize parse-cache chunks and cheapen ParsedFile loads Hash-bucket membership so worker count and add/delete no longer reshuffle cache keys; GC and path sidecars keep small-shard scope-resolution from full-store JSON and empty GCs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(review): apply review findings Drop the unused pool argument from cache-budget resolution, reuse path compare helpers, and copy durable sidecars via full shard paths. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): copy durable path sidecars via full shard paths Keep restore destinations relative to the run store even when sidecar names are derived from absolute json paths. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): fail closed on truncated ParsedFile path sidecars Skip JSON only when a sidecar is complete (NUL-free, trailing newline). Truncated listings without a NUL were able to omit wanted paths. Co-authored-by: Cursor <cursoragent@cursor.com> * style: apply prettier to ParsedFile store and tests Match the PR autofix formatter so CI quality does not flag wrap-only diffs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): tighten ParsedFile path sidecars from review Skip sidecar writes when a path contains CR/LF, and assert the skip path does not open non-intersecting JSON shards. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): yield on sidecar skips and assert restore copies listing bytes Skipped shards now count toward the 128-shard event-loop yield, and restore tests check sidecar contents rather than existence only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): treat path sidecars as best-effort after a JSON shard write A sidecar ENOSPC/EACCES must not fail persist; load already falls back to the JSON shard when the listing is missing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): drop stale path sidecars when a shard is no longer listing-safe Rewriting a shard with a newline-bearing path must unlink the old listing so load does not skip the JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(parse): keep worker-integration tests aligned with hash buckets Quarantine cache-skip asserts the poison pack hash, clone-skip keeps poison and survivors in one bucket, and restore unlinks a stale dest sidecar when the durable source has none. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(parse): address review follow-ups for cache packs and sidecars Record SCHEMA_BUMP 80, pin pack locality and sidecar load/restore tests, and keep sidecar I/O best-effort with shared ENOENT handling. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): drop stale path sidecars after a failed listing write A leftover .paths file after ENOSPC (or similar) made load skip the new JSON shard. Hash expected packs with the same env budget production uses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(store): drop path sidecars before overwriting parsed-file JSON Load trusts a leftover .paths listing, so rewriting a shard must unlink that listing first. Otherwise an interrupted sidecar refresh can hide newly written files. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(store): fail closed on truncated or CR path sidecars Count-prefix listings so a newline-terminated partial sidecar cannot skip the JSON shard, and reject CR instead of stripping it. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): expect single-file watch refresh telemetry The production analyze --watch e2e was still pinned to the old pack-cascade "8 re-parsed" line, so shard 1/3 timed out after a correct 1-file refresh. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): expect one reparsed file on a non-bean incremental touch Pack-cascade leftover: the drift-skip test still required 7 reparsed files after logger.ts-only edits. Cheap ParsedFile loads now reparse just that file. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
bf7dcf98ca
|
feat(analyze): add incremental watch mode (#3072)
* feat(analyze): add incremental watch mode * fix(watch): harden control file reads * fix(watch): contain refresh errors and bound reads * fix(watch): stream strict control file reads * fix(watch): harden refresh recovery and lifecycle * fix(watch): report ignored repository defaults * fix(analyze): preserve signal exit semantics * style(analyze): format signal exit helper * test(config): exercise descriptor growth guard * test(watch): await source event before rename * fix(watch): keep live-index retries honest and ignore analyzer writes Hold retry backoff when events merge, stop only after a live-index mutation, skip .gitnexus self-writes, and reject the remaining one-shot watch flags. Export impact-risk scoring from gitnexus-shared so consumers can share the same scale. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(watch): contain queue edge cases after review Preserve overflow-only refreshes, contain synchronous refresh failures, and mark successful atomic publication before later operations can fail. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
fe3d7e56be
|
feat(spring): detect non-HTTP handler entry points (#2891)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
|
||
|
|
cdc98a9cf8
|
fix(java): capture enum interface heritage (#2935)
* fix(java): capture enum interface heritage * fix(java): harden enum heritage dispatch * test(java): refresh synthetic capture baselines --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
7468cc915b
|
fix(analyze): replace the hand-incremented schema version with a derived DDL fingerprint (#2798) (#2808)
* feat(schema): derive a fingerprint from the DDL this build creates `SCHEMA_FINGERPRINT` is a sha256 digest of the node and relation DDL that `runSchemaCreationQueries` actually executes, in the same shape as the existing `taintModelVersion` stamp (hex, sliced to 12). It exists because `INCREMENTAL_SCHEMA_VERSION` is hand-picked and has to *predict* whether an on-disk database matches this build's DDL. That number has collided with `main` eight times, twice exactly — and an exact clash is the quiet one, because the reuse gate is a strict `===`. `EMBEDDING_SCHEMA` is deliberately excluded: its `FLOAT[N]` width comes from `GITNEXUS_EMBEDDING_DIMS` at module load, so folding it in would make the digest a function of the environment rather than of code, and two runs of the same build under different env would thrash full rebuilds. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(storage): record the DDL fingerprint in RepoMeta `RepoMeta.schemaFingerprint` stores the digest of the DDL an index's tables were actually created from. It is the derived companion to `schemaVersion`, not its replacement: both are compared, and both must match. Absent means mismatch, deliberately. Grandfathering a missing fingerprint would let an incremental top-up stamp a fresh one onto a database whose DDL was never verified, permanently certifying exactly the wrong-shaped index the field exists to catch. The cost is one full rebuild per pre-existing index. The version ladder gains a note that its "re-check against origin/main before merge" ritual now only guards *semantic* bumps. v25, v26, v30, v31 and v34 all changed emitted ids, edges or wire formats while leaving the DDL byte-identical, and the fingerprint cannot see any of them — but DDL collisions no longer need renumbering. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(analyze): gate index reuse on the DDL fingerprint, not just the version (#2798) `INCREMENTAL_SCHEMA_VERSION` is a hand-incremented integer that has to predict a derived fact: whether the on-disk DDL matches the code's DDL. It has collided with `main` eight times, and twice the collision was *exact*. An exact clash is the silent one. Two builds stamp the same number over different DDL, the `===` reuse gate reads the index as current, every `CREATE ... TABLE` is then skipped as "already exists" (suppressed in `runSchemaCreationQueries`), and the edges whose endpoint pair the live database cannot hold are dropped by `fallbackRelationshipInserts`' bare `catch`. The result is a wrong graph, with no error anywhere. Reuse now requires the version AND the DDL fingerprint to match, in both the pre-pipeline force-rebuild guard and the `isIncremental` predicate, and the fingerprint is stamped alongside the version at the end of a run. Both conditions are necessary. The fingerprint does not replace the integer: most entries in the version ladder change emitted ids, edges or wire formats while the DDL stays byte-identical, and a fingerprint-only gate would stop forcing rebuilds for all of them. What it does buy is that two branches picking the same number no longer need renumbering. The new branch sits above the `alreadyUpToDate` fast path for the same reason the version guard does — a clean tree at an unchanged commit would otherwise early-return before either check ran. Closes #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(analyze): pin the DDL fingerprint gate and its two failure cases `schema-fingerprint.test.ts` pins the properties the gate rests on: the digest covers exactly the node and relation DDL that gets executed (recomputed from the exported lists, so adding a table or a FROM/TO pair without the fingerprint moving is impossible), it excludes the environment-derived embedding DDL, and it moves when any covered string moves. The two `incremental-orchestration` cases exercise the production path rather than modelling it: an index carrying the *current* version with a foreign fingerprint, and one with no fingerprint at all. Both were run against the pre-fix tree first and both failed there with `alreadyUpToDate === true` — the fast path swallowing the mismatch, which is the #2798 symptom exactly. `call-summary-schema-version.test.ts` widens its gate model to two equalities. The second argument defaults to the current fingerprint so all 33 existing version cases read unchanged, and a new case covers the collision, the legacy absence, and the semantic bump the fingerprint cannot see. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(review-skill): point the schema-constant check at the fingerprint, not the deleted integer All four `gitnexus-review` SKILL.md mirrors told reviewers to verify `INCREMENTAL_SCHEMA_VERSION` "was bumped or regenerated". That constant no longer exists, so the instruction sent every future reviewer looking for something they could not find — and, worse, past its replacement. The check for graph DDL is now derived: `SCHEMA_FINGERPRINT` moves on its own, so the question is whether the diff changed a string in `NODE_SCHEMA_QUERIES` / `REL_SCHEMA_QUERIES`, and whether a newly added DDL array was folded into the fingerprint at all — the one way the derived gate can still be bypassed. What did NOT change is called out explicitly: the parse-store `SCHEMA_BUMP` and the bench fingerprint sets are still hand-maintained and still need the re-check-against-base ritual, and semantic changes that leave the DDL untouched fall outside the fingerprint entirely — those rely on the analyzer runner-identity receipt. Found by the review swarm's docs lane. The original plan for #2798 claimed no documentation mentioned the constant; that sweep covered five root docs and never looked at `.claude/skills/**` or the three mirrors. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(migration): record the one-time rebuild the fingerprint switch costs Replacing `schemaVersion` with `schemaFingerprint` means every index written by an earlier GitNexus carries no fingerprint, reads as a mismatch, and is rebuilt once. That is deliberate — grandfathering absence would stamp a fresh fingerprint onto a database whose DDL was never verified — but until now it was undocumented, so a user's first post-upgrade analyze would announce a full re-analyze with nothing to explain it. MIGRATION.md already sets the precedent: PR #2363's meta.json → gitnexus.json rename was equally automatic and equally in need of an entry. This follows that shape, and is explicit about the parts that are easy to undersell: - the cost is per INDEX, and branch-scoped slots (#2106) each pay separately; on a large repository a full re-analyze is substantial, not a blip; - rollback is safe — an older binary sees no `schemaVersion` and forces its own rebuild, which is a cost, never a stale graph; - alternating between an old and a new binary rebuilds on every switch, because the end-of-run meta is written as a fresh literal so neither field survives the other's run. The retired ladder's per-version rationale is pointed at in git history rather than reproduced: `git show 561f913a3:.../repo-manager.ts`. That commit is an ancestor of origin/main, so the pointer survives this branch being squash-merged. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(identity): cover workspace-linked packages in the analyzer dependency digest `dependencyNames` enumerated `dependencies`, `optionalDependencies` and `peerDependencies` only. `gitnexus-shared` is declared as a devDependency (`file:../gitnexus-shared`), and in a source-mode run the build root is the gitnexus package tree, which does not contain that sibling. So a change to gitnexus-shared moved neither `build.digest` nor `dependencyRuntime.digest`. That gap matters more since #2798 deleted `INCREMENTAL_SCHEMA_VERSION`. A DDL-affecting edit there is still caught by `SCHEMA_FINGERPRINT`, but a SEMANTIC-only edit — a new `REL_TYPES` member, say, where the relation table carries a bare `type STRING` column so no CREATE statement moves — was covered by nothing at all. Roughly thirty of the retired ladder's entries were exactly that change class, and the runner-identity receipt is what now carries them. Only checkout-local specifiers are added: `file:`, `link:`, `workspace:`, `portal:` and npm's bare local-path shorthands. Pulling in every devDependency was rejected — vitest, eslint and typescript would enter the digest and force a full re-analyze on unrelated tool bumps, which is worse than the hole. Scanning the linked sibling for the first time exposed a latent throw: `collectArtifacts` honoured `PRUNED_RUNTIME_DIRECTORIES` only for a real directory, so a SYMLINKED `node_modules` fell through to the payload branch and died with "Analyzer identity input is not a file". Worktree-style dev layouts and pnpm shared stores hit this immediately — verified in this worktree, where `gitnexus-shared/node_modules` is such a symlink. Pruning it loses nothing: packages beneath are still reached through `resolveDependencyPackageRoot`. Verified: a real `analyze` in this worktree succeeds with `packageCount` 259; editing the linked package's source moves the digest, bumping an installed registry devDependency does not, and removing the link moves it. `DEPENDENCY_RUNTIME_CANONICALIZATION` is deliberately not bumped — freshness compares digests, not the label, and the input-set change already moves them. Follow-up worth having: no fixture in the suite declares `devDependencies`, so this has no regression test yet. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(analyze)!: delete INCREMENTAL_SCHEMA_VERSION, gate reuse on the DDL fingerprint alone The integer and its ~180-line version ladder are gone, along with `RepoMeta.schemaVersion`. Index reuse is now decided solely by `SCHEMA_FINGERPRINT`; a mismatch — including the absent stamp every pre-existing index carries — warns and forces a full re-analyze, which wipes and recreates the database so the tables are built from the current DDL. Deleting the integer is safe because it was already redundant: the runner-identity guard deep-compares the whole schema-v4 receipt, including a digest over the build tree, and forces a rebuild on ANY analyzer delta. Verified empirically — a comment-only edit to logger.ts, with the fingerprint byte identical, produced "runner identity changed ... forcing a full rebuild". The fingerprint is not thereby redundant. It fires where that guard cannot: a DDL-affecting change in `gitnexus-shared`, which is a workspace-linked devDependency and so sat outside both digests until the companion commit closed that gap. Review findings folded in, each correcting a line this rewrite itself introduced and never published: - B1: two assertions matched a log string the rewrite had renamed; both tests failed. They now assert what production emits. - B2: the pre-existing downgrade test perturbed `schemaVersion: 7`, a field this change deletes, so the spread carried a valid fingerprint, every guard passed, and the run legitimately took the fast path. It perturbs the fingerprint now, restoring the only integration coverage of the gate-above-the-fast-path ordering invariant. - N5: duplicate `schemaFingerprint` keys silently collapsed two assertions into one (TS1117). - N6: the absent-stamp message told non-git repositories their index was "built by an older GitNexus version" — on every run, about an index this exact build had just written. Non-git repos never record a fingerprint, and now the message says so. - N9: the on-disk stamp is shape-checked before being echoed, so a crafted gitnexus.json cannot push ANSI escapes through the CLI log. - N7: a test case that re-computed the same digest expression with its operands swapped, mislabelled as a randomness check on a module-level const. - N10: comments claiming the digest "cannot collide" (it is 48 bits), pointing at a vector-column gate that does not exist, and asserting storage/ is free of a core/ dependency two lines below a core/ value import. None of these were caught by `tsc -p tsconfig.json`, which covers src only, nor by eslint, where no-dupe-keys is off. `tsconfig.test.json` reports all three test defects and is not currently wired into CI. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(schema): pin that the fingerprint covers every DDL statement init executes `SCHEMA_QUERIES` is the list `runSchemaCreationQueries` iterates — the DDL that actually runs. The fingerprint hashes only two of its three members, and until now no test imported `SCHEMA_QUERIES` at all, so nothing tied the two together. A fourth member appended to that array — the one literally named for what init executes — would have been invisible to the gate. Every existing test would still pass, because they all recompute the digest from the same two arrays the fingerprint already uses. An index whose gate passed would then run `initLbug` over the old database, where `runSchemaCreationQueries` suppresses "already exists", so the new table would never be created and its edges would be dropped by `fallbackRelationshipInserts`' bare catch. A wrong graph, no error — exactly the failure #2798 exists to end. The check is a pure predicate over (executed, fingerprinted, documented exclusions) rather than a positional `toEqual`, so `EMBEDDING_SCHEMA` is named as an exclusion with its reason — its FLOAT[N] width is environment-derived — rather than sitting in a list where a future reader might "fix" it by folding it in. It asserts both directions and is order-insensitive, leaving ordering to the digest assertion that already pins it. The negative case is pinned in CI rather than checked by hand once: the same predicate over a synthetic fourth member must report it. If a refactor ever makes the predicate vacuous, that case fails even though the positive one would not. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(analyze): name the invariant the version deletion now rests on Deleting `INCREMENTAL_SCHEMA_VERSION` moved a load-bearing guarantee into an implicit one. Roughly thirty of the retired ladder's entries changed no DDL at all — node ids, wire formats, resolution tiers — and the fingerprint is structurally incapable of firing on any of them. Their only remaining cover is the analyzer runner-identity receipt, and nothing in the suite said so. This adds a table over the real `analyzerRunnerIdentitiesEqual` with a well-formed schema-v4 receipt: byte-identical reuses; an entrypoint-only difference reuses (CLI vs analyze worker); a moved build digest with unchanged DDL forces — that case IS the invariant, commented as such; and a dependency change, an ABI change, undefined, null, a schema-v3 legacy receipt, a missing build section and a non-sha256 digest all fail closed. The deleted `expect(INCREMENTAL_SCHEMA_VERSION).toBe(35)` pin is also worth naming: it failed CI on every bump by design, which is what made an author stop and think. Nothing replaced it. This does not restore that — a digest has no literal to pin — but it does make the mechanism that took over the job visible to the next person who reads the file. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(spring): pin CLASS_SCHEMA's membership in the fingerprinted DDL set When `INCREMENTAL_SCHEMA_VERSION` went away, its sibling in basicblock-callee-ids-schema.test.ts got a replacement assertion tying BASICBLOCK_SCHEMA to the fingerprint's input set. This file's `>= 23` floor was deleted with nothing put in its place. The file still asserts CLASS_SCHEMA's CONTENT — that the `frameworkAnnotations` column exists — but not that CLASS_SCHEMA is part of what the digest covers, and the second is what makes an index built before that column carry a different fingerprint and get rebuilt. Mirrors the sibling so the two read the same way. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(identity): stop a symlinked directory from aborting the whole analyze `collectArtifacts` fused two orthogonal facts into one condition: that four directory names never carry runtime payload, and that a symlink where a real directory was assumed falls through to the payload branch, where `snapshotReadableFile` stats the target, sees a directory, and throws "Analyzer identity input is not a file". The second was only fixed for those four names. Every other symlinked directory in a scanned package root still aborted the run — `dist -> build`, a vendored grammar link, anything inside a linked sibling checkout. Newly reachable, because making workspace-linked packages scannable pointed the scanner at a live checkout instead of an immutable registry tarball for the first time. Split along the actual seam: prune on the NAME alone, and give symlinks their own branch in the type dispatch, ahead of the payload branch. Link text is recorded rather than followed. Following was rejected on three grounds, each checked in source: the traversal is a stack with no visited set, so a self-referential link would recurse to `runtimeDepth` — which throws, trading one hard abort for another; `snapshotDirectory` rejects a symlink outright, so the directory guard could not accept one without a realpath rewrite of its canonical-path identity; and a link into an already-scanned tree double-counts against `runtimeEntries`/`runtimeBytes`, which also throw. The cost is stated in code: a link out of the package contributes its text, not its target's content. Links resolving to a regular file keep the existing content digest. The new `'unfollowed-symlink'` kind is threaded through every consumer, including the cache validator — which re-probes with `mode: 'link'`, since the readable-file probe resolves the target and would return null for exactly this kind, silently failing every warm validation. No canonicalization or cache-schema bump. Digest content changes only for trees that previously crashed: a delta scan over all 258 scanned roots of this install found no regular file bearing a pruned name and no symlink failing to resolve to a file, so `dependencyRuntime.digest` is byte-identical here. Six of the eight new tests fail against the unfixed tree with the exact production error; all eight pass after. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(analyze): give the reuse gate a real seam and sanitize logs at the funnel Cleanup pass over the #2798 branch. Net -183 lines. The gate had no extracted predicate, so its own test asserted it by regex-matching run-analyze.ts SOURCE TEXT. That pinned production formatting: one pattern froze three back-to-back single-name imports from './lbug/schema.js', so merging them — the obvious tidy-up — failed a test named "still imports the DDL digest itself". `schemaFingerprintMismatch` and `isSchemaFingerprintShaped` now live in core/lbug/schema.ts beside the constant. Not in run-analyze.ts next to `pdgModeMismatch`, because storage/ must stay off the analyze pipeline and mcp/resources.ts is a plausible second consumer — the same reasoning that puts `cjkSegmentationModeMismatch` in core/search/. The regex block is gone; the test calls the predicate. The three imports are merged. ANSI sanitation moved from one field to the funnel. The per-field guard's own comment stated the general hazard — gitnexus.json is parsed with no runtime shape validation and the notice reaches console.log — while two sibling guards twelve lines away echoed `runnerIdentity.schemaVersion` and `cjkSegmentation` from that same file raw into the same log. `log()` now strips C0/C1 controls, covering all seven guard messages and any written later. Also: - Deleted a duplicate integration test. After the downgrade test was repointed at `schemaFingerprint` it became the same scenario as the new one, differing only by an extra log assertion — which is now folded into the survivor. Saves a fixture and two full pipeline runs per CI pass. - Replaced a 3-parameter set-difference helper with one set equality. Its doc was false at one call site (arguments semantically swapped) and it needed a fourth test purely to prove itself non-vacuous; set equality cannot go vacuous. - Removed ~115 lines of runner-identity table that duplicated analyzer-identity.test.ts. The three genuinely uncovered cases moved there, and the #2798 invariant — build digest moved while the DDL did not — now asserts against a REAL analyzer-build-tree edit rather than a hand-built literal, which is strictly stronger than what it replaces. - MIGRATION.md quoted a log line the code cannot emit; it was written before the placeholder changed. - Restored the rationale on the `capabilities` docstring, which a previous pass replaced with its consequence — leaving a maintainer reading "duplicated by hand" as a wart to fix by importing, which is what the original forbade. - Marked the `isIncremental` conjunct as belt-and-braces: `!options.force` short-circuits before it, so it cannot decide anything. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(analyze): force a rebuild when the vector column width changes `CodeEmbedding.embedding` is declared `FLOAT[EMBEDDING_DIMS]`, resolved from `GITNEXUS_EMBEDDING_DIMS` at module load. Nothing gated it. Flip the variable on a same-commit clean tree and no guard fired at all: `alreadyUpToDate` returned over a `FLOAT[384]` table while the process embedded at 768. The only reaction anywhere discards the embedding CACHE and re-embeds — into a column whose type it never revisits. This predates #2798; `INCREMENTAL_SCHEMA_VERSION` never covered dims either. It surfaced because the fingerprint work had to reason about why `EMBEDDING_SCHEMA` must stay OUT of the digest: its width is environment-derived, so folding it in would make the same build disagree with itself and thrash rebuilds. That exclusion is correct, and it leaves the width needing its own guard. Modelled on `cjkSegmentation`, the closest sibling: an env-resolved scalar stamped at write time and compared by a small exported predicate that forces on mismatch. `embeddingDimsMismatch` sits in core/lbug/schema.ts beside `EMBEDDING_DIMS`, so the query side can adopt it without importing the analyze pipeline — mcp/local/local-backend.ts already warns on a cjkSegmentation disagreement and has the identical claim here, since the query path embeds at the live width against a table of unknown width with no validation at all today. ABSENCE IS NOT A MISMATCH, deliberately. Forcing on it would be dead code: `embeddingDims` and `schemaFingerprint` ship together, and a missing fingerprint already forces exactly one rebuild — which is where this stamp lands. Absence also carries no signal here, unlike the fingerprint: a missing fingerprint means "DDL this build cannot vouch for" and ships WITH a DDL change, whereas a missing dims stamp means only "written before the field existed", and that run's table agreed with that run's width. Drift requires the env to change, which absence says nothing about. The `cjkSegmentation` trick of folding absence into the default was unavailable — there is no width that is safe to assume for an existing table — so the stamp is instead written unconditionally, giving absence exactly one meaning. Malformed values are not grandfathered: null, '384', NaN and objects all read as a mismatch and err toward a rebuild. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(mcp): warn when the served index's vector width differs from the query embedder's The analyze side now forces a rebuild when the vector column width changes. The query side had no equivalent: a serving process embeds a query at its own width and searches a table whose width was fixed when the index was built. Disagree and the user gets wrong or missing semantic results with nothing explaining why. Mirrors the cjkSegmentation drift warning immediately above it — same warnings[] array, same per-query recomputation, agent-visible in the tool response, and it warns rather than refuses. A width mismatch degrades the semantic lane only; keyword results are unaffected, so `partial` is deliberately not set. Compares against `getEmbeddingDims()` — the width the query embedder actually produces — NOT schema.ts's `EMBEDDING_DIMS`. The two diverge exactly when GITNEXUS_EMBEDDING_DIMS is set on a server that embeds LOCALLY: the query path ignores that variable and embeds at 384, so comparing against the env-derived constant would report drift on a lane that works fine. The recorded width is what the vector CAST actually binds. `embeddingDimsMismatch` is imported from core/lbug/schema.js rather than restated, so "absent is not a mismatch" cannot drift between the analyze and query sides. That predicate was placed in schema.ts precisely so this consumer could reach it without importing the analyze pipeline. Two gates keep it quiet when it would be noise: it fires only for a repo where this process actually produced a query vector, so an index analyzed without --embeddings (or a server whose embedder is unavailable) never carries it. An untrusted recorded value — meta.json is schema-less JSON — is reported as "an unrecognized width" rather than echoed. `loadMeta` is hoisted out of the neighbouring try so both diagnostics share one read and an invalid GITNEXUS_FTS_CJK_SEGMENTATION cannot take this one down with it. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(identity): detect an npm-linked dev dependency the specifier cannot see `isLocallyLinkedSpecifier` admits a devDependency whose SPECIFIER is checkout-local. `npm link <pkg>` leaves the specifier a registry range while the node_modules entry symlinks to a checkout — locally linked, invisible to a specifier check, so a semantic-only edit there still moves neither digest. The obvious placement is unaffordable, measured rather than assumed: probing every dev-only name inside collectRuntimePackages costs 1998 resolutions, not the ~8 it looks like, because dependencyNames runs for every package in the BFS and published tarballs retain their devDependencies. Persisted path guards go 2221 -> 11050 (+398%), and every guard is re-probed on each warm validation — the path `status` takes. Scoped to the root package instead. The declared-intent half is untouched and still enumerated everywhere: it alone can emit the `<missing>` edge for a declared link whose checkout is absent, where resolution returns null and cannot distinguish that from an uninstalled dev tool. The new resolved-location half runs only when `parent.root === packageRoot`, resolves through the existing resolver so its path guards are recorded, and admits a name iff the realpath'd root carries no node_modules segment. Bounded against mis-fire by EXPANSION. "Not under node_modules" is a proxy for "checkout-local"; under a relocated pnpm virtual store every dev dep passes it and the whole dev tree folds into the receipt — against limits that THROW, so a legitimate install would abort. Measured here: uncapped, that shape takes 259 -> 347 packages and 2250 -> 3786 guards. The cap admits at most four and DROPS THE WHOLE CHANNEL on overflow rather than an arbitrary prefix, because the abort comes from the transitive payload of whichever trees get folded in — four of a mis-fired thirteen is still unbounded, and a sorted-prefix receipt would be arbitrary. Overflow falls back to the specifier-only receipt that ships today. Cost on this install: 259 packages unchanged, 13 dev names resolved, guards 2221 -> 2250 (+29, +1.3%). Verified against the real implementation, not just a replay: validation guards 16295 -> 16324, packageCount and artifactCount unchanged, and `dependencyRuntime.digest` byte-identical — so this forces no re-analysis for anyone. Each test fails on the defect it targets: disabling the channel kills the npm-link and cap cases; dropping the root-only scope makes the differential guard-count case fail at 2.8x guards; removing the specifier half kills the `<missing>` case. Refs #2798 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1147646518
|
feat(spring): model AOP transactions, caching, and security (#2783)
* feat(spring): model AOP advice and proxy behavior * fix(spring): address AOP review findings --------- Co-authored-by: Shining <xuenning@qiyi.com> |
||
|
|
de84ad6297
|
feat(spring): index @Bean factories and @Resource injection (#2740)
* feat(spring): index Bean factories and Resource injection * fix(spring): address Bean and Resource review findings * refactor(lbug): keep relation pair parsing in router * test(lbug): preserve schema exports in WAL mocks * test(cache): align schema bump pin --------- Co-authored-by: Shining <xuenning@qiyi.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> |
||
|
|
ff86ccf1e7
|
feat(spring): model profiles, conditions, and auto-configuration (#2678)
* feat(spring): model conditions and auto-configuration * fix(spring): align auto-configuration declarations * perf(spring): streamline auto-configuration indexing * test(spring): move timing benchmark out of vitest --------- Co-authored-by: Shining <xuenning@qiyi.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
7f7255aef8
|
fix(analyze): load VECTOR before the incremental writeback touches embedding rows (#2623) (#2624)
* feat(lbug): add ensureEmbeddingRowDmlSafe VECTOR gate for embedding-row DML LadybugDB refuses every mutation of a table carrying an HNSW index while the VECTOR extension is not loaded on that connection: DELETE and CREATE raise a Binder exception, DROP TABLE is refused while the index references it, and SET segfaults the process. Dropping the index is not an available recovery either — CALL DROP_VECTOR_INDEX is itself a VECTOR-extension function and is undefined in exactly that state. Add a single primitive that loads VECTOR under the analyze install policy and, only when that fails, reads CALL SHOW_INDEXES (which works without the extension) to decide whether an index actually exists to trip over. No call sites yet. Refs #2623 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(lbug): pin the #2623 VECTOR gate for embedding-row DML Three cases: no index + VECTOR unavailable stays safe (no needless escalation); index present + VECTOR unavailable is reported blocked AND the raw deleteNodesForFiles genuinely throws 'extension is not loaded' (proving the hazard is real, not theoretical); index present + VECTOR loadable is safe, the delete works, and the HNSW index survives — the invariant run-analyze relies on when it keeps the index across a surgical incremental run. Refs #2623 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(analyze): load VECTOR before the incremental writeback touches embedding rows Incremental analyze died on every content change once a repo had built code_embedding_idx: Analysis failed: Binder exception: Trying to delete from an index on table CodeEmbedding but its extension is not loaded. The surgical writeback's first statement is deleteNodesForFiles' CodeEmbedding join-delete, but nothing on that path loaded VECTOR until Phase 4 — so the engine refused the delete. This is an ordering defect, not an environment one: it reproduces on machines where VECTOR loads fine. The dirty-flag recovery then forced a full rebuild on the next run, which is why it read as 'just slow'. Call ensureEmbeddingRowDmlSafe() once, before the escalation gate and before any row is touched — the same 'index lifecycle before row DML' seam dropSearchFTSIndexes occupies for FTS (#2589). Unconditional, because a DB carrying the index from an earlier --embeddings run hits the same wall on a plain incremental run. When VECTOR truly cannot load the table is immutable (the index cannot be dropped without the extension either), so the run falls through to the existing wipe-and-COPY escalation with a message naming cause, consequence and remedy. Fixes #2623 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(analyze): pin the #2623 VECTOR-before-embedding-DML ordering end-to-end Sibling of the #2589 FTS drop-before-delete suite, same shape: drive the real runFullAnalysis incremental path over a real git repo and a real LadybugDB, seed real embedding rows, build the HNSW index, then assert the index state at the exact moment deleteNodesForFiles is invoked. Both cases were confirmed to discriminate — with the run-analyze change reverted they fail with the reported 'Trying to delete from an index on table CodeEmbedding but its extension is not loaded', and pass with it: - surgical path: the run completes, the index is still present AND extension_loaded at delete time, exactly one row per nodeId survives, and the untouched file's rows are preserved - blocked path: with GITNEXUS_LBUG_EXTENSION_INSTALL=never the run escalates to a full DB write and says so, instead of crashing Also applies prettier's reindent to the run-analyze log ternary. Refs #2623 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(lbug): cite the pinned LadybugDB version in the #2623 probe note The probe matrix behind ensureEmbeddingRowDmlSafe was first recorded on 0.18.0, but gitnexus/package-lock.json pins 0.18.2 (#2587). Re-ran every case on 0.18.2: refused DELETE, refused CREATE, SIGSEGV on SET, DROP_VECTOR_INDEX undefined, DROP TABLE refused, SHOW_INDEXES readable with extension_loaded intact. Identical on both, so the design is unchanged — only the citation was wrong. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(analyze): preserve embeddings across the VECTOR-blocked rebuild, and check the catalog before loading Three follow-ups from reviewing the fix itself. 1. Data loss on the blocked path. Escalating wipes the DB files, and Phase 3.5 restores embedding rows from cachedEmbeddings — which deriveEmbeddingMode only populates when meta.stats.embeddings > 0. A DB holding embedding rows that its meta does not account for therefore had every vector destroyed silently by a rebuild it never asked for. Probe on a 3-file repo: 3 rows before, 0 after, no warning. Read the rows before escalating (a plain MATCH, no extension needed) so the existing restore has something to restore, and say so in the log. The blocked-path test now asserts the seeded rows survive exactly once, and that assertion fails without this rescue. 2. Catalog before extension. ensureEmbeddingRowDmlSafe loaded VECTOR first and only read SHOW_INDEXES on failure, so every incremental analyze on a machine without VECTOR paid a bounded out-of-process INSTALL attempt plus an 'extension unavailable' warning — including repos that never built an embedding index and can never hit this bug. One local catalog read settles that case first; the load is attempted only when an index actually gates DML, or when the catalog cannot be read. 3. Dead branch. targetConn is always the module singleton there, so the isSharedSingletonConn ternary could never take its second arm. Collapsed to withConnLock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(doctor): live-probe the VECTOR extension instead of printing the static platform capability Review finding on #2624 (MEDIUM), and exactly what #2623's reporter hit: doctor printed 'VECTOR index: available' — derived from a static platform check — while every incremental analyze on the same machine was dying on an unloaded VECTOR extension. The FTS line was switched to a live LOAD probe for the identical contradiction under #2374; VECTOR now gets the same treatment. probeVectorExtensionLoad shares the FTS probe's implementation (bounded, offline-safe, never runs the installer) and doctor's semantic-mode line now follows the probe, not the platform: without a loadable extension the vector index can be neither built nor queried, so search really is on exact scan. The load-error classifier's remedies are label-parameterized so the VECTOR row stops dispensing FTS-specific advice — 'run analyze --repair-fts' repairs FTS indexes only and was actively wrong for a missing vector extension. Default label stays 'FTS'; every existing caller and pinned remedy string is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lbug): remove the stale Windows VECTOR gate — the extension ships for win_amd64 The codebase categorically refused VECTOR on Windows (platform !== 'win32' in isVectorExtensionSupportedByPlatform, plus a hard early-return in loadVectorExtension) on the strength of an early-era report that in-process INSTALL VECTOR could SIGSEGV (#1365). That belief is stale, verified directly: - the extension server hosts win_amd64 VECTOR artifacts for every 0.18.x extension version — v0.18.0 and v0.18.1 both serve a real 14 MB PE32+ DLL (curl-probed; 'file' confirms PE32+ x86-64) - the pinned 0.18.2 core resolves its extension directory to 0.18.1 (strace-verified LOAD open()), so the pinned version's Windows artifact exists too - INSTALL now runs in a spawned child (installDuckDbExtensionOutOfProcess), so even a crashing installer kills only the child and degrades to unavailable — the original hazard cannot reach the parent process any more Windows now takes the same runtime path as every other OS: try LOAD, install out-of-process when policy allows, degrade to exact scan when it truly fails. The MCP semantic-search lane loses its static platform gate too — it always attempts the vector index and falls back to the exact scan on runtime failure, with a once-per-backend diagnostic naming the real error instead of a platform-policy message. isVectorExtensionSupportedByPlatform is deleted; getRuntimeCapabilities reports the platform capability as available everywhere and defers machine truth to the live probe. Windows CI is the enforcement: the vector suites skip visibly only when the extension genuinely cannot load, so green Windows lanes now actually exercise VECTOR instead of silently skipping by policy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(lbug): pin the catalog-read-failure fallback in ensureEmbeddingRowDmlSafe Review finding on #2624 (LOW): the one branch where the gate cannot cheaply prove safety — SHOW_INDEXES itself erroring — was exercised only by inference. Force it with a Connection.prototype.query spy over the real DB: the catalog read fails, and the gate must fall through to actually attempting the extension load (asserted via the recorded statement stream) rather than guessing, returning true here because the extension is loadable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): load VECTOR on the pool's shared Database so the semantic vector lane actually works Review finding on #2624 (MEDIUM): extension load scope is per-Database (probe-verified — LOAD on one connection enables QUERY_VECTOR_INDEX on every connection of the same Database), and the pool pre-warm loaded only FTS. So LocalBackend's vector lane has ALWAYS raised 'Catalog exception: function QUERY_VECTOR_INDEX is not defined' through the pool and silently fallen back to the exact scan — repos above the 10k exact-scan cap got empty semantic results. The serve path was unaffected (the embedding pipeline loads the extension itself). Mirror the FTS line at BOTH load sites — doInitLbug's pre-warm and initLbugWithDb's external-Database adoption — under the same load-only contract (the read pool never triggers a network install), tracked by a new SharedDB.vectorLoaded flag reset where ftsLoaded resets. The new pool test is discriminating and deliberately closes the writable core adapter before the pool opens: a shared/injected Database would inherit the VECTOR load from test seeding and pass either way, so the case forces the pool onto its OWN fresh read-only Database where only the pre-warm can make the lane legal. Verified: fails at the pre-fix tree with the exact Catalog exception, passes with the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run the #2623 ordering suite on Windows/macOS and pre-install VECTOR alongside FTS Two review findings on #2624, both landing in existing seams: - scripts/cross-platform-tests.ts gains incremental-vector-extension-ordering .test.ts: the win32 VECTOR gate is gone in this PR, so the #2623 drop-ordering + blocked-path escalation must be proven on the windows-latest native addon, not just Ubuntu. (The review's claim that lbug-delete-nodes-for-files.test.ts was also missing was wrong — it has been on the roster since #2409.) - scripts/ensure-fts.ts now pre-installs VECTOR under the same best-effort auto-policy contract, so every sharded CI process LOADs from ~/.lbdb instead of racing its own bounded out-of-process INSTALL; the workflow's extension cache already covers it (path is the whole extension dir — key kept for cache continuity). The cross-platform job sets GITNEXUS_REQUIRE_VECTOR=1 beside GITNEXUS_REQUIRE_FTS so a genuinely unavailable VECTOR is a loud failure, never a silent skip. Windows/macOS cannot be executed locally; the PR's CI lanes are the proof for this commit. Linux smoke: ensure-fts.ts reports both extensions ready; all 79 roster entries resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(pool): register loadVectorExtension in the pool unit-suite mocks The pool adapter's new loadVectorExtension import surfaced in four suites that mock lbug-adapter.js with explicit factories (vitest fails loudly on a missing mocked export). Register the export in each — resolving false where the suite's world assumes no vector, true where it mirrors FTS — and extend lbug-pool-fts-load.test.ts, the suite that owns pre-warm extension loading, with the vector pair: successful load cached per shared Database, failed load retried on the next open, both pinned to policy load-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(analyze): use POSIX literals for graph paths in the #2623 ordering suite First Windows CI run of this suite (it joined the cross-platform roster this PR) failed with 'Parser exception: Invalid input <MATCH (n:Function) WHERE n.filePath = '>' — path.join produces backslashes on Windows, and a backslash inside the seed helper's single-quoted Cypher literal breaks the parser. The graph stores repo-relative filePaths with forward slashes on every OS, so graph-side paths are POSIX literals now (the incremental-orchestration convention); path.join stays only for real filesystem access. The same Windows lane also proved the substance this suite exists for: lbug-vector-extension passed 7/7 on windows-latest — the extension installed, loaded, and built a real HNSW index there — and the pool vector-lane and DML gate suites passed too. This commit fixes the harness, not the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
41e590fed7 | fix(spring): harden configuration bindings | ||
|
|
2cfbc4a259
|
feat(spring): build bean candidate inventory (#2494)
* feat(java): inventory Spring bean candidates * fix(java): fail closed on Spring annotation shadowing * fix(java): resolve Spring beans after imports * fix(java): remove stale bean extraction path * style: satisfy locked Prettier version * fix(spring): address PR review findings * feat(spring): share bean inventory across Java and Kotlin * fix(spring): gate bean inventory analysis completeness * fix(kotlin): avoid reloading cached scope source * chore(autofix): apply prettier + eslint fixes via /autofix command --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
df1fc36094
|
fix: make large incremental writebacks commit reliably (#2409) (#2425) | ||
|
|
dbc73adcf1
|
fix: surface incremental dirty state diagnostics (#2410)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* fix: surface incremental dirty state diagnostics * address incremental dirty diagnostics review * stabilize windows analyze e2e timeout --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> |
||
|
|
1029a8ddd7
|
feat: add Spring DI resolver for @Autowired List<T> injection (#2200)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* feat: add Spring DI resolver for @Autowired List<T> injection Addresses all P0/P1 findings from tri-review (#2200): - P0: Register INJECTS in RelationshipType union (compiles) - P0: Rewrite execute() to emit consumer→implementation edges from graph data only - P1: Register in VALID_RELATION_TYPES, single-pass O(N) indexes - P1: Java-only gate with early exit on non-Java repos - P1: Update FULL_ORDER golden test - 8 unit tests covering all edge cases * test: make VALID_RELATION_TYPES size assertion array-driven (no hardcoded count) The security test hardcoded toBe(16) for the relation type count, but PR #2200 added INJECTS, bumping it to 17. Replace the magic number with an EXPECTED_RELATION_TYPES array whose .length drives the size assertion, so future additions only need to append to the list. Fixes CI failure on PR #2200. * fix(ingestion): thread raw generic field types onto Property nodes so Spring DI matching works (review 4616076037 P0) Production declaredType is generics-stripped by design (extractSimpleTypeName: List<Shape> -> "List"), so the spring-di phase's anchored regexes could never match real extraction output — the phase was a silent no-op on every real Java repository, while its unit tests passed against hand-built node shapes. Add FieldInfo.rawDeclaredType captured verbatim from the field's type node (.text, generics and qualifiers preserved — same precedent as the JVM method extractor), thread it through both parse-worker Property sites, add it to the shared NodeProperties contract, and match on rawDeclaredType ONLY (no declaredType fallback: it can never match real data and would mask future plumbing regressions as quiet no-ops). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): gate Spring DI on real injection annotations, honest edge reason (review 4616076037 P1) Extract Java field annotations (shared extractAnnotations helper, moved verbatim from the method extractor) onto Property nodes and require @Autowired or @Inject before a collection field becomes an INJECTS candidate. Previously every edge's reason string fabricated "@Autowired" without any annotation ever being checked, and any plain collection field would have fanned out false edges once matching worked. @Resource is deliberately excluded: JSR-250 resolves by bean name first (defaulting to the field name), injecting a single named collection bean — the opposite of the collect-all-implementers fan-out INJECTS models. Pinned by a test. An annotated candidate missing rawDeclaredType now logs an isDev warning (plumbing-contract breach signal) instead of vanishing silently. SCHEMA_BUMP 9 -> 10: Property nodes gained rawDeclaredType + annotations; warm parse caches must invalidate or the DI phase silently no-ops on replayed pre-upgrade nodes (the #2038 trap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ingestion): framework-neutral di phase + language-scoped Spring matcher registry (review 4616076037 P1) spring-di was the only pipeline phase naming a language in shared core/ingestion code (DoD.md language rule; the maintainer's direction is a generic DI solution). Split it: - di-extractors/spring.ts: the Spring matcher (annotation gate, collection type parse, @Resource exclusion rationale, framework-specific reason payload) — language-scoped home, mirroring route-extractors/. - di-extractors/index.ts: DI_MATCHERS, a single-valued ReadonlyMap<SupportedLanguages, DiFieldMatcher> mirroring the SCOPE_RESOLVERS registry shape sanctioned by AGENTS.md. Constructor injection deliberately out of scope; widen to arrays only when a second same-language framework lands. - pipeline-phases/di.ts (renamed from spring-di.ts): framework-neutral — routes Property nodes to registered matchers by node language via a typed guard, then runs the unchanged reverse-index fan-out. Zero language or framework names remain (grep-verified). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): language- and qualified-name-scoped interface resolution for DI fan-out (review 4616076037 P2) The interface index was built from ALL Interface nodes regardless of language, keyed by bare simple name with last-writer-wins overwrite — a polyglot repo with a TS and a Java 'Shape' could fan Java INJECTS edges into TypeScript classes, and two same-named Java interfaces in different packages silently collapsed to whichever parsed last (documented GitNexus bug class: #2054, PR #1956). Resolution is now per-language with qualifiedName as the primary key (Interface nodes already carry package-qualified qualifiedName); dotted element types resolve via qualifiedName, bare names via a per-language simple-name index that records ambiguity and fails CLOSED. Ambiguity skips are observable: DIOutput.ambiguousSkipped + an aggregated isDev debug log, so 'no DI fields' is distinguishable from 'all candidates ambiguous'. Same-package tiebreaking is a pinned, documented follow-up. Order-independence pinned by running collision tests in both insertion orders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ingestion): depth-aware Spring collection-type parser for idiomatic generics (review 4616076037 P3) The two anchored regexes silently skipped idiomatic Spring shapes: Map<Pair<A,B>, IFoo> (nested-generic key broke the [^,]+ split), List<? extends IFoo> / List<? super IFoo> (bounded wildcards), java.util.List<IFoo> (qualified wrapper), and whitespace/multi-line declarations. Replace them with a small scanner: whitespace normalization, wrapper matched by last dotted segment, depth-aware top-level-comma split, wildcard bound stripping, and a final plain-dotted-type-name gate so anything else (nested-generic elements, arrays, unbounded wildcards, embedded comments, unbalanced brackets) fails closed. Every accept and reject is documented in the module docstring and pinned by 27 table-driven cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(integration): prove Spring DI end-to-end through the real pipeline (review 4616076037 P1) Both no-op incarnations of this feature shipped with a green unit suite because every test hand-built the exact graph shape the phase expected — no test ever ran real Java source through the actual extraction pipeline. Add test/integration/spring-di-pipeline.test.ts: real .java fixtures via runPipelineFromRepo, pinning (a) the extraction contract on the annotated field's Property node (declaredType 'List', rawDeclaredType 'List<IFoo>', annotations ['@Autowired']), (b) set-equality on ALL INJECTS edges (exactly Consumer->FooA and Consumer->FooB; the non-annotated 'plain' field of the same type contributes nothing; no self-edges), and (c) a negative-control fixture with no injection annotations producing zero INJECTS edges. Either historical regression fails at least one of these. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(incremental): register INJECTS across product surfaces + delete-before-writeback (review 4616076037 P2) INJECTS was allowlisted in VALID_RELATION_TYPES but invisible or unhandled everywhere else. Register it deliberately: - REL_TYPES (gitnexus-shared schema-constants): web-side validRelType() otherwise silently rejects INJECTS filters (CLI/web single source of truth). - mcp/tools.ts cypher edge list (agent-facing schema discovery). - isGraphWideRelType: INJECTS validity is a whole-program property — a change to a THIRD file (the interface, or a new/removed implementer) creates/invalidates edges between two untouched files (the TAINT_PATH / #2084 M4 U6 class), so incremental extraction must always re-include the full fresh set. - deleteAllInjects (lbug-adapter): mirrors deleteAllInterprocTaintPaths — COUNT-then-DELETE under withConnLock, benign missing-table carve-out, re-throw otherwise (CodeRelation has no PK and there is no read-side dedup; a fail-soft delete + re-add would silently duplicate rows). - run-analyze.ts: the delete is UNCONDITIONAL, next to the Communities delete — deliberately NOT inside the options.pdg block: the di phase runs on every persisting analyze while the graph-wide re-include is unconditional, so a pdg-gated delete would append without deleting on every non-pdg incremental run (N runs = N copies). - local-backend.ts comment: opt-in traversal by design (not in default impact()/context() lists; no IMPACT_RELATION_CONFIDENCE entry per the WRAPS/FETCHES precedent — edges carry their own 0.8). - ARCHITECTURE.md: 14 -> 15 phases, DAG diagram, phase table, skip-list. Note: the tools.ts edge list also predates WRAPS/QUERIES/USES — that drift is pre-existing and left for a follow-up. Idempotency pinned end-to-end: two successive incremental runs (real runFullAnalysis + real LadybugDB, unrelated-file touches) leave the INJECTS row count stable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe INJECTS' actual precondition; drop stale fixed-at-16 comments (review 4616076037 P3) The shared-schema doc for INJECTS claimed an @Autowired precondition the code (pre-fix) never checked, and hardwired Spring semantics into what is now a framework-neutral edge type. Reword: precondition is an injection annotation recognized by a per-language matcher in di-extractors/; framework specifics live in the reason payload, not the type contract. security.test.ts comments still said the allow-list size 'stays fixed at 16' (it is 17 and the assertion derives from EXPECTED_RELATION_TYPES). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: simplify DI surfaces — narrow matcher contract, dedup delete-alls, derive tools edge list Post-implementation simplification pass (4 review angles): - DiFieldMatch/CandidateField carried collectionType + matchedAnnotation that no consumer read (the matcher bakes both into reason) — narrowed to {elementTypeName, reason}. - parseElementTypeName had two guard branches fully subsumed by the final plain-dotted-type-name gate — deleted, rationale folded into the regex comment. - The three byte-identical delete-all-by-rel-type functions in lbug-adapter (TAINT_PATH / CALL_SUMMARY / INJECTS) are now one parameterized helper + thin wrappers with identical names, signatures, and message text (character-diff verified) — the missing-table regex and abort policy now live in exactly one place. - The cypher tool's hand-maintained edge-type list (already missing WRAPS/QUERIES/USES) is now derived from the canonical REL_TYPES — the drift class is gone rather than patched. - di phase: interface indexes are built only for languages that actually have candidates; test builder gained a rawDeclaredType opt-out replacing a hand-rolled node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: apply Tier-2 review findings — qualified-name fail-closed, honest cypher docs, pinned delete contract, hook isolation - byQualifiedName was last-writer-wins on duplicate qualified names (reproduced: order-dependent INJECTS edges with ambiguousSkipped 0 — same package+interface duplicated across monorepo modules/source roots; Java qualifiedName has no file-path component). Both indexes now share the AMBIGUOUS fail-closed sentinel; order-flip test added. - The REL_TYPES-derived cypher edge list advertised pdg-gated types with no caveat (LLM queries on them silently return zero rows on default indexes) — caveat appended, INJECTS example added, impact relationTypes description now names the DI fan-out opt-in. - The delete-all re-throw contract (only defense against duplicate CodeRelation rows) was untested — error classification extracted to a pure classifyDeleteAllError and pinned exhaustively. - extractRawType/extractAnnotations hooks lacked the per-hook try/catch the pipeline applies elsewhere (#2286 pattern): a throwing hook would silently drop every remaining file in the language group. Hardened, degradation tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
400cc6a440
|
feat(search): add opt-in CJK bigram segmentation for FTS search (#2339) | ||
|
|
8bef64baa1
|
feat(ingestion/routes): give Route nodes a (method, url) identity (#2289) (#2302)
* feat(ingestion/routes): give Route nodes a (method, url) identity (#2289) Route node identity was URL-only, so a same-URL multi-verb pair (GET /x + POST /x) collapsed into a single node and the second verb's handler and execution flow were silently lost. Route identity is now (method, url) via routeNodeKey(method, url): a known, specific verb keys as "METHOD url", while a method-less route (filesystem routes — Next.js / Expo / PHP — and Laravel resource/apiResource) or a wildcard "*" route (e.g. Django function views) falls back to URL-only. The fallback is byte-identical to the previous URL-only ids, so only genuine declaration-style multi-verb routes split into separate nodes. The identity key is shared across the three phases that must agree on the Route node id: - routes phase: registry key + node id + handler-symbol lookup; the Route node still carries the bare URL as its display name. - call-processor: resolveRouteHandlerSymbols re-keyed by identity so each verb resolves its own handler; a verb-less fetch() consumer matches by URL and connects to every Route node at that URL (one per verb). - processes phase: ENTRY_POINT_OF targets the identity-keyed node id. Bumps INCREMENTAL_SCHEMA_VERSION 4 -> 5: persisted pre-v5 Route nodes use the old url-only ids, so an incremental top-up would strand them alongside new composite-keyed nodes — force a full re-analyze instead. Part of #2280. * fix(ingestion/routes): address PR #2302 review (P1/P2/P3) P1 — Schema v5 fast-path bypass (run-analyze.ts): Adds a schemaVersion-mismatch guard above the alreadyUpToDate early-return, mirroring the pdgModeMismatch slot. Without it, a same-commit re-analyze on a pre-v5 stamp returned alreadyUpToDate without ever reaching the isIncremental gate, defeating the v5 schema bump's migration intent. Regression test covers: analyze (stamps v5) → meta downgrade to v4 → same commit re-analyze must NOT early-return and meta restamps to v5. P2 — ENTRY_POINT_OF handler-aware linking (processes.ts): Pre-fix routesByFile fanned every same-file Route to every same-file process, cross-wiring same-file GET/POST handlers. Now reads handlerSymbolId off the Route graph node (the source of truth routes.ts stamps) into routesByHandlerId, with a routesWithoutHandlerByFile fallback — mirrors the Tool linking precedent 10 lines below. Two regression tests: weak form (only one handler has a process; sibling verb does not get spuriously attached) and strong form (both handlers form distinct processes; each Route links to exactly its own entryPoint, 2 edges not pre-fix 4). P2 — Roundtrip composite-id (route-{method,handler-symbol}-roundtrip): Both tests now seed the Route node with generateId('Route', routeNodeKey('POST', '/api/orders')) and run the Cypher MATCH against the composite id, exercising the literal-space-in-id through CSV→COPY→HANDLES_ROUTE_QUERY. A space-in-id escape regression would surface here instead of being silently swallowed by the extractor's catch. P3 — doc-drift + test if: - route-path.ts:4 — header updated to "(method, url) via routeNodeKey" - java.ts:684 — drop "Route nodes are URL-keyed"; #2289 closes that gap - manifest-extractor.ts:196 — explicit that Route node *id* is composite while route.name remains the bare URL - multi-verb-route-identity.test.ts:88 — forEachRelationship+if rewritten as a .filter().map() chain (no test-level conditional). New route-process-linking tests are also if-free. Validation: tsc clean, prettier clean, 9 touched suites / 43 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ingestion/routes): drop routes.ts re-export, fix CI fast-path tests Two follow-ups on PR #2302's CHANGES_REQUESTED review: 1. Drop `routes.ts` re-export of `normalizeExtractedRoutePath` / `normalizeRouteMethod` / `routeNodeKey` (per @magyargergo's inline comment at routes.ts:153 — the symbols already live in `route-extractors/route-path.ts` and consumers should import them from the source, not via a routes-phase indirection that was kept only as a compat shim during the #2289 refactor). Updated the two remaining callers (blade-template-routes / spring-route-extractor- parity tests) to import directly from `route-extractors/route-path.js`. `call-processor.ts` and `processes.ts` already import from the source. 2. Fix two `run-analyze.test.ts` fast-path tests that started failing on CI after the schema-version mismatch guard landed ( "creates .gitnexus/.gitignore on the already-up-to-date fast path" and "reports isPrimaryBranch false for an up-to-date non-primary branch"). The test fixtures hand-built a RepoMeta with NO schemaVersion field; with the guard now checking `existingMeta.schemaVersion !== INCREMENTAL_SCHEMA_VERSION`, that pre-versioning shape was treated as a mismatch and forced a rebuild, short-circuiting the fast path the tests exercise. Stamp the current schemaVersion on those fixtures so they reflect the post-#2289 meta shape production actually writes (`runFullAnalysis` always stamps the field on git repos — see meta save site). Validation: tsc clean, prettier clean, 11 touched suites / 80 tests pass. --------- Co-authored-by: henry <zhangwei2017@unipus.cn> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5bf8a17cd5
|
feat(ingestion): add control-flow-graph layer for TS/JS (#2081) (#2099)
* feat(cfg): language-agnostic CFG construction core (#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 (#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 (#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 #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 (#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) (#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 (#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 (#2081) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): apply ce-code-review autofix feedback (#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 (#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 (#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 (#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 (#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 (#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 #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 (#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 #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 (#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 #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 (#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 #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 (#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 #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 (#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 #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> |
||
|
|
4fa40e9881
|
feat(analyze): incremental indexing (parse cache + DB writeback + scope-res short-circuit) (#1479)
* docs: incremental indexing design spec Captures the design agreed in brainstorming on 2026-05-10: - Transitive importer closure with public-surface-change optimization - Git-only change detection (non-git repos: full rebuild as today) - New default behavior; --force opts out - New hydratePhase + loadGraphFromLbug primitive - Iterative closure expansion with parseCache reuse - incrementalInProgress dirty flag for crash recovery Prior art: PR #592 (zenprocess), PR #533 (davidbeesley), PR #1146 (azeemshaik025) — referenced and credited. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(communities): seed Leiden RNG for deterministic community detection The vendored Leiden algorithm defaults to Math.random for tie-breaking and randomized walks, which produces non-deterministic community assignments and modularity values across runs on the same graph. Pass a seeded mulberry32 RNG (LEIDEN_SEED=0xC0DE) so: - The same graph always produces the same partition - Modularity values are reproducible - Equivalence tests for incremental indexing can compare community assignments byte-for-byte This is foundational for the upcoming incremental-indexing feature (see docs/superpowers/specs/2026-05-10-incremental-indexing-design.md) where the correctness contract is incremental output ≡ full rebuild output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(incremental): change-detection, surface signatures, closure expansion Three new modules supporting the incremental-indexing pipeline: * core/incremental/git-diff.ts — getChangedFilesSinceCommit() unions 'git diff lastCommit HEAD' (committed) with 'git status --porcelain' (dirty tree). Renames flattened to delete(orig) + add(new). Throws LastCommitMissingError when lastCommit is gone (caller falls back to full rebuild). * core/incremental/surface.ts — extractSurfaceSignature() produces a stable hash of a file's publicly-visible symbols (functions, classes, methods, interfaces, types, heritage). Body-only edits → same hash. Signature/heritage changes → different hash. Drives the closure scoping optimization. * core/incremental/closure.ts — computeImporterClosure() iterative fixpoint: parse each closure file, extract surface, query DB importers, expand. Uses a parseCache so each file is parsed once. Generic over TParseResult so closure logic is decoupled from the pipeline's parse representation. 32 unit tests across the three modules. Tests cover edge cases: clean tree, dirty-only, mixed, renames, deletes, multi-hop cascade, cycle termination, surface invariance, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(lbug): loadGraphFromLbug, queryImporters, deleteAllCommunitiesAndProcesses Three new primitives in lbug-adapter.ts to support incremental indexing: * loadGraphFromLbug(graph, unchangedFilePaths) — streams all nodes for files in the set across every hydratable node table (excludes Community/Process — graph-wide, regenerated downstream). Then loads edges where both endpoints belong to loaded nodes, excluding MEMBER_OF / STEP_IN_PROCESS edges (also graph-wide). FilePaths chunked at 200 per query to keep statement size bounded on huge repos. Endpoint-level join filters by source-side filePath in the query, target-side checked JS-side via the loadedNodeIds set. * queryImporters(targetFilePath) — returns DISTINCT a.filePath where a -[IMPORTS]-> b and b.filePath = target. Powers closure expansion: when a changed file's surface signature changes, all its importers must be re-parsed. * deleteAllCommunitiesAndProcesses() — drops Community/Process nodes (and their edges via DETACH DELETE) at the start of each incremental run so the communities/processes phases regenerate them from the fully-merged graph. Required for the 'Leiden runs on full graph' correctness invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pipeline): hydrate phase + parse-filter for incremental indexing Wires the incremental-indexing infrastructure into the phase-based pipeline. Three coordinated changes: * New hydratePhase (deps: structure) — loads node/edge state for files OUTSIDE ctx.options.filesToParse from the existing LadybugDB index. Runs before parse so the parse phase can produce a partial graph while downstream phases (mro, communities, processes) still see the full graph. No-op in full-rebuild mode (filesToParse unset). * PipelineOptions.filesToParse: optional ReadonlySet<string>. When set, parse phase filters scanned files to this set; hydrate fills the complement. Set by runFullAnalysis when it detects an eligible incremental run; never set by callers directly. * gitnexus-shared PipelinePhase enum: 'hydrate' added so progress callbacks can report the new phase distinctly from 'structure'. Phase order: scan → structure → hydrate → markdown,cobol → parse → routes,tools,orm → crossFile → scopeResolution → mro → communities → processes. Communities (Leiden) still runs on the full graph, satisfying the correctness invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(analyze): incremental orchestrator branch + meta schema Wires incremental indexing into runFullAnalysis. Highlights: * RepoMeta schema extended: schemaVersion, surfaceSignatures, and incrementalInProgress fields. INCREMENTAL_SCHEMA_VERSION = 1. * core/incremental/file-hash.ts — v1 surface signature: SHA-256 of file content. v2 will switch to a true surface-only signature (defined in surface.ts) so body-only edits don't expand the closure. The plumbing is signature-agnostic so the swap is local. * core/incremental/orchestrator.ts — eligibility check, closure computation (uses file-hash as the surface signal), dirty-flag management, subgraph extraction, signature merge. * run-analyze.ts adds: - hasDirtyTree() check on the existing 'lastCommit==HEAD' early-exit so an uncommitted edit triggers re-index (was a coarse equality check before). - incremental branch: try incremental first; fall through to full rebuild on any setup failure or eligibility miss. - runIncrementalBranch() — opens existing DB, deletes closure-file rows + Community/Process, runs pipeline with filesToParse, writes only the changed-subgraph back, refreshes FTS, updates meta with new surfaceSignatures and clears the dirty flag. - Full-rebuild path now populates surfaceSignatures + schemaVersion in meta.json so the next run is eligible for incremental. Crash recovery: incrementalInProgress is set BEFORE any DB mutation and cleared on success by overwriting meta.json. A crash anywhere in between leaves the flag set, and the next analyze run forces a full rebuild (cheapest path back to a known-good index). v1 limitation documented: body-only edits trigger 1-hop closure expansion (content-hash signal). True surface-only optimization is deferred to v2 — see design doc for the integration path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(incremental): drop invalid --no-renames=false from git diff The flag --no-renames=false isn't valid git syntax (it's parsed as a file path). Git's default rename detection is on; removing the flag keeps that behavior. Caught while running an end-to-end smoke test against a small fixture repo: incremental setup failed with 'Command failed: git diff --name-status -z --no-renames=false ...'. After the fix, the incremental path runs cleanly: closure is computed, hydrate phase loads unchanged-file state from DB, parse phase only re-parses files in closure, and the writeback updates only changed nodes/edges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert v1 incremental indexing (5 commits) Reverts the v1 design that parsed only closure files into a fresh graph and tried to hydrate the rest from DB. Real-repo equivalence test failed: cross-file resolution operates on partial parse data (closure files only), so CALLS edges that resolve through unchanged files silently fall off. Diff against full rebuild on the same edited state: -50 nodes, -425 edges, -5 communities, -48 processes. Architecture pivot: switch to PR #533-style content-addressed parse cache. Pipeline parses every file (cache-served when possible), giving cross-file resolution full data, with DB writeback then restricted to changed-file rows. Reverts: |