resolver: row-level provenance, capability-gated pools and explicit language bridges #77
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.
Blocks
Depends on
#27 feat: explained similarity across active plugin generations
h-dv/code-index
#28 feat: generation-aware SCC and configured layering analysis
h-dv/code-index
#69 resolver: profile-driven member/property binding with zero-phantom capability gates
h-dv/code-index
#73 perf: project_overview needs generation-scoped O(1) aggregates, not repeated refs scans
h-dv/code-index
Reference
h-dv/code-index#77
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?
Child of #75. Depends on #76 and on resolving #65's unbounded resolver path; integrates with #79.
Problem
The resolver currently treats producer identity as an unstated invariant: a row whose language is csharp is assumed to have been emitted by the compiled C# plugin and therefore to obey that plugin’s semantic guarantees.
Dynamic plugins invalidate that premise. Candidate pools are keyed primarily by name, language and pool class. Injecting one plausible symbol can create false edges, destroy correct edges by changing uniqueness, alter reachability anchors and corrupt resolution-gap diagnostics.
The previous proposal added files.producer. That is insufficient. An embedded-language file can contain:
Provenance and language identity must be row-level.
Outcome
Dynamic facts are searchable without affecting existing bindings by default. Resolver participation is an explicit, project-approved capability. Cross-language resolution occurs only through declared bridges. Every edge and every failed binding can disclose whether dynamic evidence influenced the decision.
Schema model
Introduce normalized package/generation/component identity and attach it to every extraction contribution.
Conceptual tables:
Symbols, refs and imports reference file_contributions or carry an equivalent non-null producer key. Diagnostics do as well.
Requirements:
files.lang may remain a compatibility summary, but symbols, refs and imports use their own source language. The primary file language cannot determine mixed-region resolver behavior.
Dynamic language identity
A package-defined language uses a stable namespaced id, for example org.example.xaml/xaml. It may not stamp csharp merely to enter C# pools.
Language identity and resolution domain are separate:
This removes the prior “own-lang is inert, host-lang contaminates” false choice.
Language profiles
Move every resolution-relevant hardcoded language branch into either:
Inventory includes at least:
A profile cannot provide SQL or arbitrary predicates. It selects bounded algorithms and data from a closed core-owned registry. Unknown algorithms fail package activation.
Capability model
The package manifest requests capabilities; installation and project activation grant a subset. Facts carry capability classes validated against that grant.
Default grant:
Separate capability axes include:
Capabilities are intentionally finer than “participates in tier N.” Resolver tiers combine multiple evidence classes, and a package should not receive unrelated authority because one use case needs a single class.
Dangerous capabilities such as module/file-key anchors, extension-method definitions and partial-class scope require explicit host support and stronger conformance gates. They are never inferred from emitted kinds.
Cross-language bridges
A bridge declaration is directional and bounded:
Examples:
Binding paths whose destination type is unproven remain unresolved. A package cannot assert that a string names a target.
Undeclared language pairs never share candidate pools even when normalized names match.
Pool construction
All candidate and anchor CTEs/tables must be audited. A dynamic row reaches a pool only when:
This applies to symbol_buckets, exported pools, directory buckets, qualified candidates/anchors, file keys, extension candidates, C# partial scope and diagnostic pool CTEs. There must be no “one forgotten CTE” escape hatch.
Default-inert dynamic rows must not alter:
Evidence provenance
resolved_by continues to identify the core resolver mechanism. Do not overload it with producer identity.
Add computed resolution influence:
The exact representation may be an enum plus deciding generation/component ids. It is recomputed by the resolver and never asserted by the plugin.
An absent field from an old daemon is “not reported,” never builtin_only.
Search and tool behavior
Response formats need concise shapes that do not turn every symbol row into a manifest dump; stable package handles can reference a package block once per response.
Migration and builtins
Backfill every existing row with a builtin/text contribution identity without changing resolved targets.
Migration equivalence requires:
Compiled and package versions of the migration language can run in shadow databases and compare canonical fact projections.
Tests
Structural gates:
Mutation gates:
Corpus gate:
Index the same corpus with no package, inert package and progressively granted package. The no-package and inert projections of all pre-existing bindings and diagnostics must be identical. Every movement after a grant must fall inside the bridge/capability’s declared source population, with a positive control proving the capability did useful work.
Acceptance
buildagent referenced this issue2026-08-26 12:33:55 +02:00
resolver: candidate pools have no producer axis — runtime-defined symbols are indistinguishable from plugin symbolsto resolver: row-level provenance, capability-gated pools and explicit language bridgesEMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119Closing with residuals. Shipped in v0.23.0 — not v0.25.0, and the "one release at #80's gate" policy this stayed open under was retired days ago.
Audited criterion by criterion against the tree, with the test that grades each one named. 6 of 7 met.
resolver_containment.rs::step_one_equals_step_two_for_every_pre_existing_row;capability_isolation.rs::an_inert_row_reaches_no_aggregatebridge_isolation.rs, 15 tests incl.an_undeclared_language_pair_resolves_nothinginfluence_classification.rs, 12 tests incl.influence_is_recomputed_and_never_latchedprovenance_invariants.rs::deleting_one_contribution_takes_only_its_own_rowsactivation_identity.rs::the_backfill_is_what_a_default_project_recomputes;upgrade_equivalence.rs::a_v42_corpus_index_migrates_to_the_same_projection_a_fresh_one_buildscapability_isolation.rs×5, plus the structural gatepool_capability_registry.rs(1811 lines)Criterion 2, stated honestly rather than resolved by wording
EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0— routing is whole-file, single-owner (dirty.rs:181-191). A mixed-language file with two producers is not merely untested; there is no configuration of this build in which it could be true.The half this issue owns is delivered:
m0044_row_languagemade language a row-level fact, andfile_contributionscarriesregion_start/region_endkeyed per generation — the storage for two contributions in one file exists and is graded. What is missing is the dispatcher, which belongs to the routing layer.I am deliberately not calling that "met because the schema supports it." That move is the one this project keeps paying for: #84's axis C graded zero and passed; #109's weekly jobs skip on every scheduled run; #116's ceiling cannot fail for either regression its own comment names. So criterion 2 is split out as #119 with its own acceptance, rather than absorbed into this close. If you read C2 literally, this issue stays open — I would not argue hard against that, and #119 exists so the requirement is owned either way.
The body requirement that is genuinely incomplete
This issue says: "Move every resolution-relevant hardcoded language branch into a core invariant or a validated profile field," naming qualified-name separators, positional type-kind demotion, and same-directory eligibility.
Three survive, and each measurably costs a packaged language real output — verified in source and independently confirmed by the #84 Ruby port, which hit all three as pinned deltas:
writer.rs:848qualified_name_separator(lang)→ every packaged language getsqualified_name = NULLcore/kinds.rs:236POSITION_TYPE_KINDS_BY_LANG→ packaged Ruby loses the type-position exemption; 19 refs demoted tomember_accessindex.rs:4405st.lang IN ('php','ruby','csharp')→ packaged Ruby never getsTIER1B_SAME_DIRECTORY; 53 refs, every one builtinresolved_by == 12and package-unresolvedFiled as #112. No manifest can close any of them.
A near-miss worth recording
The audit nearly reported "no registry coverage for those three" and would have been wrong.
pool_capability_registry.rsdoes register the C# gates (NeverDynamic("s.lang = 'csharp'")×4, pluss.is_extension = 1) and verifies each predicate really appears in the source. Its scope is pool relations; these three are a writer function, a post-resolution re-kinding table, and a tier-1bCASEarm — structurally outside it.That is exactly why the registry's gate is green while the gap is real, and it is the reason to prefer "audit by behaviour" over "grep for the name". Two other claims in the same audit were made from a name-grep and both were false.
Residuals
Nonetook the delta 876→0). Commented there rather than filed separately, since #86 already carries the two sibling ABI gaps.Close attempted and correctly refused — leaving it open.
Dependencies are #76 (open) and #65 (closed today). So #76 is the single formal blocker, and the refusal is right rather than bureaucratic: #76 defines the ABI and manifest contract that this issue's provenance model consumes, and #76 has a criterion that is genuinely unmet (the declarative extractor tier —
packages.rs:1537refusesTier::Declarativeoutright).Leaving the tracker's judgement in place rather than unlinking the dependency to force the close I had queued. The assessment in the comment above stands unchanged: 6 of 7 criteria met, residuals filed as #119, #112 and a comment on #86. This issue closes when #76 does, and nothing here needs re-auditing at that point.
Noting it explicitly because this is the second time the dependency graph has refused a close on this epic — #79 hit the same refusal on 2026-09-03 — and both times the graph was more accurate than the close would have been.
response_format: "concise"dropsinfluenceandresolved_by, so the dynamic-influence disclosure is invisible in the cheapest format we ship #139EMBEDDED_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 #166packages.rs:4478-4480says the wire has no bit foris_extension, contradictingrecord.rs:18and a comment fifteen lines above it in the same expression #185packages.rs:4478-4480says the wire has no bit foris_extension, contradictingrecord.rs:18and a comment fifteen lines above it in the same expression #185Visibility::Unknownhas no wire slot, and it costs a packaged JavaScript 19% of ALL its resolutions — MEASURED #166EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119