Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions ADAPTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,20 @@ a declared key). A guard script itself cannot be excepted by presence - that wou
the check rather than waive it; a `kind: "content"` exception on a guard is allowed, since
the guard still runs and must pass.

**Where your copy came from:** when the run finishes, write this repository's commit SHA
into your `stack.manifest.json` as `provenanceCommit`. `version` says which release you took;
the commit says which tree, and that is what the next update diffs against. Without it the
stack half of an update has nothing to compare but a version string, and every `merge` entry
- which is most of them - goes unenumerated. The core manifest carries the same field for its
own layer and the two move independently: updating one stack never implies the core or
another stack.

| Entry | The repo probably has | The move |
|---|---|---|
| `biome.json` | ESLint + Prettier | Two-step, never big-bang: install Biome alongside, port rule intents (`biome migrate eslint --write` gets most), run both until the diff stabilizes, then remove ESLint/Prettier in their own PR. Keep Prettier only if SCSS stays (Biome does not format SCSS - that is the pick's own escape hatch). |
| `biome.json` | ESLint + Prettier | Two-step, never big-bang: install Biome alongside, port rule intents (`biome migrate eslint --write` gets most), run both until the diff stabilizes, then remove ESLint/Prettier in their own PR. Keep Prettier only if SCSS stays (Biome does not format SCSS - that is the pick's own escape hatch). The entry is required and the migration is deliberately not: while you are between the two steps you have no `biome.json`, so record the exception with the step in its reason - `"reason": "ESLint + Prettier; Biome migration step 1 of 2, see ADAPTING.md"` - and delete it when step 2 lands. The exception is how the unfinished migration stays visible; making the entry optional would hide it. |
| `pnpm-workspace.yaml` | npm, yarn, bun, or Deno, maybe no workspace | `pnpm import` converts the existing lockfile where that source tool is actually supported - see the by-source-tool breakdown below, it is not all four. Workspaces map 1:1. The policy block (release-age cooldown, allowed build scripts) is the point of the entry - merge it even if the workspace globs differ. Without a migration path, or without migration appetite: record the exception (see above); the cooldown has no equivalent outside pnpm's own config, name that trade-off. |
| `tsconfig.base.json` | a looser tsconfig | Stage strictness: `strict` first, then `noUncheckedIndexedAccess` and friends one flag per PR - each flag surfaces real findings; a hundred errors at once teaches nothing. Extend, do not replace: their paths/aliases stay theirs. |
| `vitest.config.ts` | Jest, or tests mixed in one pile | Vitest reads most Jest suites as-is (`vitest run` first, fix the stragglers). The entry's real content is the tier split - unit vs integration as separate projects; introduce the naming convention (`*.test.ts` / `*.integration.test.ts`) before touching any test body. |
| `vitest.config.ts` | Jest, or tests mixed in one pile | Vitest reads most Jest suites as-is (`vitest run` first, fix the stragglers). The entry's real content is the tier split - unit vs integration as separate projects; introduce the naming convention (`*.test.ts` / `*.integration.test.ts`) before touching any test body. A workspace whose packages each carry a config already has the tiers where they run - configure them there and leave the root without the file, rather than adding a projects config that runs every suite a second time. That is why this entry is optional: for that shape the root file is wrong, not missing. |
| `playwright.config.ts` | Cypress, or nothing | Nothing: start with the starter's two journeys (auth, home) adapted to their routes. Cypress: do not port wholesale - write the 3-5 journeys that matter in Playwright, retire Cypress when they cover the old suite's intent. |
| `docker-compose.test.yml` | mocks, or a shared dev database | The move is philosophical (DECISIONS: real dependencies, not mocks) - land it with the integration tier, non-default ports so it never collides with their dev stack. |
| `perf/` + `scripts/perf-budget.sh` | nothing, or another perf tool | Run it once against the built app before changing a number: the first run writes the baseline, and a baseline nobody argued about is worth more than a target somebody guessed. It is deliberately not a CI gate - a budget that blocks a merge on day one gets disabled by the end of the week. Adapt the script's seed/build/start hooks to their stack, keep the committed history so a regression is a diff rather than an opinion. If a perf tool already exists, keep one, not both. |
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,38 @@ exists to make unnecessary.

## Unreleased

### An adopter's copy of this manifest says which tree it came from (2026-09-04)

`stack.manifest.json` gains `provenanceCommit`, shipping `null`, written into the adopting
repository's copy by the run that applies or updates this layer. The core's manifest has
carried the equivalent since standard ADR-052, and the difference showed the first time a real
repository updated: the core half of the delta is an exact diff between two commits, while the
stack half had a version string and nothing else to compare. Most entries here are `merge`
class and carry no hash, so that half was not merely coarse - it was unenumerated, and a
release's additions could go unapplied with the drift number still reading zero. The two
layers advance on separate clocks, so the field records a commit of this repository and never
one of the core's.

`vitest.config.ts` becomes optional. The requirement behind it is the tier split, not the
path: a workspace whose packages each configure the tiers satisfies DECISIONS#9 where the
tests actually run, and a root projects config on top of those runs every suite twice. Marked
required, the entry manufactured drift for a repository that had done nothing wrong, and the
exception recording it sat beside the exceptions that mean something. What the schema cannot
yet say - this path, or one per workspace package - is the core's to fix, and it is filed as
SELF-11 rather than papered over here.

One consequence lands in this repository's own adoption: it carried a `kind: file` exception
for `vitest.config.ts`, waiving an entry that can no longer fail. A stale exception is the
thing this change is against, so it is deleted, and the `stack-contract` spec's open question
now says three entries rather than four - a measured claim, kept measured.

`biome.json` deliberately stays required. The adaptation path prescribes a two-step Biome
migration, so a repository following it correctly has no `biome.json` between the steps -
which is a real, unfinished deviation from DECISIONS#3 and should read as one. `ADAPTING.md`
now gives the exception to record, with the migration step in its reason, and says it comes
out when step 2 lands. That is the difference between the two entries: one is a decision
satisfied elsewhere, the other is a decision not yet taken.

### The stack layer became an adopter of the standard it belongs to (2026-08-06)

This repository carried a `stack.manifest.json` and nothing else the core standard asks for.
Expand Down
7 changes: 6 additions & 1 deletion SHIPPED.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ decision; changing it is deviating, and deviating is fine if you record it. **Me
yours** means the substance is a block inside the file, not the whole file: your globs,
your job names, your paths stay yours.

**Required** means the verifier counts its absence as drift - not that you must take it
today. A repository part-way through one of the migrations in [adapting](ADAPTING.md)
records the entry as an exception naming the step it is on, and deletes the exception when
the step lands; that is the migration's visible state, not a way around the check.

| Path | What it is | How to take it |
|---|---|---|
| `.nvmrc` | the Node version pin - one runtime, no drift between machines | copy as-is · required |
| `biome.json` | lint + format, one tool - the reference copy encodes the strictness the picks assume | merge into yours · required |
| `tsconfig.base.json` | strict TypeScript base every package extends | merge into yours · required |
| `pnpm-workspace.yaml` | workspace globs + the supply-chain policy (release-age cooldown, allowed build scripts, save-exact) - the policy block is the point | merge into yours · required |
| `vitest.config.ts` | unit and integration tiers as separate Vitest projects | merge into yours · required |
| `vitest.config.ts` | unit and integration tiers as separate Vitest projects - a workspace may configure them per package instead | merge into yours · optional |
| `playwright.config.ts` | the e2e tier - real browser journeys against the running app | merge into yours · optional |
| `docker-compose.test.yml` | ephemeral real dependencies for integration/e2e - not mocks | copy as-is · optional |
| `perf/` | local Lighthouse perf-budget runner - report-only, committed baseline + dated history, no external upload; edit config.mjs routes | copy as-is · optional |
Expand Down
1 change: 1 addition & 0 deletions backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Technology picks do not belong here. A pick that should change is a change to
| SELF-8 | Decide this repository's manifest profile - `core` or `scale` | The elicitation ledger's `green.stack` point asked which profile applies. The first answer given was `scale`; raised back was the actual cost - `self-verify --profile scale` checks roughly nine more entries than `core`, unverified whether this repository already satisfies them - and whether to switch now, expanding the update-to-1.0.6 PR, or defer. No reconfirmation came either way, so `standard.manifest.json` keeps `"profile": "core"` unchanged rather than silently picking a side | a deliberate answer, reached on its own rather than folded into an unrelated PR: either the profile moves to `scale` with the new entries measured and satisfied, or `core` is confirmed as right for this repository's actual reach and the reasoning is recorded | todo |
| SELF-9 | Confirm backlog row attribution style | The elicitation ledger's `adopt.backlog` point asks whether rows should name a role, a person, or nothing. The question that got answered instead was a different one - where the backlog physically lives, not how a row is attributed - so this repository's rows keep the pattern already in use (no owner/role column, matching every row here) as a default, not a confirmed choice | either the current pattern is confirmed as intentional, or rows start naming a role per the standard's own recommended default | todo |
| SELF-10 | Reconfirm scope and acceptance criteria for `specs/stack-contract/spec.md` with a human | The elicitation ledger's `spec.scope` and `spec.acceptance` points were reached when this update added the spec's missing Interface contracts section - both sections' existing content was carried forward unchanged, not re-decided, so the provenance is recorded as `provisional` rather than `human` | a human either confirms the existing Scope and Acceptance criteria sections still hold, or revises them, and the ledger rows move to `human` | todo |
| SELF-11 | Give the manifest a way to say "this path, or one per workspace package" | `vitest.config.ts` is now optional because the schema cannot express where a monorepo really configures the test tiers: `required: true` manufactured drift for a repository that satisfies DECISIONS#9 in nineteen places instead of one, and `altPaths` takes literal paths, not globs. Optional is the honest reading of a check the schema cannot make, but it also stops asking - a repository with no tiers anywhere now passes the same way. The expressiveness is the core's (`scripts/self-verify.mjs` and the manifest schema), so this is a request to make there, not a change to make here | the core's manifest can express a per-package satisfying path, or records that it will not; if it can, this entry goes back to required with that spelling | todo |

## Alignment scope

Expand Down
6 changes: 6 additions & 0 deletions specs/adoption-path/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ brownfield, and what happens after each.
and brownfield walks.
- Package-manager migration covers the routes a real repository arrives on, and says plainly
where a route does not exist rather than implying one does.
- The run ends by recording where the copy came from: this repository's commit, written into
the adopting repository's copy of the stack manifest. A path that is walked but not recorded
cannot be walked again from where it stopped.
- An entry the repository is part-way to is recorded as an exception naming the step, and the
note says when the exception comes out. A migration with two steps has a state between them,
and that state is the exception rather than a silent absence.

## Open questions

Expand Down
29 changes: 26 additions & 3 deletions specs/stack-contract/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ and what each check actually needs to run.
- **`requiredKeys`** - what a `merge`-class entry must still contain after an adopter merges
it into their own file. Presence is worthless for an entry whose whole point is a block
inside a file the adopter also edits.
- **Provenance is per layer** - the adopter's copy of this manifest records which commit of
this repository it came from, and the core's manifest records the same for the core. A stack
is linked by a registry pointer rather than a version range, so the two advance on separate
clocks and neither commit can be derived from the other.
- **Required is about the check, not the calendar** - an entry marked required counts as drift
while it is absent, which is what makes a half-finished migration visible; an entry marked
optional is one the decision behind it can be satisfied without, at a path this schema cannot
name.
- **The guard** - a live command whose verdict is part of the drift number, as distinct from a
declarative entry describing layout.

Expand All @@ -45,6 +53,7 @@ and what each check actually needs to run.
|---|---|---|
| `technology` | string | the single technology this stack covers; one stack per technology is policy |
| `version` | string | this stack's own version, independent of the core's |
| `provenanceCommit` | string \| null | the commit of THIS repository an adopter's copy last aligned to; ships `null` and is written into the adopter's copy by the run that applies or updates the layer. What an update's delta is computed from - `version` names the release, this names the tree |
| `registry` | string | back-pointer to the core's `stacks.json`, which is what confers official status |
| `files` | array | entries in the core manifest's file-entry schema (`path`, `adapt`, `required`, `purpose`, and `requiredKeys` where the entry is a merge) |
| `guards` | array | entries in the core manifest's guard schema (`id`, `run`) |
Expand Down Expand Up @@ -74,6 +83,14 @@ behind the one-drift-number promise in Success metric above.
- A guard's failure is drift. It cannot be waived by a manifest exception - waiving a live
check removes it rather than recording a deviation from it.
- The manifest carries no version range against the core.
- `provenanceCommit` ships `null` and is never a commit of the core repository. A release
that changes shipped content is what makes a recorded commit stale; nothing in this
repository rewrites an adopter's copy.
- An entry is `required` only where its absence is itself the deviation. Where a decision can
be satisfied at a path the schema cannot express - one config per workspace package rather
than one at the root - the entry is optional, because a required entry there manufactures
drift for a repository that did nothing wrong, and an exception recording it buries the
cases that mean something.

## Acceptance criteria

Expand All @@ -84,14 +101,20 @@ behind the one-drift-number promise in Success metric above.
passing on the file's existence.
- Given a manifest exception targeting a guard, when the verifier runs, then the exception is
rejected as drift rather than silently honoured.
- Given an adopting repository whose copy of this manifest records a `provenanceCommit`, when
the next update of this layer runs, then the delta is computed from that commit and every
entry is enumerated, rather than from the version string with the `merge` entries unlisted.
- Given a workspace that configures the test tiers in each package and carries no root
`vitest.config.ts`, when the verifier runs, then that entry is reported as an unmet optional
rather than as drift, and the repository records no exception for it.

## Open questions

- **This repository cannot satisfy its own stack manifest, and that is not yet decided
either way.** The entries describe a repository that *consumes* the stack - `biome.json`,
`tsconfig.base.json`, `pnpm-workspace.yaml` and `vitest.config.ts` at the repository root -
while this repository *publishes* it and ships those files inside
[`starter/`](../../starter). Measured: with both manifests present, the four entries fail
`tsconfig.base.json` and `pnpm-workspace.yaml` at the repository root - while this
repository *publishes* it and ships those files inside
[`starter/`](../../starter). Measured: with both manifests present, the three entries fail
and the `stack-check-all` guard fails with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND`, because
the guard runs `pnpm check:all` at a root that has no `package.json`. The manifest schema
has no notion of publisher versus consumer, and the guard cannot be excepted by design. The
Expand Down
7 changes: 4 additions & 3 deletions stack.manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$about": "The stack contract and manifest in one (ADR-016 in the core repo): the same schema (files/guards, adapt classes, per-entry rule anchors - here DECISIONS#N instead of R#) the core's standard.manifest.json uses, so the align engine and self-verify read it whole. Paths are the adopted repo's; the reference copy of every merge-class file lives in starter/. An adopting repo carries this file beside standard.manifest.json; self-verify then counts one drift across both layers. The registry pointer is the linkage to the core - discovery metadata, never a version requirement (core ADR-022).",
"$about": "The stack contract and manifest in one (standard ADR-016): the same schema (files/guards, adapt classes, per-entry rule anchors - here DECISIONS#N instead of R#) the core's standard.manifest.json uses, so the align engine and self-verify read it whole. Paths are the adopted repo's; the reference copy of every merge-class file lives in starter/. An adopting repo carries this file beside standard.manifest.json; self-verify then counts one drift across both layers. The registry pointer is the linkage to the core - discovery metadata, never a version requirement (standard ADR-022). `provenanceCommit` is the full commit SHA of THIS repository that an adopter's copy last aligned to: it ships `null`, and the run that applies or updates this layer writes it into the adopter's copy. It is what the stack half of an update's delta is computed from, the way the core manifest's own field is (standard ADR-052); `version` stays the human-facing bookmark. The two layers are independent - a stack rides its own clock (standard ADR-022) - so this field records a commit here, never one in the core repository.",
"technology": "node",
"version": "0.3.1",
"provenanceCommit": null,
"description": "Node/TypeScript best practices - Next.js + Fastify through one proxy, Better Auth, tiered testing; distilled from production repos and boot-verified",
"registry": "repository-standards/core",
"files": [
Expand Down Expand Up @@ -44,9 +45,9 @@
},
{
"path": "vitest.config.ts",
"purpose": "unit and integration tiers as separate Vitest projects",
"purpose": "unit and integration tiers as separate Vitest projects - the tiers are the requirement, not the path: a workspace whose packages each configure them satisfies it there, and a root projects config on top of those runs every suite twice, so this entry is optional rather than drift for that shape",
"adapt": "merge",
"required": true,
"required": false,
"profile": "core",
"rule": "DECISIONS#9"
},
Expand Down
Loading
Loading