task: build SemanticIndex and the concept-retrieval pipeline - #222
Conversation
…al pipeline Plans the fifth task under PRD #4's Project Intelligence Layer scope: a 10-step plan implementing ConceptEntry and a two-stage concept -> subsystem -> candidate symbol -> confirmed-reference pipeline, per the design doc's Data Model item 3 and Concept Retrieval reasoning rules. Confirmed before planning: #210 has a REAL stated dependency on #206 (unlike #208/#209) -- "Depends on #206 for the content to embed and summarize" -- so this branches from origin/task/207-project-graph (which carries both #206's and #207's already-merged-locally commits) rather than from `launchpad` directly, since #213/#214 haven't merged into launchpad yet. Key design decision recorded in the plan: "subsystem" is implemented as a real second ConceptEntry level scoped to file (the schema's own stated scope kinds are symbol_id | file | doc_section), not collapsed into a single flat symbol ranking -- so the pipeline's stated shape is actually built as two literal ranking stages, not simplified away. Mechanical checks clean via check-plan.sh. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Adds ConceptEntry, matching the design doc's schema (scope, embedding, summary), and SemanticIndex, an in-process store keyed by scope. embedding is a tuple of (token, weight) pairs, not a dict, so the frozen dataclass stays genuinely immutable -- same reasoning as #209's MemoryEntry.evidence being a tuple rather than a mutable list. scope accepts symbol_id, file, or doc_section per the design doc's own schema -- this is deliberate groundwork for STEP 4/5's two-level pipeline (per-symbol and per-file "subsystem" entries), not unused generality. Verified: `python3 -m unittest test_semantic_index` -- 3 passed. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Adds summarize_symbol(): a deterministic natural-language gloss built only from #206's already-extracted Symbol structural facts (qualified_name, kind, signature, calls, tests, config_dependencies, documentation_links) -- generated once, not guessed fresh per query, matching the design doc's own stated constraint. Empty fields are omitted rather than printed blank. Test fixture is a real Symbol from buzz-core, hand-constructed (not built via indexer.build_index(), which shells out to rql and is kept out of this committed hermetic suite, same reasoning as test_indexer.py/test_graph.py) from fields cross-checked directly against crates/buzz-core/src/kind.rs:219-221 and confirmed against ARCHITECTURE.md:142 (which references kind.rs by file path -- the documentation_links match is on file mention, not literal function name, matching #206's with_documentation_links() logic). Verified: `python3 -m unittest test_semantic_index` -- 6 passed. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…_similarity Adds tokenize() (word-boundary plus camelCase/snake_case splitting, so identifiers decompose into the same word tokens a natural-language concept query would use), embed_text() (a bag-of-words frequency vector -- a deliberate, documented lightweight stand-in for a trained ML embedding model, matching #210's own "out of scope: any embedding- model selection process beyond what's needed to demonstrate the pipeline once"), and cosine_similarity() between two such vectors, guarding the zero-vector case rather than dividing by zero. Verified: `python3 -m unittest test_semantic_index` -- 13 passed, including hand-computed (not real-symbol) cases per the plan's STEP 3 done-when: a vector against itself is 1.0, disjoint vocabularies are 0.0, and one fully hand-worked partial-overlap case (a a b / a c c -> 2/5) checked by hand in the test's own comment before being asserted. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…ry levels (RUNS HERE) Adds SemanticIndex.from_symbols(), building two levels of ConceptEntry from real Symbol records: one per symbol, and one per file (aggregating that file's symbols' summaries) -- the design doc's own ConceptEntry schema names file as a valid scope kind alongside symbol_id, so this is the schema's own coarser "subsystem" level, not invented machinery. Self-caught bug from live verification (not from a test I wrote in advance): the first version keyed per-symbol entries by qualified_name, which raised ValueError on real buzz-core data -- multiple distinct symbols (e.g. several "build_event" functions in different modules) share one qualified_name, so it is not a safe unique key. Switched to symbol_id (a real, per-symbol-unique RepoQL URI from #206's index_crate()), which the design doc also explicitly names as a valid scope kind. Added qualified_name_for(), since #207's ProjectGraph addresses nodes by qualified_name, not symbol_id -- the pipeline's later confirmation step needs to translate between the two. Verified: `python3 -m unittest test_semantic_index` -- 17 passed, including a synthetic two-symbols-sharing-one-qualified_name regression test for the exact collision this fix addresses. Verified live against ALL 453 real buzz-core symbols (not just the hand-picked fixtures): building the full index raises nothing, and crates/buzz-core/src/kind.rs's file-level entry aggregates both is_shared_gated_kind and is_unshared_gated_event's real content. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…ol identity weighting
Adds SemanticIndex.search(): rank file-level "subsystem" entries
first, then rank symbol-level entries scoped to the top file(s) --
concept -> candidate subsystem(s) -> candidate symbols, as two literal
ranking stages, returned as SearchResult(subsystem, subsystem_score,
candidate, candidate_score) sorted by candidate_score.
Adds embed_symbol(), weighting a symbol's own identity (kind +
qualified_name + signature) 2x over context mentions (calls/tests/
config/docs) in its embedding. Without this, a caller's summary
absorbs its callees' name tokens too (since "calls X" contributes X's
own identifier tokens), so a caller can outrank the callee it calls
for a query about the callee's own behavior -- found empirically
verifying this step's own worked example: is_unshared_gated_event
(which calls is_shared_gated_kind) initially outranked
is_shared_gated_kind itself once the query touched a token unique to
the caller's own name ("event"). identity_weight=2.0 was checked
empirically against the real worked example, not derived
analytically, and is documented as such in embed_symbol()'s docstring.
Retrofitted STEP 4's from_symbols() to use embed_symbol() instead of a
plain embed_text(summary) for both the per-symbol and per-file
embeddings.
Verified: `python3 -m unittest test_semantic_index` -- 19 passed,
including a hand-checked identity-weighting assertion (event's weight
> kind's absorbed-context weight for is_unshared_gated_event) and the
corrected two-stage search test (kind.rs wins as subsystem,
is_shared_gated_kind wins as candidate, over an unrelated real symbol
from crates/buzz-core/src/invite.rs).
Verified live against the full real buzz-core index (453 symbols, via
indexer.build_index): searching "which function decides if a kind is
gated for shared visibility" ranks is_shared_gated_kind first (0.5706)
by a clear margin over the next real result
(tests::shared_gated_kinds_membership, 0.4300), with kind.rs correctly
winning as the top subsystem.
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Adds confirm_via_graph(): the pipeline's final confirmation step, calling directly into #207's ProjectGraph.edges_from() for tested_by/called_by edges on a candidate symbol -- real structural confirmation, not semantic similarity alone. Returns empty tuples (never hidden) when nothing confirms a candidate, so a caller can see an unconfirmed guess for what it is. Verified: `python3 -m unittest test_semantic_index` -- 21 passed. Cross-checked confirm_via_graph(graph, "is_shared_gated_kind") against #207's own already-proven demo output for the identical symbol (graph.py's __main__, STEP 6): tested_by -> tests::shared_gated_kinds_membership, called_by -> is_unshared_gated_event -- matches exactly. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Adds find_it_for_me(): concept -> subsystem -> candidate -> confirm, tied into one call. Translates the top candidate's scope (symbol_id) back to its qualified_name via SemanticIndex.qualified_name_for() before confirming through #207's ProjectGraph, since the two components address symbols differently. Returns an empty result (candidate/confirmation None) rather than crashing when the index has nothing to rank. Verified: `python3 -m unittest test_semantic_index` -- 23 passed. One test builds both a SemanticIndex and a ProjectGraph from the same three real symbols and confirms find_it_for_me() ties all of it together correctly in one call: is_shared_gated_kind as the resolved qualified_name, kind.rs as the subsystem, and real callers/tests matching STEP 6's own already-verified edges. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…to end Adds WORKED_EXAMPLE_CONCEPT, a named module-level constant for STEP 8's own worked concept-search example against this repo's real code (not the design doc's fictional OnboardingMailer example): "which function decides if a kind is gated for shared visibility" -- resolves to is_shared_gated_kind via subsystem -> candidate -> confirmed-reference. Verified: `python3 -m unittest test_semantic_index` -- 25 passed, including an explicit check that the concept sentence contains no contiguous substring match of "is_shared_gated_kind" (nor its underscores-as-spaces form) -- proving this is genuine token/concept overlap, not an accidental literal substring hit, per #210's own Definition of done. Verified live against the FULL real buzz-core index (453 symbols, via indexer.build_index + graph.ProjectGraph.from_symbols): find_it_for_me resolves the exact same concept sentence to is_shared_gated_kind (candidate_score 0.5706, subsystem kind.rs at 0.4184), confirmed by 2 real called_by edges (tests::shared_gated_kinds_membership, is_unshared_gated_event) and 1 real tested_by edge. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…ry case Demonstrates the documented boundary from #210's own side (#207's graph.py already showed the mirror: a vague description has no symbol_id for reachable() to start from). Poses the SAME 2-hop relationship #207's own reachable() demo already proves (tests::is_unshared_gated_event_author_always_allowed -> is_unshared_gated_event -> is_shared_gated_kind, all real symbols cross-checked against crates/buzz-core/src/kind.rs:997-1007) through THIS pipeline instead. No new production code -- STEPS 1-7 already implement everything this exercises; this step is the issue's own required negative demonstration, not new functionality. Verified: `python3 -m unittest test_semantic_index` -- 27 passed. Confirms structurally (PipelineResult/Confirmation have no hop or path field at all -- checked via dataclasses.fields(), not by reading the source) and behaviorally (confirm_via_graph() only ever returns direct edges; is_shared_gated_kind never appears in a one-hop confirmation for a symbol two hops away) that this pipeline cannot express or verify a multi-hop path, while reachable() answers the identical relationship exactly. Verified live against the FULL real buzz-core index (453 symbols): the same flow-tracing question resolves this pipeline to an unrelated weak match (tests::test_unspecified) with an EMPTY confirmation -- it cannot even find a sensible candidate for a flow-tracing question, let alone a verified 2-hop path -- while reachable() returns the exact real path (tests::is_unshared_gated_event_author_always_allowed -> is_unshared_gated_event -> is_shared_gated_kind). Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…ples Wires __main__ to run both worked examples end to end against the real buzz-core index, matching #206/#207/#208/#209's demo style: the positive concept -> subsystem -> candidate -> confirmation trace, and the negative flow-tracing contrast (this pipeline's result vs ProjectGraph.reachable()'s exact answer), side by side. Named the negative example's constants (NEGATIVE_EXAMPLE_FLOW_QUESTION, NEGATIVE_EXAMPLE_START_SYMBOL) alongside STEP 8's WORKED_EXAMPLE_CONCEPT, and updated STEP 9's test to reference them instead of duplicating the literal strings. Verified live: `python3 semantic_index.py` prints both traces with real results -- is_shared_gated_kind resolved and confirmed for the positive example, and tests::test_unspecified (an unrelated weak match, empty confirmation) for the negative example, contrasted against reachable()'s real verified 2-hop path. Verified: `python3 -m unittest test_semantic_index` -- 27 passed. This completes all 10 steps of #210's plan. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
benmitchell11
left a comment
There was a problem hiding this comment.
Confirmed the stacking is legitimate — diff against task/207-project-graph only touches this PR's own 3 files (plan doc, semantic_index.py, test_semantic_index.py), nothing re-touching #213/#214.
The symbol_id-not-qualified_name scope key is a genuinely good empirical catch — finding that multiple real symbols in this codebase share a short qualified_name (several build_event functions in different modules) by checking against all 453 actual indexed symbols, rather than assuming qualified_name was unique, is exactly the kind of thing that would otherwise surface as a confusing bug much later. qualified_name_for() and its own test confirm the translation path back to what ProjectGraph expects is real, not just described.
The negative worked example is the more valuable of the two demonstrations here — showing this pipeline correctly resolves a flow-tracing question to the wrong answer (one hop, not the real two-hop relationship) and explicitly contrasting it against ProjectGraph.reachable() giving the right one, is a genuine boundary-case proof rather than only a positive demo. confirm_via_graph() returning empty rather than something that looks like a confirmation when nothing actually confirms a candidate is the right failure shape for a retrieval tool a human or agent might trust.
Good escalation on the embedding approach — bag-of-words with an empirically-tuned identity weight is correctly scoped as satisfying #210's 'demonstrate once' requirement, not silently presented as a validated retrieval strategy.
tucktuck101
left a comment
There was a problem hiding this comment.
Recommendation: approve — but hold the merge until #214 lands and this retargets to launchpad (per the PR description's own note; #214 currently has changes requested, so this stays queued behind it).
This cleanly fulfills #210's Definition of done, and I verified rather than trusted: I ran the suite against the base branch's graph.py/symbol.py — 27/27 pass — and CI's executed checks are green (Rust/e2e jobs correctly path-skipped).
What stands out positively:
- The DoD's two hard requirements are genuinely proven, not narrated. The positive worked example asserts the exact resolved symbol and its confirmation edges, with a test guaranteeing the concept sentence contains no substring of the target name — so it can't be an accidental keyword hit. The negative flow-tracing case is the stronger demonstration: it shows this pipeline structurally cannot express a 2-hop answer (no hop/path fields exist, asserted via
dataclasses.fields), then contrastsreachable()verifying the identical relationship exactly. confirm_via_graph()really calls #207'sedges_from()— structural confirmation, not similarity re-dressed — and returns empty tuples rather than something that masquerades as a confirmation.- symbol_id-keyed scopes with
qualified_name_for()translation is a real empirical catch (collidingqualified_names in live buzz-core), with its own regression test. - Cold-start (
find_it_for_meon an empty index) and zero-vector cosine are both handled and tested; theidentity_weight=2.0constant is explained rather than left magic.
Non-blocking observations (fine to defer; none needs to hold up the eventual merge):
- No minimum-score floor. An entirely unrelated query (I tried
"zebra xylophone") still yields a top candidate atcandidate_score=0.0, complete with real confirmation edges. The scores are exposed onPipelineResult, so callers can filter — but consider either a small threshold or a docstring warning infind_it_for_me()that a 0.0-score candidate is noise, since the confirmation attached to it can make it look legitimate. - Tie-break nondeterminism.
search()iteratesself._file_scopes(a set) before sorting; for tied scores the winner depends on set iteration order, which varies across processes under hash randomization. Sorting ties by scope name would make results reproducible. - Final ranking ignores
subsystem_score.search()sorts the flattened results bycandidate_scorealone, so a lexically strong symbol in a weakly matched file (within the top-k files) can outrank a candidate from the best-matched subsystem — and it can return up totop_k²rows. Documented behavior and the plan flags top_k semantics as unspecified, so just noting it as a known shape. - Inherited from #207: ProjectGraph merges symbols sharing a
qualified_nameonto one node, soconfirm_via_graph()on a colliding name conflates both symbols' edges. This PR correctly works around it on the index side; worth a tracking note on the graph side.
The bag-of-words embedding stand-in is correctly scoped against #210's explicit out-of-scope line and flagged in the plan's OPEN section for human confirmation — the right escalation rather than a silent product decision.
Summary
Implements all 10 steps of #210's plan:
SemanticIndexand the concept-retrieval pipeline(concept → subsystem → candidate symbols → confirmed references) from the design doc's Data
Model item 3 and Concept Retrieval reasoning rules -- including both a positive worked example
(a genuinely vague question resolving to a real symbol) and the negative flow-tracing boundary
case, both reproduced live against this repo's own real code.
Stacked on #213/#214 (this branch is
task/207-project-graph+ this PR's own commits) since#210 has a real, stated dependency on #206 for content to embed/summarize and calls into #207's
ProjectGraphfor confirmation. Base istask/207-project-graph, notlaunchpad-- review thisPR's own commits (
semantic_index.py,test_semantic_index.py, the plan doc); the earliercommits are #213/#214's, already under review there.
Related issue
Refs #210 (a stacked PR's base isn't the default branch, so GitHub won't create a real closing
link from "Closes" here -- retarget to
launchpadonce #213/#214 merge, then this can close #210for real)
Issue type
Task
Agent provenance
Objective
Build
SemanticIndexand the concept-retrieval pipeline, per the design doc's Data Model item 3and Concept Retrieval reasoning rules.
Impacted components
launchpad/project-intelligence/semantic_index.py
launchpad/project-intelligence/test_semantic_index.py
Approach and rejected alternatives
Chosen: "subsystem" is a real second
ConceptEntrylevel scoped to file (the schema's own statedscope kinds are
symbol_id | file | doc_section), with a genuine two-stagesearch()-- rankfiles first, then rank symbols within the top file(s). Rejected collapsing subsystem and symbol
into one flat ranking, since the issue's own pipeline shape names both stages explicitly.
Chosen: per-symbol
ConceptEntry.scopeissymbol_id, notqualified_name. Found empirically(not assumed) while live-verifying against all 453 real buzz-core symbols: multiple distinct
symbols share one short qualified_name (e.g. several
build_eventfunctions in differentmodules), so qualified_name is not a safe unique key -- symbol_id (a real per-symbol-unique
RepoQL URI from #206's
index_crate()) is, and the design doc names it as a valid scope kindtoo. Added
qualified_name_for()to translate back, since #207'sProjectGraphaddresses nodesby qualified_name, not symbol_id.
Chosen: embeddings are a bag-of-words frequency vector over an identifier-aware tokenizer
(camelCase/snake_case splitting), not a trained ML embedding model -- matches #210's own "out of
scope: any embedding-model selection process beyond what's needed to demonstrate the pipeline
once." A symbol's own identity (kind + qualified_name + signature) is weighted 2x over context
mentions (calls/tests/config/docs) in its embedding (
embed_symbol()) -- found empiricallynecessary while verifying the positive worked example: without it, a caller's summary absorbs
its callees' name tokens too (since "calls X" contributes X's own identifier tokens), so the
caller (
is_unshared_gated_event) initially outranked the callee it calls(
is_shared_gated_kind) itself for a query about the callee's own behavior.Chosen:
confirm_via_graph()returns empty tuples (never hidden) when nothing confirms acandidate, so a caller can see an unconfirmed guess for what it is rather than a confirmation
that looks the same as a confirmed one.
Flagged in the plan under OPEN (not decided silently): the bag-of-words embedding choice itself,
top_k defaults (3 at each stage), and whether summary generation should ever call an LLM rather
than a deterministic template (it doesn't, consistent with every other module this session).
Verification
Command run:
Raw output:
Command run (final CLI -- both worked examples, live against the real buzz-core index):
Raw output:
Not verified
Did not test the pipeline against any crate other than buzz-core, or against docs (the
doc_section scope kind is supported by the schema but nothing in this PR populates one -- no
doc-indexing pipeline exists yet). Did not tune the bag-of-words/identity-weighting approach
against a broad corpus of concept queries -- identity_weight=2.0 was checked empirically against
this PR's own two worked examples only, not derived analytically or validated at scale; a human
should treat retrieval quality as unproven beyond these two cases. Did not test behavior when the
RepoQL host is down (host was confirmed healthy via
host statusbefore every live verificationin this PR).
Security implications
None -- pure in-process data structures and string/math operations, no I/O beyond the existing
indexer.build_index()/graph.ProjectGraphcalls this task ingests from (already reviewed under#213/#214).
Escalations
Whether the bag-of-words/identity-weighting embedding approach is an acceptable long-term
retrieval strategy, or purely a placeholder to satisfy #210's own "demonstrate the pipeline once"
scope -- flagged in the plan's OPEN section and again here. This PR implements exactly what
#210's Definition of done requires and no more; deciding whether real embeddings are needed later
is a call for a human, not something to silently upgrade or silently leave unstated.