diff --git a/package.json b/package.json
index 6aa2578..86c90cc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "purview-dev",
- "version": "0.5.2",
+ "version": "0.6.0",
"private": true,
"description": "Purview-Dev public website and unified documentation portal (workspace root).",
"license": "MIT",
diff --git a/src/src/components/starlight/Header.astro b/src/src/components/starlight/Header.astro
index 053a5e8..5b6433a 100644
--- a/src/src/components/starlight/Header.astro
+++ b/src/src/components/starlight/Header.astro
@@ -4,4 +4,26 @@ import SiteHeader from '~/components/SiteHeader.astro';
const { hasSidebar } = Astro.locals.starlightRoute;
---
-
\ No newline at end of file
+
+
+
diff --git a/src/src/components/starlight/PageFrame.astro b/src/src/components/starlight/PageFrame.astro
index 0004a97..fbf6500 100644
--- a/src/src/components/starlight/PageFrame.astro
+++ b/src/src/components/starlight/PageFrame.astro
@@ -66,17 +66,14 @@ const { hasSidebar } = Astro.locals.starlightRoute;
width: 100%;
height: var(--sl-nav-height);
border-bottom: 1px solid var(--sl-color-hairline-shade);
- padding: var(--sl-nav-pad-y) var(--sl-nav-pad-x);
- padding-inline-end: var(--sl-nav-pad-x);
+ /* Horizontal padding is owned by the header's `.pv-container` wrapper (see
+ * the Header override) so the documentation header aligns with the site's
+ * page shell; the mobile menu-button reservation lives on that wrapper. */
+ padding-block: var(--sl-nav-pad-y);
+ padding-inline: 0;
background-color: var(--sl-color-bg-nav);
}
- :global([data-has-sidebar]) .header {
- padding-inline-end: calc(
- var(--sl-nav-gap) + var(--sl-nav-pad-x) + var(--sl-menu-button-size)
- );
- }
-
.sidebar-pane {
/* By default, user agents apply some opinionated styles to popovers, which we don't want. */
border: unset;
@@ -115,9 +112,6 @@ const { hasSidebar } = Astro.locals.starlightRoute;
}
@media (min-width: 50rem) {
- :global([data-has-sidebar]) .header {
- padding-inline-end: var(--sl-nav-pad-x);
- }
.sidebar-pane {
/* Make the sidebar visible on larger screens instead of deferring to popover state. */
display: block;
diff --git a/src/src/data/projects/engineering-playbook.yml b/src/src/data/projects/engineering-playbook.yml
new file mode 100644
index 0000000..c8a9775
--- /dev/null
+++ b/src/src/data/projects/engineering-playbook.yml
@@ -0,0 +1,222 @@
+$schema: ../../lib/manifest/schema.ts
+
+id: engineering-playbook
+name: Engineering Playbook
+shortDescription: The reusable Purview engineering conventions — repository shape, architecture as tests, verification tiers, and agent enablement.
+description: >-
+ The conventions the Purview projects are built on, written down once with the failure each one prevents,
+ the mechanism that makes it true, and what a smaller repository does instead. It covers how a repository
+ is laid out and named, how the architecture is held by tests that read the compiled assemblies, which
+ checks run in which verification tier, how value objects, schemas and results cross a boundary, how tests
+ are written and where their data comes from, how a repository is made legible to a coding agent, and how
+ its documentation is published. It is documentation-first and ships no library: it holds the conventions,
+ ready-to-copy templates, and a small layered reference implementation whose dependency-direction rules
+ fail the build, so the rules are demonstrated rather than asserted.
+origin: >-
+ The conventions were already real and already enforced, but they were only discoverable by reading the
+ repositories and inferring the pattern from the repetition. That worked while one person held the whole
+ picture and stopped working as soon as a second contributor or a coding agent had to decide which of two
+ plausible patterns was the intended one. Writing them down in one place — with the evidence attached, and
+ scaled so a smaller repository can take the part that fits — turned an oral tradition into something a
+ repository can be pointed at.
+useWhen: >-
+ You are starting a repository that should look and behave like the Purview projects, adopting the SDK into
+ an existing one, or deciding which of the conventions are worth the cost at your size. The playbook's
+ profiles, templates and reference implementation are all meant to be taken in part.
+avoidWhen: >-
+ You want the SDK's own behaviour documented rather than the conventions built on top of it — read the
+ Build SDK's wiki instead — or you are looking for a scaffolding tool: the templates are files to read and
+ copy, not a generator.
+repository: purview-dev/engineering-playbook
+category: developer-tooling
+status: preview
+experimental: true
+order: 95
+docs:
+ source: github-path
+ path: docs/wiki
+ rootPage: Home.md
+ exclude:
+ - _Sidebar.md
+ - index.md
+related:
+ - build-sdk
+ - containers
+ - value-objects
+ - results
+ - zodsharp
+useCases:
+ - audience: developer
+ title: A repository that satisfies every convention on its first build
+ scenario: >-
+ You are starting a repository and want the project defaults, code style, test wiring and assembly
+ identity to be right from the first commit, instead of discovering the conventions one review comment
+ at a time.
+ code: |
+ {
+ "sdk": { "allowPrerelease": false },
+ "msbuild-sdks": { "Purview.BuildSdk": "1.0.4" },
+ "test": { "runner": "Microsoft.Testing.Platform" }
+ }
+ language: json
+ outcome: >-
+ A pinned SDK plus a `Directory.Build.props` that sets one namespace prefix is the whole of the
+ repository's build configuration. Every project beneath it inherits the target framework, language
+ version, nullable context, analyzers and assembly identity, so a project file is two lines and a test
+ project file is empty.
+ evidence: >-
+ Getting Started walks the steps in the order you would create the files, and every repository-level
+ file has a copy-paste version in the templates folder with a comment explaining the one thing about it
+ that is easy to get wrong.
+ docsPage: getting-started
+ - audience: architect
+ title: Architecture that fails the build when a dependency points the wrong way
+ scenario: >-
+ Your layering is documented and agreed, and you want it to stay true — rather than being eroded one
+ convenient project reference at a time, each of which looks reasonable in isolation.
+ code: |
+ [Test]
+ public async Task Application_GivenItsReferences_DependsOnTheDomainAndNotOnInfrastructure()
+ {
+ // Arrange
+ var application = typeof(ConventionCatalogue).Assembly;
+
+ // Act
+ var referenced = ReferencedAssemblyNames(application);
+
+ // Assert
+ await Assert.That(referenced).Contains($"{RepositoryPrefix}.Domain");
+ await Assert.That(referenced).DoesNotContain($"{RepositoryPrefix}.Infrastructure");
+ }
+ language: csharp
+ outcome: >-
+ The allowed dependency directions are data in a test that reads the compiled assemblies' metadata
+ references, so a reference that points the wrong way fails the build with the offending assembly
+ named — no diagram to keep in step and no separate tool to run.
+ evidence: >-
+ The reference implementation's Architecture.UnitTests project asserts four rules against three real
+ assemblies and is built by the same pipeline as everything else. The page also records the two traps
+ found by writing it, including a source generator that emits public types into its own namespace.
+ docsPage: architecture-as-tests
+ - audience: team-lead
+ title: A verification gate that stays credible
+ scenario: >-
+ The pull-request build has started needing a database and four minutes of waiting, and somebody is
+ about to make it optional — at which point nothing is gated at all.
+ code: |
+ {
+ "Build": {
+ "Solution": "src/Acme.slnx",
+ "TestRoot": "src/tests",
+ "TestPatterns": "*UnitTests.csproj",
+ "TestFilter": "/*/*/*/*[Category=Unit]"
+ }
+ }
+ language: json
+ outcome: >-
+ Tiers are expressed by where a test lives rather than by an attribute: the SDK reads the test project's
+ name suffix and emits the assembly-level category the pipeline filters on, so adding a slow suite keeps
+ it out of the fast tier without editing a filter or remembering an attribute.
+ evidence: >-
+ The verification model page documents the shared workflow's inputs, the configuration probe order, and
+ the two ways a repository and the shared workflow quietly disagree about what a green build means — and
+ names the repository that runs its container-backed suites as a second job on the same runner.
+ docsPage: verification-model
+ - audience: developer
+ title: One builder for every test's domain content
+ scenario: >-
+ You are writing a test, need a domain object, and do not want to spell out a constructor that will
+ change — nor leave the reader guessing which of the values you passed actually matter to the assertion.
+ code: |
+ public static Domain.Convention Convention(
+ Domain.ConventionId? id = null,
+ string? title = null,
+ VerificationTier? tier = null,
+ bool? enforcedByBuild = null) =>
+ Domain.Convention.Create(
+ id ?? ConventionId(),
+ title ?? Faker.Lorem.Sentence(4),
+ tier ?? Faker.Random.Enum(),
+ enforcedByBuild ?? Faker.Random.Bool());
+ language: csharp
+ outcome: >-
+ An omitted argument is generated and a supplied argument is used exactly, so a test reads as a list of
+ the values it is actually proving and everything else is realistic without the test saying so. One
+ constructor change is one edit rather than a repository-wide one.
+ evidence: >-
+ TestBuilder lives in a shared testing project the SDK references from every test project
+ automatically. The page also explains the two SDK rules that shaped its flat, ungrouped shape, and the
+ value-object exception where a test must name the rejected input itself.
+ docsPage: testing-and-test-data
+ - audience: contributor
+ title: A repository an agent can work in without hand-holding
+ scenario: >-
+ You want a coding agent to make a change that follows the repository's conventions rather than
+ confidently generalising from whichever pattern it found first.
+ code: |
+ | Path | Purpose |
+ | -- | -- |
+ | src/src/Domain | The reference implementation's value objects and aggregate |
+ | src/src/Application | The port and the service that owns the rules |
+ | src/tests/Architecture.UnitTests | Dependency-direction rules; a wrong reference fails the build |
+ language: markdown
+ outcome: >-
+ AGENTS.md states the purpose, the layout, the conventions and the definition of done, and the build
+ mirrors the SDK's own skills into the repository so the agent-facing guidance tracks the SDK version
+ rather than the repository's memory of it.
+ evidence: >-
+ The agent enablement page documents the layout of the agent folder, why the SDK's mirror writes a
+ generated .gitignore that lists exactly the files it owns, and why the guidance that makes a repository
+ obvious to an agent is the same guidance that makes it obvious to a new joiner.
+ docsPage: agent-enablement
+ - audience: architect
+ title: Take only the part of the playbook that fits
+ scenario: >-
+ You are convinced by some of the conventions and not others, and you want to know which ones are worth
+ the cost in a repository the size of yours — rather than adopting everything and discovering the
+ ceremony.
+ outcome: >-
+ Three profiles — one project, a few layers, several packages — each list what to take and what to
+ leave, and every page ends with an "at your scale" section that names the smaller version of the rule
+ explicitly. The templates are mapped to the profile that needs them.
+ evidence: >-
+ The playbook also documents what not to adopt early, with the reason each one is more expensive out of
+ order, and a migration order for an existing repository that keeps each step cheap and the build green.
+ docsPage: adopting-this-at-your-scale
+ - audience: team-lead
+ title: Data that crosses a boundary once, and is trusted after that
+ scenario: >-
+ Inbound payloads are validated in several places, an identifier is a string in one layer and a type in
+ another, and expected failures are thrown as exceptions that callers catch by message.
+ code: |
+ [Scalar]
+ public readonly partial record struct EmailAddress
+ {
+ public string Value { get; }
+
+ static partial void OnNormalize(ref string value) => value = value?.Trim().ToLowerInvariant()!;
+ }
+ language: csharp
+ outcome: >-
+ A value is a type rather than a primitive, `Create` validates at the boundary and `Hydrate` trusts
+ everything past it, normalisation runs before validation so there is exactly one canonical form, and
+ expected failures are values while exceptional circumstances still throw.
+ evidence: >-
+ The contracts page names the three public repositories that implement the rules — source-generated
+ value objects, result types with union errors, and a schema layer that also exports and imports JSON
+ Schema — and shows how a validation result is read rather than parsed.
+ docsPage: contracts-and-data-exchange
+ - audience: contributor
+ title: Extend the playbook without turning it into a second source of truth
+ scenario: >-
+ You have found a convention that is real, enforced and not written down, and you want to add it without
+ duplicating something the SDK already documents.
+ outcome: >-
+ Four tests decide whether a rule belongs: it came from a real repository and a real failure, it is
+ reusable at more than one size, it is not the SDK's to state, and it is either enforced or explicitly
+ labelled a review obligation.
+ evidence: >-
+ The contributing page also sets the expectations for changing an existing rule — update every page that
+ states it, check the reference implementation still demonstrates it, and re-check whether the
+ enforcement moved between the build and review.
+ docsPage: contributing
diff --git a/src/src/pages/about.astro b/src/src/pages/about.astro
index 52b12a2..e3c8536 100644
--- a/src/src/pages/about.astro
+++ b/src/src/pages/about.astro
@@ -28,9 +28,9 @@ const active = projects.filter((project) => project.status !== 'archived');
-
+
-
+
Why Purview?
@@ -96,7 +96,7 @@ const active = projects.filter((project) => project.status !== 'archived');
-
+
The projects
@@ -117,7 +117,7 @@ const active = projects.filter((project) => project.status !== 'archived');
-
+
How the projects fit together
@@ -147,7 +147,7 @@ const active = projects.filter((project) => project.status !== 'archived');
-
+
Building in the open
@@ -172,7 +172,7 @@ const active = projects.filter((project) => project.status !== 'archived');
-
+
Open source
diff --git a/src/src/pages/docs/index.astro b/src/src/pages/docs/index.astro
index 695cf90..be3edb5 100644
--- a/src/src/pages/docs/index.astro
+++ b/src/src/pages/docs/index.astro
@@ -33,7 +33,7 @@ const pageCount = docsManifest?.projects.reduce(
-
+
{
projects.map((project) => {
const entry = docsManifest?.projects.find((item) => item.projectId === project.id);
diff --git a/src/src/pages/index.astro b/src/src/pages/index.astro
index fcf40ad..4a89e9c 100644
--- a/src/src/pages/index.astro
+++ b/src/src/pages/index.astro
@@ -158,12 +158,14 @@ const organizationStructuredData = {
All use cases →
-
+
{examples.map(({ project, useCase }, index) => (
diff --git a/src/src/pages/projects/[project].astro b/src/src/pages/projects/[project].astro
index 2871aed..2c11bff 100644
--- a/src/src/pages/projects/[project].astro
+++ b/src/src/pages/projects/[project].astro
@@ -316,9 +316,9 @@ const packageRows: PackageRow[] = releases.packages.map((pkg) => ({
-
+
-
+
The problem
{project.description}
{
@@ -335,7 +335,7 @@ const packageRows: PackageRow[] = releases.packages.map((pkg) => ({
}
-
+
Why it became a project
{project.origin}
diff --git a/src/src/pages/projects/index.astro b/src/src/pages/projects/index.astro
index 9b727f7..9e1d501 100644
--- a/src/src/pages/projects/index.astro
+++ b/src/src/pages/projects/index.astro
@@ -35,7 +35,7 @@ const statuses = ['stable', 'preview', 'experimental'];
-
+
{
projects.map((project) => {
const { tags } = projectRepoEnrichment(project, releaseIndex.data);
@@ -61,7 +61,7 @@ const statuses = ['stable', 'preview', 'experimental'];
Open-source projects outside Purview that I contribute to or help maintain. These links
take you to each project's own site or repository.
-
+
-
+
{
entries.map(({ useCase, project, docsBase, search }, index) => (
.lg\:sl-flex {
+ justify-content: center;
+ }
+
+ [data-has-sidebar][data-has-toc] .main-pane {
+ width: var(--sl-content-width);
+ }
+
+ .right-sidebar-container {
+ width: var(--sl-sidebar-width);
+ }
+ }
+
+ @media (min-width: 120rem) {
+ :root {
+ --sl-content-width: 52rem;
+ --sl-sidebar-width: 20rem;
+ }
+ }
}
/* ---------------------------------------------------------------------------