diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 026c332..fce92ca 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": true }, "purview.build": { - "version": "0.7.1", + "version": "0.8.1", "commands": [ "purview-build" ], diff --git a/.config/org-inventory.json b/.config/org-inventory.json index 73dfe6f..3eb29f5 100644 --- a/.config/org-inventory.json +++ b/.config/org-inventory.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "org": "purview-dev", "branch": "main", - "fetchedAt": "2026-11-03T15:46:07Z", + "fetchedAt": "2026-11-07T08:43:42Z", "repositories": [ { "name": "architests", @@ -73,12 +73,31 @@ "architecture diagrams from source" ] }, + { + "name": "astroc4", + "visibility": "public", + "archived": false, + "commitSha": "90398b736b4fee4751e7241689b94a5014ac9d8b", + "pushedAt": "2026-11-06T13:47:01Z", + "docsDir": "docs/wiki", + "docsPages": 2, + "skills": [ + "purview-astroc4-grammar", + "purview-astroc4-plugin" + ], + "agentGuidance": [ + "AGENTS.md" + ], + "concerns": [ + "architecture diagrams in an Astro site" + ] + }, { "name": "build", "visibility": "public", "archived": false, - "commitSha": "d00b005dd565f59f5b8641b2b80377d4e6e52c32", - "pushedAt": "2026-11-02T23:03:07Z", + "commitSha": "ee3cfcac69ccf14624eb024dcc1684bb572ab631", + "pushedAt": "2026-11-04T19:39:01Z", "docsDir": "docs/wiki", "docsPages": 14, "skills": [ @@ -129,8 +148,8 @@ "name": "containers", "visibility": "public", "archived": false, - "commitSha": "b94d92afd52af1d509bc481b20c83fb9bb57badd", - "pushedAt": "2026-11-02T23:27:39Z", + "commitSha": "a0fac5502d5d543c5a1f3bb99a441cc2093bf232", + "pushedAt": "2026-11-03T18:57:30Z", "docsDir": "docs/wiki", "docsPages": 19, "skills": [ @@ -152,14 +171,15 @@ "name": "engineering-playbook", "visibility": "public", "archived": false, - "commitSha": "ccf5b3d14616c1cbc904167439635909ed21d14c", - "pushedAt": "2026-11-03T15:40:26Z", + "commitSha": "cbe10a734f4f4ef1e29ea5d2ba710d1ef10d2702", + "pushedAt": "2026-11-04T08:43:29Z", "docsDir": "docs/wiki", - "docsPages": 19, + "docsPages": 5, "skills": [ "purview-architecture", "purview-architecture-testing", "purview-engineering-principles", + "purview-org-harvest", "purview-persistence", "purview-telemetry", "purview-testing-conventions" @@ -213,11 +233,12 @@ "name": "pm-skills", "visibility": "public", "archived": false, - "commitSha": "8460e52f4a36fd6cb59e6169e047448082b7b6ad", - "pushedAt": "2026-11-01T23:28:22Z", + "commitSha": "c32db71ba89f66260d678e5f9f843f557459b7b4", + "pushedAt": "2026-11-04T18:06:35Z", "docsDir": null, "docsPages": 0, "skills": [ + "pm-board", "pm-checkpoint", "pm-discover", "pm-help", @@ -226,12 +247,15 @@ "pm-next", "pm-park", "pm-purview", + "pm-rejoin", "pm-resume", "pm-review", + "pm-roadmap", "pm-setup", "pm-shape", "pm-status", - "pm-strategy" + "pm-strategy", + "pm-up-next" ], "agentGuidance": [ "AGENTS.md", @@ -245,8 +269,8 @@ "name": "purview-dev.github.io", "visibility": "public", "archived": false, - "commitSha": "46fdb0e7a3155059aba132221737c1f6868542a6", - "pushedAt": "2026-11-03T15:28:00Z", + "commitSha": "0d2d72d0d6e3b69b454710622bd8bbd1bb60f46c", + "pushedAt": "2026-11-06T14:01:41Z", "docsDir": "docs", "docsPages": 2, "skills": [], @@ -356,10 +380,10 @@ "name": "zodsharp", "visibility": "public", "archived": false, - "commitSha": "d0dbc74417d4445226a58ef96464d4d3519bd3c4", - "pushedAt": "2026-11-02T23:19:42Z", + "commitSha": "667add30a57a0d0a5c0cdaeab436f7fd6af34224", + "pushedAt": "2026-11-03T22:37:59Z", "docsDir": "docs/wiki", - "docsPages": 35, + "docsPages": 5, "skills": [ "purview-zodsharp-aspnetcore", "purview-zodsharp-blazor", diff --git a/AGENTS.md b/AGENTS.md index 1e6d15f..e4e0ca3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,8 +20,7 @@ implementation whose only job is to prove the conventions compile, run, and fail | Path | Purpose | | -- | -- | -| `docs/wiki` | The playbook itself. `Home.md` is the hub and `_Sidebar.md` is the navigation. | -| `mkdocs.yml` | Wiki site configuration (`docs_dir: docs/wiki`) | +| `docs/wiki` | The playbook itself, as plain Markdown. `Home.md` is the hub and `_Sidebar.md` is the navigation. | | `src/EngineeringPlaybook.slnx` | Canonical solution for restore, build, lint and test | | `src/src/Domain` | The reference implementation's value objects and aggregate | | `src/src/Application` | The port (`IConventionRepository`) and the service that owns the rules | @@ -59,8 +58,8 @@ demonstrate it. - **One page per concern**, in `docs/wiki/
/.md`. The section is the topic folder, the file name is title case with hyphens between words, and the folder name is lower-case. - **Four sections, in order:** the rule, why, how it is enforced, at your scale. A rule a build cannot check - is labelled a review obligation — never described as enforced. `Home.md` and `index.md` are navigation, - and `Engineering-Principles.md` is an index that links to the page stating each rule in full. + is labelled a review obligation — never described as enforced. `Home.md` is navigation rather than a + rule, and `Engineering-Principles.md` is an index that links to the page stating each rule in full. - **A new page goes in `_Sidebar.md` and in the table in `Home.md`.** A page in neither does not exist. - **Link to the authority rather than paraphrasing it.** The SDK's own wiki is authoritative on the SDK's behaviour; a paraphrase here is a second source of truth that will drift. @@ -68,8 +67,8 @@ demonstrate it. - **No reference to a private repository or product, ever.** This playbook is public, and it is aggregated by a public site. A rule that can only be justified by something private cannot be reviewed by the people it is published to. -- **Fences always declare a language.** Mermaid is not enabled in `mkdocs.yml`, so raw Mermaid source goes in - a ` ```text ` block. +- **Fences always declare a language.** A wiki is plain Markdown and no diagram extension is configured, so + raw Mermaid source goes in a ` ```text ` block. ## Skills @@ -84,13 +83,14 @@ build-time `.agents/` mirror outright. The skills CLI clones the git repository, | Skill | Answers | | -- | -- | -| `skills/purview-engineering-principles/` | the ten universal conventions, the conditional persistence rules, and whether a build or a reviewer enforces each | +| `skills/purview-engineering-principles/` | the eleven universal conventions, the conditional persistence rules, and whether a build or a reviewer enforces each | | `skills/purview-architecture/` | how many projects, which layers, where validation lives | | `skills/purview-architecture-testing/` | turning a boundary into a failing build | | `skills/purview-testing-conventions/` | TUnit, AAA, `TestBuilder`, substitutes, tiers | | `skills/purview-persistence/` | storage approach, EF Core integration, well-known identifiers, GUID key strategy | | `skills/purview-telemetry/` | logging, tracing, metrics, and when a project is "too small" for them | | `skills/purview-org-harvest/` | which repository owns which convention, and how it is read at `main` before anything is adopted | +| `skills/purview-component-compatibility/` | the lowest compatible dependency version for a published component, per target framework | Every skill carries the `purview-` prefix: skills install into one flat directory per agent, shared with every skill a developer has installed from anywhere, so an unprefixed name collides and the loser is @@ -119,6 +119,10 @@ folder is installed with it. Name the other skill in prose instead. - **Factories on `TestBuilder` are flat, not nested.** `CA1034` is an `error` in the SDK's style policy and `PRSGD0008` rejects adding it to `NoWarn`. - **A test class lives in the namespace of the class under test**, and is named `{Class}Tests`. +- **A dependency version is a lookup, not a claim.** An application takes the latest version its runtime + supports; a published component declares the **lowest** version that compiles and passes, per target + framework. Never write a release number into a page or a skill — use a registry badge. See + [Dependency Versions](docs/wiki/foundations/Dependency-Versions.md). - **A family's package version is a property.** In `Directory.Packages.props`, a version shared by a family of packages — `TUnit`/`TUnit.Core`/`TUnit.Mocks` here — is declared once in a labelled `PropertyGroup` and referenced as `Version="$(TUnitVersion)"`, so a bump cannot be applied to half the family. A package with diff --git a/Directory.Packages.props b/Directory.Packages.props index e748001..1ce8175 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,10 +20,10 @@ - 10.10.0 + 10.11.0 - 5.1.0 + 5.2.0 diff --git a/README.md b/README.md index 8c3d1fb..c3dc12d 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,10 @@ coding agents are enabled to work in a codebase without hand-holding. | Path | Purpose | | -- | -- | -| `docs/wiki` | The playbook. Aggregated by the purview-dev website. | +| `docs/wiki` | The playbook, as plain Markdown. Aggregated by the purview-dev website. | | `templates/` | Ready-to-copy starter files for a repository adopting the playbook. | | `src/` | The reference implementation: a layered repository whose architecture rules are tests. | | `samples/` | Runnable samples for conventions that `src/` cannot host; run with `just samples`. | -| `mkdocs.yml` | Wiki site configuration (`docs_dir: docs/wiki`). | | `.config/purview-build.json` | Pipeline configuration for the shared Purview build workflow. | | `Justfile` | Supported local workflow commands. | | `package.json` | Authoritative version for the repository. | diff --git a/docs/wiki/Engineering-Principles.md b/docs/wiki/Engineering-Principles.md index 26debc5..3492f60 100644 --- a/docs/wiki/Engineering-Principles.md +++ b/docs/wiki/Engineering-Principles.md @@ -1,6 +1,6 @@ # Engineering Principles -These are the ten universal rules the Purview repositories are held to, plus the conditional persistence +These are the eleven universal rules the Purview repositories are held to, plus the conditional persistence rules below them. This page is an index: each rule is stated once, in full, on the page named beside it. That page carries the failure the rule prevents, the mechanism that makes it true, and what a smaller repository does instead — so a rule is never written twice and the two copies cannot disagree. Where a @@ -19,10 +19,11 @@ A rule that claims enforcement it does not have is worse than no rule, because p | 4 | The build is the only authority | A convention a build can check is checked by the build; the rest is labelled a review obligation, never described as enforced. | [Documentation Convention](foundations/Documentation-Convention.md) | | 5 | Warnings are not acceptable output | A clean build has no warnings: fix one, suppress it where it occurs with a justification, or turn the rule off deliberately. | [Testing and Test Data](testing/Testing-And-Test-Data.md) | | 6 | Every claim about a consumer is proved by a consumer | A package that documents how it must be used ships something that builds that documented consumer and asserts the documented outcome. | [Contracts and Data Exchange](data/Contracts-And-Data-Exchange.md) | -| 7 | Documentation is a build artefact of the repository | The wiki lives in the repository it describes, and the repository owns its `mkdocs.yml`, navigation and `edit_uri`. | [Documentation Convention](foundations/Documentation-Convention.md) | +| 7 | Documentation is a build artefact of the repository | The wiki is plain Markdown in the repository it describes, with its navigation in `_Sidebar.md` and no site configuration beside it. | [Documentation Convention](foundations/Documentation-Convention.md) | | 8 | Everything a test needs is built by one builder | Domain content in a test comes from `TestBuilder`; a test overrides only the value it is proving. | [Testing and Test Data](testing/Testing-And-Test-Data.md) | | 9 | An agent is a reader with no memory | A repository states its purpose, layout and conventions in `AGENTS.md`; nothing important is discoverable only from the code. | [Agent Enablement](agents/Agent-Enablement.md) | | 10 | A change is verified in proportion to its risk | Checks are tiered; a pull request runs the fast tier, and the slow tier is run deliberately. | [Verification Model](foundations/Verification-Model.md) | +| 11 | A version is a range, not a number | An application takes the latest compatible release; a published component declares the lowest compatible version per target framework, and a page shows a badge rather than a number. | [Dependency Versions](foundations/Dependency-Versions.md) | ### Conditional persistence rules @@ -36,7 +37,7 @@ silent there. Each is stated in full on its own page. | P2 | A GUID key strategy is chosen once per store | One provider-specific strategy, carried as context-options data and applied to every key by convention. | [GUID Key Strategy](data/Guid-Key-Strategy.md) | | P3 | EF Core is the persistence boundary | `DbContext` used directly, value objects mapped once, design-time and runtime options in step, migrations applied deliberately. | [EF Core Integration](data/EF-Core-Integration.md) | -Enforcement is stated per rule on the linked page. Three of the ten are enforced by the build in a way +Enforcement is stated per rule on the linked page. Three of the eleven are enforced by the build in a way worth knowing at a glance: identity (rule 2) by `PurviewProjectFileNameMismatch` and the SDK's derivation, architecture (rule 3) by the architecture tests, and the verification tiers (rule 10) by the pipeline's `TestFilter`. The rest are enforced partly by the build and partly by review, and each page diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 8189262..a015ae0 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -26,12 +26,12 @@ There are **two** versions in play here, and the file pins both: ```json { "sdk": { - "version": "10.0.401", + "version": "LATEST", "rollForward": "disable", "allowPrerelease": false }, "msbuild-sdks": { - "Purview.BuildSdk": "1.0.6" + "Purview.BuildSdk": "LATEST" }, "test": { "runner": "Microsoft.Testing.Platform" @@ -39,6 +39,12 @@ There are **two** versions in play here, and the file pins both: } ``` +Both numbers are a **lookup, not a rule**. `LATEST` stands for the newest version your runtime supports: +`dotnet --list-sdks` for the .NET SDK, and the badge for the SDK package — +![Purview.BuildSdk](https://img.shields.io/nuget/v/Purview.BuildSdk). This page does not name either +number, because a page that names a version is wrong within a release. See +[Dependency Versions](foundations/Dependency-Versions.md). + `sdk.version` pins the **.NET SDK** — the `dotnet` that runs the build. `rollForward: "disable"` means exactly that version and nothing else, so a machine with several SDKs installed cannot pick a different one. `msbuild-sdks` pins **`Purview.BuildSdk`**, the MSBuild SDK that supplies the conventions: it is what @@ -56,10 +62,15 @@ bootstrapping](https://github.com/purview-dev/build-sdk/blob/main/docs/wiki/Repo ### Step 2 — Declare the versions the SDK injects, in `Directory.Packages.props` Central Package Management is on by default, so every `PackageReference` the SDK adds has to have a -matching `PackageVersion` or restore fails with `NU1010`. The versions below are the ones this -repository builds with today — check them against the SDK's current release before you commit, as -[`templates/README.md`](https://github.com/purview-dev/engineering-playbook/blob/main/templates/README.md) -says. +matching `PackageVersion` or restore fails with `NU1010`. What follows is the **shape**, not a version +list: a family whose packages ship together gets one property, and every `PackageVersion` in that family +references it, so a bump cannot be applied to half of it. Each `LATEST` is the version the badge for that +package reports, or what `dotnet add package ` writes for you. + +If the repository publishes a package rather than running an application, the number you declare is the +**lowest compatible** version rather than the latest — see +[Dependency Versions](foundations/Dependency-Versions.md) and the `purview-component-compatibility` +skill. ```xml @@ -69,13 +80,13 @@ says. - 10.10.0 + LATEST - 5.0.4 + LATEST - 1.75.0 + LATEST @@ -86,13 +97,13 @@ says. - + - + ``` @@ -350,24 +361,12 @@ itself lives in one shared repository. See [Verification Model](foundations/Veri ### Step 10 — Add the wiki -`mkdocs.yml`: - -```yaml -site_name: Acme -site_description: Developer documentation for Acme -repo_url: https://github.com/acme/acme -edit_uri: edit/main/docs/wiki/ -docs_dir: docs/wiki - -theme: - name: material -plugins: [search, techdocs-core] -markdown_extensions: [admonition, attr_list, md_in_html, pymdownx.details, pymdownx.superfences] -``` - -Then `docs/wiki/index.md`, `docs/wiki/Home.md` and `docs/wiki/_Sidebar.md`, and the pages behind them. -`_Sidebar.md` is the navigation list, so a new page that is not in it is a page nobody finds. See -[Documentation Convention](foundations/Documentation-Convention.md). +Create `docs/wiki/Home.md` and `docs/wiki/_Sidebar.md`, and the pages behind them. That is the whole of +it: the wiki is plain Markdown, `_Sidebar.md` is the navigation list so a new page that is not in it is a +page nobody finds, and there is no site configuration in the repository. The site that publishes the +pages reads them from `docs/wiki` and takes its navigation, its root page and its cross-project grouping +from an entry in the catalogue, not from anything you add here. Write Markdown that still reads correctly +when it is rendered as Markdown. See [Documentation Convention](foundations/Documentation-Convention.md). ### Step 11 — Make the repository legible to an agent @@ -389,12 +388,12 @@ installed globally per machine: "isRoot": true, "tools": { "csharpier": { - "version": "1.3.0", + "version": "LATEST", "commands": ["csharpier"], "rollForward": true }, "purview.build": { - "version": "0.5.3", + "version": "LATEST", "commands": ["purview-build"], "rollForward": false } @@ -404,7 +403,10 @@ installed globally per machine: `Purview.Build` belongs here too, for the same reason CSharpier does: the manifest pins the version, and `dotnet tool restore` is the one command that installs it. A repository should never hand-roll -`dotnet tool install --tool-path` for it. +`dotnet tool install --tool-path` for it. Both `LATEST` values are a lookup — +![Purview.Build](https://img.shields.io/nuget/v/Purview.Build) and +![CSharpier](https://img.shields.io/nuget/v/csharpier) — because the manifest needs a number and this page +must not be the place it comes from. Run `dotnet tool restore` once after adding it. diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 8c9f5d4..eb40854 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -27,10 +27,9 @@ it changes topic. | Adoption and governance | `governance/` | | The reference implementation | `examples/` | -Five pages stay at the wiki root: `Home.md` is this hub, `index.md` is the landing page, `_Sidebar.md` -is the navigation, `Getting-Started.md` is the first page to read, and `Engineering-Principles.md` is -the index that links each rule to the page stating it in full. Every other content page belongs to a -topic folder. +Four pages stay at the wiki root: `Home.md` is this hub, `_Sidebar.md` is the navigation, +`Getting-Started.md` is the first page to read, and `Engineering-Principles.md` is the index that links +each rule to the page stating it in full. Every other content page belongs to a topic folder. ## How to read a page @@ -47,7 +46,7 @@ Rules a build can enforce are labelled as such. Rules only a reviewer can enforc review obligations. A convention that claims to be enforced when it is not is worse than no convention at all, so the two are never blurred. -This page and [`index.md`](index.md) are navigation rather than rules, and +This page is navigation rather than a rule, and [Engineering Principles](Engineering-Principles.md) is an index that links to the page stating each rule in full. @@ -72,6 +71,7 @@ documentation itself is written. | [Architecture as Tests](foundations/Architecture-As-Tests.md) | How the build refuses a dependency that points the wrong way. | | [Verification Model](foundations/Verification-Model.md) | Which checks run, in which tier, for which change. | | [Documentation Convention](foundations/Documentation-Convention.md) | How the wiki is written, structured and published. | +| [Dependency Versions](foundations/Dependency-Versions.md) | Which version to declare — the latest for an application, the lowest compatible for a published component. | ## Data and persistence diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index ae65486..632fe8e 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -13,6 +13,7 @@ - [Architecture as Tests](foundations/Architecture-As-Tests.md) - [Verification Model](foundations/Verification-Model.md) - [Documentation Convention](foundations/Documentation-Convention.md) + - [Dependency Versions](foundations/Dependency-Versions.md) - **Data and persistence** - [Contracts and Data Exchange](data/Contracts-And-Data-Exchange.md) - [Well-Known Identifiers](data/Well-Known-Identifiers.md) diff --git a/docs/wiki/agents/Agent-Guidance-Skills.md b/docs/wiki/agents/Agent-Guidance-Skills.md index 7d3b70e..dcafb55 100644 --- a/docs/wiki/agents/Agent-Guidance-Skills.md +++ b/docs/wiki/agents/Agent-Guidance-Skills.md @@ -7,13 +7,14 @@ library, and they are the only place that guidance is written: | Skill | Answers | | -- | -- | -| `purview-engineering-principles` | What are the ten universal conventions and the conditional persistence rules, what does each prevent, and is it enforced by a build or by a reviewer? | +| `purview-engineering-principles` | What are the eleven universal conventions and the conditional persistence rules, what does each prevent, and is it enforced by a build or by a reviewer? | | `purview-architecture` | How much structure does this project actually need, and what do I need to know before saying? | | `purview-architecture-testing` | How does a boundary become a failing build rather than a review comment? | | `purview-testing-conventions` | How are tests written and fed — framework, structure, naming, and `TestBuilder`? | | `purview-telemetry` | What instrumentation does a project of this size get? (Answer: always some.) | | `purview-persistence` | How is data stored with EF Core, how are identifiers derived when they are known in advance, and — only when it applies — how are GUID keys generated? | | `purview-org-harvest` | Which repository owns which convention, and how is it read at `main` before anything is adopted? | +| `purview-component-compatibility` | Which dependency version does a published component declare, per target framework — and how is that floor proved rather than assumed? | Two obligations fall out of that, and both are enforced only by review: diff --git a/docs/wiki/foundations/Dependency-Versions.md b/docs/wiki/foundations/Dependency-Versions.md new file mode 100644 index 0000000..adda067 --- /dev/null +++ b/docs/wiki/foundations/Dependency-Versions.md @@ -0,0 +1,94 @@ +# Dependency Versions + +## The rule + +Two audiences, and the two rules point in opposite directions. + +| You are publishing | Declare | Because | +| -- | -- | -- | +| an application, a test suite or a tool — something that runs, and nobody installs | the **latest** version compatible with your runtime | you own the runtime, and a version you do not take is a fix you did not get | +| a component — a NuGet or npm package that somebody else installs | the **lowest** version that compiles, passes its tests and satisfies the contract you document, per target framework | a consumer's resolution is a set of lower bounds, so your floor becomes their floor | + +The second row is the golden rule of package development, and it is the one that is broken by accident +rather than by decision: the version you happened to have installed is never the one you should declare. + +**A page never states a release number.** A version in prose is wrong within one release, and the cost is +a reader pinning something three releases old because a page they trusted said so. Where a current +version is genuinely useful, show a badge: it is generated from the registry, so it cannot drift. + +| Component | Registry | Current | +| -- | -- | -- | +| `Purview.BuildSdk` | NuGet | ![Purview.BuildSdk](https://img.shields.io/nuget/v/Purview.BuildSdk) | +| `Purview.Build` (tool) | NuGet | ![Purview.Build](https://img.shields.io/nuget/v/Purview.Build) | +| `Purview.Skills.Testing` | NuGet | ![Purview.Skills.Testing](https://img.shields.io/nuget/v/Purview.Skills.Testing) | +| `Purview.Architecture.Testing` | NuGet | ![Purview.Architecture.Testing](https://img.shields.io/nuget/v/Purview.Architecture.Testing) | +| `Purview.Telemetry.SourceGenerator` | NuGet | ![Purview.Telemetry.SourceGenerator](https://img.shields.io/nuget/v/Purview.Telemetry.SourceGenerator) | +| `Purview.Containers` | NuGet | ![Purview.Containers](https://img.shields.io/nuget/vpre/Purview.Containers) | +| `TUnit` | NuGet | ![TUnit](https://img.shields.io/nuget/v/TUnit) | +| `Bogus` | NuGet | ![Bogus](https://img.shields.io/nuget/v/Bogus) | +| `Microsoft.SourceLink.GitHub` | NuGet | ![Microsoft.SourceLink.GitHub](https://img.shields.io/nuget/v/Microsoft.SourceLink.GitHub) | +| `Microsoft.Extensions.Telemetry.Abstractions` | NuGet | ![Microsoft.Extensions.Telemetry.Abstractions](https://img.shields.io/nuget/v/Microsoft.Extensions.Telemetry.Abstractions) | +| `@purview-dev/astroc4` | npm | ![astroc4](https://img.shields.io/npm/v/@purview-dev/astroc4) | + +A badge is not a recommendation to move. It answers "what is current?"; which version *you* declare is +the decision the two rules above make. + +## Why + +**A documented version rots at the speed of the release.** This playbook told a reader to pin +`Purview.BuildSdk` 1.0.6 in `global.json` while the repository the page documents had already moved to +1.2.0 — accurate when it was written, wrong within a release, and nothing in the build could tell. The +badge replaces the claim with a lookup, so the failure cannot recur. + +**A floor is a floor you cannot take back.** NuGet and npm resolve a graph to the **highest** minimum +among all constraints. A library that declares `Testcontainers` 4.16 because 4.16 is what its author had +installed forces every consumer onto 4.16 — including the consumer whose own constraint is 4.0 and whose +runtime is pinned to it. The consumer cannot refuse an upgrade the library never needed to ask for. The +declared floor is a promise about how little you require, and raising it is a breaking change wearing a +minor version number. + +**The floor is per target framework.** `Purview.ValueObjects` targets `net8.0` through `net11.0`; +`Purview.Skills.Testing` is `net10.0` only. A multi-targeting component has a different floor on each +framework, and the one that matters is the oldest framework it still claims to support — not the newest +one it builds against. + +**The floor is not what you test with.** A repository's own build should resolve the latest, because that +is what will be installed alongside it. The floor needs its own leg, or the first person to discover it +is a consumer. + +## How it is enforced + +| Rule | Enforced by | +| -- | -- | +| An application takes the latest compatible version | Review, with `dotnet list package --outdated` as the evidence | +| A component declares the lowest compatible version, per target framework | A lowest-compatible CI leg that restores against the floor and runs the suite | +| The floor still satisfies every declared target framework | `dotnet build` on each framework, and an API-compatibility check against the previous release | +| No release number is stated in the documentation | A badge, or a link to the registry — never a number in prose | +| A pin that must exist lives where a tool can check it | `global.json`, `Directory.Packages.props`, `.config/dotnet-tools.json` — the only place a number belongs | + +`global.json` and a tool manifest **must** name a version; that is a pin rather than a claim, and it is +the one place a number is correct. Everything a reader copies from it is a starting point, not a target. + +Deciding and checking a floor is a workflow rather than a paragraph, because it means walking a component +hierarchy: which package depends on which, at what minimum, on which framework. That workflow is the +`purview-component-compatibility` skill, and it ships a script that reads the dependency graph out of the +registry instead of trusting a table. + +## At your scale + +- **One application.** Take the latest compatible version and let `--outdated` tell you what you missed. + Badges are enough for the documentation. +- **One library.** The golden rule applies from the first release. One floor, one CI leg, and the floor + written down in the README as a badge beside the package badge. +- **A family of packages.** Keep the family on one version property so a bump cannot be applied to half of + it, and give the floor its own job rather than a checkbox on the ordinary one. +- **A platform.** The floor is a matrix: every supported target framework, and every component that + declares a dependency on another. `purview-component-compatibility` exists for this row. + +## See also + +- [Testing and Test Data](../testing/Testing-And-Test-Data.md) — the tiers a floor check belongs to +- [Verification Model](Verification-Model.md) — which check runs in which tier +- [Cross-Repository Harvest](../governance/Cross-Repository-Harvest.md) — how a component's real floor is + read from the organisation rather than remembered + diff --git a/docs/wiki/foundations/Documentation-Convention.md b/docs/wiki/foundations/Documentation-Convention.md index c624ef7..11d952f 100644 --- a/docs/wiki/foundations/Documentation-Convention.md +++ b/docs/wiki/foundations/Documentation-Convention.md @@ -2,16 +2,15 @@ ## The rule -The wiki lives in the repository it describes, in `docs/wiki`, and the repository owns its `mkdocs.yml`, -its navigation and its `edit_uri`. +The wiki lives in the repository it describes, in `docs/wiki`, as plain Markdown. The repository owns **no +site configuration at all** — no theme, no plugin list, no navigation file to build — because the site +that publishes the pages lives in one place and reads them out of the repository. | Path | Purpose | | -- | -- | -| `docs/wiki/index.md` | The landing page. Short, with links in. | | `docs/wiki/Home.md` | The hub: what the project is, how to read a page, and a table linking every page. | | `docs/wiki/_Sidebar.md` | The navigation list, grouped by section. The leading underscore keeps it out of the content listing. | | `docs/wiki/
/.md` | One page per concern, in the lower-case folder for its topic. Title case file name, hyphens between words. | -| `mkdocs.yml` | Site configuration; `docs_dir: docs/wiki`. | | `README.md` | The repository's front door on GitHub. Points into the wiki; does not duplicate it. | | `AGENTS.md` | The agent's front door. Points into the wiki; does not duplicate it. | @@ -20,16 +19,16 @@ its navigation and its `edit_uri`. `Home.md` tables carry those sections in the same order. A page's folder is part of its published URL, so `foundations/Repository-Shape.md` is published under `foundations/`; move a page between folders only when its topic changes, because every inbound link has to move with it. Only the navigation and entry pages stay -at the wiki root: `Home.md`, `index.md`, `_Sidebar.md`, and the getting-started and principles pages that -the landing page links first. A wiki with one page needs no folders; add a folder when a topic has enough +at the wiki root: `Home.md`, `_Sidebar.md`, and the getting-started and principles pages that the hub +links first. A wiki with one page needs no folders; add a folder when a topic has enough pages that a flat list stops being readable. -**Scope.** This is the layout of a repository that publishes a site, and `docs/wiki` is the folder -this playbook publishes from. Of the sixteen public repositories in the organisation, twelve use -`docs/wiki`; `architests`, `value-objects` and `purview-dev.github.io` publish from `docs`, and +**Scope.** This is the layout of a repository whose documentation is published, and `docs/wiki` is the +folder this playbook publishes from. Of the seventeen public repositories in the organisation, thirteen +use `docs/wiki`; `architests`, `value-objects` and `purview-dev.github.io` publish from `docs`, and `pm-skills` publishes no documentation folder at all, documenting itself through its skills. What the -rule requires is the part those repositories share: the documentation lives with the code it -describes, and the repository owns its `mkdocs.yml`. The folder name is the part that varies. +rule requires is the part those repositories share: the documentation lives with the code it describes, +as Markdown, with its navigation in the folder beside it. The folder name is the only part that varies. [Cross-Repository Harvest](../governance/Cross-Repository-Harvest.md) records the deviations by name. ## Why @@ -50,44 +49,50 @@ the reader where in the playbook they are about to land. needs the layout and the conventions. They link to the same pages and should not restate them; a fact stated twice is a fact that will disagree with itself. -## `mkdocs.yml` - -```yaml -site_name: Acme -site_description: Developer documentation for Acme -repo_url: https://github.com/acme/acme -edit_uri: edit/main/docs/wiki/ -docs_dir: docs/wiki - -theme: - name: material - palette: - - media: "(prefers-color-scheme: light)" - scheme: default - primary: indigo - accent: cyan - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: indigo - accent: cyan - features: [navigation.instant, navigation.sections, navigation.top, search.highlight, search.suggest, content.code.copy] - -plugins: [search, techdocs-core] -markdown_extensions: [admonition, attr_list, md_in_html, pymdownx.details, pymdownx.superfences] -``` - -Three of those lines are load-bearing: - -- **`edit_uri`.** It is the "edit this page" link, and it is the only mechanism that turns a reader into a - contributor. Get the branch wrong and the link 404s. -- **`docs_dir: docs/wiki`.** The wiki is a folder inside the repository rather than the repository root, so - the root stays available for the things that belong there. -- **`plugins: [search, techdocs-core]`.** `techdocs-core` is what the aggregating portal expects. Without - it the site builds and the portal does not pick the pages up. - -> **Mermaid is not enabled.** `pymdownx.superfences` is configured without a Mermaid custom fence, so a -> ` ```mermaid ` block renders as unstyled text. Raw Mermaid source in a ` ```text ` block is the honest -> workaround: it is copy-pasteable into any renderer, and it does not pretend to be a diagram. +## Plain Markdown, and nothing else + +There is no site configuration in the repository: no theme, no plugin list, no `pip install`, no +lockfile beside the docs. A document is a `.md` file, and the tooling that renders it belongs to the +site rather than to the repository. That is not minimalism for its own sake — a second configuration +file is a second place for the navigation to be wrong, and a navigation that lives in a build tool's +file is edited by whoever last touched the build rather than by whoever last touched the pages. + +Two consequences of owning no configuration: + +- **`_Sidebar.md` is the whole navigation.** It is a document in the wiki, so it is reviewed with the + pages it lists and a link that is dead is a link a reviewer can see. A page that is not in it is + published nowhere that a reader can reach. +- **No diagram extension, no custom syntax.** A ` ```mermaid ` fence has nothing to render it, so raw + Mermaid source goes in a ` ```text ` block — copy-pasteable into any renderer, and honest about being + source rather than a picture. + +## How the portal publishes a wiki + +The public site reads the Markdown out of the repositories; there is no site to configure and nothing to +build. For each project it reads an entry in the catalogue (`src/src/data/projects/.yml` in +`purview-dev.github.io`), fetches the Markdown that entry names, and renders it. The behaviour is +defined by that repository's own `docs-aggregation-rules` skill and its `src/src/lib/docs/`; what +follows is the part an author has to know: + +| The catalogue entry declares | What the portal does with it | +| -- | -- | +| `docs.source: github-path` and `docs.path` | Walks `/**/*.md` at the channel's ref. `wiki` and `readme` are the other two sources. | +| `docs.rootPage` | The page served at `/docs//`. In this playbook's entry it is `Home.md`. | +| `docs.exclude` | Base file names never published. This playbook excludes `_Sidebar.md`. | +| `docs.channels` | A second published view of the same tree, normally a prerelease channel pinned to a tag. | +| `docs.redirects` | Old addresses that keep working after a page moves into a topic folder. | + +Page order comes from `docs.order`, then from the `_Sidebar.md` link order, then from a default. That is +why `_Sidebar.md` is a published contract rather than a local convenience. + +Two consequences worth acting on: + +- **The catalogue entry, not the repository, decides what is published.** A project joins the portal by + an entry in that catalogue — there is no file in the repository to add. Its `path`, `rootPage` and + `exclude` are the published surface, so a page the entry cannot reach exists only in a clone. +- **Write Markdown that survives being rendered as Markdown.** The portal converts or strips everything + that is not plain Markdown: HTML attribute lists, template syntax and include directives are removed. + Anything that works only because a renderer extension happens to be installed cannot carry meaning. ## How to write a page @@ -100,10 +105,10 @@ Every content page uses the same four sections, in this order: | **How it is enforced** | The mechanism that makes it true — or an explicit statement that it is a review obligation. | | **At your scale** | What to do when the repository is smaller or larger than the one the rule came from. | -Three pages are deliberately not content pages. `Home.md` is the hub and `index.md` is the landing -page, so both are navigation rather than rules. `Engineering-Principles.md` is an index: it states each -of the ten universal rules in one line, plus the conditional persistence rules, and links to the page -that states it in full, so a rule is never written twice. +`_Sidebar.md` is navigation rather than a content page. `Home.md` is the hub rather than a rule, and +`Engineering-Principles.md` is an index: it states each of the eleven universal rules in one line, plus +the conditional persistence rules, and links to the page that states it in full, so a rule is never +written twice. The shape is not decoration. It forces three questions that a page written as prose usually skips: *is this actually a rule, or an opinion?*; *what breaks if we drop it?*; and *is it enforced, or are we asking a @@ -149,7 +154,7 @@ Harvest](../governance/Cross-Repository-Harvest.md) is that comparison, and the | Rule | Enforced by | | -- | -- | -| The site configuration is present and points at `docs/wiki` | Review | +| The wiki is plain Markdown in `docs/wiki`, with no site configuration beside it | Review | | Every content page, including pages in topic folders, is in `_Sidebar.md` and the `Home.md` table | `DocumentationTests` | | The sidebar sections and the `Home.md` headings carry the same labels, in the same order | `DocumentationTests` | | Every relative link in every page resolves | `DocumentationTests` | @@ -166,7 +171,7 @@ obligation, and this page says so rather than implying otherwise. ## At your scale -- **One page.** `docs/wiki/index.md` plus `Home.md` and `_Sidebar.md`, and a `mkdocs.yml`. Three small files. +- **One page.** `docs/wiki/Home.md` and `_Sidebar.md`. Two small files. No folders, and the sidebar is a flat list. - **A library.** One page per public concern, plus a getting-started page that is the shortest path to a working example. Add a folder once a topic has two or three pages. Keep the consumer contract on its own diff --git a/docs/wiki/foundations/Repository-Shape.md b/docs/wiki/foundations/Repository-Shape.md index 9b79e12..a3c1fcb 100644 --- a/docs/wiki/foundations/Repository-Shape.md +++ b/docs/wiki/foundations/Repository-Shape.md @@ -16,7 +16,7 @@ Repository/ ├── .agents/skills/ skills installed from elsewhere by the skills CLI; gitignored ├── .purview/ SDK state; gitignored ├── docs/ -│ └── wiki/ the wiki; docs_dir in mkdocs.yml +│ └── wiki/ the playbook, as plain Markdown ├── src/ │ ├── .slnx canonical solution │ ├── Directory.Build.props NamespacePrefix + the SDK import @@ -34,7 +34,6 @@ Repository/ ├── global.json SDK pin + test runner ├── Justfile supported local commands ├── LICENSE.md -├── mkdocs.yml wiki site configuration ├── nuget.config ├── package.json authoritative version └── README.md diff --git a/docs/wiki/governance/Adopting-This-At-Your-Scale.md b/docs/wiki/governance/Adopting-This-At-Your-Scale.md index 5961b27..d1b5a01 100644 --- a/docs/wiki/governance/Adopting-This-At-Your-Scale.md +++ b/docs/wiki/governance/Adopting-This-At-Your-Scale.md @@ -52,7 +52,7 @@ Adopt: - `AGENTS.md`. The cheapest thing here, and the one with the largest effect on an agent's first ten minutes. - `TestBuilder` in the test project, from the first test. Retrofitting it later is a mechanical edit across every file, and it is the kind of edit that is postponed forever. -- `docs/wiki/index.md`, `Home.md` and `_Sidebar.md` — three small files that make the project documentable. +- `docs/wiki/Home.md` and `_Sidebar.md` — two small files that make the project documentable. Skip: diff --git a/docs/wiki/governance/Cross-Repository-Harvest.md b/docs/wiki/governance/Cross-Repository-Harvest.md index dcb06c2..99a6895 100644 --- a/docs/wiki/governance/Cross-Repository-Harvest.md +++ b/docs/wiki/governance/Cross-Repository-Harvest.md @@ -57,9 +57,12 @@ harvest records a SHA rather than an impression. The rule is now in `.gitignore` are true rather than merely consistent. **An index maintained by hand under-counts silently.** The public catalogue in -`purview-dev.github.io` carries fifteen project files, one of which is archived, against sixteen -public live repositories: `openapi-xmlcomments` has no entry. A catalogue is a good reading order and -a bad membership list, because it omits by never being edited. `gh repo list` decides membership. +`purview-dev.github.io` carries seventeen project files, one of which is archived, against seventeen +public live repositories, and the single repository without an entry is the catalogue's own: a +catalogue does not list itself. `openapi-xmlcomments` gained its entry after the previous harvest, +which is exactly how a hand-maintained index behaves — the omission is silent, and so is its +correction. A catalogue is a good reading order and a bad membership list. `gh repo list` decides +membership. **A convention copied from the wrong repository is worse than none.** `build-sdk` owns what the tooling does; this playbook owns why a repository is shaped that way. Reading either one for the @@ -68,14 +71,15 @@ other's question produces an answer with a citation that does not support it. ## Deviations the first harvest found The [Documentation Convention](../foundations/Documentation-Convention.md) says a wiki lives in `docs/wiki`. Of the -sixteen public repositories, twelve do. `architests`, `value-objects` and `purview-dev.github.io` +seventeen public repositories, thirteen do. `architests`, `value-objects` and `purview-dev.github.io` publish from `docs`, and `pm-skills` publishes no documentation folder at all, documenting itself -through its fourteen skills. +through its eighteen skills. -That is not sixteen repositories getting a rule wrong; it is one repository's shape written down as if -it were universal. What the rule actually requires is that the documentation lives with the code and -that the repository owns its site configuration — the folder name is the part that varies. The -convention page now says so, and names each deviation rather than pretending the rule was followed. +That is not seventeen repositories getting a rule wrong; it is one repository's shape written down as if +it were universal. What the rule actually requires is that the documentation lives with the code — the +folder name is the part that varies, and nothing else does, because no repository carries a site +configuration any more (`astroc4` publishes `docs/wiki` and nothing beside it). The convention page now +says so, and names each deviation rather than pretending the rule was followed. ## How it is enforced @@ -93,7 +97,7 @@ shell — so the same assertions execute on Windows and on Linux, which is the p Node script rather than a PowerShell one and a Bash one that disagree. It replays a recorded membership with `--from` and `--details` instead of faking `gh` on `PATH`; a shim was tried first and rejected, because a non-shell spawn will not resolve a `.cmd` and the tests then reached the real `gh` and passed -against sixteen live repositories. +against seventeen live repositories. The second row is the one that catches a private repository by name, and it is deliberately blunt: it reads every `.md`, `.cs`, `.json`, `.yml`, `.props` and `.mjs` file in the repository rather than the diff --git a/docs/wiki/index.md b/docs/wiki/index.md deleted file mode 100644 index 4d51265..0000000 --- a/docs/wiki/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Engineering Playbook - -The reusable engineering conventions behind the Purview projects. - -[Documentation overview](Home.md){ .md-button .md-button--primary } -[Get started](Getting-Started.md){ .md-button } -[Engineering principles](Engineering-Principles.md){ .md-button } diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index b51f4ad..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,21 +0,0 @@ -site_name: Engineering Playbook -site_description: The reusable Purview engineering conventions — repository shape, architecture as tests, verification tiers, contracts and agent enablement -repo_url: https://github.com/purview-dev/engineering-playbook -edit_uri: edit/main/docs/wiki/ -docs_dir: docs/wiki - -theme: - name: material - palette: - - media: "(prefers-color-scheme: light)" - scheme: default - primary: indigo - accent: cyan - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: indigo - accent: cyan - features: [navigation.instant, navigation.sections, navigation.top, search.highlight, search.suggest, content.code.copy] - -plugins: [search, techdocs-core] -markdown_extensions: [admonition, attr_list, md_in_html, pymdownx.details, pymdownx.superfences] diff --git a/skills/purview-component-compatibility/SKILL.md b/skills/purview-component-compatibility/SKILL.md new file mode 100644 index 0000000..b017681 --- /dev/null +++ b/skills/purview-component-compatibility/SKILL.md @@ -0,0 +1,116 @@ +--- +name: purview-component-compatibility +description: >- + Use when developing, reviewing or releasing a published component - a NuGet package, or an npm + package - and choosing which dependency version to declare, when a consumer reports a version + conflict or an unexpected upgrade, or when a component's supported target frameworks change. Applies + the golden rule that a published component declares the LOWEST compatible version rather than the + latest, walks the Purview component hierarchy to find each floor, and verifies it by building + against it. Not for applications: they take the latest compatible version instead. +license: MIT +metadata: + author: Purview-Dev + version: "1.0" + repository: purview-dev/engineering-playbook +--- + +# Purview component compatibility + +A published component makes a promise with every version it declares: *this is the least I require of +you*. Raise that number and every consumer is pulled up with you, whether or not they wanted it. + +**The golden rule: declare the lowest version that compiles, passes your tests, and is supported on +every target framework you claim.** The version you happen to have installed is never the answer, and +neither is the latest. + +## Which direction? + +| What you are publishing | Declare | Why | +| -- | -- | -- | +| an application, a test suite, a tool — it runs, and nobody installs it | the **latest** compatible version | you own the runtime, and a version you skip is a fix you did not take | +| a component — a NuGet or npm package somebody else installs | the **lowest** compatible version | dependency resolution takes the highest minimum in the graph, so your floor becomes theirs | + +This skill is about the second row. If the repository only ever runs, stop here and take the latest. + +## Why a floor matters + +NuGet and npm resolve a graph to the **highest** minimum among all constraints. If a package declares +`Testcontainers` 4.16 because 4.16 is what its author had installed, every consumer is forced onto 4.16, +including the one whose own constraint is 4.0 and whose runtime is pinned to it. The consumer cannot +refuse an upgrade the package never needed to ask for, and the symptom appears in *their* build, not +yours. + +## The workflow + +### 1. Name the component and its frameworks + +The floor is per target framework, not per package. Write down every `TargetFramework` the component +publishes; a multi-targeting component has a different floor on each, and the one that matters is the +oldest framework still supported, not the newest one you build against. + +### 2. Read the hierarchy, do not remember it + +The dependencies — and their minimums — are in the published `.nuspec`, not in anyone's memory: + +```bash +node scripts/dependencies.mjs Purview.Containers +node scripts/dependencies.mjs Purview.Architecture.Testing --tfm net11.0 +node scripts/dependencies.mjs Purview.Containers --walk +``` + +A bare version is a **minimum**; a bracketed one (`[1.2.3]`, `[1.0,2.0)`) is a range. The script prints +the distinction, because it is the whole rule: a minimum you raise is a minimum every consumer inherits. + +`references/purview-components.md` records which Purview package depends on which, and which target +frameworks each family supports. Where the two disagree the registry wins — that file is the map, the +`.nuspec` is the territory. + +### 3. Fix the floor, per framework + +For each dependency, find the oldest version that: + +- exposes every API the component calls (the compiler decides this, not the changelog); +- passes the component's own tests and its consumer-verification suite; +- is supported on every framework in that dependency group. + +Then declare that version. Not `latest`, not floating (`1.*`), and never the version that resolved on +your machine by accident. + +### 4. Prove the floor + +A floor nobody builds against is a guess. Add a CI leg that restores the component with every dependency +held at its declared floor and runs the suite — the ordinary leg keeps resolving the latest, which is +what a consumer will actually get. `references/lowest-compatible.md` has the mechanics. + +### 5. Keep the ceiling visible, and the floor written down + +A component's README carries a badge for the current release and a sentence naming the floor it +supports. Badges are generated from the registry, so they cannot go stale; a version written into prose +is wrong within one release. + +## Pitfalls that produce a floor nobody meant + +| Pitfall | What it looks like | The fix | +| -- | -- | -- | +| Declaring what is installed | `dotnet add package X` writes the latest; right for an application, wrong for a component | edit the version down, then prove it in step 4 | +| Transitive uplift | a dependency pulls in a newer transient package than your floor allows | override it deliberately — the persistence sample does this for a security-patched package — and comment why | +| One framework's floor | a `net10.0-windows` group needs a higher floor than `net10.0` | read each group; never average them | +| Prerelease in the graph | a family that ships prereleases depends on them at the same version | pin the family to one property so the floor moves together | +| Analyzer and source-generator packages | compile-time only, so they rarely constrain a consumer | still declare a real floor; they appear in the graph and in transitive audits | +| Floating versions | `1.*` resolves differently on every restore | banned: a floor is a number | + +## Checks before you answer + +- Is each declared version the lowest that passes, or the one that happened to be installed? +- Does every declared target framework have a floor that compiles on that framework? +- Was the floor proved by a build, or only by your machine? +- Does any page state a version number? It should be a badge or a link to the registry. +- If a floor was raised, is that a breaking change the version number admits to? + +## See also + +`references/lowest-compatible.md` for the CI mechanics and the override patterns, and +`references/purview-components.md` for the Purview hierarchy. The rule itself, and the application-side +half of it, is stated in the Engineering Playbook's Dependency Versions page; the +`purview-engineering-principles` skill summarises it as one of the eleven conventions. + diff --git a/skills/purview-component-compatibility/references/lowest-compatible.md b/skills/purview-component-compatibility/references/lowest-compatible.md new file mode 100644 index 0000000..ccdb3b6 --- /dev/null +++ b/skills/purview-component-compatibility/references/lowest-compatible.md @@ -0,0 +1,93 @@ +# Proving the floor + +The rule is "declare the lowest compatible version". This file is the mechanics: how to find the floor, +how to prove it in a build, and the patterns that keep it from silently rising. + +## What "compatible" means, precisely + +A version is a valid floor when all three hold: + +1. **It compiles against every API the component uses.** The compiler decides this, not a changelog. If + you cannot say which member forced the version up, you have not found the floor. +2. **The component's own tests pass against it**, including the consumer-verification suite, which + builds a throwaway consumer rather than the component's own test project. +3. **It is supported on every target framework in that dependency group.** A floor is per group, and a + multi-targeting component can have several. + +## Finding it + +```bash +dotnet list package --outdated # what you declared against what exists +dotnet list package --include-transitive # what actually resolved +dotnet nuget why # which path pulled a version in +``` + +Then work downward: lower the declared version, build, and read the compiler. The first version that +compiles *and* passes is the floor. Do not stop at the first failure and pick the version above it — +that is how a floor ends up one minor above what the code needs. + +## Making the floor a build leg + +Keep one property per family (so a family cannot be bumped halfway) and make each conditional on a floor +switch. Nothing here is NuGet-specific magic: it is MSBuild, and central package management already +gives you the single place to put it. + +```xml + + LATEST + FLOOR + +``` + +```bash +dotnet restore -p:DependencyFloor=true +dotnet build -p:DependencyFloor=true --no-restore +dotnet test -p:DependencyFloor=true --no-build +``` + +`LATEST` and `FLOOR` are literals in the file; the point is that they are two properties, not two files. +Run the ordinary leg at `LATEST` and a second, slower leg at `FLOOR`, and give the floor leg its own CI +job so a failure names the floor rather than the build. + +Where a component multi-targets, add the framework dimension and run the pair per framework: + +```bash +for tfm in net8.0 net9.0 net10.0; do + dotnet build -p:DependencyFloor=true -f "$tfm" +done +``` + +## Transitive pins + +A floor can be raised by something you never declared. Transitive pinning puts those versions where you +can see them: + +```xml + + true + +``` + +Then an override is a deliberate line with a reason beside it — the persistence sample in this +repository does exactly that for a security-patched package the SQL client drags in. An override without +a comment is indistinguishable from an accident, and the next person deletes it. + +## The ceiling, and the API surface + +- The floor leg proves the **bottom**; the ordinary leg proves the **top**. Both, or you know neither. +- An API-compatibility check against the previous release catches the other direction of the same + mistake: a floor can stay put while the public surface grows a member that only the newest dependency + provides. `Microsoft.DotNet.ApiCompat.Task` wires that into the build; `Microsoft.DotNet.ApiCompat.Tool` + runs it from a pipeline. +- A prerelease dependency is a floor with an expiry: it cannot be consumed by anyone who has not opted + into prereleases. Say so in the package's README rather than letting the error message explain it. + +## What not to do + +| Temptation | Why it is wrong | +| -- | -- | +| `dotnet add package X` and commit | it writes the latest; correct for an application, wrong for a component | +| floating versions (`1.*`) | resolves differently on every restore, so the floor is nobody's decision | +| "it restores on my machine" | that proves one graph, on one date, with one SDK | +| raising a floor in a patch release | the consumer's build changes without their version changing | +| one floor leg for all frameworks | the framework with the lowest floor is the one nobody tested | diff --git a/skills/purview-component-compatibility/references/purview-components.md b/skills/purview-component-compatibility/references/purview-components.md new file mode 100644 index 0000000..6e13b65 --- /dev/null +++ b/skills/purview-component-compatibility/references/purview-components.md @@ -0,0 +1,60 @@ +# The Purview component hierarchy + +Which Purview package depends on which, and which target frameworks each family supports, so a harvest +or a floor decision starts from the shape rather than from a memory of it. + +Every row was read from the registry with `scripts/dependencies.mjs`, which reads the published +`.nuspec`. **The registry is the authority; this file is the map.** Re-run the script before acting on +any row — a family that multi-targets changes its floors on every release. + +## The families + +| Family | Packages | Target frameworks | Depends on | +| -- | -- | -- | -- | +| Build SDK | `Purview.BuildSdk` | `netstandard2.0` (an MSBuild SDK) | nothing | +| Test-support rule sets | `Purview.Skills.Testing`, `Purview.Architecture.Testing`, `Purview.Architecture.Testing.Cli` | `net10.0`; `Architecture.Testing` also `net11.0` | `TUnit.Core`, `TUnit.Assertions`, `JsonSchema.Net` | +| Telemetry | `Purview.Telemetry.SourceGenerator` | `netstandard2.0` (an analyzer) | nothing | +| Containers | `Purview.Containers` + `.Core`, `.Docker`, `.Wsl`, `.MsSql`, `.PostgreSql` | `net10.0`; `.Wsl` adds `net10.0-windows10.0.19041` | its own family at the same version, `Microsoft.Extensions.Telemetry.Abstractions`, `Testcontainers` | +| Results | `Purview.Results`, `.AspNetCore`, `.ZodSharp`, `.ZodSharp.AspNetCore` | `net10.0`, `net11.0` | nothing; the adapters depend on the core and on ZodSharp | +| ZodSharp | `Purview.ZodSharp`, `.AspNetCore`, `.SystemTextJson`, `.NewtonsoftJson` | `net9.0`, `net10.0`, `net11.0` | `Microsoft.Extensions.Options`, `Microsoft.Extensions.DependencyInjection.Abstractions` | +| Value Objects | `Purview.ValueObjects` | `net8.0`, `net9.0`, `net10.0`, `net11.0` | nothing | +| Source Generator Framework | `Purview.SourceGeneratorFramework`, `.Testing`, `.Testing.TUnit` | `netstandard2.0` | Roslyn (`Microsoft.CodeAnalysis.CSharp`), `System.Collections.Immutable`, `System.Reflection.Metadata` | +| Aspire ResourceKit | `Purview.Aspire.ResourceKit` | `net8.0`, `net9.0`, `net10.0` | `Aspire.Hosting`, OpenTelemetry exporter and hosting, `Microsoft.Extensions.*` | +| Astroc4 | `@purview-dev/astroc4` (npm, not NuGet) | an Astro/Starlight plugin | `likec4`, `@astrojs/starlight`, `astro` as peers | + +## Reading the shape + +Three patterns are worth internalising, because each one changes what "the floor" means: + +- **A family ships at one version.** `Purview.Containers.Core` is required at the umbrella's own version, + and `Purview.Results.ZodSharp` requires `Purview.Results` and `Purview.ZodSharp` at their current ones. + Declare the family with one property, so its floor moves as a unit. +- **An analyzer has no consumer-side floor worth arguing about.** `Purview.BuildSdk`, + `Purview.Telemetry.SourceGenerator` and `Purview.SourceGeneratorFramework` target `netstandard2.0` and do + their work at compile time. They still appear in a transitive audit, so they still get a real version. +- **A multi-targeting component has a different floor per framework**, and ZodSharp shows it most + clearly: the `net11.0` group declares older `Microsoft.Extensions.*` minimums than the `net10.0` group, + because the lower framework carries the newer requirements. Reading one group and applying it to + another is how a floor is set wrong in the direction that breaks consumers. + +## What the script prints + +```text +Purview.Containers 1.0.0-prerelease.11 + net10.0: Purview.Containers.Core >= 1.0.0-prerelease.11 + net10.0: Purview.Containers.Docker >= 1.0.0-prerelease.11 + net10.0: Purview.Containers.Wsl >= 1.0.0-prerelease.11 + net10.0: Microsoft.Extensions.Telemetry.Abstractions >= 10.11.0 + net10.0: Testcontainers >= 4.16.0 + Purview.Containers.Core 1.0.0-prerelease.11 + net10.0: Microsoft.Extensions.Telemetry.Abstractions >= 10.11.0 +``` + +A bare version is a minimum; brackets would mean a range. The numbers above are what the registry said +when this page was written — the shape is the part that lasts, which is why the command is here and the +versions are not presented as targets. + +## Related components that are not Purview packages + +`AspireC4.Hosting` is published under its own name and versions on its own schedule, so it is not part of +a Purview family floor. Read it the same way, with the same script. diff --git a/skills/purview-component-compatibility/scripts/dependencies.mjs b/skills/purview-component-compatibility/scripts/dependencies.mjs new file mode 100644 index 0000000..3069ef6 --- /dev/null +++ b/skills/purview-component-compatibility/scripts/dependencies.mjs @@ -0,0 +1,239 @@ +#!/usr/bin/env node +/** + * Prints a package's dependency groups exactly as NuGet declares them, so the floor a component + * inherits can be read rather than guessed. + * + * The golden rule of package development is to declare the LOWEST compatible version, and that decision + * needs the real graph: which dependency, at what minimum, on which target framework. This script reads + * it from the registry - the `.nuspec` the published package carries - instead of from a table that + * would be stale by the next release. + * + * Zero dependencies: it uses the platform `fetch`, so it runs anywhere Node 18 or later does. + * + * node dependencies.mjs Purview.Containers + * node dependencies.mjs Purview.Architecture.Testing --tfm net11.0 + * node dependencies.mjs Purview.Containers --walk # recurse into Purview.* dependencies + * node dependencies.mjs Purview.Results --json + * + * An npm package is not a NuGet id; read it with `npm view versions --json`. + * + * Exit codes: 0 on success, 1 when the package, version or network is not available. + */ + +/** The NuGet flat-container root: the same host that serves the package's own `.nuspec`. */ +const REGISTRY = "https://api.nuget.org/v3-flatcontainer"; + +/** How deep `--walk` follows dependencies before it stops. */ +const MAX_DEPTH = 3; + +/** True when a version string carries a prerelease label. */ +function isPrerelease(version) { + return version.includes("-"); +} + +/** Every published version of a package, oldest first. */ +async function versionsOf(id) { + const response = await fetch(`${REGISTRY}/${id.toLowerCase()}/index.json`); + if (!response.ok) { + throw new Error(`${id} is not on nuget.org (HTTP ${response.status}).`); + } + + const body = await response.json(); + return body.versions; +} + +/** The version to describe: the one asked for, else the newest stable, else the newest anything. */ +function chooseVersion(available, requested) { + if (requested) { + const match = available.find((version) => version.toLowerCase() === requested.toLowerCase()); + if (!match) { + throw new Error(`'${requested}' is not a published version. Newest is ${available.at(-1)}.`); + } + + return match; + } + + const stable = available.filter((version) => !isPrerelease(version)); + return stable.at(-1) ?? available.at(-1); +} + +/** The package's `.nuspec`, which is what a consumer's restore actually reads. */ +async function readNuspec(id, version) { + const name = id.toLowerCase(); + const response = await fetch(`${REGISTRY}/${name}/${version.toLowerCase()}/${name}.nuspec`); + if (!response.ok) { + throw new Error(`No .nuspec for ${id} ${version} (HTTP ${response.status}).`); + } + + return response.text(); +} + +/** The value of one XML attribute on a tag, or null. */ +function attribute(tag, name) { + const match = new RegExp(`${name}\\s*=\\s*"([^"]*)"`).exec(tag); + return match ? match[1] : null; +} + +/** + * How a nuspec version string constrains a consumer: a bare version is a MINIMUM, and a bracketed one is + * a range. This is the distinction the whole rule turns on, because a minimum you raise is a minimum + * every consumer inherits. + */ +function classify(range) { + if (!range) { + return "any"; + } + + return range.startsWith("[") || range.startsWith("(") ? "range" : "minimum"; +} + +/** The dependency elements inside a fragment, normalized. */ +function dependenciesIn(fragment) { + const dependencies = []; + const pattern = /]*)\/?>/g; + let match; + + while ((match = pattern.exec(fragment)) !== null) { + const tag = match[1]; + const version = attribute(tag, "version"); + dependencies.push({ id: attribute(tag, "id"), version, constraint: classify(version) }); + } + + return dependencies; +} + +/** The dependency groups a nuspec declares, one per target framework. */ +function dependencyGroups(nuspec) { + const groups = []; + const pattern = /]*)>([\s\S]*?)<\/group>/g; + let match; + + while ((match = pattern.exec(nuspec)) !== null) { + groups.push({ + framework: attribute(match[1], "targetFramework") ?? "(any)", + dependencies: dependenciesIn(match[2]), + }); + } + + if (groups.length === 0) { + const flat = dependenciesIn(nuspec); + if (flat.length > 0) { + groups.push({ framework: "(any)", dependencies: flat }); + } + } + + return groups; +} + +/** A printable name for a target framework, spelled the way the ecosystem usually writes it. */ +function shortFramework(framework) { + return framework + .replace(/^\.NETStandard/i, "netstandard") + .replace(/^\.NETCoreApp/i, "net") + .replace(/^\.NETFramework/i, "netframework") + .toLowerCase(); +} + +/** Prints one package's groups, recursing into Purview components when asked. */ +async function describe(id, requestedVersion, options, depth, seen) { + const version = chooseVersion(await versionsOf(id), requestedVersion); + const groups = dependencyGroups(await readNuspec(id, version)); + const indent = " ".repeat(depth); + + console.log(`${indent}${id} ${version}`); + + for (const group of groups) { + const framework = shortFramework(group.framework); + if (options.tfm && framework !== options.tfm.toLowerCase()) { + continue; + } + + if (group.dependencies.length === 0) { + console.log(`${indent} ${framework}: no dependencies`); + continue; + } + + for (const dependency of group.dependencies) { + const relation = dependency.constraint === "minimum" ? ">=" : dependency.constraint; + console.log( + `${indent} ${framework}: ${dependency.id} ${relation} ${dependency.version ?? "any"}`, + ); + } + + if (options.walk && depth < MAX_DEPTH) { + for (const dependency of group.dependencies) { + const isPurview = /^purview\./i.test(dependency.id ?? ""); + if (!(options.all || isPurview) || seen.has(dependency.id.toLowerCase())) { + continue; + } + + seen.add(dependency.id.toLowerCase()); + await describe(dependency.id, null, options, depth + 1, seen); + } + } + } + + return { id, version, groups }; +} + +/** Parses the command line. An unknown argument is a mistake worth failing on. */ +function parseArguments(argv) { + const options = { packages: [], tfm: null, walk: false, all: false, json: false }; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + + if (argument === "--walk" || argument === "-w") { + options.walk = true; + } else if (argument === "--all") { + options.all = true; + } else if (argument === "--json") { + options.json = true; + } else if (argument === "--tfm") { + options.tfm = argv[(index += 1)]; + if (!options.tfm) { + throw new Error("--tfm needs a value, e.g. --tfm net10.0."); + } + } else if (argument.startsWith("-")) { + throw new Error(`Unknown argument '${argument}'.`); + } else { + options.packages.push(argument); + } + } + + if (options.packages.length === 0) { + throw new Error( + "Name at least one package, e.g. `node dependencies.mjs Purview.Containers`.\n" + + "A package may carry a version: `Purview.Containers@1.0.0-prerelease.11`.", + ); + } + + return options; +} + +/** The command. */ +async function main() { + const options = parseArguments(process.argv.slice(2)); + const results = []; + + for (const entry of options.packages) { + const separator = entry.indexOf("@", 1); + const id = separator === -1 ? entry : entry.slice(0, separator); + const version = separator === -1 ? null : entry.slice(separator + 1); + + results.push(await describe(id, version, options, 0, new Set([id.toLowerCase()]))); + + if (!options.walk && !options.json) { + console.log(""); + } + } + + if (options.json) { + console.log(JSON.stringify(results, null, "\t")); + } +} + +main().catch((error) => { + console.error(`error: ${error.message}`); + process.exitCode = 1; +}); diff --git a/skills/purview-engineering-principles/SKILL.md b/skills/purview-engineering-principles/SKILL.md index d193187..24fe707 100644 --- a/skills/purview-engineering-principles/SKILL.md +++ b/skills/purview-engineering-principles/SKILL.md @@ -4,8 +4,9 @@ description: >- Use when setting up, reviewing or rationalising a .NET repository against the Purview engineering conventions - convention over configuration, derived identity, architecture as tests, warnings as errors, proving consumer-facing claims, documentation ownership, test data builders, agent - legibility, tiering verification by risk, and the conditional persistence rules. Each rule names the - failure it prevents and whether a build or a reviewer enforces it. + legibility, tiering verification by risk, the version policy for applications and for published + components, and the conditional persistence rules. Each rule names the failure it prevents and + whether a build or a reviewer enforces it. metadata: product: Purview engineering playbook repository: purview-dev/engineering-playbook @@ -13,7 +14,7 @@ metadata: # Purview engineering principles -Ten rules, each with the failure it prevents and the mechanism that makes it true. The distinction +Eleven rules, each with the failure it prevents and the mechanism that makes it true. The distinction that matters throughout: **a convention a build can check is checked by the build; a convention only a reviewer can check is labelled a review obligation and never described as enforced.** A rule that claims enforcement it does not have is worse than no rule, because people stop looking. @@ -100,8 +101,9 @@ repository's own tests already are the consumer. ## 7. Documentation is a build artefact of the repository -The wiki lives in the repository it describes, in `docs/wiki`, and the repository owns its -`mkdocs.yml`, its navigation and its `edit_uri`. +The wiki lives in the repository it describes, in `docs/wiki`, as plain Markdown. The repository owns no +site configuration at all: `_Sidebar.md` is the navigation, `Home.md` is the hub, and the site that +publishes them lives in one place outside the repository. Documentation that lives elsewhere is updated elsewhere. A page with no owner and no review path is wrong within one release. @@ -154,6 +156,23 @@ hand-written attribute. *At your scale:* one tier is fine until the slow tests exist. Then split by project name, not by attribute. +## 11. A version is a range, not a number + +An application takes the latest version its runtime supports. A published component declares the +**lowest** version that compiles, passes its tests and satisfies the contract it documents, on each +target framework it claims — because dependency resolution is a set of lower bounds, so the floor a +component declares becomes the floor every consumer inherits. + +A page states no release number. It shows a badge generated from the registry, or links to the +registry, so a version cannot be wrong within a release. + +*Enforced by:* a lowest-compatible CI leg that restores against the declared floor, and an +API-compatibility check per target framework. Nothing enforces the floor of a package that has no +consumer yet — that tier arrives with the first release. + +*At your scale:* with one application there is nothing to earn yet; take the latest and let +`dotnet list package --outdated` report the rest. The golden rule starts at the first published package. + ## Applying these in review Three questions catch most of it: @@ -168,11 +187,12 @@ Three questions catch most of it: Three further rules apply only when a repository actually stores data: a value known before storage is derived with UUIDv5, every other GUID key uses one strategy per store, and EF Core is the persistence -boundary. They are not part of the ten because a stateless CLI or a pure library must not inherit them. +boundary. They are not part of the eleven because a stateless CLI or a pure library must not inherit them. `purview-persistence` owns them and stays silent when persistence is not real. ## Related skills `purview-architecture` for project-shape decisions, `purview-architecture-testing` for turning a boundary into a failing build, `purview-telemetry` and `purview-persistence` for the per-concern -guidance. +guidance, and `purview-component-compatibility` for deciding and verifying a published component's +lowest compatible dependency versions. diff --git a/skills/purview-org-harvest/SKILL.md b/skills/purview-org-harvest/SKILL.md index 12652c3..2c05ea1 100644 --- a/skills/purview-org-harvest/SKILL.md +++ b/skills/purview-org-harvest/SKILL.md @@ -15,7 +15,7 @@ metadata: # Purview organisation harvest -Sixteen public repositories, each with its own wiki, its own `skills/` folder and its own `AGENTS.md`. +Seventeen public repositories, each with its own wiki, its own `skills/` folder and its own `AGENTS.md`. They are not equally up to date, and the one you have cloned is not necessarily the one that has the answer. Two failures follow, and this skill exists to prevent both. @@ -89,11 +89,11 @@ That is the same script, not a variant, so what you review is what runs. ## 4. Harvest in two tiers -Sixteen repositories is not a reading list; it is a budget. Two tiers keep it affordable. +Seventeen repositories is not a reading list; it is a budget. Two tiers keep it affordable. | Tier | Call | Answers | | -- | -- | -- | -| 1 — shape | `gh api repos/purview-dev//git/trees/main?recursive=1`, for all sixteen | Which folders exist, how many pages the wiki has, whether `skills/` is authored, whether `AGENTS.md` exists. Cheap enough for everything. | +| 1 — shape | `gh api repos/purview-dev//git/trees/main?recursive=1`, for all seventeen | Which folders exist, how many pages the wiki has, whether `skills/` is authored, whether `AGENTS.md` exists. Cheap enough for everything. | | 2 — content | `gh api -H 'Accept: application/vnd.github.raw' repos/purview-dev//contents/`, **only for the owner of the concern** | What the convention actually says. | Tier 1 tells you where the evidence is. Tier 2 reads it, in the one repository that wrote it down. diff --git a/skills/purview-org-harvest/references/best-in-class.md b/skills/purview-org-harvest/references/best-in-class.md index 48a579b..9ab69cc 100644 --- a/skills/purview-org-harvest/references/best-in-class.md +++ b/skills/purview-org-harvest/references/best-in-class.md @@ -19,9 +19,10 @@ regenerated rather than trusted. | Logging, tracing, metrics | `telemetry-sourcegenerator` | `docs/wiki`, `skills/purview-telemetry-*` | | Event streams and event storage | `event-sourcing` | `docs/wiki` — **no skills folder** | | Operation outcomes without exceptions | `results` | `docs/wiki`, `skills/purview-results-*` | -| Validated identity types | `value-objects` | `docs` — **no `docs/wiki`, no `mkdocs.yml`** | +| Validated identity types | `value-objects` | `docs` — **no `docs/wiki`** | | Aspire hosting resources | `aspire-resourcekit` | `docs/wiki`, `skills/purview-resourcekit-*` | | Architecture diagrams generated from source | `aspirec4` | `docs/wiki`, `skills/purview-aspirec4-*` | +| LikeC4 diagrams inside an Astro or Starlight site | `astroc4` | `docs/wiki`, `skills/purview-astroc4-*` — an npm package, not .NET | | Source generator authoring | `sourcegenerator-framework` | `docs/wiki`, `skills/purview-sourcegenerator-*` | | Skill-set design, and the dated-snapshot pattern | `pm-skills` | `skills/pm-*` — **no documentation folder at all** | | OpenAPI generated from XML comments | `openapi-xmlcomments` | `docs/wiki`, `skills/purview-openapi-xmlcomments-*` | @@ -49,6 +50,12 @@ test-time resource — a real SQL Server, PostgreSQL or queue — and `aspire-re development-time orchestration. A test that needs a database is `containers`; an AppHost that needs one is `aspire-resourcekit`. +**`aspirec4` and `astroc4` both draw LikeC4 diagrams.** `aspirec4` owns the diagram generated from a +running Aspire resource graph; `astroc4` owns putting a `.c4` model inside an Astro or Starlight site — +the fence transform and the LikeC4 syntax highlighting. A question about the .NET host is `aspirec4`; a +question about the documentation site is `astroc4`, which is the one owner here that is not a .NET +package, so it is read for its TypeScript conventions rather than for its MSBuild ones. + ## Reading a repository you have not cloned Tier 1 first — the shape, for everything: diff --git a/skills/purview-org-harvest/references/harvest-commands.md b/skills/purview-org-harvest/references/harvest-commands.md index 76f6f0a..f4b4653 100644 --- a/skills/purview-org-harvest/references/harvest-commands.md +++ b/skills/purview-org-harvest/references/harvest-commands.md @@ -92,7 +92,7 @@ It resolves the snapshot path from `git rev-parse --show-toplevel`, so it writes you are standing in rather than into the one the script was installed from. That is what lets another repository adopt it unchanged. -`--repo` merges on write, so reading one repository does not erase the other fifteen. +`--repo` merges on write, so reading one repository does not erase the other sixteen. ## Replaying a harvest, with no network diff --git a/skills/purview-org-harvest/scripts/inventory.mjs b/skills/purview-org-harvest/scripts/inventory.mjs index 76fcad1..affc46f 100644 --- a/skills/purview-org-harvest/scripts/inventory.mjs +++ b/skills/purview-org-harvest/scripts/inventory.mjs @@ -51,6 +51,7 @@ const CONCERNS = new Map([ ["architests", ["architecture policy", "dependency and naming rules"]], ["aspire-resourcekit", ["Aspire hosting resources"]], ["aspirec4", ["architecture diagrams from source"]], + ["astroc4", ["architecture diagrams in an Astro site"]], ["build", ["pipeline", "release"]], ["build-sdk", ["repository shape", "analyzers and style", "MSBuild defaults", "skill authoring"]], ["containers", ["local dependencies in tests"]], diff --git a/src/tests/Repository.UnitTests/DocumentationTests.cs b/src/tests/Repository.UnitTests/DocumentationTests.cs index 210f409..658be56 100644 --- a/src/tests/Repository.UnitTests/DocumentationTests.cs +++ b/src/tests/Repository.UnitTests/DocumentationTests.cs @@ -14,8 +14,8 @@ namespace Purview.EngineeringPlaybook.Repository; /// /// /// The page walk is recursive, so a page in a topic folder is held to the same rules as one at the wiki -/// root. index.md is the landing page rather than a content page, so the navigation checks -/// exclude it; the link and fence rules apply to every page, including the landing page. +/// root. _Sidebar.md is the navigation list rather than a content page, so the navigation checks +/// exclude it; the link and fence rules apply to every page. /// /// public sealed partial class DocumentationTests @@ -151,7 +151,7 @@ public async Task Pages_GivenTheirLinks_AllResolve(CancellationToken cancellatio public async Task Pages_GivenTheirFencedBlocks_AlwaysDeclareALanguage(CancellationToken cancellationToken) { // Arrange - // AGENTS.md: Mermaid is not enabled in mkdocs.yml, so Mermaid source lives in a text fence. A + // AGENTS.md: no diagram extension is configured, so Mermaid source lives in a text fence. A // bare fence renders as an untyped block and is the easiest convention to break by accident. List offenders = []; @@ -190,7 +190,7 @@ .. Directory .EnumerateFiles(WikiDirectory(), "*.md", SearchOption.AllDirectories) .Select(path => Path.ChangeExtension(Path.GetRelativePath(WikiDirectory(), path), null)) .Select(name => name.Replace('\\', '/')) - .Where(name => name is not "_Sidebar" and not "index") + .Where(name => name is not "_Sidebar") .Order(StringComparer.Ordinal), ]; diff --git a/templates/Directory.Packages.props b/templates/Directory.Packages.props index 2929160..82c3e99 100644 --- a/templates/Directory.Packages.props +++ b/templates/Directory.Packages.props @@ -23,10 +23,12 @@ Group with Label so the file reads as a short list of decisions rather than one long alphabetical block. Delete the groups you do not need: the ones below are illustrative, not a required set. - Check these versions against the SDK's current release before you commit. They are the versions - this playbook builds with today, plus two illustrative package families — Purview.Containers, which - you delete if no suite needs a container, and BDTest, which you delete if no suite is - acceptance-based. + Check these versions against the SDK's current release before you commit: they are what this + repository resolved on the day the file was written, not a target. Take the latest version your + runtime supports. If you are publishing a package rather than running an application, take the + LOWEST version that compiles and passes instead — a consumer inherits your floor, so the newest + version you happen to have installed is the wrong default. See + docs/wiki/foundations/Dependency-Versions.md. --> true @@ -34,15 +36,15 @@ - 10.10.0 + 10.11.0 - 5.0.4 + 5.2.0 - 1.0.6 + 1.2.0 - 1.0.0-prerelease.6 - 1.75.0 + 1.0.0-prerelease.11 + 1.81.10 - +