diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d3ec9bd8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,49 @@ +# The style and analyzer severities of the build (ADR-0002 D3, ADR-0003 D2). +# Nothing here lowers a rule's severity (ADR-0007 D6). +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{csproj,props,targets,slnx,config,json,yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.cs] +# Usings are explicit (ImplicitUsings disable), and an unused one is an error. +dotnet_diagnostic.IDE0005.severity = warning +csharp_style_namespace_declarations = file_scoped:warning +dotnet_diagnostic.IDE0161.severity = warning +csharp_using_directive_placement = outside_namespace:warning +dotnet_sort_system_directives_first = true + +# ADR-0003 D2: the trim, AOT and single-file analyzers' diagnostics are +# errors, whatever their default severity. +dotnet_diagnostic.IL2026.severity = error +dotnet_diagnostic.IL2046.severity = error +dotnet_diagnostic.IL2050.severity = error +dotnet_diagnostic.IL2055.severity = error +dotnet_diagnostic.IL2057.severity = error +dotnet_diagnostic.IL2058.severity = error +dotnet_diagnostic.IL2059.severity = error +dotnet_diagnostic.IL2060.severity = error +dotnet_diagnostic.IL2062.severity = error +dotnet_diagnostic.IL2067.severity = error +dotnet_diagnostic.IL2070.severity = error +dotnet_diagnostic.IL2072.severity = error +dotnet_diagnostic.IL2075.severity = error +dotnet_diagnostic.IL2087.severity = error +dotnet_diagnostic.IL2091.severity = error +dotnet_diagnostic.IL3000.severity = error +dotnet_diagnostic.IL3001.severity = error +dotnet_diagnostic.IL3002.severity = error +dotnet_diagnostic.IL3050.severity = error +dotnet_diagnostic.IL3051.severity = error +dotnet_diagnostic.IL3053.severity = error diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6ac59892 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Line feeds in every checkout, Windows included: the gate scripts run under +# bash on every runner, and a build's bytes do not depend on the platform. +* text=auto eol=lf + +# The vendored fixtures are compared by sha256, so git never touches their bytes. +tests/fixtures/ddd/** -text diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index 36f2a391..1b249d52 100644 --- a/.github/workflows/gates.yml +++ b/.github/workflows/gates.yml @@ -14,6 +14,10 @@ concurrency: group: gates-${{ github.event.pull_request.number }} cancel-in-progress: true +env: + DOTNET_NOLOGO: "true" + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + jobs: commits: name: commits @@ -63,7 +67,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: No Rust, Node or Python toolchain file at the root + - name: The only toolchain is .NET, pinned in global.json run: eng/gates/no-toolchain.sh . selftest: @@ -73,3 +77,77 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Each check fails on its plant and passes its clean case run: eng/gates/selftest.sh + + # The .NET build (ADR-0002, ADR-0003, ADR-0013). Checked out at the pull + # request's head, not GitHub's merge commit, so the commit the host stamps + # is the head that is reviewed. + build-test: + name: build-test + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 + with: + global-json-file: global.json + - name: Build, warnings as errors + run: dotnet build ProductCli.slnx -c Release + - name: Test, the layering and build-policy probes included + run: dotnet test -c Release --no-build + + # ADR-0003 D3, D5: the Native AOT publish of the host on every supported + # runtime identifier, each on a runner of its own operating system (Native + # AOT does not cross-compile between them), and the smoke test of the + # published binary. + aot: + name: aot (${{ matrix.rid }}) + strategy: + fail-fast: false + matrix: + include: + - rid: linux-x64 + os: ubuntu-24.04 + - rid: linux-arm64 + os: ubuntu-24.04-arm + - rid: osx-arm64 + os: macos-15 + - rid: win-x64 + os: windows-2025 + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 + with: + global-json-file: global.json + - name: The runtime identifier is one the build policy supports + env: + RID: ${{ matrix.rid }} + run: | + supported=$(dotnet msbuild src/ProductCli.Host/ProductCli.Host.csproj -getProperty:ProductCliRuntimeIdentifiers) + echo "supported: $supported" + case ";$supported;" in + *";$RID;"*) ;; + *) echo "aot: $RID is not in ProductCliRuntimeIdentifiers"; exit 1 ;; + esac + - name: Publish, Native AOT + env: + RID: ${{ matrix.rid }} + run: dotnet publish src/ProductCli.Host/ProductCli.Host.csproj -c Release -r "$RID" -o "artifacts/publish/$RID" + - name: Smoke test of the published binary + env: + RID: ${{ matrix.rid }} + run: | + ext="" + if [ "$RID" = win-x64 ]; then ext=.exe; fi + binary="artifacts/publish/$RID/product$ext" + version=$(dotnet msbuild src/ProductCli.Host/ProductCli.Host.csproj -getProperty:Version) + eng/gates/smoke.sh "$binary" "$version" "$(git rev-parse HEAD)" + size=$(wc -c < "$binary" | tr -d ' ') + echo "aot: $RID: $binary is $size bytes" + echo "| $RID | \`product$ext\` | $size bytes |" >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f38dcbcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +artifacts/ +*.user +.vs/ diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 00000000..42647054 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,6 @@ +# Analyzer options for the whole compilation (ADR-0002 D3, ADR-0013 D2). +is_global = true + +# DD0001: a ProductCli.* project that declares no ArchLayer is itself an error, +# so no project can stay unplaced in the layer table. +dd_require_layer = true diff --git a/AGENTS.md b/AGENTS.md index a66f1264..61fe1cc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,10 +14,85 @@ ADRs and its tests**, and its release descriptors (D1.24). - `docs/traceability/` holds one record per agent session (ADR-0007 D2). - `releases/` holds the release descriptors (ADR-0007 D3). - `eng/gates/` holds the checks `.github/workflows/gates.yml` runs. +- `src/` holds the eight layer projects, `tests/` their tests and the probes + under `tests/fixtures/` (slice 0, D1.25, #167). -There is no source code yet. Slice 0 (ADR-0002, ADR-0003, ADR-0004) brings -the eight layer projects, the layering analyzer, the AOT publish gate and the -package register, in its own pull request. +## Build, test, publish + +The SDK is pinned in `global.json` (`10.0.401`, `rollForward: latestFeature`), +with Microsoft.Testing.Platform as the test runner. Warnings are errors. + +```bash +dotnet build ProductCli.slnx -c Release +``` + +```bash +dotnet test -c Release --no-build +``` + +```bash +dotnet publish src/ProductCli.Host/ProductCli.Host.csproj -c Release -r linux-x64 -o artifacts/publish/linux-x64 +``` + +```bash +eng/gates/smoke.sh artifacts/publish/linux-x64/product "$(dotnet msbuild src/ProductCli.Host/ProductCli.Host.csproj -getProperty:Version)" "$(git rev-parse HEAD)" +``` + +- **Build the solution before testing.** The layering and build-policy probes + are built by the tests with `dotnet build`, against the solution's outputs. +- **Native AOT publishes on its own operating system only.** The supported + runtime identifiers are `ProductCliRuntimeIdentifiers` in + `Directory.Build.props`: `linux-x64`, `linux-arm64`, `osx-arm64` and + `win-x64` (ADR-0003 D5). CI publishes each one on its own runner. +- **`product --version`** is the host's one behaviour in slice 0. Every other + argument exits 2 with one line on stderr naming the step of the order of + work its verb arrives in. **That line is not a format**: it is for a person, + nothing may parse it, and it changes as verbs are built. + +## The layers + +ADR-0002 D1. Each project references only the one below it, Varve and the +BCL. `ArchLayer` is set in each project file, `ArchFamily` is `ProductCli`, +and DD0001 (`DecisionDriven.Analyzers`, ADR-0013) fails the build on a +reference that is not strictly downward, and on a `ProductCli.*` project with +no layer (`.globalconfig`). + +| Layer | Project | Holds | +|---|---|---| +| 0 | `ProductCli.Vocabulary` | the generated node records, the `sdlc:` terms, the edition | +| 1 | `ProductCli.Canonical` | the RDFC graph, the exporters, the `dpp/1` record; references `Varve.Rdf` | +| 2 | `ProductCli.Shapes` | the shape-engine contract, over Varve | +| 3 | `ProductCli.Draft` | the draft-store contract, over Varve; DPP commits | +| 4 | `ProductCli.Simulation` | derivation, rule evaluation, findings | +| 5 | `ProductCli.Runner` | the wire protocol, conformance | +| 6 | `ProductCli.Session` | the verbs, the phase gate | +| 7 | `ProductCli.Host` | the composition root and the only executable, `product` (Native AOT) | + +Every library is `IsAotCompatible` and `IsTrimmable`, with the AOT and trim +analyzers' diagnostics as errors (ADR-0003 D2), and has a public API baseline, +`PublicAPI.Shipped.txt` and `PublicAPI.Unshipped.txt`, beside its project +file (ADR-0002 D3). A new public member is a line in that diff. + +`ProductCli.Vocabulary.Generator`, `ProductCli.Workbench` and +`ProductCli.Workbench.Contracts` do not exist yet: each arrives with the slice +that needs it. + +## Packages + +`Directory.Packages.props` is the register (ADR-0004, as amended by +ADR-0013 D4). Versions are central: no project file carries one. + +- **A new package needs an ADR**, and a `PackageVersion` entry whose `Adr=` + names it. A test-only entry also carries `Scope="tests/"`. +- **The build fails** on an entry without `Adr=` (`PCLI0001`), on an `Adr=` + naming no file in `docs/decisions/` (`PCLI0002`), and on a reference from + outside an entry's `Scope=` (`PCLI0003`). `BuildPolicyTests` builds each + failing case on every run. +- **Versions are resolved from nuget.org**, never from memory, and the date is + recorded in the commit that adds or changes them. +- Every `DecisionDriven.Analyzers` rule is on, as an error. The first public + interface needs a decision set in that package's format, so that + `[Contract]` can cite it (ADR-0013, Consequences). **The Rust workspace is frozen at the tag `rust-final`** (commit `70395df`). Nothing at the tag changes. Anything the rewrite reads from the Rust tool — the @@ -56,8 +131,8 @@ interim rule of Writ ADR 0008); `docs/decisions/README.md` states the rule. were. - ADR-0001 to ADR-0012 are held byte for byte against `docs/decisions/collected-2026-11-03.md`; `adr-text` checks it. -- Every `PackageReference` carries `Adr=` naming the ADR that admits it - (ADR-0004), from slice 0. +- Every `PackageVersion` in `Directory.Packages.props` carries `Adr=` naming + the ADR that admits it (ADR-0004 D1, as amended by ADR-0013 D4). ## Gates @@ -70,8 +145,10 @@ is a Bash script in `eng/gates/` and runs locally the same way: | `signatures` | `eng/gates/signatures.sh ` | GitHub does not report a commit of the pull request as verified | | `adr-shape` | `eng/gates/adr-shape.sh [dir]` | an ADR lacks its number, status, alternatives or consequences, or an unexpected file is in `docs/decisions/` | | `adr-text` | `eng/gates/adr-text.sh [dir]` | ADR-0001 to ADR-0012 differ by a byte from their sections of the collected file, with the Draft 4 notes | -| `no-toolchain` | `eng/gates/no-toolchain.sh [root]` | a Rust, Node or Python toolchain file is at the root | -| `selftest` | `eng/gates/selftest.sh` | any check above passes on the case planted for it | +| `no-toolchain` | `eng/gates/no-toolchain.sh [root]` | a Rust, Node or Python toolchain file is anywhere in the tree, or `global.json` is missing, pins another SDK, roll-forward or test runner, or has a second copy below the root | +| `selftest` | `eng/gates/selftest.sh` | any check above, or `smoke.sh`, passes on the case planted for it | +| `build-test` | `dotnet build` then `dotnet test`, both `-c Release` | the build has a warning or error, or a test fails | +| `aot ()` | `dotnet publish -r `, then `eng/gates/smoke.sh ` | the Native AOT publish fails on that runtime's own runner, or the binary's `--version` bytes, its exit code, or its refusal of a verb are not as required | `selftest` plants a failing case for every check in a scratch repository on the runner, so each run shows that each check still fails when it should. @@ -145,7 +222,8 @@ verb's result is rejected in review. - **A suppression of any rule** (ADR-0007 D6). - **Copied Rust code.** The Rust tool is read at `rust-final` for its behaviour, and its bytes are never carried. -- **A package without `Adr=`**, or a version taken from memory (ADR-0004). +- **A package without `Adr=`**, or a version taken from memory (ADR-0004, as + amended by ADR-0013). - **A commit** unsigned, without `Refs #N` or a DCO sign-off, or naming a model. - **Rewriting or deleting a tag or a branch**, force-pushing `main`, or changing anything at `rust-final`. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..e9b8fd39 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,91 @@ + + + + + + $([MSBuild]::EnsureTrailingSlash('$(MSBuildThisFileDirectory)')) + + + + net10.0 + latest + enable + + disable + + + + true + latest-recommended + true + true + + + + + true + true + true + + + + + false + false + + + + + + + + + + + false + + + + + 0.8.0-preview.2 + + false + + + + + linux-x64;linux-arm64;osx-arm64;win-x64 + + + + + ProductCli + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000..52f71f66 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,134 @@ + + + + + <_ProductCliPath>$([MSBuild]::MakeRelative('$(ProductCliRoot)', '$(MSBuildProjectDirectory)').Replace('\', '/').TrimEnd('/'))/ + src/$(MSBuildProjectName)/ + $(_ProductCliPath) + true + true + true + + + + + true + true + + + + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ProductCliAdrFile Include="$(ProductCliRoot)docs/decisions/ADR-*.md" /> + <_ProductCliAdrNumber Include="$([System.String]::Copy('%(_ProductCliAdrFile.Filename)').Substring(4, 4))" /> + + <_ProductCliEntry Include="@(PackageVersion)" PackageId="%(Identity)" DefinedIn="%(DefiningProjectFullPath)" /> + <_ProductCliCitation Include="@(_ProductCliEntry->'%(Adr)')" Condition="'%(_ProductCliEntry.Adr)' != ''" /> + <_ProductCliDangling Include="@(_ProductCliCitation)" Exclude="@(_ProductCliAdrNumber)" /> + + <_ProductCliScoped Include="@(PackageVersion)" Condition="'%(PackageVersion.Scope)' != ''" /> + <_ProductCliScopedUnused Include="@(_ProductCliScoped)" Exclude="@(PackageReference)" /> + <_ProductCliScopedUsed Include="@(_ProductCliScoped)" Exclude="@(_ProductCliScopedUnused)" /> + <_ProductCliScopedUsed Update="@(_ProductCliScopedUsed)" InScope="$(ProductCliPolicyPath.StartsWith('%(Scope)'))" /> + + + + + + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..7f279851 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,59 @@ + + + + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ProductCli.slnx b/ProductCli.slnx new file mode 100644 index 00000000..b9c4bcf6 --- /dev/null +++ b/ProductCli.slnx @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index a28eebc9..19de7597 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,40 @@ The product is defined in decision-driven-design: This repository holds the tool, its ADRs (`docs/decisions/`) and its tests. Agents start at [`AGENTS.md`](AGENTS.md). +## Build and test + +The repository is the skeleton of slice 0 (D1.25): the eight layer projects +under `src/`, their tests under `tests/`, and a host whose only behaviour is +`product --version`. It needs the .NET SDK that `global.json` pins (10.0.401, +or a later feature band of 10.0) and nothing else. + +```bash +dotnet build ProductCli.slnx -c Release +``` + +```bash +dotnet test -c Release --no-build +``` + +The tests build probe projects with `dotnet build` against the solution's +outputs, so build the solution first. + +To publish the native binary, `product`, for your platform: Native AOT +compiles for the operating system it runs on, and needs that platform's +native toolchain (clang on Linux, Xcode on macOS, the Visual C++ build tools +on Windows). + +```bash +dotnet publish src/ProductCli.Host/ProductCli.Host.csproj -c Release -r linux-x64 -o artifacts/publish/linux-x64 +``` + +```bash +artifacts/publish/linux-x64/product --version +``` + +The supported runtime identifiers are `linux-x64`, `linux-arm64`, `osx-arm64` +and `win-x64`. + ## The Rust tool The Rust workspace is frozen at the tag diff --git a/docs/decisions/ADR-0013-build-time-analyzers-and-test-stack.md b/docs/decisions/ADR-0013-build-time-analyzers-and-test-stack.md new file mode 100644 index 00000000..3072062c --- /dev/null +++ b/docs/decisions/ADR-0013-build-time-analyzers-and-test-stack.md @@ -0,0 +1,46 @@ +# ADR-0013: Build-time analyzers and the test stack + +**Status:** Accepted (2026-11-03) +**Date:** 2026-11-03 +**Definition:** chapter 7, Constraints; D1.25 +**Amends:** ADR-0004 D1 (its wording only, D4 below) + +## Context + +ADR-0002 D3 requires layering to be enforced by an analyzer that fails the build, and every library to have a public API baseline. ADR-0003 D4 bans reflection in shipped code. ADR-0004 D2 admits Varve, and "the test framework and the property-testing library … once named in slice 0's plan". Slice 0 (D1.25, #167) named them, and needs the analyzers, which no ADR has yet admitted. + +Every package here was resolved from nuget.org on 2026-11-03. + +Varve consumes `DecisionDriven.Analyzers` (source `Hafeok/decision-driven-analyzers`) for its layering. Slice 0's plan checked its `0.1.0-preview.10` package against two questions: + +- **Does its layering rule apply to a family other than Varve's?** Yes. DD0001 reads `ArchFamily=F` and `ArchLayer=n` from the project. It checks every referenced assembly whose name begins `F.`, reading that assembly's `[assembly: ArchLayer]` from metadata, and requires it strictly below `n`. Nothing in the rule names Varve. +- **Is it AOT-neutral?** Yes. The package holds `analyzers/dotnet/cs/*.dll` (netstandard2.0) and `build/` props and targets, and no `lib/`. It is marked `developmentDependency`. + +## Decision + +- **D1. The test stack.** Both packages are test-only: `Scope="tests/"` in the register, referenced only by the `*.Tests` projects, and never published with AOT. ADR-0004 D2 admits them, so their register entries cite `Adr="0004"`. + - **xunit v3 (`xunit.v3` 4.0.2)**, on Microsoft.Testing.Platform. `global.json` names the platform as the test runner, and `xunit.v3` 4.x brings its v2 adapter, `xunit.v3.mtp-v2`. This is what Varve uses. + - **CsCheck (5.0.1)** for property tests. It is Apache-2.0 and has no dependencies of its own. +- **D2. The analyzers.** These are admitted by this ADR, and their entries cite `Adr="0013"`. + - **`DecisionDriven.Analyzers` (0.1.0-preview.10)**, for ADR-0002 D3's layering rule: DD0001 under `ArchFamily=ProductCli`, with every project's layer declared and `dd_require_layer = true`. It is referenced from `Directory.Build.props` by every project, with `PrivateAssets="all" IncludeAssets="analyzers;build"`. It is a preview, so a bump is a register change, made in a PR. + - **`Microsoft.CodeAnalysis.PublicApiAnalyzers` (5.6.0)**, for ADR-0002 D3's public API baselines. Every library carries `PublicAPI.Shipped.txt` and `PublicAPI.Unshipped.txt`, and the build fails on a library without them, since the analyzer is silent when the files are absent. + - **`Microsoft.CodeAnalysis.BannedApiAnalyzers` (5.6.0)**, for ADR-0003 D4. `eng/BannedSymbols.txt` bans, in every project under `src/`, the reflection that defeats trimming and Native AOT: `System.Reflection.Emit`, `Type.GetType` by name, reflective `Activator.CreateInstance`, and `Assembly.Load`, `LoadFrom` and `LoadFile`. Each entry cites ADR-0003 D4. +- **D3. Nothing ships.** No analyzer assembly reaches the binary or any output: every analyzer reference is `PrivateAssets="all"`, and none of the three packages has a `lib/` asset. The analyzers do add bytes to our assemblies, and only these: the `DecisionDriven.Analyzers` generator compiles internal attribute types into each assembly it analyzes (`DecisionDriven.ArchLayerAttribute` and the decision-citation attributes), and one `[assembly: ArchLayer(n)]`. They are plain attributes, read by no code of ours at run time, with no reflection and no effect on AOT. +- **D4. The register entry is the `PackageVersion`.** Under central package management, the register is `Directory.Packages.props`, Varve's form, which ADR-0004 D1 adopts. A `PackageReference` there carries no version. + - This ADR therefore **amends the wording of ADR-0004 D1**, "Every `PackageReference` carries `Adr=`". It now reads: every `PackageVersion` in `Directory.Packages.props` carries `Adr=` naming the ADR that admits it, and a test-only entry also carries `Scope=`. The build fails on an entry without `Adr=`, on an `Adr=` that names no ADR in `docs/decisions/`, and on a reference from a project outside the entry's `Scope=`. + - ADR-0004 is cited from here on as amended by this ADR. Its file is not edited (ADR-0001 D3). + +## Alternatives considered + +- **FsCheck for property tests.** Rejected. FsCheck 3.6.0 depends on `FSharp.Core`, which puts a second language's runtime library in every test closure for no capability CsCheck lacks. +- **No property-testing library in slice 0.** Rejected: ADR-0004 D2 names one in the starting register, and slice 0 has a property to test (every argument but `--version` exits 2). +- **A local layering analyzer**, the fallback ADR-0002 D3 allows. Rejected: the package's DD0001 applies to our family unchanged, and a local analyzer is code to keep for no gain. +- **Enabling only DD0001 of `DecisionDriven.Analyzers`.** Not possible without lowering the other rules' severity, which is a suppression: DD0008 reports it, and ADR-0007 D6 forbids it. +- **Requiring `Adr=` on each `PackageReference` as well.** Rejected: it would repeat on every reference what the register already states once, and two copies of one fact can disagree. +- **Compiling the layering probes in process.** Rejected: that needs `Microsoft.CodeAnalysis.CSharp` as one more test package, and an analyzer configuration built by hand to imitate MSBuild. The probes are built with `dotnet build` under the real build files instead, which tests the wiring as well as the rule. + +## Consequences + +- **Every rule of `DecisionDriven.Analyzers` is on as an error**, not only DD0001. That includes, among others, `[Contract]` on every public interface (DD0009), the contract type vocabulary (DD0010), no naked primitives on contract and model surfaces (DD0013), and no mutable static state (DD0004). This is ADR-0002 D3's intent. None fires on the slice 0 skeleton. **The first public interface needs a decision set in the package's format** (YAML front matter in a markdown file, read through `DdLedgerDirectory`), so that `[Contract]` can cite it. That is a question for the slice that adds the interface. +- The register is a gate in the build, not a convention. Its failing cases are permanent tests (`BuildPolicyTests`), built on every run. +- A new package needs an ADR and a `PackageVersion` entry with `Adr=`, and `Scope=` when it is test-only. `AGENTS.md` states the rule. diff --git a/docs/traceability/2026-11-03-issue-167-slice-0.md b/docs/traceability/2026-11-03-issue-167-slice-0.md new file mode 100644 index 00000000..28042773 --- /dev/null +++ b/docs/traceability/2026-11-03-issue-167-slice-0.md @@ -0,0 +1,314 @@ +# Slice 0: the repository skeleton (D1.25) + +> **Recorded contemporaneously**, by the session that did the work, under +> ADR-0007 D2, in the shape of Writ's `docs/traceability/` records (Writ ADR +> 0008). The maintainer's messages are verbatim. The transcript is held by the +> maintainer. + +| | | +|---|---| +| **Issue** | [#167](https://github.com/mindovermachine-dev/product-cli/issues/167), opened by this session | +| **Pull request** | [#168](https://github.com/mindovermachine-dev/product-cli/pull/168) | +| **Date** | 2026-11-03 | +| **Tool** | Claude Code, a cloud session | +| **Model** | not recorded (ADR-0007 D1) | +| **Session identifier** | `session_01NbWSYTf9GvhrtXDW5cHcYg` | +| **Branch** | `claude/affectionate-darwin-89uq7g`, from `main` at `53bd54758ba71f7a810f91ddf3a6c83d5e653145` | +| **Identity** | `Claude (Emil) `, SSH-signed with key `SHA256:xDu5EyJd05ROum1g+8Aj1Up3MjE6RbHnI70/xNHljDY` | +| **Definition** | decision-driven-design `799cbe84f2a010fb5b65a687eb253666948c73c8`, D1.25 | + +## The prompts + +The first message: + +> # Session prompt: slice 0, the repository skeleton (D1.25) +> +> You work in `mindovermachine-dev/product-cli`. This session holds Emil's signing key. Every commit is signed and must show as verified on GitHub. +> +> **Stop and ask before you start if any of these is false:** +> - the Rust-removal PR, #166 (D1.24), is merged: `main` is at `53bd547` or a descendant of it. It holds `AGENTS.md`, `CLAUDE.md`, `README.md`, `LICENSE`, `NOTICE`, `docs/`, `releases/`, `.github/workflows/gates.yml` and `eng/gates/`, and nothing else; +> - every new commit must pass the `gates.yml` checks already on `main` (`commits`, `signatures`, `adr-shape`, `adr-text`, `no-toolchain`, `selftest`). This slice adds a .NET toolchain at the root, so `no-toolchain` must change in the same PR to allow it. Say so in the plan, with the exact rule that replaces it; +> - `docs/decisions/` holds ADR-0001 to ADR-0012; +> - decision-driven-design `main` carries D1.25, step 0 of the order of work, in `applications/sdlc/product-cli/product-definition.md`. Outside-in: the definition change lands first. Cite its commit. +> +> This slice needs no `sdlc:` edition and must not wait for one. It adds no vocabulary, no node kind and no product behaviour. +> +> ## Read first +> +> 1. The definition: chapters 6 and 7, D1.24 and D1.25. +> 2. In this repository: +> - ADR-0002, the layers; +> - ADR-0003, AOT and the CI gate; +> - ADR-0004, packages and the register; +> - ADR-0006, tests observe bytes; +> - ADR-0007, commits and sessions; +> - ADR-0010 D6. The workbench projects are *not* in this slice. +> 3. Varve `main`: +> - `Directory.Build.props`, `Directory.Build.targets` and `Directory.Packages.props`: the build policy, the `Adr=` register, `ArchFamily` and `ArchLayer`, and central package management; +> - `global.json` (SDK `10.0.401`, Microsoft.Testing.Platform); +> - `eng/`: the banned-symbol lists and the agent-review gates. +> +> Adopt the pattern. Copy nothing that names Varve's own packages or ADRs. +> 4. The `DecisionDriven.Analyzers` package. Its source is `Hafeok/decision-driven-analyzers`; Varve consumes `0.1.0-preview.10` from nuget.org under its ADR 0063. Confirm from its README and source that: +> - its layering rule (`DD0001`, from `ArchFamily` and `[ArchLayer]`) applies to a family other than Varve's; +> - it is AOT-neutral (an analyzer only). +> +> If either is false, stop and report. ADR-0002 D3 then calls for a local analyzer, which is a plan change. +> +> ## Plan first +> +> Open the tracking issue, "Slice 0: the repository skeleton (D1.25)", and post the plan as a comment. Wait for Emil's approval. Write no files before then. +> +> The plan names: +> - every file; +> - every project and its layer number; +> - every package, with its version as resolved from nuget.org today, its `Adr=` and its scope; +> - the runtime identifiers; +> - the CI jobs; +> - each test, and what it observes. +> +> ## Deliverables (one PR, `Refs #`) +> +> **1. ADR-0013: build-time analyzers and the test stack.** ADR-0004 D2 admits Varve and "the test framework and the property-testing library once named in slice 0's plan". This ADR does three things. +> - It names the test stack: +> - xunit v3 on Microsoft.Testing.Platform, as Varve uses; +> - a property-testing library, with its alternatives: FsCheck or CsCheck, chosen by the plan with a reason. +> +> These are test-only packages and are not published with AOT. +> - It admits the analyzers ADR-0002 D3 requires: +> - `DecisionDriven.Analyzers`, for layering; +> - `Microsoft.CodeAnalysis.PublicApiAnalyzers`, for the baselines; +> - `Microsoft.CodeAnalysis.BannedApiAnalyzers`, if the plan adopts banned-symbol lists. +> - It states why each one is admitted, and that none ships in the binary. +> +> `Adr=` on each entry names ADR-0004 or ADR-0013, whichever admits it. +> +> **2. The build policy.** +> - **`global.json`**: SDK `10.0.401`, `rollForward: latestFeature`, with Microsoft.Testing.Platform as the test runner. +> - **`Directory.Build.props` and `Directory.Build.targets`:** +> - nullable on; +> - warnings as errors; +> - deterministic builds; +> - `IsAotCompatible` and `IsTrimmable` on every library, with the AOT and trim analyzers as errors (ADR-0003 D2); +> - `ArchFamily` set to `ProductCli`, and `ArchLayer` declared per project; +> - the public API baseline files required on every library. +> - **`Directory.Packages.props`:** +> - central versions; +> - every `PackageVersion` carries `Adr=`, and test-only entries also carry `Scope=`; +> - the build fails on a package without `Adr=`. Show the failing case. +> - **`Varve.*` packages** at `0.1.0-preview.15` or the latest preview on nuget.org today, whichever is resolved. Only those the skeleton references: `Varve.Rdf` at least, in `ProductCli.Canonical`. +> +> **3. The eight projects (ADR-0002 D1).** Each lives under `src/` with its test project under `tests/`. +> +> | Project | Layer | +> |---|---| +> | `ProductCli.Vocabulary` | 0 | +> | `ProductCli.Canonical` | 1 | +> | `ProductCli.Shapes` | 2 | +> | `ProductCli.Draft` | 3 | +> | `ProductCli.Simulation` | 4 | +> | `ProductCli.Runner` | 5 | +> | `ProductCli.Session` | 6 | +> | `ProductCli.Host` | 7, the composition root; the only executable | +> +> - Each project references only the projects below it, Varve and the BCL (D2). +> - Each library has its `PublicAPI.Shipped.txt` and `PublicAPI.Unshipped.txt`, empty apart from what deliverable 5 needs. +> - `ProductCli.Vocabulary.Generator`, `ProductCli.Workbench` and `ProductCli.Workbench.Contracts` are **not** created. They arrive with the slices that need them. +> +> **4. The layering tests.** +> - For each layer, a test compiles a probe that references a layer above it, and asserts that `DD0001` (or the analyzer's actual ID) refuses it. The test observes the diagnostic in the compiler's output, not a return value of ours (ADR-0006). +> - One positive probe shows that a downward reference compiles. +> - If `DecisionDriven.Analyzers` provides a test harness, use it. Otherwise compile the probes with Roslyn in the test. +> +> **5. The host and the AOT gate (ADR-0003).** +> - **`ProductCli.Host`** publishes one Native AOT executable, `product`. Its only behaviour in this slice is `product --version`. That prints the product version and the build's source commit, read from assembly metadata stamped at build time, and exits 0. Every other argument exits 2 with a one-line message naming the slice the verb arrives in. This is not scaffolding: it is the smoke test's subject. +> - **The supported runtime identifiers** are recorded in the build policy (D5): `linux-x64`, `linux-arm64`, `osx-arm64` and `win-x64`. If one cannot be built on the available CI runners, the plan says which and why. +> - **CI**, as jobs added to `gates.yml` and running on every PR: +> - build and test, with warnings as errors; +> - for each runtime identifier, a native `dotnet publish -c Release -r ` on a runner of that OS. Native AOT does not cross-compile across operating systems; +> - for each runtime identifier, the smoke test runs the published binary, reads its stdout bytes, and checks the version line and exit code 0. +> +> Also check that a planted reflection-based `System.Text.Json` call in a library fails the build with the AOT analyzer error. Show the failing run, then remove the plant. +> +> **6. The Varve admission evidence (ADR-0004 D2).** +> +> The skeleton references a Varve preview that has not been checked from here. Earlier checks of the canonicaliser and of SHACL ran against Varve `main` at `c0a776b`. One test in `ProductCli.Canonical.Tests` does the following: +> - reads decision-driven-design's `applications/sdlc/what/examples/acme-shop.ttl`, vendored under `tests/fixtures/ddd/` at a stated commit, with the commit and the file's sha256 in a `SOURCE.md` beside it; +> - parses it with `Varve.Turtle` and canonicalises it with `Varve.Rdf`'s `RdfCanonicaliser`; +> - asserts that the sha256 of the canonical N-Quads equals `vectors.json`'s `graphHash`, `sha256:10cd90893f7fadb1bdf80064a43795be95e2175684c6d3a93a7736abc7c2156b`. Vendor `vectors.json` beside the example. +> +> This is a test of the package, not product behaviour, and it lives in the layer that will use it. If it fails, stop: that is a Varve issue first (ADR-0002 D5). +> +> **7. Updates.** +> - `AGENTS.md`: the build, test and publish commands, the layer table, and the rule that a new package needs an ADR and an `Adr=` entry. +> - `README.md`: build and test instructions. +> - `releases/`: the descriptor for this slice. +> - `docs/traceability/-issue--slice-0.md`. +> +> ## Rules +> +> - Commit identity: `Claude (Emil) `, signed. +> - Every commit carries `Refs #` and `Signed-off-by:`, and no model name anywhere. +> - Resolve every version from nuget.org, never from memory, and record in the plan the date you resolved it. +> - No suppression: no `NoWarn`, `#pragma warning disable` or `SuppressMessage`. A rule that is wrong is changed by ADR. +> - Nothing in this slice writes to Varve, Writ or decision-driven-design. A gap there is an issue in that repository, linked from the PR. +> - `main` is fast-forwarded only by Emil, from his machine, at the head he approved with an `Approved at ` comment on the PR. GitHub's merge button is not used: it would rewrite or add unsigned commits. +> +> ## Done when +> +> - `gates.yml` is green on every job of the PR head, the AOT matrix included. +> - Every planted failure has been shown failing and has been removed. +> - Every commit verifies on GitHub; paste the API verification for each SHA. +> - ADR-0013 is accepted, and the PR is merged by Emil. +> +> Report: +> - the merge SHA; +> - each commit's verification; +> - the package versions as resolved; +> - the AOT binary size per runtime identifier; +> - any deviation from this prompt, with its reason. +> +> ## Reconciliation +> +> If another session lands on `main` while you work, merge `main`, re-run every check, and record the reconciliation in the traceability record. If it touched the build policy, the package register or a layer, stop and ask. + +The second message, approving the plan posted on #167: + +> Approved, with these answers. +> +> 1. Agreed: build the probes with `dotnet build` under the real build files. +> 2. Accepted: all `DecisionDriven.Analyzers` rules are on, as errors. That is ADR-0002 D3's intent. ADR-0013 states the consequence: the first public interface needs a decision set in the package's format. +> 3. The wording is accepted: no analyzer assembly ships; the generator adds internal attribute types and one assembly attribute to each of our assemblies. +> 4. Enforce `Adr=` on `PackageVersion` only. ADR-0013 says that it amends ADR-0004 D1's wording, and names ADR-0004 as amended (ADR-0001 D3: no silent divergence). AGENTS.md follows. +> 5. Commit ADR-0013 as `Proposed`. I change it to `Accepted (date)` in my own signed commit, together with the descriptor's `accepted-by` line. +> 6. The verb-to-step table is right. The exit-2 message is not a format and is not read-forever. Say so in AGENTS.md. +> 7. Yes to permanent tests: `BuildPolicyTests` builds plants 1 to 4 on every run. So no plant commits on the PR branch. Every commit that reaches `main` builds green, and the permanent tests are the evidence. Paste one test run in the PR. +> 8. `0.8.0-preview.2`, agreed. +> +> After the PR is up I'll add the new jobs to `main`'s required checks. + +## What was read + +- **The definition**, decision-driven-design at `799cbe84f2a010fb5b65a687eb253666948c73c8`: chapters 3 (the verb table), 6 and 7, D1.24 and D1.25. +- **This repository** at `53bd547`: `AGENTS.md`, ADR-0002, 0003, 0004, 0006 and 0007, ADR-0010 D6, `docs/decisions/README.md`, `.github/workflows/gates.yml`, every script in `eng/gates/`, `releases/v0.8.0-preview.1.yaml`, #166, and #165's record. +- **Varve** `main` at `62c0afb`: `Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props`, `global.json`, `nuget.config`, `.editorconfig`, `eng/BannedSymbols*.txt`, the head of `eng/dependency-register.cs`, a test project and the CI's runners. At tag `v0.1.0-preview.15` (`6cdb402`): `RdfCanonicaliser`, `InMemoryDatasetBuilder`, `TurtleParser`, `TurtleOptions` and the canonicaliser's conformance harness, for the admission test's calls. +- **decision-driven-analyzers** at `main` (`cae65f3`) and at tag `v0.1.0-preview.10` (`a041cb2`): the README, `docs/rules/DD0001.md`, `LayerReferenceAnalyzer.cs`, `GeneratedAttributes.cs`, the package's project file, and the published `0.1.0-preview.10` nupkg's contents and props. +- **nuget.org** on 2026-11-03, 12:14 UTC: the versions and nuspecs of every package in the register, and of FsCheck for the comparison. + +## What the session found + +- **Every precondition held.** `main` was at `53bd547`, exactly, with the expected tree. #166 was merged. `docs/decisions/` held ADR-0001 to ADR-0012. decision-driven-design `main` was `799cbe8`, which carries D1.25. +- **DD0001 applies to any family**, from `ArchFamily` and each referenced assembly's `[assembly: ArchLayer]`, and the package is AOT-neutral: analyzers and build props only, no `lib/`. Its generator does compile internal attribute types into each assembly, which ADR-0013 D3 states. +- **The package ships no consumer test harness**, so the probes are built with `dotnet build` (approved, question 1). +- **Varve's preview predates the earlier checks.** `0.1.0-preview.15` is 120 commits before `c0a776b`, and no commit in between touches the canonicaliser or the Turtle parser. The admission test passes on the preview. +- **Today's `no-toolchain` would have passed a .NET root.** It listed only Rust, Node and Python files, at the root. It was tightened anyway, to the rule the plan stated. +- **The SDK found no `.editorconfig` or `.globalconfig` for a project without source.** Its discovery walks up from the project's `Compile` items at evaluation, and an empty library has none, so `dd_require_layer` and the IL severities did not reach the seven libraries. The probe caught it: an unplaced probe built green. `Directory.Build.props` now names both files for every project, with discovery off so no file is passed twice. +- **The native binary is written after `Publish`** by the ILC targets' own `CopyNativeBinary`, so the rename to `product` runs after that target. +- **GitHub's Windows runners check out with CRLF** by default, which would break the gate scripts under bash. `.gitattributes` makes every checkout LF. + +## What the session did + +- Opened #167 and posted the plan ([comment](https://github.com/mindovermachine-dev/product-cli/issues/167#issuecomment-6603551451)). It wrote no file until the approval above. +- Wrote ADR-0013 as `Proposed`, the build policy, the eight projects, the tests, the CI jobs, the gate changes, `AGENTS.md`, `README.md` and the release descriptor. +- Verified every commit on its own, before pushing: each was checked out into a clean worktree and run through `commits`, `adr-shape`, `adr-text`, `no-toolchain` and `selftest`, then, from the first commit with a solution, `dotnet build -c Release` and `dotnet test -c Release`. All passed. +- Opened the pull request. It merged nothing, tagged nothing and wrote no acceptance. +- Wrote nothing to Varve, Writ or decision-driven-design. No gap there was found, so no issue was filed. + +## Commits + +In order, on `53bd547`. The verification is GitHub's, read through the REST +API (`GET /repos/mindovermachine-dev/product-cli/pulls/168/commits`). + +| SHA | verified | reason | subject | +|---|---|---|---| +| `3587d29197e194e8ca3279ded1b0c5ba408dc017` | true | valid | docs(decisions): ADR-0013, build-time analyzers and the test stack | +| `303bc8c8caa48d39b30f0371ae296f5bd38870fd` | true | valid | build: the build policy and the package register | +| `83827833e12c372800e2b9c187dab42277c1ee5e` | true | valid | ci(gates): no-toolchain allows exactly one toolchain, .NET | +| `748673f20b7ec2350aa78371d6f769a5613f2336` | true | valid | feat: the seven layer libraries, with their API baselines | +| `191b7904d01f0ad679ee709b9284a06abe7e47e7` | true | valid | feat(host): product --version, and exit 2 for every verb not yet built | +| `fb989e3af9c999ace2e470d0764cd553627d6002` | true | valid | test: the layering probes, one per layer | +| `af27ad42f0ef624fca60867b324bdf3eeef45e24` | true | valid | test(canonical): the Varve admission test, with the vendored example | +| `b0048205d0d88bd1a520aea66521ba7ecf059e71` | true | valid | test(host): the version bytes, the verb messages, the argument property | +| `ceb7226625dba590fa143e7793e967382b290906` | true | valid | test(build): the build policy's planted failures, built on every run | +| `0d2836d83a11c329cda896b6dbbc9bddac6dc854` | true | valid | ci(gates): build-test and the AOT publish and smoke matrix | +| `0bbb9ff7a880d3daa1c8d967b1aabec0039b3baf` | true | valid | docs: AGENTS.md and README for the build | + +Two commits follow: the release descriptor, and this record. Their +verification is in the pull request, since a record cannot name its own SHA. + +## Checks + +**Each commit on its own, before the push.** Each commit was checked out +into a clean worktree and run through the gates, then, from the first commit +with a solution, `dotnet build ProductCli.slnx -c Release` and +`dotnet test -c Release --no-build`. + +| Commit | commits | adr-shape | adr-text | no-toolchain | selftest | build | tests | +|---|---|---|---|---|---|---|---| +| `3587d29` | ok | ok | ok | ok | ok | no solution | — | +| `303bc8c` | ok | ok | ok | ok | ok | no solution | — | +| `8382783` | ok | ok | ok | ok | ok | no solution | — | +| `748673f` | ok | ok | ok | ok | ok | ok | no tests | +| `191b790` | ok | ok | ok | ok | ok | ok | no tests | +| `fb989e3` | ok | ok | ok | ok | ok | ok | 10 passed | +| `af27ad4` | ok | ok | ok | ok | ok | ok | 12 passed | +| `b004820` | ok | ok | ok | ok | ok | ok | 38 passed | +| `ceb7226` | ok | ok | ok | ok | ok | ok | 43 passed | +| `0d2836d` | ok | ok | ok | ok | ok | ok | 43 passed | +| `0bbb9ff` | ok | ok | ok | ok | ok | ok | 43 passed | + +**`gates.yml` on `0bbb9ff`**, [run 41638171084](https://github.com/mindovermachine-dev/product-cli/actions/runs/41638171084): all eleven jobs succeeded. + +| Job | Result | Output | +|---|---|---| +| `commits` | success | | +| `signatures` | success | | +| `adr-shape` | success | 13 ADRs | +| `adr-text` | success | ADR-0001 to ADR-0012 | +| `no-toolchain` | success | the new rule | +| `selftest` | success | every clean case passes, every plant fails with its finding | +| `build-test` | success | 43 tests, 0 failed | +| `aot (linux-x64)` | success | smoke passed at `0bbb9ff`; `product` is 1,829,032 bytes | +| `aot (linux-arm64)` | success | smoke passed at `0bbb9ff`; `product` is 1,914,896 bytes | +| `aot (osx-arm64)` | success | smoke passed at `0bbb9ff`; `product` is 1,766,640 bytes | +| `aot (win-x64)` | success | smoke passed at `0bbb9ff`; `product.exe` is 1,692,672 bytes | + +**The planted failures**, built by `BuildPolicyTests` on every run. Each is +also shown here built directly (`ProbePlant=`, `-c Release`): + +``` +ProbePlant=no-adr: exit 1 + error PCLI0001: PackageVersion 'ProductCli.PlantedPackage' carries no Adr= naming the ADR that admits it (ADR-0004 D1, as amended by ADR-0013 D4). +ProbePlant=dangling-adr: exit 1 + error PCLI0002: PackageVersion 'ProductCli.PlantedPackage' cites Adr="9999", and docs/decisions/ holds no ADR-9999-*.md (ADR-0013 D4). +ProbePlant=out-of-scope: exit 1 + error PCLI0003: ProductCli.PolicyProbe (src/ProductCli.PolicyProbe/) references 'CsCheck', whose register entry is scoped to tests/ (ADR-0013 D4). +ProbePlant=reflection-json: exit 1 + error IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' … + error IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' … +``` + +**A mutation, run once by hand and reverted:** `ProductCli.Draft` set to +layer 2 failed the solution build itself, with +`DD0001: 'ProductCli.Draft' is at layer 2 and references 'ProductCli.Shapes', which is at layer 2`. + +**The Varve admission test passed.** `Varve.Turtle` and `Varve.Rdf` +`0.1.0-preview.15` reproduce `sha256:10cd90893f7fadb1bdf80064a43795be95e2175684c6d3a93a7736abc7c2156b` +for `acme-shop.ttl`. + +## Deviations from the plan + +- **No plant commits.** The plan had four planted failures as commits on the branch, shown red and removed. The approval replaced them with `BuildPolicyTests`, which builds each plant on every run. The prompt's "show the failing run, then remove the plant" is met by those tests' runs, pasted in the PR. +- **`Scope=` and the probes.** The plan said a scoped package fails "from a project whose path the scope does not cover". The probes live under `tests/fixtures/`, inside `tests/`, so they are checked as if they were at `src//` (`Directory.Build.targets`). That is what lets a probe stand in for a library. +- **Three files beyond the plan's list:** + - `.gitattributes`, for the Windows runner's line endings; + - `tests/Harness/`, the shared process and probe code, linked into each test project; + - one source file per probe, so the compilation sits in the repository. +- **The layer-6 probe references the host.** A probe at layer L references the L+1 project, and for L = 6 that is `ProductCli.Host`. It is refused like the others. +- **The analyzer configuration is named explicitly** (found above). The plan did not foresee it. +- **The publish folder holds only `product` and its symbol file.** The libraries' documentation and pdb files are kept out of it. +- **The register has no `Microsoft.CodeAnalysis.CSharp`**, because the probes use `dotnet build` (question 1). + +## Reconciliation + +None. `main` stayed at `53bd547` throughout. diff --git a/eng/BannedSymbols.txt b/eng/BannedSymbols.txt new file mode 100644 index 00000000..8b1b1058 --- /dev/null +++ b/eng/BannedSymbols.txt @@ -0,0 +1,30 @@ +// Banned symbols, for all product code (src/). Directory.Build.targets passes +// this file to Microsoft.CodeAnalysis.BannedApiAnalyzers (ADR-0013 D2). +// +// ADR-0003 D4: no reflection, Reflection.Emit or dynamic in shipped code. Each +// entry below defeats trimming or does not exist under Native AOT, so it is +// banned outright rather than left for the trim analyzer to catch at a call +// site. `dynamic` is DD0020's (ADR-0013 D2). +// +// Format: one documentation ID per line, ';', then the message shown at the +// call site. Every message cites the decision that bans it. + +// Runtime code generation: none under Native AOT. +N:System.Reflection.Emit; Native AOT has no Reflection.Emit; generate the code at compile time. ADR-0003 D4. + +// Types named in strings: the trimmer cannot see the reference. +M:System.Type.GetType(System.String); A type named in a string defeats trimming; name the type in code. ADR-0003 D4. +M:System.Type.GetType(System.String,System.Boolean); A type named in a string defeats trimming; name the type in code. ADR-0003 D4. +M:System.Type.GetType(System.String,System.Boolean,System.Boolean); A type named in a string defeats trimming; name the type in code. ADR-0003 D4. + +// Reflective activation. +M:System.Activator.CreateInstance(System.Type); Reflective activation defeats trimming; call a constructor or a factory. ADR-0003 D4. +M:System.Activator.CreateInstance(System.Type,System.Boolean); Reflective activation defeats trimming; call a constructor or a factory. ADR-0003 D4. +M:System.Activator.CreateInstance(System.Type,System.Object[]); Reflective activation defeats trimming; call a constructor or a factory. ADR-0003 D4. +M:System.Activator.CreateInstance(System.String,System.String); Reflective activation defeats trimming; call a constructor or a factory. ADR-0003 D4. + +// Loading assemblies at run time: none under Native AOT. +M:System.Reflection.Assembly.Load(System.String); Native AOT loads no assembly at run time. ADR-0003 D4. +M:System.Reflection.Assembly.Load(System.Byte[]); Native AOT loads no assembly at run time. ADR-0003 D4. +M:System.Reflection.Assembly.LoadFrom(System.String); Native AOT loads no assembly at run time. ADR-0003 D4. +M:System.Reflection.Assembly.LoadFile(System.String); Native AOT loads no assembly at run time. ADR-0003 D4. diff --git a/eng/gates/no-toolchain.sh b/eng/gates/no-toolchain.sh index 36b19cee..2d5ec783 100755 --- a/eng/gates/no-toolchain.sh +++ b/eng/gates/no-toolchain.sh @@ -1,10 +1,21 @@ #!/usr/bin/env bash -# The toolchain gate: no Rust, Node or Python toolchain file exists at the repository root (chapter 7). +# The toolchain gate: the only toolchain is .NET, pinned at the root (chapter 7; ADR-0013, #167). +# +# Fails when: +# 1. a Rust, Node or Python toolchain file, or a requirements*.txt, is +# anywhere in the tree (outside .git/, bin/, obj/ and artifacts/); +# 2. the root has no global.json, or its sdk.version is not 10.0.401, its +# sdk.rollForward is not latestFeature, or its test.runner is not +# Microsoft.Testing.Platform; +# 3. a global.json exists below the root, a second pin. # # Usage: eng/gates/no-toolchain.sh [root] (default: .) set -euo pipefail root=${1:-.} +sdk_version=10.0.401 +roll_forward=latestFeature +test_runner=Microsoft.Testing.Platform rust=(Cargo.toml Cargo.lock rust-toolchain rust-toolchain.toml .cargo) node=(package.json package-lock.json npm-shrinkwrap.json yarn.lock .yarnrc.yml @@ -15,20 +26,45 @@ python=(pyproject.toml setup.py setup.cfg Pipfile Pipfile.lock poetry.lock uv.lo shared=(.tool-versions) failed=0 + +# 1. No other toolchain, anywhere. +names=() for f in "${rust[@]}" "${node[@]}" "${python[@]}" "${shared[@]}"; do - if [ -e "$root/$f" ]; then - echo "no-toolchain: $f is at the root" - failed=1 - fi + names+=(-o -name "$f") done -shopt -s nullglob -for f in "$root"/requirements*.txt; do - echo "no-toolchain: ${f#"$root"/} is at the root" +while IFS= read -r -d '' path; do + echo "no-toolchain: ${path#"$root"/} is a non-.NET toolchain file" failed=1 -done -shopt -u nullglob +done < <(find "$root" \( -name .git -o -name bin -o -name obj -o -name artifacts \) -prune -o \ + \( -name 'requirements*.txt' "${names[@]}" \) -print0 | sort -z) + +# 2. The .NET pin, at the root. +pin="$root/global.json" +if [ ! -f "$pin" ]; then + echo "no-toolchain: global.json is not at the root" + failed=1 +else + check() { # check + local actual + actual=$(jq -r "$1 // \"(absent)\"" "$pin" 2>/dev/null || echo "(unreadable)") + if [ "$actual" != "$2" ]; then + echo "no-toolchain: global.json $1 is $actual, not $2" + failed=1 + fi + } + check .sdk.version "$sdk_version" + check .sdk.rollForward "$roll_forward" + check .test.runner "$test_runner" +fi + +# 3. One pin only. +while IFS= read -r -d '' path; do + echo "no-toolchain: ${path#"$root"/} is a second global.json" + failed=1 +done < <(find "$root" \( -name .git -o -name bin -o -name obj -o -name artifacts \) -prune -o \ + -name global.json ! -path "$root/global.json" -print0 | sort -z) if [ "$failed" -ne 0 ]; then exit 1 fi -echo "no-toolchain: no Rust, Node or Python toolchain file at the root" +echo "no-toolchain: the only toolchain is .NET, SDK $sdk_version pinned in global.json" diff --git a/eng/gates/selftest.sh b/eng/gates/selftest.sh index 1874a1c6..68650144 100755 --- a/eng/gates/selftest.sh +++ b/eng/gates/selftest.sh @@ -177,17 +177,91 @@ expect_fail "adr-text: an ADR file missing" "ADR-0012: expected one file, found # --- no-toolchain ------------------------------------------------------------ -mkdir -p "$work/root-clean" -cp -R "$repo/docs" "$work/root-clean/" 2>/dev/null || true -expect_pass "no-toolchain" "$gates/no-toolchain.sh" "$work/root-clean" +# A root with the .NET pin and a project tree, which every plant starts from. +toolchain_root() { # toolchain_root + local r="$work/$1" + rm -rf "$r" + mkdir -p "$r/src/ProductCli.Probe" "$r/tests" + cp "$repo/global.json" "$r/global.json" + : > "$r/src/ProductCli.Probe/ProductCli.Probe.csproj" + echo "$r" +} + +r=$(toolchain_root root-clean) +expect_pass "no-toolchain" "$gates/no-toolchain.sh" "$r" + +# bin/, obj/ and .git/ are not the tree: a file in them is not a toolchain. +r=$(toolchain_root root-ignored) +mkdir -p "$r/src/ProductCli.Probe/obj" "$r/.git" +: > "$r/src/ProductCli.Probe/obj/package.json" +: > "$r/.git/global.json" +expect_pass "no-toolchain: bin, obj and .git are not the tree" "$gates/no-toolchain.sh" "$r" + for f in Cargo.toml rust-toolchain.toml package.json pnpm-lock.yaml pyproject.toml requirements.txt .python-version; do - r="$work/root-$f" - mkdir -p "$r" + r=$(toolchain_root "root-$f") : > "$r/$f" - expect_fail "no-toolchain: $f" "no-toolchain: $f is at the root" "$gates/no-toolchain.sh" "$r" + expect_fail "no-toolchain: $f" "no-toolchain: $f is a non-.NET toolchain file" "$gates/no-toolchain.sh" "$r" done -mkdir -p "$work/root-cargo-dir/.cargo" -expect_fail "no-toolchain: .cargo/" "no-toolchain: .cargo is at the root" "$gates/no-toolchain.sh" "$work/root-cargo-dir" +r=$(toolchain_root root-cargo-dir) +mkdir -p "$r/.cargo" +expect_fail "no-toolchain: .cargo/" "no-toolchain: .cargo is a non-.NET toolchain file" "$gates/no-toolchain.sh" "$r" + +r=$(toolchain_root root-nested-node) +: > "$r/src/ProductCli.Probe/package.json" +expect_fail "no-toolchain: package.json under src/" "no-toolchain: src/ProductCli.Probe/package.json is a non-.NET toolchain file" "$gates/no-toolchain.sh" "$r" + +r=$(toolchain_root root-no-pin) +rm "$r/global.json" +expect_fail "no-toolchain: no global.json" "no-toolchain: global.json is not at the root" "$gates/no-toolchain.sh" "$r" + +r=$(toolchain_root root-wrong-sdk) +jq '.sdk.version = "9.0.100"' "$repo/global.json" > "$r/global.json" +expect_fail "no-toolchain: another SDK version" "global.json .sdk.version is 9.0.100, not 10.0.401" "$gates/no-toolchain.sh" "$r" + +r=$(toolchain_root root-wrong-rollforward) +jq '.sdk.rollForward = "latestMajor"' "$repo/global.json" > "$r/global.json" +expect_fail "no-toolchain: another rollForward" "global.json .sdk.rollForward is latestMajor, not latestFeature" "$gates/no-toolchain.sh" "$r" + +r=$(toolchain_root root-no-runner) +jq 'del(.test)' "$repo/global.json" > "$r/global.json" +expect_fail "no-toolchain: no test runner" "global.json .test.runner is (absent), not Microsoft.Testing.Platform" "$gates/no-toolchain.sh" "$r" + +r=$(toolchain_root root-second-pin) +cp "$repo/global.json" "$r/tests/global.json" +expect_fail "no-toolchain: a second global.json" "no-toolchain: tests/global.json is a second global.json" "$gates/no-toolchain.sh" "$r" + +# --- smoke ------------------------------------------------------------------- + +# Each stub stands in for a published binary. The clean one behaves as the +# host must; each plant breaks one thing the smoke test reads. +sha=0123456789abcdef0123456789abcdef01234567 +stub() { # stub : an executable script + local f="$work/stub-$1" + printf '#!/usr/bin/env bash\n%s\n' "$2" > "$f" + chmod +x "$f" + echo "$f" +} +good_version="printf 'product 1.2.3 (commit $sha)\\n'" +good_domain="echo \"product: 'domain' arrives in step 4\" >&2; exit 2" +body() { # body + printf 'if [ "$1" = --version ]; then %s; else %s; fi' "$1" "$2" +} + +expect_pass "smoke" "$gates/smoke.sh" "$(stub clean "$(body "$good_version" "$good_domain")")" 1.2.3 "$sha" +expect_fail "smoke: another version" "smoke: --version wrote other bytes than expected" \ + "$gates/smoke.sh" "$(stub version "$(body "printf 'product 1.2.4 (commit $sha)\\n'" "$good_domain")")" 1.2.3 "$sha" +expect_fail "smoke: another commit" "smoke: --version wrote other bytes than expected" \ + "$gates/smoke.sh" "$(stub commit "$(body "printf 'product 1.2.3 (commit ffff)\\n'" "$good_domain")")" 1.2.3 "$sha" +expect_fail "smoke: a CRLF line end" "smoke: --version wrote other bytes than expected" \ + "$gates/smoke.sh" "$(stub crlf "$(body "printf 'product 1.2.3 (commit $sha)\\r\\n'" "$good_domain")")" 1.2.3 "$sha" +expect_fail "smoke: --version exits 1" "smoke: --version exited 1, not 0" \ + "$gates/smoke.sh" "$(stub exit1 "$(body "$good_version; exit 1" "$good_domain")")" 1.2.3 "$sha" +expect_fail "smoke: --version writes to stderr" "smoke: --version wrote to stderr" \ + "$gates/smoke.sh" "$(stub stderr "$(body "$good_version; echo warning >&2" "$good_domain")")" 1.2.3 "$sha" +expect_fail "smoke: a verb exits 0" "smoke: 'domain' exited 0, not 2" \ + "$gates/smoke.sh" "$(stub verb0 "$(body "$good_version" "exit 0")")" 1.2.3 "$sha" +expect_fail "smoke: a verb writes to stdout" "smoke: 'domain' wrote to stdout" \ + "$gates/smoke.sh" "$(stub verbout "$(body "$good_version" "echo nodes; exit 2")")" 1.2.3 "$sha" if [ "$failed" -ne 0 ]; then echo "selftest: a check did not behave as its plant requires" diff --git a/eng/gates/smoke.sh b/eng/gates/smoke.sh new file mode 100755 index 00000000..0252872b --- /dev/null +++ b/eng/gates/smoke.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# The smoke test of a published binary (ADR-0003 D3): what it writes, byte for byte, and how it exits. +# +# --version writes exactly "product (commit )\n" +# to stdout, nothing to stderr, and exits 0; +# domain writes nothing to stdout and exits 2. +# +# The expected line is built here, from the arguments, and compared with cmp, +# so a CRLF, a trailing space or a missing line feed fails. +# +# Usage: eng/gates/smoke.sh +set -euo pipefail + +if [ "$#" -ne 3 ]; then + echo "usage: eng/gates/smoke.sh " >&2 + exit 1 +fi +binary=$1 version=$2 commit=$3 + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +failed=0 +fail() { + echo "smoke: $1" + failed=1 +} + +printf 'product %s (commit %s)\n' "$version" "$commit" > "$work/expected" + +status=0 +"$binary" --version > "$work/stdout" 2> "$work/stderr" || status=$? +if [ "$status" -ne 0 ]; then + fail "--version exited $status, not 0" +fi +if ! cmp -s "$work/expected" "$work/stdout"; then + fail "--version wrote other bytes than expected:" + echo " expected: $(od -An -c "$work/expected" | tr -s ' ' | tr -d '\n')" + echo " actual: $(od -An -c "$work/stdout" | tr -s ' ' | tr -d '\n')" +fi +if [ -s "$work/stderr" ]; then + fail "--version wrote to stderr: $(head -c 200 "$work/stderr")" +fi + +status=0 +"$binary" domain > "$work/stdout" 2> "$work/stderr" || status=$? +if [ "$status" -ne 2 ]; then + fail "'domain' exited $status, not 2" +fi +if [ -s "$work/stdout" ]; then + fail "'domain' wrote to stdout: $(head -c 200 "$work/stdout")" +fi + +if [ "$failed" -ne 0 ]; then + exit 1 +fi +echo "smoke: $binary prints 'product $version (commit $commit)' and exits 0; 'domain' exits 2" diff --git a/global.json b/global.json new file mode 100644 index 00000000..9773a5f5 --- /dev/null +++ b/global.json @@ -0,0 +1,10 @@ +{ + "sdk": { + "version": "10.0.401", + "rollForward": "latestFeature", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..6f44751a --- /dev/null +++ b/nuget.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/releases/v0.8.0-preview.2.yaml b/releases/v0.8.0-preview.2.yaml new file mode 100644 index 00000000..ee81123e --- /dev/null +++ b/releases/v0.8.0-preview.2.yaml @@ -0,0 +1,67 @@ +format: 1 +version: v0.8.0-preview.2 +title: The repository skeleton — eight layers, one native binary +date: 2026-11-03 +basis: + - issue: 167 + - definition: mindovermachine-dev/decision-driven-design@799cbe84f2a010fb5b65a687eb253666948c73c8 + - definition-ruling: D1.25 + - sdlc-edition: none (slice 0 needs no edition) + - varve: Varve.Rdf 0.1.0-preview.15 + - adr: "0002" + - adr: "0003" + - adr: "0004" + - adr: "0013" +accepted-by: mailto:emil@okkels-klein.dk +summary: | + ### The repository skeleton (#167) + + Step 0 of the order of work, D1.25. The repository builds the product as + ADR-0002, ADR-0003 and ADR-0004 rule, and ships its first binary, + `product`, whose only behaviour is `product --version`. It adds no + vocabulary, no node kind and no product behaviour, and needs no `sdlc:` + edition. + + #### Added + + - **The eight layer projects** of ADR-0002 D1, `ProductCli.Vocabulary` + (0) to `ProductCli.Host` (7), each referencing only the layer below it. + Each library has a public API baseline and is AOT-compatible and + trimmable, with the AOT and trim analyzers' diagnostics as errors. + - **The layering rule**, DD0001 of `DecisionDriven.Analyzers`, with every + project placed. One test per layer builds a probe that references the + layer above and reads DD0001 in the compiler's output. + - **The package register**, `Directory.Packages.props`. Every entry names + the ADR that admits it, and the build fails when one does not, when the + ADR does not exist, or when a test-only package is referenced from + product code. ADR-0013 admits the analyzers, names the test stack + (xunit v3 on Microsoft.Testing.Platform, and CsCheck), and amends + ADR-0004 D1's wording: the register entry is the `PackageVersion`. + - **`product`**, one Native AOT executable per supported runtime + identifier. `product --version` prints + `product 0.8.0-preview.2 (commit )` and exits 0. Every other + argument exits 2 with one line naming the step its verb arrives in. + - **CI on every pull request**: the build and tests with warnings as + errors, and the Native AOT publish and smoke test of the binary on each + runtime's own runner. + + | Runtime | Runner | Binary size | + |---|---|---| + | `linux-x64` | `ubuntu-24.04` | 1,829,032 bytes | + | `linux-arm64` | `ubuntu-24.04-arm` | 1,914,896 bytes | + | `osx-arm64` | `macos-15` | 1,766,640 bytes | + | `win-x64` | `windows-2025` | 1,692,672 bytes (`product.exe`) | + + #### Varve + + `Varve.Rdf` and `Varve.Turtle` `0.1.0-preview.15` are admitted on + evidence: they reproduce decision-driven-design's published RDFC-1.0 + graph hash of `acme-shop.ttl`, + `sha256:10cd90893f7fadb1bdf80064a43795be95e2175684c6d3a93a7736abc7c2156b`, + byte for byte. + + #### Changed + + - **`no-toolchain`** now requires exactly one toolchain, .NET, pinned in + `global.json`. A Rust, Node or Python toolchain file anywhere in the + tree fails it, not only one at the root. diff --git a/src/ProductCli.Canonical/ProductCli.Canonical.csproj b/src/ProductCli.Canonical/ProductCli.Canonical.csproj new file mode 100644 index 00000000..60d50e2c --- /dev/null +++ b/src/ProductCli.Canonical/ProductCli.Canonical.csproj @@ -0,0 +1,16 @@ + + + + + 1 + + + + + + + + + + + diff --git a/src/ProductCli.Canonical/PublicAPI.Shipped.txt b/src/ProductCli.Canonical/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Canonical/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Canonical/PublicAPI.Unshipped.txt b/src/ProductCli.Canonical/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/ProductCli.Draft/ProductCli.Draft.csproj b/src/ProductCli.Draft/ProductCli.Draft.csproj new file mode 100644 index 00000000..bd27bd71 --- /dev/null +++ b/src/ProductCli.Draft/ProductCli.Draft.csproj @@ -0,0 +1,12 @@ + + + + + 3 + + + + + + + diff --git a/src/ProductCli.Draft/PublicAPI.Shipped.txt b/src/ProductCli.Draft/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Draft/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Draft/PublicAPI.Unshipped.txt b/src/ProductCli.Draft/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/ProductCli.Host/ProductCli.Host.csproj b/src/ProductCli.Host/ProductCli.Host.csproj new file mode 100644 index 00000000..79f53868 --- /dev/null +++ b/src/ProductCli.Host/ProductCli.Host.csproj @@ -0,0 +1,43 @@ + + + + + 7 + true + Exe + true + true + true + + + + + + + + + + + + + + + + false + false + .none + + + diff --git a/src/ProductCli.Host/Program.cs b/src/ProductCli.Host/Program.cs new file mode 100644 index 00000000..8045bd05 --- /dev/null +++ b/src/ProductCli.Host/Program.cs @@ -0,0 +1,106 @@ +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; + +namespace ProductCli.Host; + +/// +/// The entry point of `product`. In slice 0 (D1.25) its only behaviour is +/// `product --version`; every other argument vector exits 2 and names the step +/// of the order of work its verb arrives in. +/// +internal static class Program +{ + private const int Success = 0; + private const int NotBuilt = 2; + + private static int Main(string[] args) + { + if (args.Length == 1 && args[0] == "--version") + { + Write(Console.OpenStandardOutput(), VersionLine()); + return Success; + } + + Write(Console.OpenStandardError(), Refusal(args)); + return NotBuilt; + } + + /// + /// product <version> (commit <sha>) and a line feed, read + /// from the metadata the build stamps into this assembly. + /// + private static string VersionLine() + { + Assembly assembly = typeof(Program).Assembly; + string version = assembly.GetCustomAttribute()?.InformationalVersion + ?? throw new InvalidOperationException("The build stamped no informational version."); + + string? commit = null; + foreach (AssemblyMetadataAttribute metadata in assembly.GetCustomAttributes()) + { + if (metadata.Key == "SourceCommit") + { + commit = metadata.Value; + } + } + + return string.IsNullOrEmpty(commit) + ? throw new InvalidOperationException("The build stamped no source commit.") + : "product " + version + " (commit " + commit + ")\n"; + } + + /// The one line, with its line feed, for an argument vector this build does not serve. + private static string Refusal(string[] args) + { + if (args.Length == 0) + { + return "product: no verb given; this build has only --version (step 0 of the order of work, D1.25)\n"; + } + + string first = args[0]; + if (first == "--version") + { + return "product: --version takes no arguments\n"; + } + + int step = Verbs.StepOf(first); + return step == 0 + ? "product: '" + Printable(first) + "' is not a verb\n" + : "product: '" + first + "' arrives in step " + step.ToString(CultureInfo.InvariantCulture) + " of the order of work (D1.25)\n"; + } + + /// + /// The argument as printable ASCII: every other character is written as + /// \uXXXX, so the message stays one line whatever was typed. + /// + private static string Printable(string argument) + { + StringBuilder text = new(argument.Length); + foreach (char c in argument) + { + if (c is >= ' ' and <= '~') + { + text.Append(c); + } + else + { + text.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); + } + } + + return text.ToString(); + } + + /// Writes the text as ASCII bytes, so the bytes are the same on every platform and console. + private static void Write(Stream stream, string text) + { + using (stream) + { + stream.Write(Encoding.ASCII.GetBytes(text)); + stream.Flush(); + } + } +} diff --git a/src/ProductCli.Host/Verbs.cs b/src/ProductCli.Host/Verbs.cs new file mode 100644 index 00000000..28df3b97 --- /dev/null +++ b/src/ProductCli.Host/Verbs.cs @@ -0,0 +1,20 @@ +namespace ProductCli.Host; + +/// +/// Where each verb of the definition's chapter 3 arrives in the order of work +/// (D1.25). The message that uses this is not a format: it is for a person, +/// and it changes as verbs are built. +/// +internal static class Verbs +{ + /// The step a verb arrives in, or 0 for an argument that is not a verb. + internal static int StepOf(string verb) => verb switch + { + "init" or "product" or "domain" or "diff" => 4, + "proposal" or "view" or "mcp" => 5, + "decider" or "projector" or "primitive" => 6, + "scenario" or "conformance" => 7, + "question" => 8, + _ => 0, + }; +} diff --git a/src/ProductCli.Runner/ProductCli.Runner.csproj b/src/ProductCli.Runner/ProductCli.Runner.csproj new file mode 100644 index 00000000..3e56b517 --- /dev/null +++ b/src/ProductCli.Runner/ProductCli.Runner.csproj @@ -0,0 +1,12 @@ + + + + + 5 + + + + + + + diff --git a/src/ProductCli.Runner/PublicAPI.Shipped.txt b/src/ProductCli.Runner/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Runner/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Runner/PublicAPI.Unshipped.txt b/src/ProductCli.Runner/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/ProductCli.Session/ProductCli.Session.csproj b/src/ProductCli.Session/ProductCli.Session.csproj new file mode 100644 index 00000000..165d8e30 --- /dev/null +++ b/src/ProductCli.Session/ProductCli.Session.csproj @@ -0,0 +1,12 @@ + + + + + 6 + + + + + + + diff --git a/src/ProductCli.Session/PublicAPI.Shipped.txt b/src/ProductCli.Session/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Session/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Session/PublicAPI.Unshipped.txt b/src/ProductCli.Session/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/ProductCli.Shapes/ProductCli.Shapes.csproj b/src/ProductCli.Shapes/ProductCli.Shapes.csproj new file mode 100644 index 00000000..e819b747 --- /dev/null +++ b/src/ProductCli.Shapes/ProductCli.Shapes.csproj @@ -0,0 +1,12 @@ + + + + + 2 + + + + + + + diff --git a/src/ProductCli.Shapes/PublicAPI.Shipped.txt b/src/ProductCli.Shapes/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Shapes/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Shapes/PublicAPI.Unshipped.txt b/src/ProductCli.Shapes/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/ProductCli.Simulation/ProductCli.Simulation.csproj b/src/ProductCli.Simulation/ProductCli.Simulation.csproj new file mode 100644 index 00000000..3f8f0219 --- /dev/null +++ b/src/ProductCli.Simulation/ProductCli.Simulation.csproj @@ -0,0 +1,12 @@ + + + + + 4 + + + + + + + diff --git a/src/ProductCli.Simulation/PublicAPI.Shipped.txt b/src/ProductCli.Simulation/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Simulation/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Simulation/PublicAPI.Unshipped.txt b/src/ProductCli.Simulation/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/ProductCli.Vocabulary/ProductCli.Vocabulary.csproj b/src/ProductCli.Vocabulary/ProductCli.Vocabulary.csproj new file mode 100644 index 00000000..a2acf0d9 --- /dev/null +++ b/src/ProductCli.Vocabulary/ProductCli.Vocabulary.csproj @@ -0,0 +1,8 @@ + + + + + 0 + + + diff --git a/src/ProductCli.Vocabulary/PublicAPI.Shipped.txt b/src/ProductCli.Vocabulary/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/ProductCli.Vocabulary/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ProductCli.Vocabulary/PublicAPI.Unshipped.txt b/src/ProductCli.Vocabulary/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/Harness/Command.cs b/tests/Harness/Command.cs new file mode 100644 index 00000000..38a4a038 --- /dev/null +++ b/tests/Harness/Command.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace ProductCli.Testing; + +/// What a process wrote and how it exited: the bytes, not an interpretation of them (ADR-0006). +internal sealed record CommandResult(int ExitCode, byte[] Stdout, byte[] Stderr) +{ + /// Stdout and stderr, decoded as UTF-8, for assertions on a compiler's messages. + public string Output => Encoding.UTF8.GetString(Stdout) + Encoding.UTF8.GetString(Stderr); +} + +/// Runs a process to completion and keeps its output bytes. +internal static class Command +{ + internal static CommandResult Run(string fileName, IEnumerable arguments, string? workingDirectory = null) + { + ProcessStartInfo start = new(fileName) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory ?? Repository.Root, + }; + foreach (string argument in arguments) + { + start.ArgumentList.Add(argument); + } + + // The test runs under `dotnet test`, which sets MSBuild's own variables + // for its child processes. A probe build is a fresh build: none of + // them may reach it. + foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables()) + { + string name = (string)variable.Key; + if (name.StartsWith("MSBUILD", StringComparison.OrdinalIgnoreCase) + || name.StartsWith("MSBuild", StringComparison.Ordinal) + || name.Equals("DOTNET_HOST_PATH", StringComparison.OrdinalIgnoreCase)) + { + start.Environment.Remove(name); + } + } + + using Process process = Process.Start(start) + ?? throw new InvalidOperationException($"{fileName} did not start."); + process.StandardInput.Close(); + + using MemoryStream stdout = new(); + using MemoryStream stderr = new(); + Task copyOut = process.StandardOutput.BaseStream.CopyToAsync(stdout); + Task copyErr = process.StandardError.BaseStream.CopyToAsync(stderr); + process.WaitForExit(); + Task.WaitAll(copyOut, copyErr); + + return new CommandResult(process.ExitCode, stdout.ToArray(), stderr.ToArray()); + } +} diff --git a/tests/Harness/Probe.cs b/tests/Harness/Probe.cs new file mode 100644 index 00000000..5dd4215a --- /dev/null +++ b/tests/Harness/Probe.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace ProductCli.Testing; + +/// +/// Builds a probe under tests/fixtures/ with `dotnet build`, under the +/// repository's real build files (Directory.Build.*, .globalconfig, the +/// analyzers' build props), and returns what the compiler wrote. +/// +/// +/// The probe builds into its own temporary directory, against the outputs the +/// solution build already made (`--no-dependencies`, `RestoreRecursive=false`), +/// so probes run in parallel and write nothing under src/. +/// +internal static class Probe +{ + /// The layering probe, declaring (none if null) and referencing the src/ project (none if null). + internal static CommandResult Layering(int? layer, string? references) + { + Dictionary properties = []; + if (layer is int value) + { + properties["ProbeLayer"] = value.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + if (references is not null) + { + string project = Repository.PathOf($"src/{references}/{references}.csproj"); + string assembly = Repository.PathOf($"src/{references}/bin/{Repository.Configuration}/net10.0/{references}.dll"); + if (!File.Exists(assembly)) + { + throw new InvalidOperationException($"{assembly} is not built. Build the solution first: the probe builds against its outputs."); + } + + properties["ProbeReference"] = project; + } + + return Build("tests/fixtures/layering/ProductCli.Probe.csproj", properties); + } + + /// The build-policy probe, with one plant (none if null). + internal static CommandResult BuildPolicy(string? plant) + { + Dictionary properties = []; + if (plant is not null) + { + properties["ProbePlant"] = plant; + } + + return Build("tests/fixtures/build-policy/ProductCli.PolicyProbe.csproj", properties); + } + + private static CommandResult Build(string probe, Dictionary properties) + { + string artifacts = Path.Combine(Path.GetTempPath(), "productcli-probe-" + Guid.NewGuid().ToString("N")); + try + { + List arguments = + [ + "build", Repository.PathOf(probe), + "-c", Repository.Configuration, + "--no-dependencies", + "-nologo", + "-p:RestoreRecursive=false", + $"-p:ProbeArtifacts={artifacts}", + ]; + foreach (KeyValuePair property in properties) + { + arguments.Add($"-p:{property.Key}={property.Value}"); + } + + return Command.Run("dotnet", arguments); + } + finally + { + if (Directory.Exists(artifacts)) + { + Directory.Delete(artifacts, recursive: true); + } + } + } +} diff --git a/tests/Harness/Repository.cs b/tests/Harness/Repository.cs new file mode 100644 index 00000000..0e7a3e42 --- /dev/null +++ b/tests/Harness/Repository.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Reflection; + +namespace ProductCli.Testing; + +/// Where the repository is, and what the build stamped into this test assembly. +internal static class Repository +{ + /// The repository root: the nearest directory above the test's output holding global.json and ProductCli.slnx. + internal static string Root { get; } = FindRoot(); + + /// The configuration this test assembly was built in, so a probe builds against the same outputs. + internal static string Configuration => Stamped("ProductCliConfiguration"); + + /// The product version of this build (Directory.Build.props). + internal static string Version => Stamped("ProductCliVersion"); + + /// A path below the root, given with forward slashes. + internal static string PathOf(string relative) => + Path.Combine(Root, relative.Replace('/', Path.DirectorySeparatorChar)); + + private static string Stamped(string key) + { + foreach (AssemblyMetadataAttribute metadata in typeof(Repository).Assembly.GetCustomAttributes()) + { + if (metadata.Key == key && !string.IsNullOrEmpty(metadata.Value)) + { + return metadata.Value; + } + } + + throw new InvalidOperationException($"The build stamped no {key} into this test assembly (Directory.Build.targets)."); + } + + private static string FindRoot() + { + for (DirectoryInfo? directory = new(AppContext.BaseDirectory); directory is not null; directory = directory.Parent) + { + if (File.Exists(Path.Combine(directory.FullName, "global.json")) + && File.Exists(Path.Combine(directory.FullName, "ProductCli.slnx"))) + { + return directory.FullName; + } + } + + throw new InvalidOperationException($"No repository root above {AppContext.BaseDirectory}."); + } +} diff --git a/tests/ProductCli.Canonical.Tests/DownwardReferenceTests.cs b/tests/ProductCli.Canonical.Tests/DownwardReferenceTests.cs new file mode 100644 index 00000000..21a47a97 --- /dev/null +++ b/tests/ProductCli.Canonical.Tests/DownwardReferenceTests.cs @@ -0,0 +1,19 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Canonical.Tests; + +/// ADR-0002 D2: a reference to a layer below compiles, so the layering tests refuse only what they should. +public sealed class DownwardReferenceTests +{ + /// A probe at layer 1 referencing ProductCli.Vocabulary, at layer 0, builds with no DD0001. + [Fact] + public void AReferenceFromLayer1ToLayer0Compiles() + { + CommandResult build = Probe.Layering(layer: 1, references: "ProductCli.Vocabulary"); + + Assert.True(build.ExitCode == 0, build.Output); + Assert.DoesNotContain("DD0001", build.Output, StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Canonical.Tests/LayeringTests.cs b/tests/ProductCli.Canonical.Tests/LayeringTests.cs new file mode 100644 index 00000000..30ea2a06 --- /dev/null +++ b/tests/ProductCli.Canonical.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Canonical.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 1 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 1 referencing ProductCli.Shapes, at layer 2, fails with DD0001. + [Fact] + public void AReferenceFromLayer1ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 1, references: "ProductCli.Shapes"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 1 and references 'ProductCli.Shapes', which is at layer 2.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Canonical.Tests/ProductCli.Canonical.Tests.csproj b/tests/ProductCli.Canonical.Tests/ProductCli.Canonical.Tests.csproj new file mode 100644 index 00000000..fa5a7459 --- /dev/null +++ b/tests/ProductCli.Canonical.Tests/ProductCli.Canonical.Tests.csproj @@ -0,0 +1,21 @@ + + + + + Exe + + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Canonical.Tests/VarveAdmissionTests.cs b/tests/ProductCli.Canonical.Tests/VarveAdmissionTests.cs new file mode 100644 index 00000000..28a27ad4 --- /dev/null +++ b/tests/ProductCli.Canonical.Tests/VarveAdmissionTests.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text.Json; +using ProductCli.Testing; +using Varve.Rdf; +using Varve.Turtle; +using Xunit; + +namespace ProductCli.Canonical.Tests; + +/// +/// The admission evidence for the Varve preview this repository references +/// (ADR-0004 D2): its Turtle parser and its RDFC-1.0 canonicaliser reproduce +/// decision-driven-design's published graph hash of acme-shop, byte for byte. +/// A test of the package, not of product behaviour; a failure is a Varve +/// issue first (ADR-0002 D5). +/// +public sealed class VarveAdmissionTests +{ + private static string Fixture(string name) => Path.Combine(Repository.PathOf("tests/fixtures/ddd"), name); + + /// The vendored files are the bytes SOURCE.md records. + [Fact] + public void TheVendoredFilesAreTheRecordedBytes() + { + string source = File.ReadAllText(Fixture("SOURCE.md")); + + Assert.Contains($"`{Sha256(File.ReadAllBytes(Fixture("acme-shop.ttl")))}`", source, StringComparison.Ordinal); + Assert.Contains($"`{Sha256(File.ReadAllBytes(Fixture("vectors.json")))}`", source, StringComparison.Ordinal); + } + + /// The sha256 of acme-shop's canonical N-Quads is vectors.json's graphHash. + [Fact] + public void TheCanonicalNQuadsOfAcmeShopHashToThePublishedGraphHash() + { + InMemoryDatasetBuilder builder = new(); + ParseResult parsed = TurtleParser.Parse( + File.ReadAllBytes(Fixture("acme-shop.ttl")), + (in QuadView quad) => builder.Add( + quad.Subject.Materialise(), + quad.Predicate.Materialise(), + quad.Object.Materialise(), + quad.HasGraph ? quad.Graph.Materialise() : null), + new TurtleOptions { Syntax = RdfSyntax.Turtle }); + Assert.True(parsed.Succeeded, $"acme-shop.ttl does not parse: {parsed.FirstError}"); + + CanonicalDataset canonical = RdfCanonicaliser.Canonicalise(builder.ToDataset(), cancellationToken: TestContext.Current.CancellationToken); + + using JsonDocument vectors = JsonDocument.Parse(File.ReadAllBytes(Fixture("vectors.json"))); + string? published = vectors.RootElement.GetProperty("graphHash").GetString(); + Assert.Equal("sha256:10cd90893f7fadb1bdf80064a43795be95e2175684c6d3a93a7736abc7c2156b", published); + Assert.Equal(published, "sha256:" + Sha256(canonical.NQuads.Span)); + } + + private static string Sha256(ReadOnlySpan bytes) => Convert.ToHexStringLower(SHA256.HashData(bytes)); +} diff --git a/tests/ProductCli.Draft.Tests/LayeringTests.cs b/tests/ProductCli.Draft.Tests/LayeringTests.cs new file mode 100644 index 00000000..2c7f3e34 --- /dev/null +++ b/tests/ProductCli.Draft.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Draft.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 3 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 3 referencing ProductCli.Simulation, at layer 4, fails with DD0001. + [Fact] + public void AReferenceFromLayer3ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 3, references: "ProductCli.Simulation"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 3 and references 'ProductCli.Simulation', which is at layer 4.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Draft.Tests/ProductCli.Draft.Tests.csproj b/tests/ProductCli.Draft.Tests/ProductCli.Draft.Tests.csproj new file mode 100644 index 00000000..8fd973f4 --- /dev/null +++ b/tests/ProductCli.Draft.Tests/ProductCli.Draft.Tests.csproj @@ -0,0 +1,20 @@ + + + + + Exe + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Host.Tests/ArgumentPropertyTests.cs b/tests/ProductCli.Host.Tests/ArgumentPropertyTests.cs new file mode 100644 index 00000000..3914902f --- /dev/null +++ b/tests/ProductCli.Host.Tests/ArgumentPropertyTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using CsCheck; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Host.Tests; + +/// The host's refusal, as a property over argument vectors (CsCheck, ADR-0013 D1). +public sealed class ArgumentPropertyTests +{ + /// + /// For any argument vector other than exactly ["--version"], the + /// empty one included: exit 2, nothing on stdout, and exactly one line on + /// stderr, beginning product: . + /// + [Fact] + public void EveryArgumentVectorButVersionExitsTwoWithOneLine() + { + // Any character but NUL, which no operating system passes in an + // argument, and the surrogates, which are not characters alone. + Gen anyText = Gen.Char['\u0001', '퟿'].Array[0, 12].Select(chars => new string(chars)); + Gen nearMiss = Gen.OneOf("--version", "--VERSION", "-v", "--help", "domain", "init", "", " "); + Gen vectors = Gen.OneOf(anyText, nearMiss).Array[0, 4] + .Where(arguments => !(arguments.Length == 1 && arguments[0] == "--version")); + + vectors.Sample( + arguments => + { + CommandResult run = HostProcess.Run(arguments); + + Assert.Equal(2, run.ExitCode); + Assert.Empty(run.Stdout); + Assert.True(run.Stderr.Length > 0 && run.Stderr[^1] == (byte)'\n', "stderr does not end with a line feed"); + Assert.Equal(1, run.Stderr.Count(b => b == (byte)'\n')); + Assert.True(run.Stderr.AsSpan().StartsWith("product: "u8), "stderr does not begin with 'product: '"); + }, + iter: 100, + print: arguments => "[" + string.Join(", ", arguments.Select(a => "\"" + a + "\"")) + "]"); + } +} diff --git a/tests/ProductCli.Host.Tests/BuildPolicyTests.cs b/tests/ProductCli.Host.Tests/BuildPolicyTests.cs new file mode 100644 index 00000000..fe88df46 --- /dev/null +++ b/tests/ProductCli.Host.Tests/BuildPolicyTests.cs @@ -0,0 +1,66 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Host.Tests; + +/// +/// The build policy's failing cases, built on every run (ADR-0003 D2, +/// ADR-0013 D4). Each plant is a property of the probe under +/// tests/fixtures/build-policy/; the test reads the error in the build's +/// output. The probe stands in for a library under src/. +/// +public sealed class BuildPolicyTests +{ + /// With no plant, the probe builds: each failure below is the plant's, not the probe's. + [Fact] + public void TheProbeBuildsWithNoPlant() + { + CommandResult build = Probe.BuildPolicy(plant: null); + + Assert.True(build.ExitCode == 0, build.Output); + } + + /// A register entry with no Adr= fails the build with PCLI0001. + [Fact] + public void AnEntryWithoutAdrFails() + { + AssertFails( + Probe.BuildPolicy("no-adr"), + "error PCLI0001: PackageVersion 'ProductCli.PlantedPackage' carries no Adr="); + } + + /// A register entry citing an ADR that does not exist fails the build with PCLI0002. + [Fact] + public void AnEntryCitingNoAdrFileFails() + { + AssertFails( + Probe.BuildPolicy("dangling-adr"), + "error PCLI0002: PackageVersion 'ProductCli.PlantedPackage' cites Adr=\"9999\", and docs/decisions/ holds no ADR-9999-*.md"); + } + + /// A test-only package referenced from product code fails the build with PCLI0003. + [Fact] + public void ATestOnlyPackageInProductCodeFails() + { + AssertFails( + Probe.BuildPolicy("out-of-scope"), + "error PCLI0003: ProductCli.PolicyProbe (src/ProductCli.PolicyProbe/) references 'CsCheck', whose register entry is scoped to tests/"); + } + + /// Reflection-based System.Text.Json in a library fails the build with the trim and AOT analyzers' errors. + [Fact] + public void ReflectionBasedJsonInALibraryFails() + { + CommandResult build = Probe.BuildPolicy("reflection-json"); + + AssertFails(build, "error IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)'"); + AssertFails(build, "error IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)'"); + } + + private static void AssertFails(CommandResult build, string error) + { + Assert.NotEqual(0, build.ExitCode); + Assert.Contains(error, build.Output, StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Host.Tests/HostProcess.cs b/tests/ProductCli.Host.Tests/HostProcess.cs new file mode 100644 index 00000000..1c6b02db --- /dev/null +++ b/tests/ProductCli.Host.Tests/HostProcess.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.IO; +using ProductCli.Testing; + +namespace ProductCli.Host.Tests; + +/// Runs the built host, `product`, as a process, and keeps what it wrote (ADR-0006). +internal static class HostProcess +{ + private static readonly string Assembly = Path.Combine(AppContext.BaseDirectory, "ProductCli.Host.dll"); + + internal static CommandResult Run(params string[] arguments) + { + List command = ["exec", Assembly]; + command.AddRange(arguments); + return Command.Run("dotnet", command); + } +} diff --git a/tests/ProductCli.Host.Tests/LayerTableTests.cs b/tests/ProductCli.Host.Tests/LayerTableTests.cs new file mode 100644 index 00000000..cd00ab26 --- /dev/null +++ b/tests/ProductCli.Host.Tests/LayerTableTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace ProductCli.Host.Tests; + +/// +/// The layers the built assemblies declare are ADR-0002 D1's table. Read from +/// each assembly's metadata, where DD0001 reads them, not from a project file. +/// +public sealed class LayerTableTests +{ + /// ADR-0002 D1, from the bottom. + public static TheoryData Table => new() + { + { "ProductCli.Vocabulary", 0 }, + { "ProductCli.Canonical", 1 }, + { "ProductCli.Shapes", 2 }, + { "ProductCli.Draft", 3 }, + { "ProductCli.Simulation", 4 }, + { "ProductCli.Runner", 5 }, + { "ProductCli.Session", 6 }, + { "ProductCli.Host", 7 }, + }; + + /// Each assembly's [assembly: ArchLayer(n)] is its row of the table. + [Theory] + [MemberData(nameof(Table))] + public void TheAssemblyDeclaresItsLayer(string assembly, int layer) + { + Assert.Equal([layer], DeclaredLayers(Path.Combine(AppContext.BaseDirectory, assembly + ".dll"))); + } + + private static List DeclaredLayers(string path) + { + using FileStream file = File.OpenRead(path); + using PEReader pe = new(file); + MetadataReader metadata = pe.GetMetadataReader(); + + List layers = []; + foreach (CustomAttributeHandle handle in metadata.GetAssemblyDefinition().GetCustomAttributes()) + { + CustomAttribute attribute = metadata.GetCustomAttribute(handle); + if (attribute.Constructor.Kind != HandleKind.MethodDefinition) + { + continue; + } + + TypeDefinition type = metadata.GetTypeDefinition( + metadata.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType()); + if (metadata.GetString(type.Namespace) == "DecisionDriven" && metadata.GetString(type.Name) == "ArchLayerAttribute") + { + // The value blob: the prolog 0x0001, then the one int32 argument. + BlobReader value = metadata.GetBlobReader(attribute.Value); + Assert.Equal(1, value.ReadUInt16()); + layers.Add(value.ReadInt32()); + } + } + + return layers; + } +} diff --git a/tests/ProductCli.Host.Tests/LayeringTests.cs b/tests/ProductCli.Host.Tests/LayeringTests.cs new file mode 100644 index 00000000..bc4ae018 --- /dev/null +++ b/tests/ProductCli.Host.Tests/LayeringTests.cs @@ -0,0 +1,39 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Host.Tests; + +/// +/// ADR-0002 D3 at the top of the table. Nothing sits above layer 7, so its +/// refusal is a reference within its own layer. And no ProductCli project +/// may stay out of the table (dd_require_layer). +/// +public sealed class LayeringTests +{ + /// A probe at layer 7 referencing ProductCli.Host, also at layer 7, fails with DD0001. + [Fact] + public void AReferenceFromLayer7WithinItsLayerIsRefused() + { + CommandResult build = Probe.Layering(layer: 7, references: "ProductCli.Host"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 7 and references 'ProductCli.Host', which is at layer 7.", + build.Output, + StringComparison.Ordinal); + } + + /// A ProductCli project that declares no layer fails with DD0001. + [Fact] + public void AnUnplacedProjectIsRefused() + { + CommandResult build = Probe.Layering(layer: null, references: null); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is in family 'ProductCli' and declares no layer", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Host.Tests/ProductCli.Host.Tests.csproj b/tests/ProductCli.Host.Tests/ProductCli.Host.Tests.csproj new file mode 100644 index 00000000..9844f93e --- /dev/null +++ b/tests/ProductCli.Host.Tests/ProductCli.Host.Tests.csproj @@ -0,0 +1,21 @@ + + + + + Exe + + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Host.Tests/VerbTests.cs b/tests/ProductCli.Host.Tests/VerbTests.cs new file mode 100644 index 00000000..bb6c98e1 --- /dev/null +++ b/tests/ProductCli.Host.Tests/VerbTests.cs @@ -0,0 +1,63 @@ +using System.Text; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Host.Tests; + +/// +/// Every argument vector but `--version` exits 2, with one line on stderr that +/// says where its verb arrives. The lines are for a person, not a format; the +/// test holds them so a change to one is a reviewed change. +/// +public sealed class VerbTests +{ + /// Each verb of the definition's chapter 3 names the step of the order of work it arrives in. + [Theory] + [InlineData("init", 4)] + [InlineData("product", 4)] + [InlineData("domain", 4)] + [InlineData("diff", 4)] + [InlineData("proposal", 5)] + [InlineData("view", 5)] + [InlineData("mcp", 5)] + [InlineData("decider", 6)] + [InlineData("projector", 6)] + [InlineData("primitive", 6)] + [InlineData("scenario", 7)] + [InlineData("conformance", 7)] + [InlineData("question", 8)] + public void AVerbNamesTheStepItArrivesIn(string verb, int step) + { + AssertRefused( + HostProcess.Run(verb, "--product", "shop"), + $"product: '{verb}' arrives in step {step} of the order of work (D1.25)\n"); + } + + /// An argument that is no verb is named as such, escaped to one printable line. + [Fact] + public void AnArgumentThatIsNoVerbIsNamed() + { + AssertRefused(HostProcess.Run("nodes\nlist"), "product: 'nodes\\u000alist' is not a verb\n"); + } + + /// No argument at all is refused, and the line says what this build has. + [Fact] + public void NoArgumentIsRefused() + { + AssertRefused(HostProcess.Run(), "product: no verb given; this build has only --version (step 0 of the order of work, D1.25)\n"); + } + + /// The version flag takes nothing after it. + [Fact] + public void VersionWithAnArgumentIsRefused() + { + AssertRefused(HostProcess.Run("--version", "--json"), "product: --version takes no arguments\n"); + } + + private static void AssertRefused(CommandResult run, string line) + { + Assert.Equal(2, run.ExitCode); + Assert.Empty(run.Stdout); + Assert.Equal(Encoding.ASCII.GetBytes(line), run.Stderr); + } +} diff --git a/tests/ProductCli.Host.Tests/VersionTests.cs b/tests/ProductCli.Host.Tests/VersionTests.cs new file mode 100644 index 00000000..4c136e89 --- /dev/null +++ b/tests/ProductCli.Host.Tests/VersionTests.cs @@ -0,0 +1,28 @@ +using System.Text; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Host.Tests; + +/// `product --version`, the host's one behaviour in slice 0 (D1.25), read as bytes. +public sealed class VersionTests +{ + /// + /// Stdout is exactly product <version> (commit <sha>) and a + /// line feed; stderr is empty; the exit code is 0. The version is the + /// build's, and the commit is the checkout's HEAD, read from git. + /// + [Fact] + public void VersionWritesTheVersionAndTheSourceCommitAsOneLine() + { + CommandResult git = Command.Run("git", ["rev-parse", "HEAD"]); + Assert.Equal(0, git.ExitCode); + string commit = Encoding.ASCII.GetString(git.Stdout).Trim(); + + CommandResult run = HostProcess.Run("--version"); + + Assert.Equal(0, run.ExitCode); + Assert.Equal(Encoding.ASCII.GetBytes($"product {Repository.Version} (commit {commit})\n"), run.Stdout); + Assert.Empty(run.Stderr); + } +} diff --git a/tests/ProductCli.Runner.Tests/LayeringTests.cs b/tests/ProductCli.Runner.Tests/LayeringTests.cs new file mode 100644 index 00000000..7913ad46 --- /dev/null +++ b/tests/ProductCli.Runner.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Runner.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 5 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 5 referencing ProductCli.Session, at layer 6, fails with DD0001. + [Fact] + public void AReferenceFromLayer5ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 5, references: "ProductCli.Session"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 5 and references 'ProductCli.Session', which is at layer 6.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Runner.Tests/ProductCli.Runner.Tests.csproj b/tests/ProductCli.Runner.Tests/ProductCli.Runner.Tests.csproj new file mode 100644 index 00000000..0f30710a --- /dev/null +++ b/tests/ProductCli.Runner.Tests/ProductCli.Runner.Tests.csproj @@ -0,0 +1,20 @@ + + + + + Exe + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Session.Tests/LayeringTests.cs b/tests/ProductCli.Session.Tests/LayeringTests.cs new file mode 100644 index 00000000..e27e6270 --- /dev/null +++ b/tests/ProductCli.Session.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Session.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 6 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 6 referencing ProductCli.Host, at layer 7, fails with DD0001. + [Fact] + public void AReferenceFromLayer6ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 6, references: "ProductCli.Host"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 6 and references 'ProductCli.Host', which is at layer 7.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Session.Tests/ProductCli.Session.Tests.csproj b/tests/ProductCli.Session.Tests/ProductCli.Session.Tests.csproj new file mode 100644 index 00000000..e7e61352 --- /dev/null +++ b/tests/ProductCli.Session.Tests/ProductCli.Session.Tests.csproj @@ -0,0 +1,20 @@ + + + + + Exe + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Shapes.Tests/LayeringTests.cs b/tests/ProductCli.Shapes.Tests/LayeringTests.cs new file mode 100644 index 00000000..c814069f --- /dev/null +++ b/tests/ProductCli.Shapes.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Shapes.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 2 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 2 referencing ProductCli.Draft, at layer 3, fails with DD0001. + [Fact] + public void AReferenceFromLayer2ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 2, references: "ProductCli.Draft"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 2 and references 'ProductCli.Draft', which is at layer 3.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Shapes.Tests/ProductCli.Shapes.Tests.csproj b/tests/ProductCli.Shapes.Tests/ProductCli.Shapes.Tests.csproj new file mode 100644 index 00000000..cf21e9c7 --- /dev/null +++ b/tests/ProductCli.Shapes.Tests/ProductCli.Shapes.Tests.csproj @@ -0,0 +1,20 @@ + + + + + Exe + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Simulation.Tests/LayeringTests.cs b/tests/ProductCli.Simulation.Tests/LayeringTests.cs new file mode 100644 index 00000000..e1c4769b --- /dev/null +++ b/tests/ProductCli.Simulation.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Simulation.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 4 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 4 referencing ProductCli.Runner, at layer 5, fails with DD0001. + [Fact] + public void AReferenceFromLayer4ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 4, references: "ProductCli.Runner"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 4 and references 'ProductCli.Runner', which is at layer 5.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Simulation.Tests/ProductCli.Simulation.Tests.csproj b/tests/ProductCli.Simulation.Tests/ProductCli.Simulation.Tests.csproj new file mode 100644 index 00000000..8e1563ac --- /dev/null +++ b/tests/ProductCli.Simulation.Tests/ProductCli.Simulation.Tests.csproj @@ -0,0 +1,20 @@ + + + + + Exe + + + + + + + + + + + + + + + diff --git a/tests/ProductCli.Vocabulary.Tests/LayeringTests.cs b/tests/ProductCli.Vocabulary.Tests/LayeringTests.cs new file mode 100644 index 00000000..9c4af832 --- /dev/null +++ b/tests/ProductCli.Vocabulary.Tests/LayeringTests.cs @@ -0,0 +1,25 @@ +using System; +using ProductCli.Testing; +using Xunit; + +namespace ProductCli.Vocabulary.Tests; + +/// +/// ADR-0002 D3: the layering analyzer refuses a reference from layer 0 to the +/// layer above it. The test reads the refusal in the compiler's output. +/// +public sealed class LayeringTests +{ + /// A probe at layer 0 referencing ProductCli.Canonical, at layer 1, fails with DD0001. + [Fact] + public void AReferenceFromLayer0ToTheLayerAboveIsRefused() + { + CommandResult build = Probe.Layering(layer: 0, references: "ProductCli.Canonical"); + + Assert.NotEqual(0, build.ExitCode); + Assert.Contains( + "error DD0001: 'ProductCli.Probe' is at layer 0 and references 'ProductCli.Canonical', which is at layer 1.", + build.Output, + StringComparison.Ordinal); + } +} diff --git a/tests/ProductCli.Vocabulary.Tests/ProductCli.Vocabulary.Tests.csproj b/tests/ProductCli.Vocabulary.Tests/ProductCli.Vocabulary.Tests.csproj new file mode 100644 index 00000000..1a0a5471 --- /dev/null +++ b/tests/ProductCli.Vocabulary.Tests/ProductCli.Vocabulary.Tests.csproj @@ -0,0 +1,20 @@ + + + + + Exe + + + + + + + + + + + + + + + diff --git a/tests/fixtures/Directory.Build.props b/tests/fixtures/Directory.Build.props new file mode 100644 index 00000000..f9b0e365 --- /dev/null +++ b/tests/fixtures/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + $(ProbeArtifacts)/obj/ + $(ProbeArtifacts)/bin/ + + + + + diff --git a/tests/fixtures/build-policy/Plants/ReflectionJson.cs b/tests/fixtures/build-policy/Plants/ReflectionJson.cs new file mode 100644 index 00000000..00f77a95 --- /dev/null +++ b/tests/fixtures/build-policy/Plants/ReflectionJson.cs @@ -0,0 +1,12 @@ +using System.Text.Json; + +namespace ProductCli.PolicyProbe.Plants; + +/// +/// The planted failure of ADR-0003 D2: reflection-based serialisation in a +/// library. The AOT and trim analyzers must refuse it, as errors. +/// +internal static class ReflectionJson +{ + internal static string Serialise(object value) => JsonSerializer.Serialize(value); +} diff --git a/tests/fixtures/build-policy/PolicyProbe.cs b/tests/fixtures/build-policy/PolicyProbe.cs new file mode 100644 index 00000000..35cf7732 --- /dev/null +++ b/tests/fixtures/build-policy/PolicyProbe.cs @@ -0,0 +1,10 @@ +namespace ProductCli.PolicyProbe; + +/// +/// The probe's one source file. It places the compilation in this directory, +/// so the compiler reads the repository's .editorconfig and .globalconfig as +/// it does for a library under src/. +/// +internal static class PolicyProbe +{ +} diff --git a/tests/fixtures/build-policy/ProductCli.PolicyProbe.csproj b/tests/fixtures/build-policy/ProductCli.PolicyProbe.csproj new file mode 100644 index 00000000..f2d1b8cb --- /dev/null +++ b/tests/fixtures/build-policy/ProductCli.PolicyProbe.csproj @@ -0,0 +1,37 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/build-policy/PublicAPI.Shipped.txt b/tests/fixtures/build-policy/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/tests/fixtures/build-policy/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/tests/fixtures/build-policy/PublicAPI.Unshipped.txt b/tests/fixtures/build-policy/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/ddd/SOURCE.md b/tests/fixtures/ddd/SOURCE.md new file mode 100644 index 00000000..e692f21f --- /dev/null +++ b/tests/fixtures/ddd/SOURCE.md @@ -0,0 +1,18 @@ +# Vendored from decision-driven-design + +These files are copied byte for byte, for the Varve admission test +(`tests/ProductCli.Canonical.Tests/VarveAdmissionTests.cs`, ADR-0004 D2, #167). +They are never edited here. `.gitattributes` marks them `-text`, so no checkout +changes their bytes. The test checks each one's sha256 against this file. + +- **Repository:** `mindovermachine-dev/decision-driven-design` +- **Commit:** `799cbe84f2a010fb5b65a687eb253666948c73c8` (its `main` on + 2026-11-03, which carries D1.25). Both files were last changed in `0c94c32`. + +| File here | Path there | sha256 | +|---|---|---| +| `acme-shop.ttl` | `applications/sdlc/what/examples/acme-shop.ttl` | `4018cb9ad45c13fceafd7e585649777e7c8b9e7231fab46a42eb3286df91b777` | +| `vectors.json` | `applications/sdlc/what/vectors.json` | `953666f42051e50632290f73200298c9b5d518d2a153c20a31f9589f94374a1d` | + +`vectors.json`'s `graphHash`, the RDFC-1.0 hash of `acme-shop.ttl`, is +`sha256:10cd90893f7fadb1bdf80064a43795be95e2175684c6d3a93a7736abc7c2156b`. diff --git a/tests/fixtures/ddd/acme-shop.ttl b/tests/fixtures/ddd/acme-shop.ttl new file mode 100644 index 00000000..666aa7f4 --- /dev/null +++ b/tests/fixtures/ddd/acme-shop.ttl @@ -0,0 +1,124 @@ +# The acme-shop What — the worked example of applications/sdlc/what.md. +# +# The How test product's What (applications/sdlc/how-examples/), landed in the ruled identity +# form (DDD-dec-63): the product node is the product IRI; every other element is +# P/what/, minted once by what/mint.py on 2026-10-28 and never changed. Names are +# rdfs:label. A test product only: nothing here is acceptable into a real product (ruling 4 of +# 2026-10-28). Blank nodes appear only inside an element's description (scenario lists, scope +# keys, payload bindings). +@prefix sdlc: . +@prefix rdfs: . +@prefix xsd: . +@prefix w: . + + a sdlc:Product ; + rdfs:label "Acme Shop" ; + sdlc:purpose "Sell to consumers and refund them, over one ordering model." . + +# ---- §3.1 the Ordering domain +w:01M610JNAMYTKTBXP8KD0BQ2G0 a sdlc:Domain ; sdlc:within ; rdfs:label "Ordering" . +w:01M610JNAMH5Y3FP9BFRZJ6QSS a sdlc:BoundedContext ; sdlc:within w:01M610JNAMYTKTBXP8KD0BQ2G0 ; rdfs:label "Ordering" ; + sdlc:ubiquitousTerm "Order" , "Refund" . + +w:01M610JNAMCAGMKBDKSY0GQ0S1 a sdlc:Entity ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Order" ; + sdlc:definition "A placed, paid order." ; sdlc:isAggregateRoot true . +w:01M610JNAM71CMH9AJ4YSGX3MV a sdlc:Field ; sdlc:within w:01M610JNAMCAGMKBDKSY0GQ0S1 ; rdfs:label "customer_id" ; sdlc:fieldType xsd:string . +w:01M610JNAMBV0GKE9KYASYPJCA a sdlc:Field ; sdlc:within w:01M610JNAMCAGMKBDKSY0GQ0S1 ; rdfs:label "paid_total" ; sdlc:fieldType xsd:integer . +w:01M610JNAMHGKDY5GHJRB7FGRM a sdlc:Field ; sdlc:within w:01M610JNAMCAGMKBDKSY0GQ0S1 ; rdfs:label "refunded_total" ; sdlc:fieldType xsd:integer . + +w:01M610JNAMBF0DD4V6Y2W1XFC7 a sdlc:Invariant ; sdlc:within w:01M610JNAMCAGMKBDKSY0GQ0S1 ; rdfs:label "order has lines" ; + sdlc:wireName "order-has-lines" ; sdlc:statement "An order cannot be placed with no lines." . +w:01M610JNAMRDJE9PM59PYRRD3J a sdlc:Invariant ; sdlc:within w:01M610JNAMCAGMKBDKSY0GQ0S1 ; rdfs:label "refund by owner" ; + sdlc:wireName "refund-by-owner" ; sdlc:statement "Only the order's customer may request its refund." . +w:01M610JNAMBBMM4NHG16BQTNRE a sdlc:Invariant ; sdlc:within w:01M610JNAMCAGMKBDKSY0GQ0S1 ; rdfs:label "refund within paid" ; + sdlc:wireName "refund-within-paid" ; sdlc:statement "refunded_total + amount <= paid_total." . + +# ---- §3.2 events and commands, with payload fields in order +w:01M610JNAMCQ2R6MG31Q07DWMX a sdlc:Command ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Place order" ; sdlc:wireName "place-order" ; + sdlc:targets w:01M610JNAMCAGMKBDKSY0GQ0S1 ; sdlc:emits w:01M610JNAM3FKSYQJ0NPEMRJQ5 ; sdlc:fieldOrder ( w:01M610JNAMPJPTACP44JCE5B2S ) . +w:01M610JNAMPJPTACP44JCE5B2S a sdlc:Field ; sdlc:within w:01M610JNAMCQ2R6MG31Q07DWMX ; rdfs:label "lines" ; sdlc:wireName "lines" ; sdlc:fieldType xsd:integer . +w:01M610JNAMC74SDA3DVDE332WB a sdlc:Command ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Refund order" ; sdlc:wireName "refund-order" ; + sdlc:targets w:01M610JNAMCAGMKBDKSY0GQ0S1 ; sdlc:emits w:01M610JNAMJPDE0DG2T4TY6X9A ; sdlc:fieldOrder ( w:01M610JNAMXAPW6HP60ZS6H37V ) . +w:01M610JNAMXAPW6HP60ZS6H37V a sdlc:Field ; sdlc:within w:01M610JNAMC74SDA3DVDE332WB ; rdfs:label "amount" ; sdlc:wireName "amount" ; sdlc:fieldType xsd:integer . + +w:01M610JNAM3FKSYQJ0NPEMRJQ5 a sdlc:Event ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Order placed" ; sdlc:wireName "order-placed" ; + sdlc:changes w:01M610JNAMCAGMKBDKSY0GQ0S1 ; sdlc:fieldOrder ( w:01M610JNAM0RKJ7T4099SW1N77 w:01M610JNAM959BR6Y50HZ3WME4 ) . +w:01M610JNAM0RKJ7T4099SW1N77 a sdlc:Field ; sdlc:within w:01M610JNAM3FKSYQJ0NPEMRJQ5 ; rdfs:label "customer_id" ; sdlc:wireName "customer_id" ; sdlc:fieldType xsd:string . +w:01M610JNAM959BR6Y50HZ3WME4 a sdlc:Field ; sdlc:within w:01M610JNAM3FKSYQJ0NPEMRJQ5 ; rdfs:label "paid_total" ; sdlc:wireName "paid_total" ; sdlc:fieldType xsd:integer . +w:01M610JNAMJPDE0DG2T4TY6X9A a sdlc:Event ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Order refunded" ; sdlc:wireName "order-refunded" ; + sdlc:changes w:01M610JNAMCAGMKBDKSY0GQ0S1 ; sdlc:fieldOrder ( w:01M610JNAM2HPRB9QBTYD15XZ7 ) . +w:01M610JNAM2HPRB9QBTYD15XZ7 a sdlc:Field ; sdlc:within w:01M610JNAMJPDE0DG2T4TY6X9A ; rdfs:label "amount" ; sdlc:wireName "amount" ; sdlc:fieldType xsd:integer . + +# ---- §3.3 the Decider, its logic by digest, and one scenario +w:01M610JNAMMP5V59XRNHD39R5A a sdlc:Decider ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Order" ; + sdlc:decidesFor w:01M610JNAMCAGMKBDKSY0GQ0S1 ; sdlc:handles w:01M610JNAMCQ2R6MG31Q07DWMX , w:01M610JNAMC74SDA3DVDE332WB ; + sdlc:reads w:01M610JNAM71CMH9AJ4YSGX3MV , w:01M610JNAMBV0GKE9KYASYPJCA , w:01M610JNAMHGKDY5GHJRB7FGRM , w:01M610JNAMQHZ7H4Y0GVEQAWBG ; + sdlc:rejects w:01M610JNAMBF0DD4V6Y2W1XFC7 , w:01M610JNAMRDJE9PM59PYRRD3J , w:01M610JNAMBBMM4NHG16BQTNRE ; + sdlc:logic "sha256:24c18e0148752eab9667545a85182b9ee7ab03d8d7c60d8259f5b69e6a260e6b" ; sdlc:logicMediaType "application/yaml" . +w:01M610JNAM51J0KCR35NQCD9YC a sdlc:Scenario ; sdlc:within w:01M610JNAMMP5V59XRNHD39R5A ; rdfs:label "refund beyond what was paid" ; + sdlc:given ( [ sdlc:event w:01M610JNAM3FKSYQJ0NPEMRJQ5 ; + sdlc:with [ sdlc:field w:01M610JNAM0RKJ7T4099SW1N77 ; sdlc:value "c-1" ] , [ sdlc:field w:01M610JNAM959BR6Y50HZ3WME4 ; sdlc:value "100"^^xsd:integer ] ] ) ; + sdlc:when [ sdlc:command w:01M610JNAMC74SDA3DVDE332WB ; sdlc:asSubject w:01M610JNAMMSAY8ZWD1TMMZ8HH ; + sdlc:with [ sdlc:field w:01M610JNAMXAPW6HP60ZS6H37V ; sdlc:value "150"^^xsd:integer ] , [ sdlc:field w:01M610JNAMQHZ7H4Y0GVEQAWBG ; sdlc:value "c-1" ] ] ; + sdlc:thenReject w:01M610JNAMBBMM4NHG16BQTNRE . + +# ---- §3.2, §3.4 the View and its Projector, keyed for scoping +w:01M610JNAMFTV81M9FR1TXEN03 a sdlc:View ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Order summary" ; + sdlc:projects w:01M610JNAM3FKSYQJ0NPEMRJQ5 , w:01M610JNAMJPDE0DG2T4TY6X9A ; sdlc:hasState sdlc:statePresent , sdlc:stateEmpty . +w:01M610JNAM10QDGKF29GWXSHK1 a sdlc:Field ; sdlc:within w:01M610JNAMFTV81M9FR1TXEN03 ; rdfs:label "customer_id" ; sdlc:wireName "customer_id" ; sdlc:fieldType xsd:string . +w:01M610JNAMD2PDDKQRCW18Z0E4 a sdlc:Projector ; sdlc:within w:01M610JNAMH5Y3FP9BFRZJ6QSS ; rdfs:label "Order summary" ; + sdlc:projectsFor w:01M610JNAMFTV81M9FR1TXEN03 ; sdlc:folds w:01M610JNAM3FKSYQJ0NPEMRJQ5 , w:01M610JNAMJPDE0DG2T4TY6X9A ; sdlc:keyedBy w:01M610JNAM10QDGKF29GWXSHK1 ; + sdlc:logic "sha256:a39a05f9c3f992f4f839c892bab65c7d45fc23a1f5e52a1ca9a793045f93770d" ; sdlc:logicMediaType "application/yaml" . + +# ---- §3.7 subject kinds, their claims, and the View's scope +w:01M610JNAMMSAY8ZWD1TMMZ8HH a sdlc:SubjectKind ; sdlc:within w:01M610JNAMYTKTBXP8KD0BQ2G0 ; rdfs:label "Customer" ; sdlc:wireName "customer" ; sdlc:nature sdlc:natureHuman . +w:01M610JNAMQHZ7H4Y0GVEQAWBG a sdlc:Field ; sdlc:within w:01M610JNAMMSAY8ZWD1TMMZ8HH ; rdfs:label "customer_id" ; sdlc:wireName "customer_id" ; sdlc:fieldType xsd:string . +w:01M610JNAMHKDH8MYYBGG3DBMA a sdlc:SubjectKind ; sdlc:within w:01M610JNAMYTKTBXP8KD0BQ2G0 ; rdfs:label "Staff" ; sdlc:wireName "staff" ; sdlc:nature sdlc:natureHuman . +w:01M610JNAMF373V3PH2FNTY837 a sdlc:ScopeRule ; sdlc:within w:01M610JNAMFTV81M9FR1TXEN03 ; rdfs:label "customers see their own orders" ; + sdlc:forSubject w:01M610JNAMMSAY8ZWD1TMMZ8HH ; + sdlc:key [ sdlc:viewField w:01M610JNAM10QDGKF29GWXSHK1 ; sdlc:claim w:01M610JNAMQHZ7H4Y0GVEQAWBG ; sdlc:operator "equals" ] . +w:01M610JNAM10YNEPYPSXB6RMCT a sdlc:ScopeRule ; sdlc:within w:01M610JNAMFTV81M9FR1TXEN03 ; rdfs:label "staff see every order" ; + sdlc:forSubject w:01M610JNAMHKDH8MYYBGG3DBMA . + +# ---- §3.2.5 the systems +w:01M610JNAMV23W7Y9YQKZPJ4ZA a sdlc:System ; sdlc:within ; rdfs:label "shop-api" ; + sdlc:systemKind "service" ; sdlc:purpose "The ordering service other systems call." ; + sdlc:referencesDomain w:01M610JNAMYTKTBXP8KD0BQ2G0 . +w:01M610JNAM8K9PG653537FE7EG a sdlc:System ; sdlc:within ; rdfs:label "shop-bff" ; + sdlc:systemKind "application" ; sdlc:purpose "The customer-facing web shop." ; + sdlc:referencesDomain w:01M610JNAMYTKTBXP8KD0BQ2G0 ; sdlc:targetsClass sdlc:GUI ; sdlc:targetsContext w:01M610JNAMPMZF1RVXMWH5WTHK . +w:01M610JNAMPMZF1RVXMWH5WTHK a sdlc:ContextOfUse ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "web" ; sdlc:dimension "platform" . +w:01M610JNAMM87QX4FY174WWP2R a sdlc:DeclaredTarget ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "Acme design system" ; + sdlc:targetKind "design-system" ; sdlc:edition "DS@1@https://design.acme.example/packages/acme-ds" . +w:01M610JNAM4J9NN1M290V2F9WH a sdlc:DeclaredTarget ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "Acme content store" ; + sdlc:targetKind "content-store" ; sdlc:edition "CS@1@https://design.acme.example/packages/acme-content" ; + sdlc:locale "en-GB" , "da-DK" . + +# ---- §3.2.0, §3.2.1, §3.2.4 triggers, a flow, a UI step, the root +w:01M610JNAMGPYXD4X3VAE9RBC0 a sdlc:Trigger ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "customer places an order" ; + sdlc:source sdlc:sourceUser ; sdlc:subject w:01M610JNAMMSAY8ZWD1TMMZ8HH ; sdlc:issues w:01M610JNAMCQ2R6MG31Q07DWMX . +w:01M610JNAMHZ5N0XY7Z0YCH227 a sdlc:Trigger ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "customer asks for a refund" ; + sdlc:source sdlc:sourceUser ; sdlc:subject w:01M610JNAMMSAY8ZWD1TMMZ8HH ; sdlc:issues w:01M610JNAMC74SDA3DVDE332WB . + +w:01M610JNAM18X2V4SPKEV6G1M5 a sdlc:SystemRoot ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "shop-bff root" ; sdlc:navigatesFromRoot w:01M610JNAMWPGKQNH3B0N3KBTH . +w:01M610JNAMWPGKQNH3B0N3KBTH a sdlc:Flow ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "refund" ; sdlc:entryPage w:01M610JNAM76HYQ9AXTP6ZDY81 . +w:01M610JNAM76HYQ9AXTP6ZDY81 a sdlc:UIStep ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "Order detail" ; sdlc:inFlow w:01M610JNAMWPGKQNH3B0N3KBTH ; + sdlc:intent "The customer checks an order and asks for part of it back." ; + sdlc:emphasis "What is still refundable." ; + sdlc:offeredTo w:01M610JNAMMSAY8ZWD1TMMZ8HH ; + sdlc:references w:01M610JNAM8Y7FS4CTRQQV5KR5 ; + sdlc:mustSatisfy . +w:01M610JNAM967F0A5307C936ZR a sdlc:Interaction ; sdlc:within w:01M610JNAM76HYQ9AXTP6ZDY81 ; rdfs:label "show the order" ; + sdlc:projection w:01M610JNAMFTV81M9FR1TXEN03 ; sdlc:typedAs sdlc:aioDisplayValue . +w:01M610JNAM9S56WC3FTWZWGS35 a sdlc:Interaction ; sdlc:within w:01M610JNAM76HYQ9AXTP6ZDY81 ; rdfs:label "enter the amount" ; + sdlc:field w:01M610JNAMXAPW6HP60ZS6H37V ; sdlc:typedAs sdlc:aioNumericEntry . +w:01M610JNAMEGJXF540DC0FMQXC a sdlc:Interaction ; sdlc:within w:01M610JNAM76HYQ9AXTP6ZDY81 ; rdfs:label "ask for the refund" ; + sdlc:command w:01M610JNAMC74SDA3DVDE332WB ; sdlc:typedAs sdlc:aioTriggerAction . +w:01M610JNAMJD5JN6VT0J0KZQW0 a sdlc:StateAnnotation ; sdlc:within w:01M610JNAM76HYQ9AXTP6ZDY81 ; rdfs:label "no such order" ; + sdlc:forState sdlc:stateEmpty ; sdlc:meaning "The order is not the customer's or does not exist; offer the way back to their orders." . +w:01M610JNAM8Y7FS4CTRQQV5KR5 a sdlc:ContentKey ; sdlc:within w:01M610JNAM8K9PG653537FE7EG ; rdfs:label "order detail heading" ; sdlc:role "heading" . + +# ---- §3.6 a product-wide architectural constraint +w:01M610JNAMGQQ2DJENWGA6MWCK a sdlc:ArchitecturalConstraint ; sdlc:within ; + rdfs:label "personal data resides in the EU" ; + sdlc:statement "Every store of personal data, in every deployment of every system, is in an EU-jurisdiction region." . diff --git a/tests/fixtures/ddd/vectors.json b/tests/fixtures/ddd/vectors.json new file mode 100644 index 00000000..e80506bc --- /dev/null +++ b/tests/fixtures/ddd/vectors.json @@ -0,0 +1,192 @@ +{ + "canon": "rdfc-1.0", + "hash": "sha256", + "example": "examples/acme-shop.ttl", + "graphHash": "sha256:10cd90893f7fadb1bdf80064a43795be95e2175684c6d3a93a7736abc7c2156b", + "synthetic": { + "nTriples": " _:l1 .\n_:l1 _:e1 .\n_:l1 .\n_:e1 \"100\"^^ .\n", + "hash": "sha256:456983e04c5fbe95df100d4fe330cb44580bc308f0a2c1ee414e580e6f7a32b7" + }, + "elements": [ + { + "iri": "https://acme.example/products/shop", + "hash": "sha256:2b1fdeb864fe39e494867c78a6227bccf93806f99ec65ce34721cf3cb6ca801a" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM0RKJ7T4099SW1N77", + "hash": "sha256:72c87f19698b2a2b3103387446144aae421202dc183f3594697a040669c6cae4" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM10QDGKF29GWXSHK1", + "hash": "sha256:73a035341e751ed2bb8baf42086a23a3aff143dcb6a8e8ab50f6de3a75d706da" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM10YNEPYPSXB6RMCT", + "hash": "sha256:ce5ba6ea8c44df4a1da04bf9b0e480f551d9472cb57174859ea69520dafeb1c6" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM18X2V4SPKEV6G1M5", + "hash": "sha256:c7b1df959361273c1efff04d77cee49748dc0ee294510c0f5492c31ab96ba9ba" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM2HPRB9QBTYD15XZ7", + "hash": "sha256:68bc7bcf89accbbf5d397bbe4539a78eaa687cf00ae61a7dea393cdf93cb247b" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM3FKSYQJ0NPEMRJQ5", + "hash": "sha256:f580f283b177fbbf9ee09b4d6216be4f0aeb7b7926c5a8e1963961c958d449ac" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM4J9NN1M290V2F9WH", + "hash": "sha256:8ddcd598734a2836d2aba2c9d8cd590f4c69367cf0a17d33a0ccbdd4ba95f286" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM51J0KCR35NQCD9YC", + "hash": "sha256:1ec36c54959643ae45a5c1ed74a633582ed09d775572d61917c3ad4841b9e38b" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM71CMH9AJ4YSGX3MV", + "hash": "sha256:b8c8d3a1ae709a8073b97c70159bf1b07ef59209419ffaa70f31bb1a8af17df5" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM76HYQ9AXTP6ZDY81", + "hash": "sha256:d1964da52198c7e58de9cd9583af19de9bde843710c43693ab6e9e876b70e1e0" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM8K9PG653537FE7EG", + "hash": "sha256:a0fd4bd4b34119fc14df665d4a313b245d9ef3c5d84698aecd8fe8e9e4cab6dc" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM8Y7FS4CTRQQV5KR5", + "hash": "sha256:dacbef67f039a6e756adca389f862d4bbd602318125305dcbd742dbae461ce47" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM959BR6Y50HZ3WME4", + "hash": "sha256:2d09be2ea9b1772167c21d1688fa9cc0a853932e0d8dc09ac6bdee110ed98538" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM967F0A5307C936ZR", + "hash": "sha256:122c07c9a064755f5577dc07357c10a7f4dbdf730422cba9de133e3105f2495f" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAM9S56WC3FTWZWGS35", + "hash": "sha256:358128c01b3ef343e2835440f9681ec596edaa869969cc7e6880e5b733806530" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMBBMM4NHG16BQTNRE", + "hash": "sha256:e9699fa7cdaec72cc90b0ecc011f7e5548424fd82efb3a744c35ad63a20210df" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMBF0DD4V6Y2W1XFC7", + "hash": "sha256:f918db3aeec1bafa5c474d4f7e983c89452261887b84ddf3e5cba5994f5d6248" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMBV0GKE9KYASYPJCA", + "hash": "sha256:0982b5a8549d51a9fa73072dd95242ef31574d795c36a789c1cef6e90db21d65" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMC74SDA3DVDE332WB", + "hash": "sha256:d95e8e5672e128ce12754bc5caeac974c19ddc9a6d0cc339a8a5964c52530883" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMCAGMKBDKSY0GQ0S1", + "hash": "sha256:ea78cb132a964543c8fa416c6ccf608c02e5789bdfe0525526d7a196f1b6c8d3" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMCQ2R6MG31Q07DWMX", + "hash": "sha256:2698921179975ced5d453809017bdc4df7cfe0ceae4b5c9a2eab04e6a5d7a7e4" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMD2PDDKQRCW18Z0E4", + "hash": "sha256:57c43ce99d87d2c3ea3cdb718408cfe5a819d23d393197e083c5082c863638d4" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMEGJXF540DC0FMQXC", + "hash": "sha256:b5bc478d40165d620c055e4cd3b4374b9395ba43e6b8e928366b55e8a9c539a9" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMF373V3PH2FNTY837", + "hash": "sha256:034943505fd3432d08a18666febb9139de521368940fa5d0d8cb168536e43df1" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMFTV81M9FR1TXEN03", + "hash": "sha256:0260544ff84f19c4792ca6655ea5fcf354bffd24fedd83a99660d201963e6672" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMGPYXD4X3VAE9RBC0", + "hash": "sha256:de8f2e6465fad36c039a11481acdf086cc479f7e33d08186b56040ee02075deb" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMGQQ2DJENWGA6MWCK", + "hash": "sha256:f08812d331db9438f22bac784a574a144f834c7f6848184f0676be201873d76a" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMH5Y3FP9BFRZJ6QSS", + "hash": "sha256:e12084a29798c0d791774b33d564aa6415e161540eb36a6951e23ee32f84abc4" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMHGKDY5GHJRB7FGRM", + "hash": "sha256:69b19ceafacf094a024cbe44d474789d7656772b805c849c9744badb61867b96" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMHKDH8MYYBGG3DBMA", + "hash": "sha256:a1736d09f1f5f8eac3a2f4cbf70a7ef89c094a286c4d54e2808b37e97fba5cd4" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMHZ5N0XY7Z0YCH227", + "hash": "sha256:45a388b9644baa86747cdc5736a5ca67ae369618259a36c491be9f90c82c825d" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMJD5JN6VT0J0KZQW0", + "hash": "sha256:6e7c0e57645eecc4e726b9ad0c2ae0f0525a7db11b24521548a9ac0f1d2acd97" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMJPDE0DG2T4TY6X9A", + "hash": "sha256:877eea6beb5557dd8f8c8455528dd8263381a1e8f68e722ea6822c324b27ab7f" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMM87QX4FY174WWP2R", + "hash": "sha256:a70da49495a985695dbe520369e092b7758a857a85618910db49c13b23e0b63f" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMMP5V59XRNHD39R5A", + "hash": "sha256:e7c5a0997a329eefb5241a729cf9c772f20d1809a204bd019daebf727670ac58" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMMSAY8ZWD1TMMZ8HH", + "hash": "sha256:f1771b80380ac51d0b6d4dead9249d6a66bd5b598441f1d256d4b849db85aff6" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMPJPTACP44JCE5B2S", + "hash": "sha256:3bde8df54e6abcff04036c81ae1381314fd5632ac42d043280062e9b2f62da16" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMPMZF1RVXMWH5WTHK", + "hash": "sha256:bcefd002d99a0cb6206ed7257ca6598d2483e61d1c3bc40826e3da01342d82e5" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMQHZ7H4Y0GVEQAWBG", + "hash": "sha256:44dbe676eab651b267e32b17bb15c115ee445c1b7b8777ffe9aba773cf4d93e2" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMRDJE9PM59PYRRD3J", + "hash": "sha256:d4b648f82fa281f0717d3ed1e3c8874f32564b97bac5d759516ef1dcd44eb143" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMV23W7Y9YQKZPJ4ZA", + "hash": "sha256:e5481e46700d51bbb6dd7ae417f94a02fa02bb31b600c14d409d528c15fde3e2" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMWPGKQNH3B0N3KBTH", + "hash": "sha256:93ecfa4f32a1190fe88899305bb1ebd59d0077b8f27b52ff76bf38e004c62dfc" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMXAPW6HP60ZS6H37V", + "hash": "sha256:e90a552b7259f80968ee6e67653667161ae1a74c124dcb40f9795bde96ee359d" + }, + { + "iri": "https://acme.example/products/shop/what/01M610JNAMYTKTBXP8KD0BQ2G0", + "hash": "sha256:3803a2cd3f5997d78955df34358001c6426d32698d98d15e910fd1b7eee232d7" + } + ] +} diff --git a/tests/fixtures/layering/Probe.cs b/tests/fixtures/layering/Probe.cs new file mode 100644 index 00000000..1c6730c6 --- /dev/null +++ b/tests/fixtures/layering/Probe.cs @@ -0,0 +1,10 @@ +namespace ProductCli.Probe; + +/// +/// The probe's one source file. It places the compilation in this directory, +/// so the compiler reads the repository's .editorconfig and .globalconfig as +/// it does for a library under src/. +/// +internal static class Probe +{ +} diff --git a/tests/fixtures/layering/ProductCli.Probe.csproj b/tests/fixtures/layering/ProductCli.Probe.csproj new file mode 100644 index 00000000..e4648221 --- /dev/null +++ b/tests/fixtures/layering/ProductCli.Probe.csproj @@ -0,0 +1,18 @@ + + + + + $(ProbeLayer) + + + + + + + diff --git a/tests/fixtures/layering/PublicAPI.Shipped.txt b/tests/fixtures/layering/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/tests/fixtures/layering/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/tests/fixtures/layering/PublicAPI.Unshipped.txt b/tests/fixtures/layering/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..e69de29b