Skip to content

refactor(core): extract the product-agnostic foundation into Nordstein.Core - #535

Merged
JabbaKadabra merged 4 commits into
masterfrom
claude/code-reuse-strategy-i4s0f7
Aug 13, 2026
Merged

refactor(core): extract the product-agnostic foundation into Nordstein.Core#535
JabbaKadabra merged 4 commits into
masterfrom
claude/code-reuse-strategy-i4s0f7

Conversation

@JabbaKadabra

@JabbaKadabra JabbaKadabra commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

A spike of the first slice of shared-code extraction, ahead of a second Nordstein product. It moves the least entangled foundation code into core/ as Nordstein.Core and validates the full mechanism — packaging, namespaces, dependency direction, Docker restore layers, CI, and the extraction itself — before a second repository exists to get any of it wrong.

Nothing is published. The feed and the licence are open decisions, recorded in core/PUBLISHING.md.

What moved

From To
Proxytrace.Common core/Nordstein.Core.Common
Proxytrace.Common.Tests core/Nordstein.Core.Common.Tests
Proxytrace.Testing core/Nordstein.Core.Testing

Core is its own solution and deliberately not part of Proxytrace.sln. The moment it only compiles as part of the product, it stops being extractable and nobody finds out until the split is attempted.

How the product consumes it

Consuming projects declare a NordsteinCoreReference item rather than a reference — see Proxytrace.Domain.csproj for a live example, and Directory.Build.targets for the expansion.

Directory.Build.targets turns that item into a ProjectReference (source mode — the default while Core's sources are present) or a PackageReference at $(NordsteinCoreVersion) (package mode).

The indirection is the load-bearing part. Without it, every cross-boundary change becomes edit → pack → bump → restore → retest, and the predictable result is that nobody makes small Core improvements any more — they copy the code into the product instead, which is the exact failure the extraction exists to prevent. Once Core is its own repository, NordsteinCorePath points at a sibling checkout (../Core/) and nothing else changes.

What keeps the boundary honest

Staging Core inside this repository is convenient but removes the natural barrier — nothing but review stops a Proxytrace type being referenced in there. Two mechanical guards replace it:

  • backend builds and tests core/Nordstein.Core.sln standalone, before the product.
  • core-package (new job) packs Core and rebuilds the whole product against the resulting .nupkg files. Project references hide two consumer-only failures: a type that is public in source but never made it into the package surface, and a dependency Core forgot to declare because source mode resolved it through the product's own graph. Packages are uploaded as an artifact, not pushed.

The extraction was dry-run, and it did not work the first time

The point of a spike is to find this now rather than on the day of the split. Splitting core/ into a scratch worktree turned up two defects, both fixed in 0efa3b0:

  1. The extracted repository would not have built at all. core/Directory.Build.props imported the parent Directory.Build.props unconditionally. Standalone there is no file above it, GetPathOfFileAbove returns empty, and importing an empty project path fails with MSB4020 before a single project compiles. The import is now conditional, and the three things the parent supplies that Core needs are defaulted locally.

  2. git subtree split is the wrong tool and loses the history. It filters strictly by path and does not follow renames — and everything under core/ was git mv'd from Proxytrace.Common/ and friends. Measured: 1 commit. git filter-repo with the pre-move paths mapped onto the current ones keeps the real history: 7 commits, back through Trace sessions: higher-level live grouping for debugging #371, fix(storage,di): invalidate the entity cache after commit; populate framework plumbing once (#450, #451) #465, Various Bugfixes #484, Bump the nuget-minor-patch group with 9 updates #508 and the background-service hosting fix. core/PUBLISHING.md now carries the verified recipe, the ordering constraint on the renames, and the standalone build/test/pack check to run before pushing the new repository.

An earlier revision of this description claimed a subtree split would preserve the past. That was wrong; this is the correction.

Other changes worth a reviewer's attention

  • Dockerfiles (Proxytrace.Api, Proxytrace.Proxy.Api, deploy/allinone) copy Directory.Build.props/.targets, nuget.config and Core's project file into the restore layer. Without them the reference expansion yields nothing and restore silently misses the dependency.
  • nuget.config is new: a clear element plus nuget.org, so a machine-level source cannot supply a package this repository did not intend to restore. The local package-mode feed comes from Directory.Build.props by absolute path — NuGet resolves a relative RestoreAdditionalProjectSources per project directory, which fails only once the packages are absent from the global cache (that is how it passed locally and failed in CI; see the thread).
  • detect-changes treats ^core/, Directory.Build.targets and nuget.config as backend changes.
  • Core's tests are not in dotnet test Proxytrace.sln. A cross-cutting local run now needs both solutions; docs/testing.md and docs/commands.md say so.

Docs

New docs/code-reuse.md (indexed in CLAUDE.md) covers the mechanism, the one rule — Core may not reference the product — and the next slices with what blocks each: the domain/storage foundation (assembly-hardwired reflection in Domain.Module/Storage.Module, internal seams, MigrationsAssembly pinning), then licensing, then the cross-cutting subsystems, then the frontend primitives.

architecture.md, ci.md, commands.md, testing.md, security.md, validation.md, code-style.md, releasing.md and the create-domain / test skills follow the rename.

No user-facing change, so no CHANGELOG entry.

Verification

All 16 CI checks are green on 0efa3b0, including core-package (compiles the product against the packed .nupkg files) and backend (both solutions, with PROXYTRACE_REQUIRE_DOCKER_TESTS=true), plus e2e and the all-in-one image boot.

Locally, all without -p:NuGetAudit=false since #534 landed:

Check Result
dotnet build core/Nordstein.Core.sln clean, standalone
dotnet test core/Nordstein.Core.sln 198 passed
dotnet build Proxytrace.sln (source mode) clean
dotnet build Proxytrace.sln (package mode, packages cleared from the global cache first) clean
dotnet test Proxytrace.sln 2,805 passed, 8 skipped, 0 failed
perf/ projects both build
split into a scratch worktree, then built/tested/packed there clean, 198 passed, 4 packages

Full suite rather than a narrow scope, because the change is cross-cutting by definition.

Open decisions (not in this PR)

  1. Feed. Proxytrace is public and Elastic-licensed, so a private feed would make it unbuildable outside the organisation (GitHub Packages has no anonymous read). Private source with public packages keeps both properties. If Proxytrace is going closed anyway, that constraint disappears.
  2. Licence. The packed core/LICENSE is a placeholder copy of the product's. A licence cannot be recalled from consumers who already restored the package.
  3. Prefix reservation for Nordstein.* on nuget.org, before any public push.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt

claude added 2 commits August 13, 2026 06:24
…n.Core

A second Nordstein product would otherwise reimplement the parts of this
repository that are not about LLM tracing. This moves the least entangled slice
of that foundation into `core/` as Nordstein.Core, on its way to a separate
private repository published as NuGet packages, and validates the whole
mechanism end to end before a second repository exists to get any of it wrong.

Moved (history preserved via git mv, all under the `core/` prefix so the split
completes with a `git subtree split` rather than a copy):

  Proxytrace.Common       -> core/Nordstein.Core.Common
  Proxytrace.Common.Tests -> core/Nordstein.Core.Common.Tests
  Proxytrace.Testing      -> core/Nordstein.Core.Testing

Core is its own solution and is deliberately not part of Proxytrace.sln: the
moment it only compiles as part of the product it stops being extractable.

Consuming projects now declare an item instead of a reference:

    <NordsteinCoreReference Include="Nordstein.Core.Common" />

Directory.Build.targets expands it into a ProjectReference (source mode, the
default while the sources are present) or a PackageReference (package mode).
The indirection is what keeps a Core change a one-build edit; without it every
cross-boundary change becomes edit/pack/bump/restore/retest, and the
predictable result is that people copy code into the product instead — the
exact failure the extraction exists to prevent.

CI covers both modes. `backend` builds and tests the Core solution standalone
before the product; the new `core-package` job packs Core and rebuilds the
whole product against the resulting .nupkg files, which is what catches a type
that is public in source but missing from the package surface, and a dependency
Core forgot to declare because source mode resolved it through the product's
own graph.

The backend Dockerfiles gain the build-config files in their restore layer:
without Directory.Build.props/.targets present, the reference expansion yields
nothing and restore silently misses the dependency.

Packaging is wired up but nothing is published — the feed and the licence are
decisions, recorded in core/PUBLISHING.md. The packed LICENSE is a placeholder
copy of the product's.

Docs: new docs/code-reuse.md (indexed in CLAUDE.md) covers the mechanism, the
one rule that Core may not reference the product, and what the next slices are
and what blocks them. architecture.md, ci.md, commands.md, testing.md,
security.md, validation.md, code-style.md, releasing.md and the create-domain
and test skills follow the rename.

No user-facing change, so no CHANGELOG entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt
The `core-package` CI job failed on every project with

    NU1301: The local source '.../Proxytrace.Domain/core/artifacts' doesn't exist

NuGet resolves a relative `RestoreAdditionalProjectSources` against each
*project* directory rather than the repository root, so passing
`core/artifacts` on the command line asks for `Proxytrace.Domain/core/artifacts`
and every project misses.

Worse, the failure is invisible whenever the packages are already in the global
packages folder: NuGet never reaches for the source, so a local run that has
restored them once before passes and only a cold cache — CI — fails. That is how
this shipped.

The source is now added in Directory.Build.props, built from
$(MSBuildThisFileDirectory) so it is absolute and no caller can get the relative
form wrong, and gated on the directory existing so a real package-mode build
against a published feed is unaffected. The CI job, nuget.config and the docs
drop the flag.

Verified with the packages deleted from ~/.nuget/packages first, so the restore
genuinely has to reach the feed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt
@JabbaKadabra

Copy link
Copy Markdown
Collaborator Author

First CI run turned up two failures. One was mine, one is not.

core-package — mine, fixed in 74ddae3. Every project failed with:

NU1301: The local source '.../Proxytrace.Domain/core/artifacts' doesn't exist

NuGet resolves a relative RestoreAdditionalProjectSources against each project directory, not the repository root, so core/artifacts asked for Proxytrace.Domain/core/artifacts and missed everywhere.

The part worth recording: this is invisible whenever the packages are already in ~/.nuget/packages. NuGet never reaches for the source, so the local run that was supposed to verify package mode passed on a warm cache and only a cold one fails. My "package mode verified" claim in the description was that false positive — the mode does work, but the flag never did.

The feed is now added in Directory.Build.props, built from $(MSBuildThisFileDirectory) so it is absolute and no caller can get the relative form wrong, and gated on the directory existing so a real build against a published feed is unaffected. The CI job, nuget.config and the docs drop the flag. Re-verified after deleting both packages from the global cache, so the restore genuinely has to reach the feed.

backend — not mine, blocked on #534. Restore fails on the transitive SSH.NET 2025.1.0 advisory that TreatWarningsAsErrors promotes to an error:

Proxytrace.Messaging.Tests.csproj : error NU1903: Warning As Error: Package 'SSH.NET' 2025.1.0
has a known high severity vulnerability, https://github.com/advisories/GHSA-q939-rpr3-3284

Reproduced on a clean master worktree at 37e8128 with no local changes, so it predates this branch and will fail on every open PR. Deliberately not fixed here — a dependency pin does not belong in a 506-file move. backend stays red until #534 lands; I'll merge master in and re-run once it does.

Local state on this branch with the advisory suppressed: Core 198 passed standalone, product 2,805 passed / 8 skipped / 0 failed, both reference modes building clean.


Generated by Claude Code

@JabbaKadabra

Copy link
Copy Markdown
Collaborator Author

#534 landed (#536), so the blocker is gone. Merged master in at 98589dc — clean auto-merge, both touched files combined as intended (Proxytrace.Messaging.Tests.csproj carries the SSH.NET pin alongside this branch's NordsteinCoreReference, and docs/testing.md has both the pin section and the Core test-scope row).

Everything below now runs without -p:NuGetAudit=false, which is the first fully honest local verification this branch has had:

Check Result
dotnet build core/Nordstein.Core.sln 0 warnings, 0 errors
dotnet test core/Nordstein.Core.sln 198 passed
dotnet build Proxytrace.sln (source mode) 0 warnings, 0 errors
dotnet test Proxytrace.sln 2,805 passed, 8 skipped, 0 failed
pack + -p:UseLocalCore=false, packages cleared from the global cache first 0 warnings, 0 errors

CI should now get past restore on both backend and core-package — the latter has never actually reached the compile step against the packages, so its first green tick is the real confirmation that the package surface and declared dependencies are complete.


Generated by Claude Code

Dry-running the extraction turned up two things that would have bitten on the
day of the split.

1. core/Directory.Build.props imported the parent Directory.Build.props
   unconditionally. In its own repository there is no file above it,
   GetPathOfFileAbove returns empty, and `<Import Project="">` fails with
   MSB4020 before a single project compiles — the extracted repository would not
   have built at all. The import is now conditional, and the three things the
   parent supplies that Core actually needs (TreatWarningsAsErrors,
   IncludeSourceRevisionInInformationalVersion, the NordsteinCoreVersion
   default) are defaulted locally, so both layouts behave identically.

   Verified by splitting core/ into a scratch worktree: it builds, its 198 tests
   pass, and it packs. Nested in Proxytrace it is unchanged (Version 0.1.0-dev,
   warnings still errors).

2. The docs claimed `git subtree split --prefix=core` would complete the move
   "rather than a copy that loses the past". It does not. Subtree split filters
   strictly by path and does not follow renames, and everything under core/ was
   git mv'd from Proxytrace.Common/ and friends — so it produces exactly one
   commit. Measured, not assumed.

   `git filter-repo` with the pre-move paths mapped onto the current ones keeps
   the real history: 7 commits on this repository, back through the changes that
   shaped the code. PUBLISHING.md now carries that recipe, the ordering
   constraint on the renames, and the standalone build/test/pack check to run
   before pushing the new repository. README.md and docs/code-reuse.md are
   corrected to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt
@JabbaKadabra
JabbaKadabra merged commit e758098 into master Aug 13, 2026
16 checks passed
@JabbaKadabra
JabbaKadabra deleted the claude/code-reuse-strategy-i4s0f7 branch August 13, 2026 16:17
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.

2 participants