Skip to content

core: add the language-neutral extension-resolution plan contract - #1124

Merged
thekevinscott merged 6 commits into
mainfrom
claude/1119-ext-plan
Sep 17, 2026
Merged

thekevinscott merged 6 commits into
mainfrom
claude/1119-ext-plan

Conversation

@thekevinbot

@thekevinbot thekevinbot commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1119. Slice A of #1111, under epic #1072.

Adds dirsql::extension_resolution, the language-neutral half of the
extension-resolution cluster the Python and TypeScript launchers each carry a
port of today. Nothing calls it yet — the SDK migrations are #1120 and #1121,
and #1122 deletes the duplicates.

Shape

Per #1078: the core owns everything except "where is package X installed on
this host".

  • plan_config_extensions(configs, suffixes) -> Option<Vec<PlanEntry>> parses
    each config's [[dirsql.extension]] array, returns None when no entry
    names a package (so the core's own loading and error reporting stay
    untouched), and otherwise plans every entry — literals resolved against
    their own config's directory, bare names as a Package the host must locate.
  • select_loadable(name, dirs, candidates, suffixes) picks the single loadable
    file out of the listing the host walked. Zero and more than one are both
    errors.
  • is_bare_name(path, suffixes) is the classification both of the above use.

The suffix list is a parameter rather than a constant because it genuinely
differs per host: Python counts .pyd, Node counts .node.

Two deliberate normalizations, both landing in the migration slices

  • is_bare_name treats \ as a separator on every platform. Python's
    os.altsep is None on posix, so a\b.so is classified as a package name
    there today. That is a bug, not a contract.
  • The not-found message renders patterns as *.so, matching Python. The
    TypeScript message currently reads (.so); #1111 Slice C: migrate TypeScript extension resolution to Rust plan #1121 brings it in line.

Neither is observable until an SDK migrates onto this module, so neither needs
a migration fragment here.

Red/green

The contract and its 14 unit tests were pushed first, on 3739490, with the
implementations todo!(). CI was observed red on that commit before the
implementation was written: the Unit tests job failed with exactly those 14
tests and nothing else, each panicking at the stub it exercises
(job).
The implementation is 9f049fb.

Manual run

Built a scratch crate against this one and drove the public API over a real
config and a real package directory:

$ cargo run -q   # config: ext/local.so + entrypoint, and bare name sqlite_vec
Some([
    Literal { path: "…/probe/ext/local.so", entrypoint: Some("sqlite3_local_init") },
    Package { name: "sqlite_vec", shadow: "…/probe/sqlite_vec", entrypoint: None },
])
shadow …/probe/sqlite_vec exists: false
select_loadable -> Ok("…/probe/site/sqlite_vec/vec0.so")

The literal resolved against the config's own directory with its entrypoint
intact, the bare name planned a package lookup with the correct shadow probe,
and select_loadable picked vec0.so out of a directory also holding
__init__.py — matching what dirsql/resolve_extension.py does today.

Gates

  • just preflight --conventions .github/workflows/dirsql-rust-ci.yml: 0 failing
    pairs (colocated-test, unit-lint, integration-lint, mutation; packaging skipped
    — needs a built artifact).
  • unit mutation: 15 mutants tested, 0 survivors.
  • cargo llvm-cov --lib --features cli --branch: regions 93.65 / functions 93.15
    / lines 94.50 / branch 79.94, all above the floors (91 / 91 / 92 / 72).
  • cargo test --workspace --features cli: all green, 882 lib tests.

PARITY.md

Not updated: no SDK's public API surface moves in this slice. PARITY.md gets
its update in #1120 / #1121, where behavior actually crosses into the SDKs.

E2E Verification

  • Ran e2e suites locally for every affected SDK
  • Python SDK e2e: pass
  • TypeScript SDK e2e: pass
  • Rust core e2e (if applicable): pass
  • packages/python/e2e-attestations/<branch>.json receipt written
  • packages/ts/e2e-attestations/<branch>.json receipt written
  • Command(s) run: just e2e-attest-python, just e2e-attest-ts, cargo test --workspace --features cli
  • Result summary: python 37 passed, ts 16 passed across 5 files. Re-run against
    the implemented tree, not just the red commit.

Changelog / Migrations

  • Changelog fragment added under packages/rust/changelog.d/
  • Migration fragment not required — additive, nothing calls the module yet

Add `dirsql::extension_resolution` with the plan/select contract and its unit
tests, implementations stubbed. Pushed on its own so CI is observed red before
the implementation lands.

The shape follows #1078: the core reads the configs and decides which entries
need a host package lookup, the launcher answers only "where is package X
installed", and the core turns that answer into a concrete path. The loadable
suffix list is a parameter because it differs per host (`.pyd` vs `.node`).
`plan_config_extensions` parses each config's `[[dirsql.extension]]` array,
returns `None` when no entry names a package, and otherwise plans every entry:
literals joined onto their own config's directory, bare names as a package
lookup the host performs. `select_loadable` turns the host's answer into the
one loadable file, or the not-found / ambiguous error.

Nothing calls it yet; the SDK migrations are #1120 and #1121.
@thekevinscott
thekevinscott merged commit 98c2126 into main Sep 17, 2026
77 checks passed
@thekevinscott
thekevinscott deleted the claude/1119-ext-plan branch September 17, 2026 20:42
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.

#1111 Slice A: add Rust extension-resolution plan contract

2 participants