0076 a core package carries no external dependency - CyrilB1531/lodestar GitHub Wiki

0076 โ€” A core package carries no external dependency, so ONNX Runtime gets its own

Status: accepted ยท Date: 2026-09-03 ยท Supersedes: 0069

Context

#427 tiers the packages: core is netstandard2.0 + net10.0 with no external dependency, satellite is net8.0+ with dependencies allowed. Decision 0069 wrote the layout down a year into it and had to record that the tier was aspirational:

The satellite tier is empty. As built, Embeddings carries ONNX Runtime and still ships both target frameworks, so it is not a satellite by that definition โ€” nothing has yet needed a net8-only floor. The tier is a rule waiting for its first member, not a description.

0069's own rule 1 โ€” split only for a distinct dependency profile, audience or release cadence, never for tidiness โ€” gave Lodestar.Embeddings its reason for existing: "ONNX Runtime must not reach a caller who only wants Levenshtein." That reason was one level short of where it led.

Measured on 2026-09-03, at main:

  • Microsoft.ML.OnnxRuntime 1.28.0 was the only external dependency in the repository. Abstractions, Text, Fuzzy, Metrics, Decomposition and Conformal declared none.
  • It was reached by one file, src/Lodestar.Embeddings/Onnx/OnnxTextEmbedder.cs, 407 lines. Nothing else in the package referenced the type; the only other mention was a <see cref>.
  • Everything else in the package โ€” the four sub-word tokenizers, the batch encoder, the pooling, the .npy reader and the 869-line SIMD kNN index โ€” needed no runtime and could not be had without one.

Lodestar.Embeddings.csproj said as much in a comment and stopped there: "ONNX Runtime is the one external dependency, isolated to this package so Lodestar.Text stays dependency-free. Only the OnnxTextEmbedder type uses it." Isolation to a package that also holds the tokenizers is isolation from Lodestar.Text, not from a caller.

0069 is also stale on two counts unrelated to this one, which is why this record supersedes it rather than amending it โ€” ADRs are not edited (README, tools/check_adr_immutable.py). It describes four packages where eight now ship, and states that "Lodestar.Abstractions was never built", which decision 0071 had already overtaken.

Decision

A core package carries no external dependency. An external dependency earns its own satellite package, named for it.

OnnxTextEmbedder moves to a new Lodestar.Onnx, in namespace Lodestar.Onnx โ€” every package sets RootNamespace equal to its PackageId, and a Lodestar.Onnx holding Lodestar.Embeddings.Onnx would have been the only exception. Lodestar.Embeddings 0.6.0 drops the type and the PackageReference, and becomes core tier like everything else.

This restates #427's tiering as a rule rather than a table, and the rule is mechanical where the table was a judgement. It does not replace 0069's rule 1: distinct dependency profile, audience or release cadence, never tidiness still governs every other split, and this decision only removes the one case where "distinct dependency profile" was being judged by eye.

The layout, as it now ships

tier package external dependency inter-package edge
core Lodestar.Abstractions โ€” โ€”
core Lodestar.Text System.Text.Json on netstandard2.0 only โ†’ Abstractions
core Lodestar.Embeddings System.Text.Json on netstandard2.0 only โ€”
core Lodestar.Fuzzy โ€” โ†’ Text
core Lodestar.Metrics โ€” โ€”
core Lodestar.Decomposition โ€” โ†’ Abstractions
core Lodestar.Conformal โ€” โ€”
satellite Lodestar.Onnx Microsoft.ML.OnnxRuntime โ†’ Embeddings

Lodestar.Onnx ships net10.0;netstandard2.0 like the rest: ONNX Runtime 1.28.0 supports both, which the previous arrangement proved, so #427's net8.0+ satellite floor is not taken here. The floor is a permission, not an obligation โ€” a satellite takes it when its dependency forces it.

The polyfills and System.Text.Json are not external dependencies for this rule's purpose. They are netstandard2.0 shims for what is in-box on net10.0, so a package carrying them offers one API at two implementations rather than a second thing to install (decision 0011).

What enforces it

tools/check_nuspec_dependencies.py asserts the shipped graph per package and per target framework, ranges included. Microsoft.ML.OnnxRuntime appears in exactly one row of EXPECTED, under Lodestar.Onnx, so the rule fails a build rather than a review the moment an external dependency reappears in a core package.

tools/check_version_floor.py gains the fourth edge, Lodestar.Onnx โ†’ Lodestar.Embeddings, floored at 0.5.0 โ€” the release in which BatchEncoder.EncodeAll and BatchEncoder.Pad became public. A floor names what the dependent needs, and that is what it needs.

0069's rules 2 and 3 are unchanged and still enforced by the same two scripts: the shipped graph is exactly the written one, and src/ references packages rather than projects.

The cost, and what it bought

Two merges rather than one, and a release between them. OnnxTextEmbedder used four internal members of Lodestar.Embeddings โ€” EncodeAll, Pad, and EncodedBatch.Ids/.Mask โ€” so no namespace arrangement could have made the split a single green pull request against the published 0.4.0. The first two shipped public in 0.5.0 (#534); the two fields stayed internal, because InputIds and AttentionMask already expose the same buffers as spans.

That last substitution costs a copy: ONNX Runtime wraps a Memory<T>, which a span cannot supply, so a padded batch is copied into a rented array before the session sees it. The single-sequence path already paid exactly that, for exactly that reason, and the alternative was a second padding implementation in Lodestar.Onnx that could drift from the one the frozen corpus asserts.

What it bought: dotnet add package Lodestar.Embeddings no longer restores a native runtime. The sub-word tokenizers, the batch encoder, the pooling, the .npy reader and the kNN index reach a caller who does not run models at all.

Options considered

Extract a Lodestar.Vectors instead โ€” move VectorMath and EmbeddingIndex out, on the grounds that a caller holding its own vectors should not need the tokenizers. It answers a different question: the tokenizers and the pooling stay behind ONNX Runtime, so the weight is unchanged for everyone but the kNN user. And once Lodestar.Onnx exists, Lodestar.Embeddings has no external dependency at all, which leaves cohesion as the only argument for the split โ€” which 0069 rule 1 refuses by name.

InternalsVisibleTo("Lodestar.Onnx") โ€” one line, no public API added, and the split needs no 0.5.0 release first. Refused: a NuGet floor of >= 0.5.0 resolves any later Lodestar.Embeddings, and internals are not a contract, so the two packages would be welded together by a guarantee neither of them states. A public API that says what the neighbour needs is the honest version of the same coupling.

Leave it โ€” the isolation is nearly true, and one file is a small prize. Refused because the tier was never going to acquire a first member on its own, and #427 is explicit that pre-1.0 is when the layout is free: "Do the full split in one go, decide the final layout on paper first." Phase 4's own constraint โ€” anything needing a numerical dependency has to be argued as satellite before it is written โ€” becomes a mechanical check instead of an argument nobody remembers to have.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ