Skip to content

semantic search: strut keeps the graph's vectors (the pass, the index reconcile, typed search through the domain indexes, the no-embed mark) - #140

Merged
Evanfeenstra merged 1 commit into
mainfrom
feature/semantic-search
Oct 11, 2026
Merged

Evanfeenstra merged 1 commit into
mainfrom
feature/semantic-search

Conversation

@Evanfeenstra

@Evanfeenstra Evanfeenstra commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Builds plans/semantic-search.md (#139), the strut side. On swarm38 a question with no keyword in it finds nothing by meaning: code nodes come from stakgraph's ingest, which never embeds, and what jarvis did embed is the bare name under a Schema index that predates its own library. This makes strut the thing that keeps the graph's vectors current, from the live :Schema meta-graph, on any Neo4j 5.

The pass — src/graph/embeddings.ts, EmbeddingPass. A timer in the serving process, started when the graph backend opens. A node of a marked type is stale when its stored Data_Bank is not the text the writer would compose today from the Schema's index fields, or its vector is missing — one comparison that catches a node never embedded, one embedded before its index widened, a field filled in or edited later, and a text stakgraph's re-sync reset. It writes what the writer writes (Data_Bank, text_embeddings, _search_fields_used, the resolver's domain label), pages by node_key (an index seek per page), strips the no-embed types last, embeds newcomers by date_added_to_graph between hourly sweeps, works 15 s a minute, and nothing waits on it. GET /graph/embeddings reports the sweep, per-type counts, and the types it skips and why. The old awaited Domain_strut backfill is gone.

The Schema index reconcile — src/graph/index-reconcile.ts, a boot step. A live index that is a non-empty strict subset of the bundled library's (Function: "name" → [name, summary, description]) is widened to the library's list — jarvis's own values, written directly, since its Schema API re-embeds a whole type inside one request and rebuilds the shared keyword index per type. Once per fixture version (a Migration ledger row), so a list jarvis narrows on purpose is never fought over. The fixture gained a version field.

Search — routeIndexes / overfetch in search.ts. A typed search reads the requested types' domain vector/fulltext indexes with an over-fetch (k from cap×10 by ×4 to 2,000 — the database cannot filter a vector query, so type is a WHERE after the nearest-k cut); an untyped one reads the global text_embeddings_vector_index, which the reader never used before (domain_all_vector_index never exists on 5.19).

The no-embed mark — embed: false on a Schema (ruling 2: Turn, StrutToolCall, StrutTurn get no vector). embed joins the core-key mirror, the resolver reads it, the writer composes Data_Bank but no vector for it and update drops a stale one, the pass strips what such nodes hold. Strut's two types carry it in code only — never flattened onto the Schema node, since an older jarvis reads an unknown Schema key as a required attribute. MiniLM runs on two intra-op threads.

Where the build differs from the plan's text (in the plan's status header): the reconcile only widens (no string→list normalization); the pass addresses a node by elementId; the newcomer tick is one range seek across all marked types; a failed page moves the sweep to the next type; a tick logs only when it did something; the per-field vector_index vectors are filled by the sweep alone; the status read has no web client.

Verified: tsc, npm test (1417), npm run test:graph (268, the full live suite). New tests: the pass end to end (another writer's node; a later description, an edit and a stakgraph-shaped reset re-embed it; a clean sweep writes nothing; unmarked types left alone and named; the cursor over 2,500 nodes with a tick resuming at it; newcomers without a sweep; no-embed on write, update and sweep; labels and hidden types; the route), the reconcile (widen / leave / stamp / no re-run under the same version), routing and over-fetch pure and live (a code type found through its domain index among 1,300 nearer decoys; a type with no domain through the global index past them), the writer's gate, the resolver's mark.

Companions, in their own repos, not stacked — strut waits on neither: stakwork/jarvis-backend#3141 (the mark as a core key, four enqueue sites gated, Turn marked) and stakwork/stakgraph#1762 (Data_Bank set once on create). Until each is deployed the pass heals what they do. After the jarvis one lands on a seed, the ontology fixture is re-dumped and one test stops setting Turn's mark by hand. Then the swarm38 validation in the plan, after the mcp pin.

…ch.md)

The pass (src/graph/embeddings.ts, EmbeddingPass): a timer in the serving
process that keeps Data_Bank + text_embeddings + the domain label current on
every node of a Schema-marked type, whoever wrote it — stale = the stored text
is not the writer's composition from the Schema's index fields, or no vector.
Pages by node_key (an index seek per page), decides in strut, writes what the
writer writes, strips the no-embed types last, embeds newcomers by
date_added_to_graph between hourly sweeps, 15 s a minute, nothing waits on it.
GET /graph/embeddings is its status. The old awaited Domain_strut backfill is
gone.

The Schema index reconcile (src/graph/index-reconcile.ts): a live index that
is a non-empty strict subset of the bundled library's is widened to the
library's list, once per fixture version (a Migration ledger row); the pass
then sees every node of the type as stale.

Search (src/graph/search.ts): a typed search routes through the types' domain
vector/fulltext indexes with an over-fetch (k from cap×10 by ×4 to 2000 — the
database cannot filter a vector query); an untyped one through the global
text_embeddings_vector_index, which the reader never used before.

The no-embed mark: `embed` joins the core Schema keys, the resolver reads it,
the writer composes Data_Bank but no vector for it (and update drops a stale
one), StrutToolCall and StrutTurn carry it in code only (never flattened onto
the Schema node — an older jarvis would read a required attribute). MiniLM
runs on two intra-op threads.

Tests: the pass (another writer's node, a later description, an edit, a
stakgraph reset, a clean sweep; skipped types named; the cursor over 2,500
nodes; newcomers; no-embed; labels; the route), the reconcile, the routing and
over-fetch (pure + live among 1,300 nearer decoys), the writer's gate, the
resolver's mark. npm test 1417, test:graph 268.
@Evanfeenstra
Evanfeenstra merged commit a50eb64 into main Oct 11, 2026
1 check passed
Evanfeenstra added a commit to stakwork/stakgraph that referenced this pull request Oct 11, 2026
… typed search through the domain indexes, the no-embed mark — stakwork/strut#140)
Evanfeenstra added a commit to stakwork/stakgraph that referenced this pull request Oct 11, 2026
…1762)

* neo4j: Data_Bank is set once on create, never reset by a re-ingest

The node MERGE put `Data_Bank = name` into $properties and ran
`ON MATCH SET node += $properties`, so an incremental sync of a file reset
every one of its nodes' search text to the bare name while text_embeddings
and _search_fields_used survived — a vector that no longer matched its text,
and jarvis's data_bank_full_index back to name-only. Data_Bank now stays out
of $properties and is seeded from the map's name under ON CREATE alone, like
date_added_to_graph (the same set-once rule, for the same reason): a later
writer — strut's embedding pass, jarvis — composes the richer text from the
type's Schema index fields, and a re-ingest of an unchanged function touches
nothing it reads. build_stream() was already ON CREATE only.

* bump strut (semantic search: the embedding pass, the index reconcile, typed search through the domain indexes, the no-embed mark — stakwork/strut#140)
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