2026-10-25
I banned the folder of packages
Restore fails. I pack the library into a folder, add the folder to nuget.config, and my machine goes green. The next machine stays red. CI stays red. A month later I have three folders and no idea which one the sample was actually built against.
I have done this enough times to know the feeling. The error is a missing Novolis.* version. The fix that types fastest is dotnet pack -o artifacts/nuget-local and a packageSources entry called something honest like novolis-local, which then gets committed because the sample has to build for the person who clones tonight. By Friday the folder has a 2.0.0 I invented, a 2026.1.99 I invented so it would sort first, and a package built from a dirty tree. My sample passes. The published feed is a different universe. I am debugging a bug that does not exist for anyone else, or missing a bug that does.
So that move is banned.
Where a package is allowed to come from
Other people's packages come from nuget.org, pinned. Mine come from GitHub Packages, on the 2026.1 line. The float I allow in a governed project is 2026.1.*. That is the platform line. It means "the newest published build of this year's line," and it only means that if the thing was actually published.
If a consumer cannot restore, the package is not published yet. That is the information. Packing to a directory so I can keep typing is me lying about the publish. The lie is comfortable because the API is the API I have open in the other window. The lie is expensive because the API I have open is not the API CI will restore tomorrow.
The strict writeup is the NuGet-only policy in novolis-governance. The short version is the banned list. No artifacts/nuget-local. No novolis-local. No pack-local.ps1 as a way of life. No committed ProjectReference whose path climbs into a sibling repo. No MSBuild property that sniffs for a clone and switches feeds. No ItemGroup condition in a csproj that picks project or package based on a file existing. Those conditions are how the lie gets permanent. The file exists on my machine. The condition is true. I commit it. You clone. The condition is false, or worse, true against a different commit than I had.
The float that lies
2026.1.* is a decision. 2026.1.10.* and 2026.1.1.* are not the same decision. A build-line float resolves to the latest CI number on that narrower line. If that build failed before publish, or publish has not finished, restore fails in a way that looks like the package does not exist. If a junk version got pushed, restore succeeds against junk.
NuGet sorts versions. Under a 2026.1.* float, 2026.1.99 sorts above a real CI build like 2026.1.10.36. I learned that by winning restore with a version I had stamped so I could "test the feed." Every consumer silently took the test. The fix is to delete the junk from the org feed, not to add another local folder that hides it. Never publish 2026.1.99 or a vanity 1.0.0 into that feed. The float will find it, and it will prefer it.
Third-party packages stay pinned. I do not float Newtonsoft, or Avalonia, or a GPU binding, on "whatever nuget.org had this morning." The platform line is mine, so I accept the movement. Everyone else's line can break me on a Tuesday without a commit of mine in the log. I want that break to require a diff I can read.
The one escape
The one escape I still allow is local and obvious. If the sibling project is on disk, the build may compile that project instead of restoring the package. That is the code I am looking at. It is a project reference for the hour I am in the forest. I do not commit a folder feed, and I do not tell anyone else to add one so their restore will pretend to pass.
The mechanism is not a hand-edited csproj. Governed projects use LibraryReference. Governance expands it. Sibling project on disk, and you are in the mode that is allowed to see it: ProjectReference. Otherwise: PackageReference at 2026.1.*. The csproj you commit does not grow a conditional to remember the trick. The big platform solution, Novolis.Platform.slnx, rewrites existing package references to sibling projects for the afternoon I am building the whole forest. A single repo's CI does not. CI restores packages. If the only way the branch builds is with every checkout present, I have not finished.
There is a property, NovolisUseProjectReferences, and the solution name turns the same switch on. Force it off and you are back on the feed, which is what I want when I am about to claim the consumer works. The map of package id to csproj is generated. It only substitutes a reference the project already had, and only if that project file exists. It does not invent a dependency because the folder happened to be cloned.
Same-repo ProjectReference is fine. Tests referencing src/ inside one repository is the normal shape. The ban is the hop across a repo boundary in a file I commit.
A few packages are allowed to stay direct PackageReference because they carry more than an assembly. Raylib and its native package, because of the runtime assets and the build targets. The Inno packaging package, because it ships MSBuild targets for an installer. Those exceptions are listed in the policy and in the verifier. A new exception is a deliberate edit to that script. It is not a comment in a csproj that says "temporary."
What "done" means
A dependency change is not done when my machine is green. It is done when the verify scripts are green across the checkout, the libraries have been published by a merge to main, and a consumer restores from nuget.org plus GitHub Packages and builds. Local project-reference mode proves the API shape early. It does not prove the publish. I have merged "it builds on the meta solution" and then watched CI fail because the package the consumer asked for was the previous one. The fix is a commit on main so the merge build ships the package, then the consumer moves. That lag is annoying. It is also the product. Sixty repos only works if a package is a door and not a roommate.
verify-nuget-only and verify-library-reference-usage are the checks I expect on library repos, on the lab, on utilities, and on apps. There is also a pass that looks for junk versions and build-line floats. I run them when I am tempted. I also run them when I am not tempted, because the temptation is quiet. It looks like a one-line nuget.config "for the demo."
I still reach for it
I still reach for the folder every time a package is an hour behind my branch. The compile error is right there. The pack command is memorised. The local feed would make the red go away before lunch. That reach is why the rule is somewhere I trip on it, in the policy, in the verify script, and in the note I leave for the next session, including the session that is a model and will pack a folder without shame because the folder makes the tool call succeed.
If you are in that session: do not add the folder. Publish, or use the platform solution, or wait. A green restore against a directory only I have is not a fix. It is a private fork of the feed, and I already have enough of those.