epic: runtime plugin architecture — dynamic grammar, extraction and resolution packages #75
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Depends on
Reference
h-dv/code-index#75
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Decision
code-index will become a runtime-extensible indexing platform. A format is supported by installing a plugin package, not by linking another Rust crate and publishing another code-index release.
This deliberately reverses the v1 non-goal “Dynamic plugin loading from disk” in ARCHITECTURE.md and _prdoc/vision/00-mission.md. The implementation must update those decisions and record the new trust model before the first plugin is enabled.
Definition of pluggable
This epic is complete only when an already-released code-index binary can:
A runtime rule file over a grammar compiled into the binary does not meet this definition. Grammar loading is foundational, not a deferred enhancement.
Governing architecture
The core resolver remains the only component allowed to declare an edge. A plugin may emit a reference site and evidence, never a target symbol id. That rule is necessary but not sufficient: candidate-pool membership is itself authority and is therefore capability-gated by #77.
Package model
A package is content-addressed and immutable. Its identity covers every byte and every interpretation input:
An edited package is a new digest even when its author forgot to bump the version. Projects activate an exact digest. Mutable “latest” identifiers may be resolved by an install command but are never stored as the active identity.
Project checkouts are untrusted. Merely cloning a repository must never execute a grammar or extractor. Packages are installed into a user-controlled registry and explicitly approved for a project. Project configuration may request a package digest but cannot silently grant executable or resolver capabilities.
Two extractor tiers
A real ecosystem needs both:
The second tier is required because the existing plugins demonstrate that node-kind tables alone are not expressive enough. Both tiers emit the same validated fact protocol and receive the same budgets. Executable extractors do not gain database, filesystem, network or resolver access.
Compiled plugins may remain as a trusted fast path during migration, but the external package path must be able to express and pass the corpus of at least one complete existing language plugin. “Markup only” is not acceptance for this epic.
Language and bridge model
Every package-defined language has its own stable namespaced language id. It must not borrow csharp, rust or another host language merely to enter existing pools.
Resolution-relevant behavior moves out of hardcoded language allowlists into a validated language profile, including:
Cross-language behavior is explicit. XAML to C#, Razor to C#, Vue to TypeScript and similar relationships are bridge capabilities with direction, source ref classes, destination symbol classes and scope constraints. A bridge is never inferred from two languages sharing a string.
Default capability is searchable-but-inert: plugin-produced symbols appear in search and outlines but participate in no cross-file candidate pool. Capabilities are promoted deliberately and are visible in payloads.
Isolation decision
The current tree-sitter WASM integration cannot be the production security boundary: an infinite lexer is not interruptible through its public API, stores may share memory/table state across grammars, and store lifetime conflicts with the current rayon parse path.
Dynamic grammars and extractor components therefore run in a supervised helper process defined by #79. The parent reads the already-eligible file and sends bytes plus bounded metadata; the plugin receives no path-based filesystem capability. A timeout kills the worker, not a rayon thread or the daemon. One plugin package never shares a WASM store with another package.
An in-process trusted fast path may be considered later, but it is not the correctness path and cannot change observable extraction output.
Data ownership and provenance
Provenance is row-level, not file-level. Embedded-language files can contain facts from the wrapper package, a host-language extractor and bridge rules simultaneously. Symbols, refs, imports and diagnostics must identify package digest, activation generation, language id and extraction component.
The existing files.lang value may remain as a compatibility summary, but it cannot be the source of truth for mixed-language resolution. #77 defines the schema and pool changes.
Activation and recovery
Plugin changes are index inputs just like source bytes. #78 replaces extension-only invalidation with generation-based activation:
Cold indexing and incremental activation must converge to the same active database projection.
Child issues and order
#81 remains independent and should ship immediately: honest disclosure of symbol-blind extensions is required before and after runtime plugins exist.
Reference implementation
XAML/C# is the first end-to-end bridge because it exercises all hard parts:
A second acceptance package must migrate one complete existing compiled language plugin and produce an equivalent fact projection on its fixtures and corpus probes. This prevents an architecture that is only nominally generic.
Production gates
The epic does not waive existing production blockers or scale contracts. Plugin activation must degrade with disclosure rather than hang, OOM or poison the active index.
Required end-to-end mutations include:
Final acceptance
Using only released binaries and plugin-management commands:
Anything less is extensible configuration, not a pluggable indexer.
epic: runtime-extensible format support — dialects as data, not as binariesto epic: runtime plugin architecture — dynamic grammar, extraction and resolution packagesStatus as of v0.26.1 — measured, not remembered
The epic has SHIPPED to master and is live. The working note that said "one release at #80's gate, nothing ships until then" is superseded: v0.25.0 and v0.26.0 both contain the plugin system, and this repository's own index currently reports a runtime package active —
That is the reference implementation loading a dynamic grammar and producing structural rows, in production, on the dogfood repo.
Phases
.cipformat, supervised killable workerkill -9at all 12 transitions, reader epoch#76/#77/#78/#79 remain open as tracker bookkeeping; their code is landed and shipping.
The three production-wiring gaps from #80 S15 are now ALL CLOSED
They were the reason "all six conformance layers implemented" was not the same as production-ready. Re-measured just now with this project's own
find_callers, the same instrument that found them:1. "A running daemon never observes an approval change" — CLOSED.
crates/daemon/src/reactivate.rspolls the approval surface (DEFAULT_POLL_INTERVAL5 s, chosen against the daemon's other periodic jobs at 60 s / 90 s / 30 min).Sentinel::moved()is true at most once per real move — the witness is adopted before returning, so a failed re-arm does not re-report forever. An unreadable surface answersfalseand adopts nothing, because a transient read error must not read as "every package was just disabled".2. "A grant-only activation change has no trigger at all" — CLOSED. The witness includes the capability grant, so a regrant that moves no file's claim is still a move. Its test says so in as many words, and its mutation — dropping the
caps=term — is recorded as "that is gap 2's only trigger".3. "
build::buildandpromotion::promotehave no production caller" — CLOSED. It was 14 callers, 14 excluded as tests, 0 production. Now:The generation pipeline is reachable from the operator surface.
What still stands between here and final acceptance
is_extension) — C# cannot migrate. Left open deliberately: admitting the bit is the authorisation into the C# extension-method binding pool, so it is a capability decision, not an ABI addition.Carried forward, measured and named
target/corpus/scale-*.jsonis still owed.tests/corpus/baseline.jsonhas not moved once across the whole of #77 and #78 and has never been blessed — that remains the evidence that the capability design is inert by default.derived_names, so a package using #86 gap 1 can never be enabled #103EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119Visibility::Unknownhas no wire slot, and it costs a packaged JavaScript 19% of ALL its resolutions — MEASURED #166Close-out sweep: the epic's true remaining count is 63 open issues, and the phase chain is DONE
Close-out lane, master
552e3a2. Six lanes landed work in the last few hours and closed almost nothing, so the open list overstated the remaining work — twice causing this epic's release gate to be under-scoped. This comment makes the list true. Full evidence lives in the closing comment on each issue; the companion comment on #80 carries the gate-side detail.Count method, because it has been wrong three times in two days: the Forgejo issues API caps at 50 rows per page and silently ignores a larger
limit. Paged until a short page — 50 + 13 + 0 = 63 open issues.The epic's own structure
Phases 0–4 are all closed. What remains structurally is #80, #84, #86 — plus the three of #80's nine formally-linked adjacent blockers that are still open: #41 (scale ceilings), #45 (generation-aware ratchets), #51 (agent-task benchmark). The other six — #65, #71, #72, #73, #81, #82 — are closed.
Closed by this sweep (7)
#110 #164 (bridge tier budget — duplicates of each other), #169 (masked #51 benchmark, verified by dispatch), #170 (parity arm split), #171 (
check_renameroster), #172 (C# generic ref names), #176 (__future__imports + a grammar-derived import-kind registry).Kept open with corrected text (10)
#93 #125 #168 #173 #174 #175 #177 #178 #179 #180. Each was reported as fixed or refused by its lane; each still has a live, reproducible residual. Seven had their titles rewritten because the filed diagnosis was measured and found wrong — notably #175 (the cause is the
method_call → POOL_METHODpool rule pluspython.rsminting no module symbol, not package directories) and #168 (both proposed directions cost ~1 300 correct binds and did not fix the two phantoms; they relocated them into the stem-anchored arm).Two facts that bear directly on this epic's readiness
1. Master has never been through CI. Local master
552e3a2is 7 commits ahead oforigin/master(a9ba058) and unpushed. Every "gates green" claim in the last few hours' lane reports is a local green.2. The Windows job is red on the last two dispatches (run 610 / job 33546; run 612 / job 33560), failing
ci_disk_preflightandschedule_liveness. The fix is local at8d9ac8eand unpushed, so it too is unverified by dispatch.windows-gategates releases, and #80 step 12 requires the runtime gate on every shipped platform.The 63, grouped
Epic structure (4): 75, 80, 84, 86
#80's remaining formally-linked blockers (3): 41, 45, 51
Roadmap / backlog under this epic (18): 24, 27, 28, 31, 32, 34, 35, 42, 46, 47, 48, 49, 50, 53, 59, 68, 69, 70
Findings, mostly from the last few days (38): 93, 98, 111, 119, 120, 124, 125, 128, 133, 137, 149, 153, 154, 158, 159, 160, 162, 166, 168, 173, 174, 175, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192
Read the last group carefully before sizing anything from it: #188 records that
precision_gate's 7/7 withphantom_count == 0covers ~47 probes over 54 fixture files and never indexes a corpus repo, and #191 records that the pinned corpus is not reachable through our own tools at all. Both are cited routinely as if they were corpus-scale evidence. They are not.Close-out round 2 — the true open count, 2026-09-06, master
fc329a8Four lanes merged since the last close-out and closed nothing. This lane verified their claims against the tree and settled ten issues. Posting the epic-level state so the next planner does not have to re-derive it.
Repository-wide: 55 open issues (was 61 before this round). Counted by paging the Forgejo issues API to a short page — it caps at 50 rows and silently ignores a larger
limit, which has produced a wrong count three times in two days.This epic's children
One child remains: #80. Phases 0–3 are done; #80 is the whole of what is left, plus the two issues that hang off it: #84 (full-language migration proof — #80 step 13) and #86 (the three plugin-ABI gaps that block migrating any language other than Ruby or PHP).
Closed in this round
#188
precision_gatereports and ratchets its own population (47 probes, 23 declared decoy sites, 87 files, 0 corpus repos; JavaScript'sforbid_sitesis 1) — floor mutation run to real red by the close-out lane. #189 the receiver locality gate was inverted, now three-valuedrecv_proof; cs-dapper −183, php-guzzle −206, seven of nine repos byte-identical, ~9:1 phantoms to correct. #111 five payload categories on the wire, summing to the measured total. #98 / #133 reconciled and closed separately, neither as the other's duplicate. #120 the "10x less context" claim corrected at five sites with an inverted pinning test.Left open, with corrected residuals
#149 (both fields shipped;
dynamic_influence.semanticshas no gate — deleting its initializer compiles and ships green), #158 (part 2's coverage half: no blessed absolute read-pathvm_stepover the corpus), #160 (items 1 and 3; tool descriptions are 38,636 chars, +1,293 above the figure the issue was filed on), #162 (implemented and unit-graded, but never executed — zerorelease.ymlruns since the step landed).One thing this epic should know
Master's own CI is red at
fc329a8. TheOSS corpus (tier 1)job failed on run #4981 while every other Linux job passed, and it was green on87a3fc8one commit earlier. The Windows jobfmt + clippy + build + test (windows)has failed on both87a3fc8andfc329a8. Per the release workflow that Windows job gates releases.#75 is not complete until [#80's] gate is green— and right now the tree it would run on is not.GUEST_ABI_MAJORis bracketed by no manifest field, andplugin pack --check-reproducibledoes not exist #153plugin enableindexes nothing on a newly-claimed extension, andpackage_fixture::XAML_BRIDGESholds a bridge key the production door refuses while its doc claims the guides show it #207Next stage of this epic: moving the seven builtin languages into first-party packages is now tracked in #309. That covers the plan, the operator's decisions (reserved plain ids, opt-in enablement, the 10%/20% performance budget, a one-release fallback, and the port order Ruby → PHP → JS → TS → C# → Rust → Python) and progress.
Phase 0 (#306) and Phase 1 (#308) are merged. Master is at
4ecf930.