-
Notifications
You must be signed in to change notification settings - Fork 0
0009 sample consumes a local feed
Status: accepted · Date: 2026-08-05
Nothing in the repository consumed the published packages. Everything built
through ProjectReference, so packaging was only ever exercised by
dotnet pack — never by installing and using the result.
That hides a whole class of defect. A package can pack cleanly and still be
broken for a consumer: a missing dependency in the netstandard2.0 group so
System.Memory is absent at run time; an XML documentation file that fails to
ship; a type that is public in source but unreachable from outside the
assembly. None of those would have failed CI.
samples/DataNet.Sample consumes the three packages by PackageReference,
restoring from a local folder fed by dotnet pack rather than from nuget.org.
The version is bound to $(Version) from the repository's root
Directory.Build.props, which applies to the sample too, so it tracks whatever
dotnet pack just produced instead of pinning a number that goes stale.
Amended by 0012. The three packages now version independently, so there is no repository-wide
$(Version)left to bind to: the sample imports each project'sVersion.propsand uses one property per package. It still tracks whatdotnet packjust produced — but preserving that took more than rebinding the property, and the rest of this document should be read with the next paragraph in mind.Listing the local feed first does not make it the feed that answers. Restore consults the global packages folder before any source, so a DataNet package declared at a version that is also on nuget.org resolves to the published assembly — the exact inversion "Why not nuget.org" below exists to prevent, now arriving silently and without editing this file. Under per-package versioning that is the normal case, not an accident: a package that did not change keeps its published version indefinitely. Three things hold the line now — a
packageSourceMappingconfiningDataNet.*to the local feed, a separateNUGET_PACKAGESfor the sample's restore in CI, andcheck_version_floor.pykeeping a declared version off the feed in the first place.
Restoring from nuget.org would test the last published version. That is more
honest as documentation — it is what a reader would actually get — but useless as
a gate: it can only fail after a broken package is already public, and it cannot
run at all before the first publish of a new version. At the time this landed,
0.2.0 was not yet on nuget.org, and the sample already worked against it.
A local feed inverts that: the gate runs on what is about to ship.
-
The packages must be packed before the sample restores. The CI job does this; running it by hand requires the same:
for p in src/DataNet.Text src/DataNet.Embeddings src/DataNet.Fuzzy src/DataNet.Metrics; do dotnet pack "$p" -c Release -o ./artifacts done dotnet run --project samples/DataNet.Sample -c Release
-
The sample is deliberately outside
DataNet.slnx. Inside the solution,ProjectReferenceresolution would quietly satisfy the references and the sample would prove nothing while appearing to work. -
It runs in CI, because a sample that is never built rots into documentation that lies.
Amended by #265: the gate is member-granular. The bullet below states the contract in terms of types, and that was the contract until 2026-08-18. It is now stated in terms of members: every public method, constructor and property of an exported type needs its own member reference, not one reference standing in for the whole type. The paragraph below is kept as written, because the reasoning for the gate is unchanged and only its granularity moved; what changed is recorded under Member granularity at the end of this document.
-
It covers every exported public type, and a gate keeps it that way. The
"unreachable public type" guarantee above is worth exactly the set of types the
sample references, and when this was measured it referenced 14 of 58 — so for
the other 44 the job was green by construction.
PackagingGatereads the exported surface of the three assemblies as NuGet resolved them for the sample and fails the run when one of them has no member referenced. Adding a public type without adding a call is now a red build rather than a silent hole; #65 and #66 added seven such types before anyone noticed. The criterion is a member reference in the compiled metadata, sotypeof(T)alone does not satisfy it, and enums — whose members are compile-time constants — are the one documented exception, satisfied by being named. -
It covers
lib/net10.0only. Thenetstandard2.0package assets are not consumed by anything: the netstandard2.0 assemblies are covered by the mirror test projects, but the package dependency group for that target is not. Adding anet8.0target to the sample would close this, since net8.0 resolveslib/netstandard2.0; it needs the 8.0 runtime in CI. - ONNX inference is not exercised. Model weights are deliberately not committed, so the sample uses the tokenizer and says so, rather than failing on a missing file.
Type granularity had a blind spot that shipped three defects green. Once any
member of a type was referenced, every other method, overload and property on it
was invisible to the gate — so Ndcg reached by its unweighted call hid the
sampleWeight parameter #223 had added, Silhouette reached by Score hid two
of its four public methods, and BpeVocabulary constructed several times hid
every one of its properties. Those are #262, #263 and #264. A samples audit over
nine merged pull requests found that type-level coverage was in fact intact —
all 14 new public types were reached — and that every gap it found was
member-level. That is the argument for moving the gate down a level rather than
widening it sideways.
What the gate now demands. One member reference per public method,
constructor and property of every exported type. A property counts as reached by
either accessor, since an object initializer emits the setter and a Console
line the getter. Enums keep the exception they already had, for the reason
already given: their members are compile-time constants that leave a type
reference and never a member one, so naming the type is all a consumer can do.
What it cost. The exclusion list went from 2 entries to 44, and the sample gained roughly 56 calls. The list did not become forty paragraphs nobody reads, because the growth is three shapes rather than forty cases, and each shape carries one reason:
| shape | entries | why no sample can reach it |
|---|---|---|
a record's synthesised Equals/GetHashCode
|
17 | a consumer compares with it, never calls it, so no line emits a reference |
the …Async twin of a loader called synchronously |
14 | a console sample reading a committed fixture has no honest reason to await, and calling both demonstrates the API twice rather than the package once |
| a result record the library constructs | 9 | its properties are exercised; constructing one by hand is what a consumer never does |
PrecompiledNormalizer.FromCharsMap / .Normalize
|
2 | a charsmap is a binary trie inside a spiece.model, and model artifacts are never committed; measured, it refuses an empty blob and a four-zero-byte header with different sentences, so there is no input to pass |
The existing discipline survives the growth: every entry carries a reason a reviewer can disagree with, and a key naming a member that no longer exists still fails the gate, so the list cannot rot into a silent omission. Type-level keys remain valid and exclude a whole type, which is what the two original entries are.
What this granularity does not catch, measured rather than assumed. An
optional parameter emits the same member reference whether it is passed or
omitted — Score(a, b, c) and Score(a, b, c, sampleWeight) are one name in
metadata — so reverting #262 does not fail this gate. Keying on arity as well
as name does catch it, and was tried: it takes the judged surface from 383 members
to 679 with 339 uncovered, which is a different decision about how much the sample
must demonstrate rather than a refinement of this one. It is left open deliberately,
and the narrower form the issue suggests — detecting a defaulted parameter no
sample ever passes, on its own — is the cheaper route to the same catch.
Reverting #263 is caught, checked by doing it: the gate reports
Silhouette.ScoreFromDistances and
Silhouette.PerSampleFromDistances by name.
- 0001-target-framework
- 0002-unicode-comparison-unit
- 0003-provenance-and-licensing
- 0004-levenshtein-myers-backlog
- 0005-hamming-jellyfish-divergence
- 0006-ratcliff-autojunk
- 0007-metaphone-scope
- 0008-italian-enza-nltk-divergence
- 0009-sample-consumes-a-local-feed
- 0010-stop-word-list-provenance
- 0011-persistence-format
- 0012-per-package-versioning
- 0013-sentencepiece-parity-scope
- 0014-precompiled-normalizer
- 0015-sonar-rules-in-the-build
- 0016-metrics-package-placement
- 0017-bpe-parity-scope
- 0018-multiclass-roc-auc-parallelism-is-opt-in
- 0019-the-net-analysers-run-in-the-build-too
- 0020-normalize-is-a-projection-not-a-parameter
- 0021-multioutput-is-a-method-not-an-enum
- 0022-added-token-matching-flags
- 0023-byte-level-decode-substitutes
- 0024-weighted-median-averages-within-scikit-learns-epsilon
- 0025-quickselect-replaces-a-full-sort-for-the-median
- 0026-r2-and-explainedvariance-split-their-undefined-cases-differently
- 0027-r2-and-explainedvariance-vectorize-only-a-single-output
- 0028-log1p-is-kahans-identity-not-math-log-1-plus-x
- 0029-balanced-accuracy-adjusted-is-left-to-ieee-754-at-the-edge
- 0030-cohen-kappa-keeps-scikit-learns-expected-matrix-orientation
- 0031-nosamplecorrect-mirrors-numpys-float64-upcast
- 0032-fbeta-substitutes-tp-predicted-and-support-algebraically
- 0033-compensated-sum-is-neumaiers-variant
- 0034-dropout-is-refused-for-want-of-a-user
- 0035-a-null-pre-split-is-removed-with-invert-not-isolated
- 0036-a-member-may-ship-without-an-oracle-if-it-says-so
- 0037-the-guards-run-before-the-commit
- 0038-the-gate-confronts-an-exception-tag-with-the-page-that-documents-it
- 0039-mutual-information-returns-zero-on-an-empty-input
- 0040-a-curve-is-a-sealed-class-per-curve
- 0041-one-sample-file-per-public-class
- 0042-phonetic-encoders-refuse-a-null-word
- 0043-the-equality-table-is-sized-to-the-pattern
- 0044-compression-belongs-to-the-caller
- 0045-a-console-call-carries-its-reason-on-the-line
- 0046-check-adr-immutable-runs-in-ci-only
- 0047-one-gate-per-kernel-not-one-per-alphabet
- 0048-the-gate-depends-on-the-kernel-and-the-alphabet
- 0049-two-gates-per-kernel-tested-where-the-width-is-known
- 0050-the-sentencepiece-bpe-lineage-stays-a-bpe-model
- benchmark_latest
- decisions
- equivalence
- matplotlib
- migration
- nightly_run
- numpy
- pandas
- performance
- pytorch
- seaborn
- sklearn
- statsmodels