STATUS: REGISTERED BEFORE ANY PRODUCT CODE. This commit holds this document and the
Snipper salvage audit (owen-extension-snipper-salvage.md),
and nothing else. A semantic change after this commit is an amendment, committed before the
official run it affects.
Base: main = 88cb8cc31c49d730290613208947102d0737cad2 (merge of #391, TB-MVP-01). The
base contains #391: git merge-base --is-ancestor 04cb779a4ce8047e563d35edc72bdce9a13eaca8 88cb8cc3
holds. main has not moved since, and nothing under Typed Builder, OwnSharp.Cli or
packaging changed after it.
The results are in § P0 results at the end.
K-1 — the host already exists. owen check (frontend/roslyn/OwnSharp.Cli, the Owen.Cli tool)
is a complete generic pipeline:
- bundled extractor, child process;
- facts;
- the authoritative Rust core
own-cli, resolved only from an explicit locator or one computed packaged path (D3/D6,RustCoreLocator.cs); --format msbuild;- a mapped exit-code contract.
On the Rust engine it needs no Python: the vendored core is unpacked only for --engine python|compare.
The core already renders the canonical VS Error List line itself (own-cli ownir --format msbuild).
A second runner or a second renderer would duplicate the authoritative path, so none is built.
K-2 — the protocol must reach the scan as source. The extractor's directory and .csproj
expansion skips bin/, obj/ and *.g.cs. An explicitly named .cs input passes through
unfiltered (Program.cs::Expand).
A probe confirmed the path end to end:
- the TB-MVP
OrderBackendsources built with the generated protocol living only underobj/.../generated/; - the extractor ran on
OrderBackend.csprojplus that generated file named explicitly; own-cli ownir --format msbuildprintedC8.cs(18): error OWN002: ….
No extractor change is needed.
K-3 — generator delivery, both variants measured.
- A (Roslyn incremental source generator).
- Setup: the existing generator's model and renderer, compiled unchanged into a
netstandard2.0analyzer against Roslyn 4.8.0, run inside the ordinary compilation. - Output: text byte-identical to the committed golden
samples/OrderBackend/OrderBackend/Domain/Order.Protocol.cs, 6880/6880 bytes. The only Roslyn-specificstring[1..]uses were rewritten asSubstring(1), with the same output. - One difference, compiler side: generated sources compile with nullable annotations off, so the golden's
string?warns CS8669. The generator wrapper therefore prepends exactly one line,#nullable enable, to the golden text. The shared renderer's output, and so the golden, do not change.
- Setup: the existing generator's model and renderer, compiled unchanged into a
- B (MSBuild pre-compile Exec).
- Full build: works, adding about 0.5 s of
dotnet execper build. - Design-time build (fails): under the design-time build Visual Studio uses for IntelliSense (
-t:CompileDesignTime -p:DesignTimeBuild=true -p:SkipCompilerExecution=true -p:ProvideCommandLineArgs=true), the generated file was neither produced nor in the compile item list. IntelliSense would show the typed API as missing until a full build, and an edit to the declaration would not regenerate it.
- Full build: works, adding about 0.5 s of
- Decision: A. A runs in every compilation, the IDE's included, and keeps the golden. B is not shipped.
K-4 — packaging.
- Owen.Cli cannot be the reference. It is a
DotnetToolpackage, and NuGet refuses it as aPackageReference(NU1212). A referenceable host package is therefore needed. - The exec bit. A
.nupkgdoes not carry Unix file modes.RustCoreLocator.ResolvePackagedalready copies the packagedown-cliinto a content-addressed cache~/.owen/rust-core/<sha256>/and marks it executable there. The host reuses that code path as is.
dotnet new web -n OrderBackend
dotnet add package Owen.TypedBuilder --version 0.1.0
dotnet build
That is all. The machine needs no Own.NET checkout, no Python, no Rust toolchain, no global
owen tool, no environment variable pointing at a repository, and no PATH lookup of a
development binary. The only executable taken from the machine is the dotnet muxer that is
already running the build: DOTNET_HOST_PATH as MSBuild sets it, then DOTNET_ROOT, then
dotnet.
| package | kind | why it exists |
|---|---|---|
Owen.Build |
generic build host | One shared host is needed. It must be referenceable (K-4), carry exactly one copy of the extractor and of each platform's Rust core however many extensions are active, and depend on no extension. It is the only new non-extension package; there is no Owen.Toolchain or Owen.CoreAssets, because nothing would separate them from it. |
Owen.TypedBuilder |
extension #1 | The generator, the extension descriptor, and a dependency on Owen.Build of the same version. |
Owen.TestExtension |
test fixture, never shipped | A synthetic second extension: a descriptor only, no analysis. It is built by the gate into the isolated feed and nowhere else. |
Version: 0.1.0 for both shipped packages, matching Owen.Cli 0.1.0, which the host payload is.
Nothing is published to nuget.org. Every acceptance run uses a locally packed candidate in an
isolated feed.
Owen.Build layout:
| path | contents |
|---|---|
tools/net8.0/any/ |
the OwnSharp.Cli publish closure (ownsharp.dll, ownsharp-extract.dll, Roslyn), the same payload as the Owen.Cli tool, without the vendored Python core |
tools/net8.0/any/rust-core/<platform-key>/own-cli[.exe] |
the packaged core, at the path RustCoreLocator.PackagedPath already computes |
build/Owen.Build.props · build/Owen.Build.targets |
the generic MSBuild host |
buildTransitive/… |
the same two files, so they flow through an extension's dependency |
One source of truth for the platform inventory. The set of supported platform keys and
binary names, and the "pack fails when a staged binary is missing" check, move into one
frontend/roslyn/OwenRustCore.props. Both OwnSharp.Cli.csproj and the Owen.Build project
import it. The Owen.Cli package payload must stay file-for-file identical to its base
payload, compared as a sorted file list plus per-file sha256.
An extension package adds, from its buildTransitive/<Id>.props:
<OwenExtensionDescriptor Include="$(MSBuildThisFileDirectory)owen-extension.json" />The descriptor (spec/OwenExtension.md, spec/owen-extension.schema.json):
{
"owen_extension": 1,
"id": "Owen.TypedBuilder",
"version": "0.1.0",
"requires": {
"host": "0.1.0",
"ownir": 2,
"capabilities": ["ownership", "state-protocol", "heap-effects", "proven-call"]
},
"frontend": { "generators": ["Owen.TypedBuilder.Generator"] }
}Host 0.1.0 supports: OwnIR 2, and the capabilities ownership, state-protocol,
heap-effects and proven-call. The host validates every descriptor before any analysis, and
every violation is a build error, never a skip. The new code family is OWENB, the host's;
no OWN code changes.
| condition | result |
|---|---|
no descriptor at all (Owen.Build referenced alone) |
warning OWENB001 "no Owen extension is active; nothing was analysed". Intentionally inactive, and loud about it. |
owen_extension ≠ 1, a missing or ill-typed field, a duplicate id |
error OWENB002 |
requires.ownir ≠ 2, or requires.host newer than the host |
error OWENB003 (incompatible extension) |
| a required capability the host does not have | error OWENB004, naming it and the extension |
| no Rust core for this platform in the package, or an unsupported platform | error OWENB005 (the locator's D3.1 message) |
| the extractor refuses a construct (exit 2) | error OWENB010 at the refused file(line) |
the core refuses the facts (exit 2, e.g. a proven_call not proven harmless) |
error OWENB011 at the file(line) the refusal names |
| an internal failure of either child | error OWENB012 |
What the host does with valid descriptors.
- Manifest. It writes
obj/owen/extensions.json, sorted byid: host version, OwnIR version, and each active extension'sid,versionand capabilities. - Logging. It logs
Owen: active extensions: <id> <version>, …at high importance. - Generator outputs. It collects every
frontend.generatorsname, takes the generated C# under$(CompilerGeneratedFilesOutputPath)/<generator assembly>/**, and passes those files to the extractor explicitly (K-2). The host props setEmitCompilerGeneratedFiles=true, so those files exist. - Analysis. It runs the existing check pipeline exactly once, whatever the number of extensions, with
--engine rust --format msbuild --severity $(OwenSeverity).
The host contains no extension identifier: the gate greps the host sources and targets for
TypedBuilder and expects zero hits. Two extensions coexist by construction, because the
descriptors are a list.
Where it lives.
- Validation, manifest, generated-file collection and refusal canonicalisation: a new
owen build-checksubcommand of the sameOwnSharp.Cliprogram. It is the generic host entry point the targets call, and it reusesCheckCommand's pipeline rather than copying it. - Analysis semantics: none in the host; they stay in the Rust core.
MSBuild properties:
OwenSeverity:warningby default,erroropt-in.- Why warning is the default: the first install never turns a legacy project red, because the existing
--severityonly changes how a finding is shown, never the verdict. - Refusals are the exception: OWENB010/011 are always errors. They only arise in a protocol the developer declared, and "cannot be checked" must never read as clean.
- Why warning is the default: the first install never turns a legacy project red, because the existing
OwenEnabled:trueby default,falseto switch the host off.
The target runs after the build has copied its output, because the extractor binds against the
built bin/. It is skipped in design-time builds and runs on every build, so warnings do not
vanish on an incremental build.
Owen.TypedBuilder.Generator: anetstandard2.0IIncrementalGeneratorover Roslyn 4.8.0.- Shared core. It and the existing CLI (
frontend/roslyn/Own.TypedBuilder) share one model/renderer library,Own.TypedBuilder.Core. The CLI's output stays byte-identical to the golden, sotyped_builder_gatekeeps passing unchanged. - The attribute vocabulary (
TypedProtocol,ProtocolState,BuilderRequired,Transition,ProtocolToken,ProtocolRegion) is emitted by the generator's post-initialisation step asinternaltypes in the global namespace. A consumer therefore writes[TypedProtocol]with nousingand no dependency, and the generated protocol text stays the golden. - A refused declaration is a generator error
OWENTB001carrying the core's refusal line. It is never empty output.
linux-x64 and win-x64: exactly the platforms Owen.Cli packs today. macOS is not
claimed. On any other platform the host fails with OWENB005; it never skips.
No change to:
- OwnIR (version 2) or the ownership, state-protocol or H0/H1 semantics;
- any
OWNcode or message; - the Rust core's analysis;
- T0 or the P-022 harness.
git diff --stat 88cb8cc3..HEAD -- ownlang rust spec/OwnIR.md spec/Bridge.md spec/ownir.schema.json frontend/roslyn/OwnSharp.Extractor docs/evidence/calibration scripts/perf_baseline.py
must be empty. If the substrate turns out to need one of these, the slice stops with a named
blocker.
The salvage audit is committed alongside this document. For this slice:
- REJECT Snipper's binary discovery (setting → bundled → PATH, with silent fall-through);
- ADAPT pack-time bundling with a hard failure on a missing binary (already the
Owen.Clirule), and the one-shot child-process discipline (drain both streams, map the exit code, kill on cancel).
Everything else is for a future Owen.VisualStudio or LSP and is not built here.
- E1 (generic VSIX host:
IErrorTag+ITableDataSource+ a long-lived Owen process). This environment has no Windows and no Visual Studio, so the spike can only be specified. The verdict will be recorded as not executed, with the reasons and the exact acceptance it would need. No VSIX is built. - E2 (Roslyn
DiagnosticAnalyzer→ native Rust core). A minimal experimental project underexperiments/owen-e2-native-analyzer/, outside every shipping path and every CI build. It is exercised on Linux withdotnet buildand repeated builds through the compiler server. Windows, the Visual Studio x64 in-proc compiler and unload/reload are recorded as not executed here. Verdict:VIABLEorREJECT. - Architecture invariant. An extension is not an IDE plugin. A future IDE host is exactly one
Owen.VisualStudio, and a future server is exactly one Owen diagnostic/language server, both serving all active extensions. NoOwen.TypedBuilder.VisualStudio.
Gate: scripts/owen_extension_gate.py. It runs on Linux and Windows CI, packs the candidate
packages, and tests them in an isolated consumer.
The isolated consumer.
- Location: a fresh directory outside the checkout.
- Environment:
PATHholds only the directory of thedotnetrunning the gate;HOME,DOTNET_CLI_HOMEandNUGET_PACKAGESare fresh temporary directories;- the
nuget.confighas<clear/>, the isolated feed and nuget.org, with package source mappingOwen.*→ the isolated feed only.
- Preconditions:
python,python3,cargo,rustcandowenresolve to nothing on thatPATH;- the consumer contains no path into the checkout.
The gate fails if any precondition fails.
Steps, every one PASS/FAIL:
| id | step | PASS iff |
|---|---|---|
| U1 | dotnet new web -n OrderBackend, dotnet add package Owen.TypedBuilder --version 0.1.0, then copy in the TB-MVP domain/data/endpoints/Shipping sources. Not Order.Protocol.cs (now generated) and not TypedBuilder.cs (now provided). |
the project file holds exactly one Owen PackageReference |
| A | dotnet build |
succeeds. The generated Order.Protocol.g.cs text = #nullable enable\n + the committed golden. obj/owen/extensions.json lists Owen.TypedBuilder 0.1.0 and its capabilities |
| B | stage C1_draft_approve |
the build fails with CS1061 naming Approve, and no manual generator step ran |
| C | stage C8_stale_draft with OwenSeverity=error |
the build fails with OWN002 |
| D | the C build's output | holds a canonical file(line): error OWN002: … line, whose file resolves from the project directory to the staged file and whose line is 18. With the default severity, the same build succeeds and shows the line as warning OWN002 |
| E | U1–D under the stripped environment | all pass; the build logs contain no path into the Own.NET checkout |
| F | the whole TB-MVP corpus (8 positive, 20 negative, 2 limits) through the package, OwenSeverity=error |
compiler cases: their CS code; extractor cases: OWENB010 with the registered text; core verdict cases: their OWN code(s); core refusals: OWENB011 with the registered text; positives: no OWN/OWENB diagnostic. K1 gives OWN001, K2 is clean, P5 and P8 have the proven_call (from --emit-facts) |
| G | the TB-MVP Acceptance runner against the consumer project |
44/44, with the transcript byte-identical to the committed samples/OrderBackend/evidence/acceptance.txt |
| H | the consumer with both Owen.TypedBuilder and Owen.TestExtension |
the manifest lists both, analysis runs once, and the result is unchanged |
Mutation and negative controls, each run in the isolated consumer:
| id | mutation | PASS iff |
|---|---|---|
| M1 | delete owen-extension.json from the installed Owen.TypedBuilder |
OWENB001 is shown, and C8 produces no OWN002: the acceptance's C check would fail, so the gate detects it |
| M2 | add an unknown capability to the descriptor | error OWENB004 naming it |
| M3 | delete the bundled own-cli for this platform |
error OWENB005, never a clean build |
| M4 | requires.ownir: 3, and separately requires.host: 9.0.0 |
error OWENB003 each |
| M5 | remove the host's build target (empty Owen.Build.targets) |
C8 produces no OWN002 and the gate's C check reports the absence |
| M6 | run from a consumer with no checkout | pass (it is E) |
| M7 | add Owen.TestExtension |
no file of the host changes (H), and the host source has 0 occurrences of TypedBuilder |
The other gates, on the final head:
tests/run_tests.py,ruff,mypy;cargo fmt --check,cargo clippy --all-targets,cargo test;protocol_gate.py --rust,heap_effects_gate.py,typed_builder_gate.py --rust;- the
Owen.Clipack/install gate (the payload byte-identity above, then install → check →OWN001); - the P-022 merge gate;
- server CI on Linux and Windows.
GO_OWEN_EXTENSION_ALPHA iff A–H, M1–M7 and every listed gate pass:
PackageReference Owen.TypedBuilder → generated API → CS diagnostics → OWN diagnostics on build
→ no external toolchain or setup → generic extension discovery proven.
Otherwise the verdict is NO_GO_OWEN_EXTENSION_ALPHA, with the exact blocker. The failing
case is preserved, and no foundation change is made to get to green. If E fails because the
package cannot run without a global Owen install, the verdict is NO_GO.
Out of scope even after GO:
- a production VSIX, LSP or VS Code extension;
- the Memory or TypeDisciplines extensions;
- BCL summaries (#394), typed write targets (#395), affine tokens (#392), diagnostic wording (#393), concurrency (#396);
- a second aggregate;
- a repo-wide rename.
These are corrections found while building the gate. They were committed before any official run.
-
M1 splits in two.
- M1a: delete
owen-extension.jsonfrom the installedOwen.TypedBuilder, but keep its props, which still declares the descriptor. Expect errorOWENB002("the extension descriptor cannot be read"), not the registeredOWENB001. - M1b: remove the extension's whole declaration (props and descriptor). Expect warning
OWENB001and noOWN002. - Why: treating a declared but missing descriptor as "no extension" would be fail-open, so it is an error. The contract (§ Extension discovery) already says a descriptor that cannot be read is
OWENB002; the registered M1 expectation contradicted it.
- M1a: delete
-
The
Owen.Clipayload criterion is restated. "File-for-file identical" is not achievable for the two assemblies built from this repository.ownsharp.dllgainsbuild-checkby design.ownsharp-extract.dllcarries commit-stamped build metadata although its source is unchanged: the bytes differed between two packs of the same extractor source from two commits, even underContinuousIntegrationBuild.The criterion is now:
- an identical file list;
- every other payload file byte-identical: Roslyn,
deps.json/runtimeconfig.json, vendored.py, Rust core; git diff 88cb8cc3..HEAD -- frontend/roslyn/OwnSharp.Extractorempty;- gate A (install →
owen check→OWN001) passing on the head.
-
G: the acceptance runner builds with
-p:OwenEnabled=false.- Why the host runs there at all: the runner references the consumer project, so the host reaches it through
buildTransitive, as it reaches any dependent project. - What the host does there: the runner opens a protocol region in top-level statements, which the extractor does not model, so the host refuses it with
OWENB010. That is fail-closed behaviour, not a defect. - Why switching it off is acceptable: the runner is a test harness, not the product under test. The consumer itself is analysed by A–F and H.
The registered criterion (44/44, transcript byte-identical) is unchanged.
- Why the host runs there at all: the runner references the consumer project, so the host reaches it through
-
The host runs with
DOTNET_ROLL_FORWARD=Major, set on the Exec. The host payload targets .NET 8, and a machine with only a newer SDK has no .NET 8 runtime. The isolated consumer keeps the registered--framework net8.0.
Run on 88cb8cc31c49d730290613208947102d0737cad2, a clean tree except for the two documents of this commit:
| gate | result |
|---|---|
python tests/run_tests.py |
rc 0, zero FAIL lines (the one textual hit is an ok[...] line quoting the word) |
ruff check . / mypy |
rc 0 / rc 0 |
cargo fmt --check / cargo clippy --all-targets / cargo test --no-fail-fast |
rc 0 / rc 0 / rc 0, 290 passed, 0 failed |
python scripts/protocol_gate.py --rust …/own-cli |
0 failures |
python scripts/heap_effects_gate.py |
PASS |
python scripts/typed_builder_gate.py --rust …/own-cli |
0 failures, transcript sha256 ba447304cdf957ee |
Owen.Cli pack/install (gate A, replayed locally) |
pass |
| P-022 merge gate | success on #391's final head 04cb779, which became this base |
protocol_gate.py covered 29 documents byte-identical on both CLIs. typed_builder_gate.py covered 8 positive, 20 negative and 2 limit cases, with 44 acceptance checks run twice.
Gate A, replayed locally.
- Pack: with this platform's release
own-clistaged underlinux-x64/. - Install: with
--tool-pathfrom an isolated feed, with a freshHOMEandNUGET_PACKAGES. - Check:
owen checkon a seeded leak givesOWN001with rc 1; on clean code it gives rc 0. - One environmental detail: this container's dotnet lives in
/root/.dotnet, so the tool's native apphost needsDOTNET_ROOT. The first attempt, without it, failed in the apphost before Owen ran.
K-4, observed rather than assumed. A PackageReference to that same Owen.Cli 0.1.0 fails restore with NU1212: Invalid project-package combination … DotnetToolReference project style can only contain references of the DotnetTool type.