Skip to content

fix: correctness of impact, callgraph and search output (SCIP descriptor parsing) - #248

Open
SimplyLiz wants to merge 21 commits into
developfrom
fix/evidence-output-bugs
Open

SimplyLiz wants to merge 21 commits into
developfrom
fix/evidence-output-bugs

Conversation

@SimplyLiz

Copy link
Copy Markdown
Owner

Fixes the output of prepareChange/analyzeImpact, getCallGraph and searchSymbols wherever it showed wrong or unusable evidence.

SCIP IDs

  • Symbol IDs are parsed with the official descriptor-aware parser, which handles backtick escaping, method/type/term suffixes and type parameters. Call graph node names are rebuilt from the parsed descriptors instead of string surgery.
  • The enclosing symbol of a reference no longer resolves to a bare namespace.
  • The container-qualified alias match now also works in the non-FTS SearchSymbols path.

Impact / prepareChange

  • Go method visibility no longer inherits the case of the receiver type, and Go methods are classified as method instead of function.
  • directDependents resolves the enclosing symbol instead of returning "unknown". A raw SCIP ID no longer shows up as the name.
  • The "no tests found" check actually runs, says what it checked, and only fires for languages that support test detection.
  • Risk score is rounded to 2 decimals at the output boundary, and the risk level matches the displayed score.
  • The backward-compat suggestion stays gated on visibility (locked in by a test).

Search / callgraph

  • Member search finds members again, and handler entrypoints are detected again.
  • Ranking puts exact-case matches above case-fold-only matches. enum counts as type-like in the tie-break.
  • Caller and callee headers are no longer swapped, and short names are no longer mangled.
  • schemaVersion-1 JSON contract preserved. Missing module IDs are derived.

Compatibility-relevant (in the CHANGELOG): unresolved directDependents[].name/.symbolId now serialize as "" instead of "unknown". The new code from #245 doesn't check for that string (verified).

Rebased onto develop after #245. The only conflict was CHANGELOG [Unreleased]; both sides are kept. Golden fixtures regenerated. go build, go vet, gofmt, golangci-lint (0 issues), sync-version --check and go test -race ./... pass. One extra commit rewords a doc comment in scip/ids.go: gofmt would have turned a doubled backtick into a typographic quote.

🤖 Generated with Claude Code

SimplyLiz and others added 21 commits September 17, 2026 11:36
…s case

ckb impact prepare reported unexported Go methods (e.g.
Engine#buildProvenance) as "public" with a "Public API change" risk
factor. This was going on the homepage as real evidence, so the wrong
call here is user-facing, not cosmetic.

Root cause: SCIPIdentifier.GetSimpleName() (internal/backends/scip/ids.go)
only split a method descriptor on the last '/', so for `pkg`/Engine#build
Provenance(). it returned "Engine#buildProvenance" instead of
"buildProvenance" — the capitalized receiver type leaked into the "simple
name". inferVisibility (internal/backends/scip/symbols.go) then tested
Go export-by-case on that mangled name, saw the uppercase 'E' from
"Engine", and called every method on an exported type "public"
regardless of the method's own case.

Fix: after stripping the trailing "()", also split on the last '#' so
the receiver type is discarded and only the method name is returned.
Verified end-to-end: inferVisibility(buildProvenance) now returns
"private" as it should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… "unknown"

ckb impact prepare showed every directDependents entry with
symbolId: "" and name: "unknown" — only file/line were populated.
The caller-resolution data actually exists: SCIP's processOccurrence
(internal/backends/scip/references.go:75) already resolves the
enclosing symbol per reference via findContainingSymbolFast and
stores it on SCIPReference.FromSymbol. It just never made it past
the backend boundary.

Root cause: backends.Reference (internal/backends/backend.go:146) had
no field for the enclosing symbol, so SCIPAdapter.convertToReference
(internal/backends/scip/adapter.go:359) silently dropped
scipRef.FromSymbol on every conversion. Downstream,
internal/query/impact.go:294 built impact.Reference without ever
setting FromSymbol/FromModule (both of which the impact.Reference type
already had fields for), so internal/impact/analyzer.go:130's
`StableId: ref.FromSymbol` was always "" and
`extractNameFromStableId("")` (analyzer.go:208) hard-coded "unknown".

Fix: added FromSymbol/FromSymbolName to backends.Reference, populated
from SCIPReference.FromSymbol in convertToReference (short name derived
via the SCIPIdentifier.GetSimpleName fix from the previous commit), and
wired FromSymbol/FromName through internal/query/impact.go into
impact.Reference. Where the backend genuinely can't resolve an
enclosing symbol (e.g. a package-level reference outside any
function), StableId/Name now come back "" rather than "unknown" —
extractNameFromStableId/extractModuleNameFromId no longer fabricate a
placeholder, and PrepareDependent/ImpactItem's symbolId, name, and
moduleId JSON fields are now `omitempty` so the response omits them
instead of showing a lie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… form

Follow-up to the directDependents fix: while validating it against the
full golden suite, ckb impact prepare's typescript fixtures showed
"name": "scip-typescript npm fixture 1.0.0 src/`main.ts`/" — the
entire raw, space-and-backtick-laden SCIP ID dumped as a caller name.
Worse than the "unknown" placeholder it replaced.

Root cause: some scip-ts "enclosing symbols" are module/namespace-level
(descriptor ends in a bare "/" with nothing after it — a whole-file
scope, not a named function), and GetSimpleName legitimately returns ""
for those. convertToReference (internal/backends/scip/adapter.go) still
set FromSymbol to the raw ID in that case, so internal/impact's
extractNameFromStableId fallback echoed the full ID as "name" once
FromName came back empty.

Fix: when GetSimpleName can't derive a short name, leave both
FromSymbol and FromSymbolName empty — same as "no enclosing symbol
resolved" — so the field is omitted rather than filled with a wall of
text. Confirmed against testdata/fixtures/typescript/expected/impact_*
.json, which previously couldn't have caught this because FromSymbol
was never populated at all before the directDependents fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…leName fix

Downstream fallout from the Go-method-visibility fix (SCIPIdentifier.
GetSimpleName, internal/backends/scip/ids.go): that function backs the
symbol "Name" the SCIP indexer stores when a symbol has no DisplayName,
which turns out to feed searchSymbols relevance, getSymbol exact-name
resolution, and listEntrypoints' naming-based classification — not just
impact visibility. These golden fixtures were snapshotting the old
mangled names ("Handler#service" for a field literally named "service",
etc.), so once the name is correct they mismatch.

Verified each diff by hand before updating:

- search_*.json: fields/methods whose old mangled name coincidentally
  substring-matched the query (e.g. "Handler#service" matching a search
  for "handler") no longer do, and now-clean names both remove that
  false-positive relevance boost and let previously-invisible symbols
  surface — this is a relevance improvement, not a regression. Go's
  search_handler total drops 10->9 by legitimately excluding one field
  that only ever matched "handler" through the type prefix, not the
  field itself.
- symbol_Model.json: exact-name lookup for "Model" was resolving to
  the *method* "Model#Clone" (because its old mangled name started with
  "Model"); it now correctly resolves to the "Model" type itself.
- trace_FormatOutput.json (typescript): the entrypoint list's
  deterministic tie-breaker sorts by Name (internal/query/navigation.go
  ~1418); with the old mangled names, the TypeScript "handle" method's
  entrypoint entry (real name "handle", camelCase) happened to also
  carry the receiver's PascalCase "Handler" prefix, which is what let
  it match the entrypoint-detection pattern list (naming.go ~1305:
  strings.HasPrefix(sym.Name, "Handle")). With the name fixed to bare
  "handle", it legitimately no longer matches a PascalCase-only
  pattern list, so it's no longer classified "api". That's a real,
  separate, pre-existing gap in ListEntrypoints' TypeScript coverage
  (naming patterns are Go-convention-only) that this fix incidentally
  un-masked — not something introduced here, and out of scope for the
  three reported evidence-output bugs, so left as-is and called out for
  a follow-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t names

Two independent bugs in `ckb callgraph <sym> --direction=callers`:

1. Human-format header said "Callees (what this calls)" for a
   callers-only query, even though every node's JSON role was
   "caller". cmd/ckb/format.go's formatCallgraphHuman grouped nodes by
   `n.Depth < 0` (callers) / `n.Depth > 0` (callees), but no code path
   in internal/query/navigation.go's GetCallGraph ever emits a negative
   Depth — caller nodes get Depth: 1 same as callees (navigation.go:512
   vs :534), Depth is always a non-negative distance from the root
   regardless of direction. Every non-root node fell into the ">0"
   bucket, so real callers always printed under "Callees". Fixed by
   grouping on the Role field instead, which the engine already sets
   correctly to "caller"/"callee"/"transitive"/"root" — and gave
   "transitive" (depth>1 BFS nodes, which also have no direction info)
   its own section instead of silently lumping it into "Callees" too.

2. Node short names were mangled, e.g.
   "com/SimplyLiz/CodeMCP/internal/query`/Engine#buildModuleLevelResponse"
   — missing the "github." prefix. Root cause:
   internal/backends/scip/callgraph.go's extractSymbolName looked for
   the *last* "." anywhere in the descriptor to find a name separator.
   scip-go descriptors quote the full module path in backticks right
   inside the descriptor (`` `github.com/org/repo/pkg`/Type#Method(). ``),
   and "github.com" has a dot of its own — extractSymbolName found that
   dot instead of a real separator and sliced after it, dropping
   "github." and everything before. Existing tests didn't catch this
   because their fixture package paths ("ckb/internal/query") don't
   contain dots.

   Fixed with a shared stripQuotedPackagePrefix helper that strips a
   backtick-quoted package-path prefix when present, and only falls
   back to last-dot splitting when there isn't one. Deliberately keeps
   the existing "Type#Method" / "Type#field" convention (extractSymbolName's
   own tests already lock that in, e.g. "Engine#Close" — unlike
   SCIPIdentifier.GetSimpleName, which intentionally strips the receiver
   for the separate Go-visibility fix) — the task's own suggested output
   is "Engine#buildModuleLevelResponse".

Golden fixtures updated: the Go fixtures previously encoded the exact
bug (`` `fixture/pkg`/NewServer `` with literal backticks in the name),
and the TypeScript fixture was worse ("ts`/runServer" — a completely
different off-by-N slice against the "src/`main.ts`/runServer()."
descriptor shape). Both now show clean names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ckb impact prepare reported "kind": "function" for Engine#buildProvenance,
a method, not a bare function.

Root cause: SCIPIdentifier.ExtractSymbolKind (internal/backends/scip/
ids.go) checked descriptor for '(' before checking for '#', and returned
KindFunction as soon as it saw '(' — so a method descriptor like
`pkg`/Engine#buildProvenance(). (which contains both markers) was always
classified as a bare function; the '#' check a few lines further down
was unreachable for any symbol with parens at all.

This isn't just an edge case: ExtractSymbolKind is the fallback used
when SymbolInformation.Kind isn't reliably set, and multiple comments
elsewhere in this package (e.g. callgraph.go's isFunctionSymbol) note
that scip-go frequently doesn't set it — so this fallback is the live
path for a lot of real Go methods, not a rare corner case.

Fix: classify as KindMethod when both '#' and '(' are present, before
falling through to the parens-only (function) and hash-only (class)
checks.

Golden fixtures updated: SearchSymbols results include Kind and are
sorted, so this ripples into search_*.json the same way the earlier
GetSimpleName fix did (verified by hand: only receiver methods and
constructors flip from "function" to "method"; plain package-level
functions like NewModel/newDefaultService correctly stay "function").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oundary

ckb impact prepare reported "score": 0.7000000000000001 instead of 0.7.

Root cause: calculatePrepareRisk (internal/query/compound.go) accumulates
score via repeated float64 addition of factor weights (0.25, 0.2, 0.15,
0.1, ...). None of these are exactly representable in binary floating
point, so the sum carries rounding noise — e.g. 0.15+0.2+0.15+0.2 is
0.7000000000000001 in Go, not 0.7. Nothing downstream ever rounded it
before it hit the JSON/human output.

Fix: round to 2 decimals (math.Round(score*100)/100) at the output
boundary, in the PrepareRisk struct literal — the individual factor
weights don't carry more precision than that anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t checks

ckb impact prepare's "No tests found" risk factor fired for
Engine#buildProvenance even though internal/query has abundant
*_test.go files right next to engine.go.

Two things, found while investigating:

1. Real bug: getPrepareTests (internal/query/compound.go) globs for
   *_test.go/*.test.ts/*.spec.ts files in the target's own module
   directory, gated on `target.ModuleId != ""`. resolvePrepareTarget
   set ModuleId straight from the SCIP backend's symbol lookup, which
   never resolves it (backends/scip/adapter.go's convertToSymbolResult
   hardcodes ModuleID: "" with the comment "resolved later by the query
   engine" — nothing downstream ever did that). So target.ModuleId was
   "" for every symbol target, the glob never ran, and the factor fired
   unconditionally regardless of whether tests existed. Fixed by
   deriving ModuleId from the symbol's file path when the backend
   doesn't supply one, same as the file/directory target branches in
   the same function already do.

2. Wording: even with (1) fixed, this check only proves test *files*
   exist in the same directory — it doesn't check whether the target
   symbol is itself referenced by any test. Renamed the factor to "No
   test files found in target module" so it says what it actually
   verifies instead of implying broader coverage knowledge the check
   doesn't have. Not touching the check's logic to claim more than
   that — per the request, wording only, no faked coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ated

ckb impact prepare suggested "Ensure backward compatibility or bump
major version" for Engine#buildProvenance, an unexported method.

Certainty: this was already fixed as a side effect of the earlier
Go-method-visibility commit (SCIPIdentifier.GetSimpleName) — not a
separate bug. calculatePrepareRisk (internal/query/compound.go) was
already correctly gated on `target.Visibility == "public"` before that
commit; the suggestion only ever fired for buildProvenance because
visibility itself came back "public" for it, which is what the
GetSimpleName fix addressed. Verified by reading calculatePrepareRisk:
no change needed here.

Adds a regression test locking in the gate itself (internal visibility
-> neither the "Public API change" factor nor the suggestion; public
visibility -> both), so a future visibility regression would be caught
at this call site too, independent of the SCIP-parsing layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arser

ParseSCIPIdentifier hand-rolled its own symbol-ID splitter, which only
handled scip-go's backtick-quoted package paths and Go-shaped method
descriptors. A prior fix on this branch (GetSimpleName stripping "Type#"
unconditionally to fix Go method visibility) broke every other language:
TypeScript member names collapsed the same way search relies on to keep
members findable by container, TS parameter descriptors like
"Handler#handle().(input)" got misclassified as "method" because
ExtractSymbolKind just checked for '#' and '(' anywhere in the descriptor,
Java's two-token maven package coordinate was mis-split by
SplitN(id, " ", 5) treating the artifact as the version, and constructor
descriptors with backtick-escaped names ("`<constructor>`") produced empty
or garbled names.

Fix: delegate to github.com/sourcegraph/scip/bindings/go/scip's
ParseSymbol, which splits a symbol into scheme, package
(manager/name/version, each correctly unescaping a doubled-space-escaped
internal space per the SCIP spec), and a chain of descriptors each
carrying its own suffix (namespace '/', type '#', term '.', method "().",
type-parameter '[..]', parameter '(..)', meta ':', macro '!'), with
backtick-escaped names unescaped. GetSimpleName is then just "the last
descriptor's name" (no receiver leakage regardless of language);
ExtractSymbolKind checks the last descriptor's suffix, distinguishing
method vs. function by whether the second-to-last descriptor is a type;
GetContainerName walks back for the nearest enclosing type descriptor;
GetPackageInfo now returns the already-correctly-parsed manager/name/
version instead of re-splitting the package string on whitespace.

Identifiers that don't conform to the strict SCIP grammar (e.g. "local
..." symbols, or malformed 4-field test fixtures missing a package
version) fall back to the original heuristic parser, so ad-hoc test
identifiers keep working.

Replaced ids_test.go's struct-literal tests (which bypassed the real
parser and only exercised the legacy fallback) with end-to-end
ParseSCIPIdentifier tests across Go, TypeScript, Java, Python, and Rust,
covering methods, free functions, constructors, parameter descriptors,
and the Java package-coordinate escaping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
extractSymbolName built call graph node labels ("Type#Member") by hand:
find the last space-delimited "part" of the ID, strip a trailing "()." or
".", then strip a backtick-quoted package prefix. Two bugs: (1)
stripQuotedPackagePrefix found the *last* backtick in the string, which
for a symbol like `pkg`/Handler#`<constructor>`() is the constructor's own
closing backtick, not the package path's — the "name" after it is empty,
so the constructor node got named "". (2) The same helper (and the
splitter feeding it) only understood Go-shaped "()." method descriptors
and receiver-name splitting; it wasn't documented as Go-specific but broke
on other languages' descriptor shapes the same way ids.go did.

Fix: parse with the official scip bindings and rebuild the label from the
parsed descriptor chain, dropping namespace (package-path) descriptors
and adding back just enough suffix punctuation per descriptor kind to
stay unambiguous — "().”  between a method and what follows it, "(...)"
around a parameter — while omitting that trailing punctuation on the
terminal descriptor. This reproduces the existing (already-reviewed)
call graph name format exactly (e.g. "Engine#buildModuleLevelResponse",
bare "Engine#", "NewServer") and additionally gives non-empty names for
constructors and other backtick-escaped descriptors. Falls back to the
original heuristic splitter for identifiers that don't conform to the
strict SCIP grammar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…espace

convertToReference resolves FromSymbol/FromSymbolName (the enclosing
function/type containing a reference) by parsing the enclosing symbol ID
and taking GetSimpleName(). Some scip-go/scip-typescript "enclosing
symbols" are whole-file, namespace-level SymbolInformation entries whose
EnclosingRange spans the entire document (scip-typescript emits one per
file) — a top-level reference with no enclosing function/type resolves
its "container" to that file-scope symbol.

Previously GetSimpleName legitimately returned "" for that case (its old
implementation only found a name after a trailing "`/" package-path
split, which a bare namespace descriptor doesn't have), so the existing
check `name != ""` correctly filtered it out. Now that GetSimpleName
always returns the last descriptor's name — correct in general, see the
prior commit — a namespace descriptor's name (e.g. "handler.ts", the file
itself) is no longer empty, so it started leaking through as if a file
were a "caller".

Fix: also check that the resolved symbol's kind isn't KindNamespace
before treating it as a real enclosing symbol, restoring the original
intent (skip file/namespace-only containers) without special-casing
GetSimpleName itself, which other callers rely on to return a namespace's
own name for real namespace symbols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…detection

Two related regressions from the GetSimpleName fix, both stemming from
symbol names becoming correctly bare (e.g. "handle" instead of
"Handler#handle" or "Handler.handle"):

1. FTS indexing built the container-qualified "signature" field (e.g.
   "Handler.handle", so searching "Handler" still finds its members) from
   SymbolInformation.EnclosingSymbol via GetSimpleName. But
   scip-go/scip-typescript frequently leave EnclosingSymbol unset (empty
   in both fixtures used by the golden tests), so that branch never ran
   and signature silently fell back to the bare name — container-qualified
   search was never actually populated, just coincidentally still working
   before because GetSimpleName used to leak the receiver into the "name"
   itself. Fix: derive the container from the symbol's own descriptor
   chain via GetContainerName (which doesn't depend on EnclosingSymbol
   being set) as the primary path, keeping the EnclosingSymbol-based
   lookup as a fallback.

2. listEntrypoints' handler-pattern detection used
   strings.HasPrefix(sym.Name, "Handle") — case-sensitive. This
   accidentally matched TS's "handle" method before, only because the
   mangled name "Handler#handle" happened to start with "Handle" (the
   capitalized *receiver type*, not the method itself). With names now
   correctly bare, "handle" no longer matches a case-sensitive "Handle"
   prefix. Go's exported convention capitalizes names Go programmers
   would call "Handle*"; TS/JS methods are conventionally lowercase for
   the same naming pattern. Made the check case-insensitive so the
   naming-convention detection isn't accidentally Go-specific.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dule ids, and make risk level agree with the displayed score

Three related evidence-output bugs in prepareChange/analyzeImpact:

1. A prior commit added `,omitempty` to PrepareDependent.SymbolId/Name/
   ModuleId and ImpactItem.StableId/ModuleId so an unresolved enclosing
   symbol wouldn't display the literal string "unknown". That's the right
   call for the *value* (empty string, not a fake placeholder) but wrong
   for the *JSON shape*: these responses are schemaVersion 1, which
   documents these as always-present string fields. Dropping the key
   entirely when the value happens to be empty is a breaking contract
   change for any strict consumer, without a version bump to announce it.
   Reverted the omitempty tags; the fields still serialize as "" rather
   than "unknown".

2. Neither PrepareDependent nor ImpactItem's construction ever derived a
   moduleId for a reference's enclosing symbol — analyzeImpact's
   reference-building loop set Kind/Location/FromSymbol/FromName but never
   FromModule, so every direct dependent's moduleId stayed "". That fed
   straight through to prepareChange's module-spread count
   (moduleId->bool set) and blastRadius.moduleCount, both of which
   silently under-reported (0 modules affected regardless of the actual
   spread). Derived FromModule from the reference's own file path
   (filepath.Dir), the same way resolvePrepareTarget already derives a
   symbol target's moduleId from its file path. Also guarded the
   module-spread set against inserting an empty "" key for any reference
   that still has no location.

3. calculatePrepareRisk derived `level` ("low"/"medium"/"high"/"critical")
   from the raw, unrounded score, but returned a Score field rounded to 2
   decimals for display. Binary-float accumulation (0.15+0.2+0.15+0.2,
   etc.) can land just under a threshold (e.g. 0.6999999999999998), which
   rounds to a displayed "0.70" while classifying as "high" — display and
   level visibly disagree at the boundary. Round once, and derive level
   from that same rounded value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…parsing, disambiguate symbol_Model by kind

Regenerated after the ids.go/callgraph.go/adapter.go/fts.go/navigation.go
fixes in this branch. Every diff falls into one of:

- search_handler (go, ts): member search restored — "handle"/"Handle" and
  other members are findable again via the container-qualified FTS
  signature; parameter/property descriptors that were misclassified as
  "method" now show their real kind; constructor names are no longer
  garbled ("Handler#`<constructor>`" -> "<constructor>").
- search_service, search_model (go, ts): same kind-classification fix —
  fields previously mislabeled "class" (any descriptor containing '#' was
  called a class) now show as "property"/"method" correctly, and
  previously-invisible field members (empty name, filtered out) are now
  found.
- search_main (go): a previously-invisible whole-file namespace symbol
  (empty name before this fix) now surfaces with its real name; harmless,
  the file's own name is now a legitimate low-rank match.
- impact_FormatOutput, impact_Handler (go, ts): moduleCount/modulesAffected
  now populated (see the FromModule-derivation fix) and riskScore.score
  changed accordingly; a constructor name resolved correctly instead of
  showing the raw escaped descriptor; a field wrongly classified "class"
  now shows "property".

golden_test.go: TestGolden_GetSymbol's "Model" case searched by name only
and took the top (Limit: 1) result. Fixing the kind-classification bug
above means the TS fixture's "model" field is now also a valid unfiltered
match for the query "Model", and it happened to rank above the "Model"
class in the TypeScript fixture (bm25 tie-break, not something this
branch controls) — the test started resolving a different symbol than
the "Data structure" case comment says it's meant to exercise. Filtered
that case by Kind: "class" to restore deterministic intent; with the
filter applied the resolved symbol is byte-identical to the pre-existing
develop baseline, so no golden file was needed for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The FTS5 unicode61 tokenizer folds case, so a query for "Model" can't be
told apart from a field named "model" by bm25() alone — ties broke on
SQLite's internal row order, which put the field ahead of the class in the
TypeScript fixture (and, by luck of insertion order, not in Go). A previous
commit on this branch papered over the symptom by adding a Kind: "class"
filter to the golden test instead of fixing the ranking, which hid a real
user-facing regression: searching "Model" could return the lowercase field
instead of the type.

Two changes close the gap:

- internal/storage/fts.go (searchExact): sort a case-sensitive exact name
  match first, then a type-like kind (class/interface/struct/type) among
  case-fold-only matches, before bm25 — and before the caller's LIMIT is
  applied. Without this, opts.Limit*ftsMultiplier (2, for a Limit:1 caller
  like GetSymbol's search-then-fetch) could truncate the FTS candidate pool
  before the correct symbol ever reached Go-side ranking, no matter how
  good that ranking was.
- internal/query/symbols.go (rankSearchResults): split the "exact" match
  tier into a case-sensitive exact match (score +100) and a case-fold-only
  match (+80, plus a further tie-break for type-like kinds over
  properties/parameters). This keeps the SCIP/tree-sitter fallback paths
  (which don't go through FTS) consistent with the FTS ordering.

Removed the Kind: "class" filter from TestGolden_GetSymbol's symbol_Model
case; it now passes on ranking alone for both fixtures.

Regenerated testdata/fixtures/{go,typescript}/expected/impact_Handler.json:
both fixtures deliberately contain a second "Handler" — a free function in
Go's main.go (explicitly commented as a disambiguation decoy) and a private
`handler` field in the TS Server class — that collided with the exported
Handler type under the old ranking. AnalyzeImpact's "Handler" lookup now
resolves to the actual Handler type (which has real callers/blast radius)
instead of the zero-impact decoy function / the field. impact_FormatOutput
was also touched by -update but is byte-identical (no name collision there).

Added unit tests: TestRankSearchResults_CaseExactOutranksFold and
TestIsTypeLikeKind in internal/query/symbols_test.go, and
TestFTSManagerSearchExactCasePriority in internal/storage/fts_test.go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FTS-backed search already indexes a "Container.member" alias
(convertSymbolToFTSRecord in internal/query/fts.go) so a query for
"Handler" finds "Handler#handle" even though the member's own display
name is just "handle". The SCIP-native SearchSymbols path never got
the same fix: all three of its code paths (the NameIndex fast path,
the ConvertedSymbols map fallback, and the on-the-fly last-resort
conversion) matched only the bare display name via matchesQuery. Any
installation with FTS disabled, unavailable, or returning no results
silently lost member findability by container name.

Add an Alias field to NameEntry (container-qualified name, built from
the already-computed SCIPSymbol.ContainerName) and check it in the
NameIndex pre-filter; matchesQuery now falls back to the same
container-qualified check for the two paths that don't use NameIndex.
Old on-disk derived caches decode fine (Alias defaults to "") and
self-heal on the next reindex, since the cache is keyed by the .scip
file's mtime+size.

Add a no-FTS regression test covering all three SearchSymbols paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
isTypeLikeKind decides which kind wins a case-fold-only search tie
(e.g. class Model vs. field model). It listed class/interface/
struct/type but omitted enum, even though enum is a real kind this
codebase emits (SCIP kind 3, see inferKindString in
internal/query/fts.go and KindEnum in internal/backends/scip/
types.go). A case-fold-only query like "STATUS" ranked an enum
`Status` no higher than a property `status`, contradicting the
type-like ranking policy's intent.

Checked the full SymbolKind set this codebase defines
(internal/backends/scip/types.go) for other type-like kinds worth
adding: there is no separate trait/protocol/typealias kind here —
Rust traits and meta descriptors both collapse to the existing
"type" kind via kindFromDescriptors in internal/backends/scip/ids.go
— so enum was the only gap.

Extends TestIsTypeLikeKind and adds a case-fold ranking regression
test (STATUS -> enum Status over property status).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getPrepareTests only implements test-file discovery for Go
(*_test.go) and TypeScript/JavaScript (*.test.ts/js, *.spec.ts/js).
calculatePrepareRisk fired "No test files found in target module"
(and its 0.2 score contribution) whenever the glob came back empty —
which for Python, Rust, Java/Kotlin, and every other language was
every single time, since discovery never runs for them. That's a
false claim ("no tests exist"), not a finding ("we didn't check").

Rather than implementing multi-language discovery now, make the
factor honest: add languageSupportsTestDiscovery, which checks the
target's file extension (or, for directory/module targets, globs the
module dir for .go/.ts/.js files) and only lets the factor fire when
discovery is actually implemented for that language. Every other
language just omits the factor instead of asserting something we
don't know.

Updates the two existing calculatePrepareRisk tests that relied on
the factor firing unconditionally to use a .go target path, and adds
a Python-target regression test confirming the factor (and its score)
is omitted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…names

Round-2 evidence review flagged that recent fixes (see
TestConvertToReference_ModuleLevelEnclosingSymbol in
internal/backends/scip/adapter_test.go, and the FromSymbol/
FromSymbolName comments in internal/query/compound.go) changed the
fallback for an unresolved dependent name/symbolId from the string
"unknown" to "" — a genuine semantic change for any consumer that
checked for the literal "unknown" sentinel, even though the JSON key
itself is unaffected (schemaVersion 1 fields aren't omitempty).

Add an Unreleased entry so downstream consumers see it before the
next release, following this changelog's existing dated-section
format.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gofmt rewrites a doubled backtick in doc comments to a typographic quote,
which would have changed what the comment says about SCIP escaping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🟢 Change Impact Analysis

Metric Value
Risk Level LOW 🟢
Files Changed 26
Symbols Changed 16
Directly Affected 0
Transitively Affected 0

Blast Radius: 0 modules, 0 files, 0 unique callers

📝 Changed Symbols (16)
Symbol File Type Confidence
CHANGELOG.md CHANGELOG.md modified 30%
cmd/ckb/format.go cmd/ckb/format.go modified 30%
internal/backends/backend.go internal/backends/backend.go modified 30%
internal/backends/scip/adapter.go internal/backends/scip/adapter.go modified 30%
internal/backends/scip/callgraph.go internal/backends/scip/callgraph.go modified 30%
internal/backends/scip/ids.go internal/backends/scip/ids.go modified 30%
internal/backends/scip/loader.go internal/backends/scip/loader.go modified 30%
internal/backends/scip/symbols.go internal/backends/scip/symbols.go modified 30%
internal/impact/analyzer.go internal/impact/analyzer.go modified 30%
internal/impact/types.go internal/impact/types.go modified 30%
internal/query/compound.go internal/query/compound.go modified 30%
internal/query/fts.go internal/query/fts.go modified 30%
internal/query/impact.go internal/query/impact.go modified 30%
internal/query/navigation.go internal/query/navigation.go modified 30%
internal/query/symbols.go internal/query/symbols.go modified 30%
+1 more

Recommendations

  • ℹ️ coverage: 16 symbols have low mapping confidence. Index may be stale.
    • Action: Run 'ckb index' to refresh the SCIP index

Generated by CKB

@github-actions

Copy link
Copy Markdown

CKB Analysis

Risk Files +1960 -494 Modules

🎯 16 changed → 0 affected · 🔥 22 hotspots · 📊 12 complex · 💣 10 blast · 📚 202 stale

Risk factors: Large PR with 46 files • High churn: 2454 lines changed • Touches 22 hotspot(s)

👥 Suggested: @lisa.welsch1985@gmail.com (22%), @talantyyr@gmail.com (9%), @lisa@tastehub.io (4%)

Metric Value
Impact Analysis 16 symbols → 0 affected 🟢
Doc Coverage 6.9306930693069315% ⚠️
Complexity 12 violations ⚠️
Coupling 0 gaps
Blast Radius 0 modules, 0 files
Index indexed (0s) 🆕
🎯 Change Impact Analysis · 🟢 LOW · 16 changed → 0 affected
Metric Value
Symbols Changed 16
Directly Affected 0
Transitively Affected 0
Modules in Blast Radius 0
Files in Blast Radius 0

Symbols changed in this PR:

Recommendations:

  • ℹ️ 16 symbols have low mapping confidence. Index may be stale.
    • Action: Run 'ckb index' to refresh the SCIP index
💣 Blast radius · 0 symbols · 10 tests · 0 consumers

Tests that may break:

  • cmd/ckb/format_test.go
  • internal/backends/scip/adapter_test.go
  • internal/backends/scip/callgraph_test.go
  • internal/backends/scip/ids_test.go
  • internal/backends/scip/symbols_test.go
  • … and 5 more
🔥 Hotspots · 22 volatile files
File Churn Score
CHANGELOG.md 6.31
cmd/ckb/format.go 7.71
internal/backends/scip/adapter.go 5.21
internal/backends/scip/callgraph.go 6.43
internal/backends/scip/ids.go 8.63
internal/backends/scip/ids_test.go 4.30
internal/query/compound.go 7.70
internal/query/compound_test.go 3.79
📦 Modules · 3 at risk
Module Files
🔴 testdata/fixtures 20
🟡 internal/backends 10
🟡 internal/query 8
📊 Complexity · 12 violations
File Cyclomatic Cognitive
cmd/ckb/format.go ⚠️ 42 ⚠️ 89
cmd/ckb/format_test.go 15 ⚠️ 21
internal/backends/scip/callgraph.go ⚠️ 35 ⚠️ 78
internal/backends/scip/callgraph_test.go ⚠️ 38 ⚠️ 88
internal/backends/scip/ids.go 13 ⚠️ 25
internal/backends/scip/loader.go ⚠️ 72 ⚠️ 243
internal/backends/scip/symbols.go ⚠️ 24 ⚠️ 61
internal/query/compound.go ⚠️ 31 ⚠️ 58
💡 Quick wins · 10 suggestions
📚 Stale docs · 202 broken references

Generated by CKB · Run details

@github-actions

Copy link
Copy Markdown

🔐 Security Audit Results

Security gate FAILED - 4 HIGH severity gosec finding(s)

Category Findings
🔑 Secrets ✅ 0
🛡️ SAST ⚠️ 28
📦 Dependencies ⚠️ 21
📜 Licenses ⚠️ 142 non-permissive

🛡️ SAST Analysis

Found 28 issue(s) across 1 scanner(s)

Details

Gosec (28 findings)

  • /home/runner/work/ckb/ckb/internal/lip/subscribe.go:207 - G115: integer overflow conversion int -> uint32...
  • /home/runner/work/ckb/ckb/internal/lip/client.go:1030 - G115: integer overflow conversion int -> uint32...
  • /home/runner/work/ckb/ckb/internal/impact/enricher.go:43 - G101: Potential hardcoded credentials...
  • /home/runner/work/ckb/ckb/internal/query/review_coupling.go:43 - G702: Command injection via taint analysis...
  • /home/runner/work/ckb/ckb/internal/tier/runner.go:45 - G204: Subprocess launched with variable...
  • ... and 23 more

📦 Dependency Vulnerabilities

Found 21 vulnerability(ies) across 2 scanner(s)

Details

Trivy (14 findings)

  • CVE-2026-15157 (MEDIUM): undici - undici: undici: HTTP header injection via unvalida...
  • CVE-2026-16728 (MEDIUM): undici - undici: undici: Response desynchronization via ret...
  • CVE-2026-16729 (MEDIUM): undici - undici: Undici: Cookie attribute injection allows ...
  • CVE-2026-33997 (MEDIUM): github.com/docker/docker - moby: docker: github.com/moby/moby: Moby: Privileg...
  • CVE-2026-41568 (MEDIUM): github.com/docker/docker - github.com/docker/docker: github.com/moby/moby: Mo...
  • ... and 9 more

OSV-Scanner (7 findings)

  • github.com/docker/docker: 11 vulnerabilities
  • github.com/go-chi/chi/v5: 3 vulnerabilities
  • github.com/klauspost/compress: 1 vulnerabilities
  • github.com/rs/cors: 2 vulnerabilities
  • go.opentelemetry.io/otel: 1 vulnerabilities
  • ... and 2 more

📜 License Issues

Found 142 non-permissive license(s)

Details
  • github.com/BurntSushi/toml: MIT (notice)
  • github.com/google/uuid: BSD-3-Clause (notice)
  • github.com/klauspost/compress: Apache-2.0 (notice)
  • github.com/klauspost/compress: BSD-3-Clause (notice)
  • github.com/klauspost/compress: MIT (notice)
  • github.com/pelletier/go-toml/v2: MIT (notice)
  • github.com/smacker/go-tree-sitter: MIT (notice)
  • github.com/sourcegraph/go-diff: MIT (notice)
  • github.com/sourcegraph/scip: Apache-2.0 (notice)
  • github.com/spf13/cobra: Apache-2.0 (notice)
  • ... and 132 more

Generated by CKB Security Audit | View Details | Security Tab

@github-actions

Copy link
Copy Markdown

CKB Review: 🟡 WARN — 42/100

46 files (+2454 changes) · 7 modules · go

Changes 46 files across 7 modules (go). 3 commonly co-changed file(s) missing from changeset; Risk score: 1.00 (high).

Check Status Detail
risk 🟡 WARN Risk score: 1.00 (high)
coupling 🟡 WARN 3 commonly co-changed file(s) missing from changeset
test-gaps ℹ️ INFO 113 untested function(s) in changed files (showing top 10)
blast-radius ℹ️ INFO No symbols with callers in changes
hotspots ℹ️ INFO 22 hotspot file(s) touched (top 10 shown)
breaking ✅ PASS No breaking API changes
format-consistency ✅ PASS No format consistency issues
tests ✅ PASS 11 test(s) cover the changes
secrets ✅ PASS No secrets detected
unwired ✅ PASS All exported symbols are reachable from entrypoints
comment-drift ✅ PASS No comment/code drift detected
dead-code ✅ PASS No dead code in changed files
complexity ✅ PASS +212 cyclomatic complexity across 17 file(s)
health ✅ PASS 3 file(s) degraded, 0 improved (avg -1.4)
bug-patterns ✅ PASS 30 new bug pattern(s) (26 pre-existing filtered) (all on unchanged lines)
layers ⚪ SKIP Cartographer not compiled in this build
arch-health ⚪ SKIP Cartographer not compiled in this build

Top Risks

  • Risk score: 1.00 (high)
  • 3 commonly co-changed file(s) missing from changeset
Findings (17 actionable, 24 informational)
Severity File Finding
🟡 internal/backends/scip/adapter.go Health B→B (85→74, -11 points)
🟡 internal/backends/scip/ids.go Health B→C (75→64, -11 points)
🟡 internal/backends/scip/loader.go Missing co-change: internal/query/engine.go (70% co-change rate)
🟡 internal/impact/analyzer.go Missing co-change: cmd/ckb/impact.go (71% co-change rate)
🟡 internal/query/compound_test.go Health B→C (80→65, -15 points)
🟡 internal/query/fts.go Missing co-change: internal/query/engine.go (71% co-change rate)
ℹ️ cmd/ckb/format.go Complexity 312→317 (+5 cyclomatic) in formatChangesHuman()
ℹ️ cmd/ckb/format_test.go Complexity 223→228 (+5 cyclomatic) in TestFormatDiffSummaryHuman()
ℹ️ internal/backends/scip/callgraph.go Complexity 140→159 (+19 cyclomatic) in mapSCIPKind()
ℹ️ internal/backends/scip/ids.go Complexity 43→69 (+26 cyclomatic) in kindFromDescriptors()

... and 7 more

Code Health — 3 degraded

Degraded:

File Before After Delta Grade Confidence
internal/backends/scip/adapter.go 85 74 -11 B→B 100%
internal/backends/scip/ids.go 75 64 -11 B→C 100%
internal/query/compound_test.go 80 65 -15 B→C 100%

^1 File could not be parsed by tree-sitter

New files: 3 (avg health: 86)

3 degraded · 0 improved · avg -1.4

Estimated review: not feasible as a single PR (46 files, 2454 lines)

Reviewers: lisa.welsch1985 (22%) · talantyyr (9%) · lisa (4%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant