Add the bulk-index provider foundation#126
Draft
samchon wants to merge 42 commits into
Draft
Conversation
Replaces the `language === "typescript"` arm inside buildLspGraph's language loop with a data-driven provider registry. A provider now states its own terms — which languages it owns, what its facts are grounded in, which edge families it can prove, why it cannot serve a build, where its executable is, and what the project needs prepared — and the indexer only asks and reports. - IGraphProvider / GRAPH_PROVIDERS / selectGraphProviders, with one-owner-per- language validated statically over the whole registry rather than over the providers that happened to resolve on this machine. - IBulkGraphSession owns a language set, so one provider can replace C and C++ atomically instead of publishing two slices that were never simultaneous. - Snapshot provenance carries provider, authority, and provable fact families; assertGraphSnapshotContract holds every published slice to what the registry declared instead of trusting the payload. - ISamchonGraphDump.provenance publishes one row per contributing provider, with manifest and content digests recomputed by the publisher. Computation mode stays out of the dump and rides on IIndexerResult, so a dump remains a pure function of its source. - Resident refresh deduplicates multi-language sessions by identity, adds a commit fence that rejects a candidate whose consumed source moved while it was preparing, and retries a bounded three times rather than publishing two checkouts' halves. - Strict SCIP ingestion: wire contract, strict parser, symbol parser, and an adapter that maps only what SCIP proves and refuses to infer calls, constructions, imports, or decorators from source punctuation. Not yet verified: per the user's instruction this branch is validated by CI rather than locally. Refs #66 #67 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v0.2.0 release built and published in two steps and checked nothing in between, so the first thing that could notice a bad release was an install in somebody else's project. npm still serves graph 0.1.0, graph-sitter never reached the registry at all, and GitHub has no release. - Both manifests declare publishConfig.access "public". A scoped package published without it is rejected as unauthorized, and saying it in the manifest means every publisher gets the same answer. - release-verify proves the tag is an immutable vX.Y.Z, that every publishable manifest carries that version, and that the tagged commit was merged into the release branch. Recovery from a failed release is a new version, never a moved tag. - The verify job runs frozen install, build, test, and coverage before anything is published, then packs both tarballs and proves each contains what its `files` advertises. - graph-sitter publishes first and the registry is polled until it actually serves it; graph follows. npm accepts a dependent whose dependency does not exist, which is precisely how the reported break happened. - A clean-install smoke test installs from the registry into an empty directory and exercises both the import and the CLI entry point — the only step that proves the published artifacts resolve each other, since every earlier step ran where workspace:* always resolves. - The GitHub release is created only after both packages install. Registry authority stays manual: publication runs in the protected `npm` environment against NPM_TOKEN, and no credential is stored in the repository. Refs #102 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The experiment catalog this replaces proved that a server started and returned some nodes: eight languages were allowed zero edges and the rest could pass on one, so a provider that linked every pair of names it saw scored better than one that resolved carefully and found few. Conformance is now stated as golden facts, each with a negative twin one property away — a name that appears only in a comment, a symbol that merely shares a spelling, a function passed as a value rather than invoked. The twin is the point: a provider cannot be shown to resolve calls by producing calls. The kit is proved against a deliberately text-heuristic provider that satisfies every positive case and fails exactly the twins. Structural invariants are checked separately because they are not about any language: dangling endpoints, duplicate ids, zero-based spans, manifest entries without a checker digest, and edge families the provider never claimed. Slice determinism is compared through the published digests, which are what the dump and the resident commit fence actually compare. Refs #68 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI reported three errors on one line: a promise executor whose arrow body returned an assignment, a first parameter not named `resolve`, and `void` as an expression-position type argument. Rewriting rather than patching the line, because the shape that caused it was also wrong in two ways CI had not reached: the exit promise was built inside the surrounding executor, shadowing its `resolve`, and the child was only registered as owned after that executor ran. A session that owned children only once they had answered would leak precisely the ones that hang, which is the case `close` exists for. The child is now registered before the first await, and the deferred is its own named helper. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion Coverage for the new provider surface, written against the behaviour each piece exists to guarantee rather than against its lines: - Provider selection: partial language ownership, a multi-language candidate, and one attributable sentence for every decline (refusal, absent tool, unprepared project). Registry overlap is proved to fail even when only one of the two providers could resolve on this machine, since the defect is static. - Snapshot contract: each way a slice can exceed what its entry declared — unowned language, foreign node, unclaimed edge family, misattributed provenance, overstated authority, and fact sets that are over, under, or the same size but different. - Digests: manifest order does not change the manifest digest, because two providers listing one program differently describe one program; publication order does change the content digest, because the dump's contract is byte-identity on an unchanged checkout. - SCIP: every descriptor form including backtick escaping and doubled backticks, the local-symbol generation scope, each unreadable symbol shape, and every validation refusal. The mapping test asserts what is published and then asserts that no `calls` edge exists — the index never proved one. Refs #66 #68 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ntion CI's export-convention gate rejected four new files that exported several symbols each. Rather than register them as exceptions, each now exports one symbol named for its file and carries its companions in a merged namespace, which is the shape IBulkGraphSession and ISamchonGraphDump already use: - selectGraphProviders.ICandidate / .IResult - adaptScipIndex.EDGE_KINDS / .IProps / .IResult - scipSymbol.IParsed / .Descriptor / .nodeKind - adaptTtscGraphDump.PROVIDER / .AUTHORITY / .EDGE_KINDS Also repairs two real defects the same run exposed: - The accumulated-session test still injected the removed resolveTtscGraphCommand and collectTtscGraph hooks in its generic-fallback case, so that case was asserting against a provider path that no longer existed. - The doubled-backtick fixture was wrong, not the parser. SCIP escapes a literal backtick inside a quoted name as two backticks, so the name `tick``tock` carries one; the fixture had opened the quote twice and asserted the parser should recover a name from it. Refs #66 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review over the pushed diff found the dependency injection point wrong, and CI's four ttscgraph failures were the symptom rather than the defect. Injecting selectGraphProviders let a caller replace the whole selection, so refusal, command resolution, project preparation, and the one-owner-per- language check ran on no path any test exercised — the integration would have been proved by a stub standing exactly where the logic under test belongs. The seam is now the registry: selectGraphProviders always runs, and a test substitutes one entry's resolve() so the shipped provider's refusal, authority, fact families, and session construction are all still exercised. Two warning sentences changed shape as a consequence, and their tests move with them rather than the reverse: a fallback now names the provider *and* the authority the build gave up, which is what #66 asks warnings to say and what a reader needs to know whether the facts they hold came from a compiler or a text scan. The library-resolver test now asserts that second clause explicitly. Refs #66 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two more defects from the same Self-Review round, both about a claim that was checked once and then trusted forever. A resident source held no reference to the registry entry behind its sessions, so assertGraphSnapshotContract ran on the initial build alone. A provider whose second generation published an unclaimed edge family, or nodes in a language its slice does not own, was merged straight into the dump — and the second generation onward is where a long-lived session spends its entire working life. IIndexerResult now carries the provider behind each kept session, and every refresh is held to the same contract as the first. SCIP external symbols were attributed to the session's first language. That is correct for a single-language indexer and a coin toss for a provider owning C and C++ together: every dependency leaf in the program would take whichever language the registry happened to list first. The language a reference appears in is a fact the index does state, so an external leaf is now created when its first occurrence names it and inherits that document's language. Refs #66 #67 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eference Two rules deferred their decision to #66; both now describe what #66 actually did rather than an omission. The reference's single-Program provenance sits exactly where this product's list does, so the rule replaces it in place instead of deleting it: one TypeScript Program resolves the reference's whole dump, while this one can be owned in part by a compiler, a Clang compilation universe, and a semantic index at once. It is optional because the generic and static lanes make no such claim. Its helper types are replaced rather than dropped. The reference's universe, root, and digest rows describe one Program's manifest; every language answers 'what decides the file set' with a different shape and a reader only ever asks whether it moved, so the product publishes a fingerprint per provider. The producer row keeps tool and version, renames typescript to compiler, and gains the schema and protocol generations a multi-provider reader needs to tell two producers apart. Also completes the registry-seam migration in the two tests that still injected the removed selectGraphProviders hook, so both now exercise real discovery. Refs #66 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review round 2. The new session repeated exactly the defect #107 fixed in the ttscgraph client, in a file written after it. close() emptied the child set and awaited those children, without caching its result. A second caller found the set already empty and returned *before* the children had exited — so two callers disagreed about whether the session was closed, and the one who was wrong is the one who asked second. IBulkGraphSession requires close to be idempotent; it now caches the shutdown promise, as the ttscgraph client does. It also awaited termination unconditionally. An indexer that ignores its first signal — mid-write, blocked on a lock, wedged in a shutdown hook — would hold close open forever, and a resident server's shutdown with it. Termination now escalates to SIGKILL after a short grace period, since nothing here needs a clean exit: the artifact directory is discarded either way. Refs #69 #107 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review round 2. contentOf hashed a node's id, kind, language, name, file, and span offsets. A slice whose modifiers, export flag, qualified name, decorators, literals, or enum members changed therefore digested identically to the one before it — and both the resident commit fence and the dump's published provenance compare exactly this value to decide whether anything moved. A digest over part of the facts is not a weaker proof of the whole; it is a proof of something else. It is now a canonical serialization of each row. Key order is sorted rather than preserved, because two structurally identical nodes built by different code paths describe one declaration and must agree; absent optional properties and ones explicitly set to undefined collapse together, which is what the graph means by them. Also fixes the release preflight's reachability check, which asked whether the tagged commit was merged into origin/master against a shallow checkout that has no such ref — a gate that would have refused every release for the same reason it refuses an unmerged one. Refs #66 #67 #102 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The structure gate passes; only the prose layer remained, which compares the JSDoc a caller reads rather than the shape alone. The dump field's own comment is condensed to one paragraph. Its longer form restated what IBulkGraphSession.IProvenance already documents in full, and a reviewed prose rule has to carry the whole comment verbatim — so the duplicate prose was both redundant to a reader and a second place to keep in step. Refs #66 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review round 2. positionEncoding was declared on the document type and then neither parsed nor consulted. A SCIP range is an offset in code units, and which code unit is the document's own declaration. Graph columns follow the LSP convention of UTF-16 code units, so an indexer counting UTF-8 bytes disagrees on every line holding a non-ASCII character — silently, and only there, which is the shape of bug that survives a test suite written in English identifiers. Spans are display evidence rather than identity, so this reports instead of rejecting. What it must not do is say nothing. An absent or unspecified encoding stays quiet: 'did not say' is not 'said something wrong', and warning on it would mark every well-behaved ASCII project. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI's deterministic suite now passes in full; this closes the coverage gaps it exposed, and they were concentrated in exactly the code Self-Review had just repaired — so the fixes now have the regressions that lock them. The session is exercised against a real child process rather than a stub, because the paths worth proving are the ones a stub would replace: an indexer that exits non-zero, one that exits cleanly having written nothing, a decoder that fails or answers with garbage, an index built for another checkout, and a child that ignores its first termination signal. Each leaves the published generation exactly where it was. Unchanged is proved by the indexer not running a second time, not by a counter that did not move. The adapter's refusals get their negative cases: a relationship carrying two flags is two claims rather than a tagged union, a relationship or occurrence naming an undeclared symbol emits nothing, a self-relationship is not an edge, an unmodelled kind publishes no node, an unreadable symbol is reported rather than guessed, and a dependency leaf nobody references is never materialized. Refs #68 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the resident transaction's own paths — a candidate discarded because its source moved under it, a project that will not hold still reported rather than papered over, and a later generation refused for publishing outside its provider's contract. These are the three fixes Self-Review made without regressions to lock them. The rest are small refusals: a SCIP descriptor suffix that closes something it never opened, optional records the graph does not read but still validates, a package declaring no dependencies at all. One defensive branch is removed rather than covered. canonical() fell back to the string "undefined" for a value JSON.stringify cannot render, which was unreachable for every value that reaches it — the object walk already filters undefined. An undefined inside an array is the one place it survives, and it now becomes null there for the reason JSON does the same: an array's length is part of its meaning, so the hole keeps its place. Refs #66 #67 #69 #102 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review round 3. The scripted session reported `initial` whenever its generation was 1, so a session whose snapshot list had run out republished its last snapshot with changed=false and mode=initial — a combination no real provider may produce, handed to every test that used the fixture. The mode now follows the refresh: initial, rebuild, or unchanged. Also ignores the release tarball directory. The published artifact is the one the registry serves; these are the copies the preflight opened to check it, and a local run should not leave them staged. Refs #68 #102 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#69 had built the ingestion, the validation, and the lifecycle, but nothing connected them to the registry — the parts existed and no provider could be made from them. scipProvider is that connection, and it is a description rather than a class because the fourteen language entries that will use it differ only in which executable to run, what to pass it, and which files decide its output. Everything else is identical by construction, so they cannot drift apart in the parts that are supposed to be the same. Every entry inherits the bare index's provable facts. That is the point rather than a limitation: a SCIP index cannot prove a call, a construction, an import, or a decorator, and an entry claiming one would be rejected by its own snapshot contract. A language that can prove more does it through typed enrichment, not by widening the list. A bounded build is refused for the same reason the compiler-owned lane refuses one — honouring a cap would mean indexing everything and then deleting facts. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unmodelled-kind case used SCIP's TypeParameter, which this graph does map — to `type` — so the case asserted the opposite of what it described and CI caught the extra node. It now names a kind outside the mapping entirely, so the descriptor fallback is what has to decline it. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI could not satisfy the fence test, and the test was not the defect. The fence guards the moment between a lane reading its source and the refresh publishing what it read. In production that moment belongs to whoever else is editing the checkout; in a test it belonged to nobody, because the last read and the fence are adjacent statements with no seam between them. A safety net that cannot be made to catch anything is not one. The static parse that closes the preparation phase is now a dependency, so a test can move the project inside it — which is the only place a candidate can be overtaken. Also corrects the unmodelled-kind case, which named SCIP's TypeParameter. This graph does map that one, to `type`, so the case asserted the opposite of what it described. Refs #67 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fence cases loaded twice against an unmoved checkout, so the second load found nothing stale and returned without ever reaching the refresh — let alone the static parse the fence fires from. The first build publishes its generation without touching the static lane, which is exactly why it cannot fire there. Refs #67 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A public schema change needs its README contract excerpt to move with it. The document still described a single inline TypeScript special case, which is what #66 replaced: indexing now asks a registry which languages each strict provider owns, holds a snapshot to the edge families that provider claims, and names the authority a fallback gave up rather than letting one read as the strict result. The new dump field is documented with the thing readers most need from it — that computation mode is deliberately absent, because it belongs to a refresh rather than to the facts, and recording it would make two dumps of the same unedited checkout differ. Refs #66 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review round 4. The comment said declarations and relationships were both matched by name and kind; only declarations were. Relationships match by name alone, and that is the right trade — a provider's ids are its own — but it costs something the harness was not collecting: a fixture reusing one display name makes every edge expectation naming it ambiguous, and an ambiguous assertion passes for the wrong reason. The fixture is now held to unique names instead. The structural pass also checked only declaration spans. An implementation span is a second real location encoded the same way, so a provider could emit a zero-based one wherever a declaration and its body are apart — every overridden method, every header-and-source pair. Refs #68 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The busy-project case wrote `value = ${revision}`, whose first revision is
byte-identical to the original source. The next attempt then found the checkout
unchanged and returned quietly — correctly, because a project edited back to a
state it already had *is* unchanged — so the retry bound was never reached and
the case failed for a reason that had nothing to do with the fence.
The discard case had the opposite problem: both its assertions passed just as
well when the fence never fired, which is how it went green while proving
nothing. It now counts preparations and requires the second one.
Refs #67
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The build that opens a session does not poll it, so the honest snapshot is what the second load commits and the drifting one arrives on the third. The case asked for the refusal a generation too early and got the honest slice instead. It now asserts the honest generation publishes first, which is what makes the refusal afterwards mean what it says: a session checked only when it was opened would never have been asked again. Refs #66 #67 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A candidate may legitimately publish fewer languages than it owns — a Clang provider asked for C and C++ can answer with only the translation units it found. What it must not do is leave the rest to the generic lane in silence: a caller who selected a compiler-owned provider for C would then be handed navigation facts for it with nothing to tell them apart, which is the same failure the inline TypeScript branch used to produce and the reason #66 exists. Every unpublished language now gets one sentence naming the provider and the authority it did not supply. Refs #66 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both failures were the same fixture error: a snapshot carrying the default provider name while the registry entry was named something else. The contract check rejected it — correctly — before either case reached the behaviour it meant to exercise, so one reported a provider failure instead of an unpublished language and the other published nothing instead of its first generation. Refs #66 #67 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coverage reached 99.91%; these are the paths that were left.
Reachable ones get cases: a SCIP bracketed descriptor with nothing inside it,
a descriptor that is only a suffix, a second enclosing scope so reference
attribution actually has to rank them, a scope starting on its owner's line so
containment compares columns rather than stopping at the line number, an
indexer that cannot be spawned, an aborted refresh, a refresh queued across a
close, a two-language session merged once rather than once per key, and an
explicit JSON null in the digest walk.
Two are not reachable. Merging a namespace onto a function compiles to an
"X || (X = {})" initialiser whose falsy arm cannot run, because the function
declaration above it is always evaluated first; and the SIGKILL escalation is
dead on Windows, which terminates on the first signal. Both are ignored with
that reason rather than covered by a test that would only be pretending.
Refs #66 #67 #69
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
samchon
force-pushed
the
feat/bulk-index-provider-foundation
branch
from
July 21, 2026 03:17
94e4e20 to
93f7bb8
Compare
…equire release-verify computed a publication order and printed it; nothing read it. The workflow publishes in fixed, named jobs — which two packages do not justify generating — so that job order is a claim about the dependency graph, and a claim nobody checks is one that silently stops being true. A third package, or a dependency reversed between the two, would publish a dependent before its dependency exactly as v0.2.0 did. The workflow now declares the order it wires, and the preflight refuses the release when the manifests disagree with it. Refs #102 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-Review round 8. The session hashed each document off disk *after* the indexer had answered and published that as `checkerDigest` — the field documented as "the bytes the checker resolved against, the ground truth for the facts". That is precisely the move IBulkGraphSession.ISnapshot.sources exists to forbid, and precisely what #103 removed from the source reader: the bytes a later read returns are not the bytes the indexer saw, a write that lands and reverts in between is invisible to both reads, and the result would let a reader prove byte-identity against text the facts were never computed from. An absent proof is a fact about this indexer; a fabricated one is a lie about the program. A SCIP index can only state this when it carries Document.text, which the parser was also dropping. When it does, the digest is real and the snapshot claims the sourceDigests capability that licenses a reader to use it. When it does not, checkerDigest stays empty, the capability is not claimed, the disk digest is still reported for what it is worth, and the snapshot says why. Refs #69 #103 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The namespace guards were ignored at the wrong line. TypeScript emits the
`X || (X = {})` initialiser at the namespace's *closing* brace, so a directive
placed before the opening one covered nothing. All three merged namespaces are
now bracketed; the constants inside run unconditionally, so the directive hides
nothing testable.
Two real branches remain and get cases: an `undefined` inside an array, which
is the one place the object filter cannot reach it and where dropping the hole
would make two different lists agree; and a reference sitting on the line its
enclosing scope opens on, so containment has to compare columns instead of
stopping once the line numbers match.
Refs #66 #69
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three branches left, all of them things a real indexer does. toolInfo is optional in the SCIP schema, so a snapshot has to name the provider that ran it when the index names no tool. A project root can be a plain absolute path as well as a file:// URI. And a failure that is not an Error still has to reach the caller as one, because nobody can read .message off a string. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moving the dependency-leaf reference onto its scope's opening line did make containment compare columns, but it also put the reference inside the narrower sibling scope that starts on the same line — so attribution landed on that declaration instead of the method, which is correct behaviour and the wrong assertion. It now sits past the column where the narrow scope ends: the comparison still has to look at columns, and the innermost scope that really contains it is the method. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 10. The README promises every decline names the provider *and* the authority the build gave up, and the resolve and prepare paths do. The SCIP refusal named only the provider, so the one thing a reader actually loses — which grade of fact they are now holding — was missing from the sentence that exists to tell them. Also drops a `predicate(..., true)` that read like a check while testing nothing; the call above it proves admission by not throwing. Refs #66 #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The queue reached ten runs and roughly eighty jobs, and the head that could actually gate a merge waited nearly two hours behind results for code it had already replaced. The experiment workflow fans out sixteen language jobs, so a branch under active review holds most of the available runners in superseded work. A green check belonging to an older SHA proves nothing about the current one — the campaign procedure says so itself — so those runs were occupying capacity to produce evidence nobody may merge on. Both workflows now keep one live run per branch. Two exclusions, because cancellation is not always right: a push to master is the verdict on what actually landed and each commit needs its own, and a manual experiment dispatch is a run somebody specifically asked for. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
samchon
force-pushed
the
feat/bulk-index-provider-foundation
branch
from
July 21, 2026 05:48
7887ce8 to
69b85e9
Compare
…e once Round 11, both defects that a real indexer would have exposed immediately. The parser dropped Occurrence.diagnostics, so the adapter only ever saw document-level findings and pinned every one to 1:1. A problem twelve lines down reported at the top of the file is worse than saying nothing, because it looks like an answer. A document-level finding still reports at the file's first position — the file is what it is about — but an occurrence-level one now keeps the range it was given. Edges were published once per occurrence. A name referenced twice in one scope is two occurrences of one relationship, and a symbol listing the same relationship under two flags is one claim per flag: legitimate in an index, duplicates in a graph. mergeGraphSlices rejects a strict slice carrying a duplicate, so the first real scip-go snapshot would have been refused whole. The first occurrence keeps its evidence, because the earliest is where a reader is sent. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 12. The import role was flattened into a bare reference, which threw away one of the few facts every SCIP indexer records the same way — and the only one that says a module boundary was crossed at this occurrence. It is checked before the access roles because a mask carries more than one bit: an imported symbol that is also read there is an import, and calling it an access loses the boundary while keeping something a reader could have inferred anyway. The fidelity matrix in #69 lists imports as needing language mapping, and that remains true of module *topology* — how the import resolved is not in the role. What the role proves is that the crossing happened here, and the graph's own `imports` edge means exactly that. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 13. A `local N` symbol is an index-local counter scoped to its document, and the identity derived from it says so — but the definition map was keyed by the symbol string alone. Two files each holding a `local 4` therefore collided: the second overwrote the first, and every reference in the earlier file then resolved to a declaration in the later one, silently and with full confidence. Definitions are now filed under a document-scoped key when the symbol is generation-stable, and lookups from inside a document use the same key, so a local reference can only ever reach the local in its own file. Global symbols are unchanged — they mean the same declaration wherever named, which is what makes them global. A collision check on the derived id remains as a backstop: two documents can still digest one identity, and publishing it twice would have mergeGraphSlices reject the whole slice. Refs #69 #95 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 14. The fence's own comment says a bulk provider's manifest digest is its revision token — and nothing read it. Bulk sessions are polled at the start of a refresh and the generic lanes read afterwards, so a provider that rebuilt in between left the transaction holding one program's facts beside another's. That is the exact mixture the fence exists to refuse, arriving through the one lane the fence was not watching. The transaction now remembers which snapshot it took from each session and refuses to commit if the session has moved on. Naming a token in a comment is not the same as checking it. Refs #67 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llision Round 15. The strict arrays reaching mergeGraphSlices are now every provider's slices concatenated, not one provider's output, and that made a duplicate node ambiguous. One provider publishing an id twice is that provider's defect; two providers publishing the same id means the registry let both own one declaration — a collision no merge can resolve, because neither slice is wrong alone and picking either silently discards facts the other proved. Both are still refused. The message now distinguishes them, which is what makes the second diagnosable: a reader told only that a node was duplicated looks for the bug inside one provider and never finds it. Endpoint closure stays a check over the strict facts as a whole, and the reason is now written down. A provider owning C and C++ resolves calls that cross between them, and publishing those is what a shared compilation universe is for; requiring each slice to close over itself would reject exactly those. Refs #66 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Branch coverage stopped at 87% in the adapter, and the gaps were all optional records that every fixture happened to carry. Two real shapes fill them: a document the indexer read and found nothing in — an empty source, or one holding only comments, which belongs in the manifest because it was read — and a *definition* occurrence whose symbol string cannot be parsed, where there is no declaration to attach the span to and inventing one would put a node in the graph named after a string nobody could read. Refs #69 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI rejected the import: mergeGraphSlices is not on the package surface, and the bundled test could not name it. It is the indexer's own merge step rather than something a consumer calls, so it is reached by path like every other internal — the same convention isBulkGraphSession already follows, and for the same reason. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #66
Closes #67
Closes #68
Closes #69
Closes #102
The bulk-index provider foundation: the contract every language provider in
#71–#84 is built on, plus the release boundary #102 owns.
What changed
#66 — the registry.
buildLspGraphaskedlanguage === "typescript",resolved
ttscgraph, and constructed a client in the middle of its languageloop. Adding a provider that way adds a branch, nothing can enumerate the set,
and a caller whose options disqualified the compiler-owned lane fell through
without a word — a fallback success indistinguishable from the strict result it
replaced. A provider now states its own terms: which languages it owns, what
its facts are grounded in, which edge families it can prove, why it cannot
serve a build, where its executable is, and what the project needs prepared.
IBulkGraphSessionowns a language set, so one provider can replace C andC++ atomically rather than publishing two slices that were never simultaneous.
assertGraphSnapshotContractholds every published slice to what its entrydeclared.
ISamchonGraphDump.provenancepublishes one row per contributingprovider — authority, provable families, producer, build-universe fingerprint,
and digests over the manifest and the facts.
#67 — one input generation. A mixed refresh prepared each slice at its own
moment: providers answered first, generic sessions re-read next, the static
lane parsed last. The final object swap was atomic to a reader; the inputs were
not. Consumed source is now re-verified after preparation, a moved candidate is
discarded whole, and the refresh retries a bounded three times rather than
publishing halves of two checkouts. Bulk slices are deliberately not re-read —
their manifest digest is their revision token.
#68 — the conformance kit. The gates it replaces were node and edge counts,
and eight languages were allowed zero edges, so a provider linking every pair
of names it saw scored better than one that resolved carefully. Conformance is
now golden facts each paired with a negative twin — a name in a comment, a
same-spelled symbol, a function passed as a value — proved against a
deliberately text-heuristic provider that satisfies every positive case and
fails exactly the twins.
#69 — strict SCIP ingestion. Wire contract, strict parser, symbol parser,
and an adapter that maps only what SCIP proves. It refuses to infer calls,
constructions, imports, or decorators from punctuation; those wait for typed
per-language enrichment.
scipProviderturns an indexer into a registry entrythrough one description, so cycle 2's eleven SCIP languages cannot drift apart
in the parts meant to be identical.
#102 — verified publication.
v0.2.0built and published in two steps andchecked nothing in between. Both manifests now declare
publishConfig.access;publication is gated on tag/version/commit agreement, frozen install, build,
test, coverage, tarball inspection, graph-sitter first with registry-
availability polling, then graph, then a clean-install smoke test, and only
then the GitHub release.
Verification
Per the user's standing instruction this branch is validated by CI rather than
locally; every command below ran on GitHub Actions.
pnpm build,pnpm test— ubuntu, macOS, Windowspnpm coverage— 100% lines, functions, branchesexperiment— 16 real language serversTwo branches are ignored rather than covered, with the reason at the
directive: TypeScript emits an
X || (X = {})initialiser at a mergednamespace's closing brace whose falsy arm cannot run, and the SIGKILL
escalation is dead code on Windows, which terminates on the first signal.
Specification changes
Called out because the development skill treats these as specification:
the build gave up, which is what [Bulk index] Add provider authority, provenance, and build-universe registry #66 asks warnings to say. Their tests moved
with them, and the library-resolver test gained an assertion on the new
clause rather than losing one.
ISamchonGraphDumpgained an optionalprovenance. Both contract-parityrules that deferred to [Bulk index] Add provider authority, provenance, and build-universe registry #66 now describe what it did; the reference's
single-Program claim is replaced in place rather than deleted.
Self-Review
Fifteen defects were found and fixed by review rather than by CI. The one worth
naming:
ScipSessionhashed each document off disk after the indexeranswered and published that as
checkerDigest— the field documented as thebytes the checker resolved against. That is what
IBulkGraphSession.ISnapshot.sourcesforbids in its own comment and what #103removed from the source reader. The digest was a formally valid hash, so no
test could look wrong while a reader was being handed proof of byte-identity
with text the facts were never computed from. Every disk read under
provider/was audited afterwards; the defect had one site.