Skip to content

docs: make the chapters agree and name the three-model pipeline - #115

Merged
ExtraToast merged 2 commits into
mainfrom
docs/36-51-on-project-intent
Sep 15, 2026
Merged

ExtraToast merged 2 commits into
mainfrom
docs/36-51-on-project-intent

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Closes #36
Closes #51
Parent #35

What this branch does

Two passes over the specification, in the vocabulary #100 landed: the model chapters stop contradicting themselves, and the "three-layer meta-model" becomes the three-model pipeline.

The chapters agree with themselves (#36)

Each of these was a sentence that said one thing while the chapter, the record or the worked example said another.

  • Hardening has no exceptions. Chapter 20 described hardening as a platform posture "plus declared exceptions", each naming a control and a reason, while ADR 0016 and the same chapter's "Hardening has no exception surface either" say there is none. The bullet, the securityContext row and CONTEXT.md's Hardening Class now say there is no per-control relaxation, and name E_HARDENING_UNMET.
  • "Context" meant Platform Intent in chapter 10's error table, chapter 16 and two places in chapter 60. Those now say Platform Intent. The ones naming the external cluster-deploy-context OCI artifact are left alone, as is historical prose in ADRs.
  • Observability. Chapter 60's onboarding step named a Service-level alertClass; it is an optional observability block, whole or absent. Its platform-facts paragraph listed an observability policy the Platform document does not have; it lists the monitor cadence. Chapter 10's scrape cadence linked a probe section when chapter 14 has its own ## Monitor cadence, which lint:links could not catch because the wrong anchor exists. The platform example README described a receivers block where platform.intent.yml has monitors. CONTEXT.md's Alert Class described routing; it describes urgency.
  • Chapter 16's derivation map drew Platform Intent inside a subgraph labelled layer 2, when it is authored in layer 1, and drew four nodes the derivation-map SVGs do not have: the Flux health timeout class, the Flux Kustomization, the reachability entry and the edge catalogs. Those are gone, and the catalogs reach the ConfigMap as the deliverables SVG shows. One node still read Application where it means a Kubernetes Service, a leftover of the rename; it reads Service.
  • The file layout and the examples now agree. Chapter 10 said platform/<project>.yml while every example, refusal fixture and suffix-selecting test uses .project.yml. The rule is platform/<project>.project.yml, the suffix naming the file's kind. Four flat env files move to env/<process>/base.env, so the per-cluster overlay has a directory to sit in. data's goes under postgres/, the Process, rather than the Application id its header already disagreed with. Chapter 00's examples table and overview diagram named paths that do not exist; they name the ones that do.
  • A duplicated dangling fragment in chapter 10 is removed.

Every comment edit in a YAML or env file keeps that file's line count, so no line reference elsewhere shifts. The suffix-selecting tests still select what they should: 14 .project.yml files for the diagram test, 4 cases for the Project Intent test, and the 4 worked files for the EMF worked-examples test.

The three-model pipeline (#51)

"Meta-model" claimed something the decision does not. The intent models, the Resolved Deployment and the Deliverable Set sit at one metalevel: each is transformed into the next, and none is a type model of another. A meta-model names a stack of metalevels, which this pipeline does not have. CONTEXT.md now defines Three-model pipeline and Metamodel apart, and says why the old name is retired.

  • docs/adr/model/0003-three-layer-meta-model.md becomes 0003-three-model-pipeline.md, with a dated note in its Why. Its decision and H1 are unchanged.
  • Chapter 00's ## The meta-model becomes ## The three-model pipeline. It was 0003's normative: target, so the frontmatter and the register's normative column move to #the-three-model-pipeline in the same commit, and lint:adrs resolves it.
  • The overview diagram is renamed to 00-overview-three-model-pipeline.drawio.svg, and its embedded name follows.
  • Every citation and link moves with it, including three occurrences found by re-grepping main rather than trusting the old list: ADRs 0105 and 0116, and the EMF ADR 0109.

The only remaining occurrences of the old name are the three sentences that say it is retired.

Verification

  • npm run lint:adrs: 96 files clean. node scripts/lint-adrs.ts emf: 10 files clean.
  • npm run lint:links: 178 files clean.
  • npm run verify exit 0: 24 requirement rows, 64 rules with 11 pending, 32 doc claims, 466 tests.
  • No em-dash added.

The EMF Maven and Tycho job runs in CI only. The one emf/ change here is prose in ADR 0109.

Left for follow-ups

  • The negative composition fixtures under spec/v1/examples/negative/*/intent*/ do not carry .project.yml. They stand in for published fragments rather than a repository's platform/ tree, and renaming them would pull them into the worked-examples tests, where one is expected to fail a parse. That wants its own change.
  • spec/v1/examples/platform/README.md still says "the Traefik Application" where it means a Service, and has no row for the hardening: block the platform document carries.
  • spec/v1/examples/data/rendered/README.md writes alertClass: none, and chapter 10 says Alert Class has no none member.

Wording fixes for rules the model has already decided, so a reader of any
one chapter meets the rule in force. No model change.

- Chapter 20 said hardening was "one platform posture plus declared
  exceptions" and derived `securityContext` "from `hardening` and its
  declared exceptions", above its own "Hardening has no exception surface
  either". 0016 deleted the exception vocabulary; the authority row and
  the derived-mechanics bullet now say so and name E_HARDENING_UNMET, and
  CONTEXT.md's Hardening Class loses its "named exceptions".
- "a Context", "the pinned context" and "a context" meaning the Platform
  Intent are gone from chapters 10, 16 and 60.
- Chapter 10 carried a truncated copy of "Writing a derived value as a
  literal is a build error" in front of the sentence. Removed.
- Chapter 60's onboarding step said an Application carries `alertClass`.
  It carries an optional `observability` block, whole or absent. Its
  platform-facts paragraph listed an observability policy the Platform
  document does not have; it now lists the monitor cadence. The worked
  platform example's README named the same non-existent block, and now
  names `monitors`.
- Chapter 10 pointed at chapter 14's probe policy for the scrape cadence.
  The monitor cadence has its own section.
- CONTEXT.md's Alert Class said how an alert "should be delivered".
  Delivery is routing, which belongs to the monitoring stack. It states
  urgency.
- Chapter 16's derivation-map mermaid labelled the Platform Intent a
  layer-2 input and carried a Flux health timeout class, a Flux
  Kustomization, a reachability entry and edge-catalog nodes the drawn
  SVGs do not. The catalogs reach the ConfigMap, as the deliverables SVG
  draws it, and the Kubernetes Service node the rename had relabelled
  "Application" is "Service" again.
- The file layout and the worked examples disagreed. The project file is
  `platform/<project>.project.yml`, the suffix naming its `kind`, which
  every example and refusal fixture already spells; the chapter, chapter
  14's foundation files, the publish workflow and the examples' header
  comments now say it too. A Process's env files live in a directory named
  for the Process, so the cluster overlay has somewhere to sit: four flat
  example env files move, and data's is filed under its Process `postgres`.
- Chapter 00's example table and its overview diagram named a `projects/`
  directory, flat env files and a `platform.yml` that do not exist.
The three layers are three models, each joined to the next by a
transformation. They are not metalevels: no layer is a type model of the
layer below it, and "meta-model" claims that relation.

- 0003's file name, chapter 00's heading and its anchor, the overview
  diagram's file name and embedded diagram name, CONTEXT.md's section and
  the README now say "three-model pipeline". 0003's decision and its H1 are
  unchanged; its Why records the rename.
- CONTEXT.md defines "Three-model pipeline" and "Metamodel", the latter
  kept for a language definition only, and says why "meta-model" is retired.
- The anchor, 0003's normative pointer, the register row and every link at
  the old file names move in this one commit, so lint:adrs and lint:links
  resolve throughout. That includes 0105, 0116 and the model-driven 0109,
  which cite 0003 and postdate the first draft of this change.
@ExtraToast ExtraToast added type: docs Documentation-only change. area: deploy homelab-deploy, deploy-v2, manifests, and rollout flow. component: docs Runbook, README, API docs, or documentation tooling. labels Sep 15, 2026
@ExtraToast
ExtraToast merged commit ad8540c into main Sep 15, 2026
19 checks passed
@ExtraToast
ExtraToast deleted the docs/36-51-on-project-intent branch September 15, 2026 10:18
ExtraToast added a commit that referenced this pull request Sep 15, 2026
The claim workflow read every bare #NNN in a title or body as a claim,
labelling any issue merely mentioned ("follows #115", "blocked by #39",
"unlike #41"), the exact shape of the labelling incident this fixes. It
also released a claimed issue's parent on every child close, and depended
on gh issue view's --json parent field without confirming it exists on
the runner.

scripts/claim-issue.ts (test/claim-issue.test.ts, no gh call) now extracts
candidates from a GitHub closing keyword (closes, fixes, resolves, and
their inflections) immediately naming an issue, or the branch name's
leading number, stripping fenced code blocks first. The workflow calls it
for the candidate list and keeps every gh call in its own bash: on claim,
the parent is claimed too if unclaimed; on release, only the named issue
is released, never the parent, which stays a human's call or a later
workflow's. Dropped synchronize from the triggers, since a push re-runs
the same extraction over the same fields and can only repeat what already
holds.

Confirmed gh issue view --json parent is real and supported: added in gh
CLI 2.94.0 (2026-06-10 changelog), and the ubuntu-latest runner image
ships 2.100.0. A gh issue view failure is now logged with its real error
text rather than assumed to mean "not an issue".

Coverage rises again with the new file's own full coverage (statements
99.09%, branches 96.32%, functions 100%, lines 99.01%), still above
main's floor; vitest.config.ts and README.md carry the measured values.
ExtraToast added a commit that referenced this pull request Sep 15, 2026
* docs: route every agent through AGENTS.md

Add AGENTS.md as the one instruction file every coding agent reads: the
estate pointer, the repository map, setup, every npm script, the testing
strategy, code style, the Boundaries section, the repository skills, the
claiming procedure, and the moved emf/ and MDE coursework sections. CLAUDE.md
becomes an import of it plus Claude-specific notes only.

* docs: configure repository skills and agent tooling

Four skills (adr, spec-change, gate-triage, new-rule) live in
.agents/skills, and .claude/skills is a committed symlink to that directory
(git ls-files -s reads mode 120000). .claude/settings.json holds a
read-only permission allowlist plus the one labelled write the claim
procedure needs, and enables the mattpocock-skills, typescript-lsp and
drawio plugins. .mcp.json holds context7. docs/agents/ configures the
mattpocock-skills engineering skills: the GitHub issue tracker with
cross-repository references written in full, the estate status: labels
mapped to the five canonical triage roles, and the single-context domain
doc layout with spec/v1 normative over an ADR.

* test: fail when a script escapes AGENTS.md's script list

Add lint:agents (scripts/lint-agents.ts), wired into the contracts CI job
and npm run verify, with a fixture (test/agents-contract.test.ts) proving it
fires on an undocumented script. Ledger rows REQ-029 and RULE-069 record it,
and docs/architecture.md's Gates table gains a row and its count moves to
nineteen, per 0102 and 0104.

The new gate's own coverage (96%/94.73%/100%/95.83%) carries the same
one-line command-guard gap every sibling gate script already does; the
suite's aggregate moves to 98.98%/96.06%/100%/98.89%, so the ratchet in
vitest.config.ts and README's quoted numbers move with it.

* feat(ci): claim the issue a pull request names

A non-gating workflow that adds status: in-progress (and removes
status: ready-for-agent) on the issue a pull request's title, body or
branch name references, and on its parent, when the pull request opens;
releases the claim when it closes. Labels only: the project board's own
Status field needs a token scope the default GITHUB_TOKEN does not carry,
and the job says so in its own log rather than claiming a fix it cannot
make.

* docs: extend CONTRIBUTING.md with change shape and release flow

Below the existing contribution policy: the shape of a change, when a
change takes a decision, Conventional Commits, what runs where across the
five non-gating workflows beside ci.yml, and release candidates and
releasing.

* test: recompute the coverage ratchet after rebasing onto main

REQ-029 collided with #118's own row landed on main; this branch's
AGENTS.md script-list requirement moves to REQ-030. #118 also moved the
coverage baseline, so the ratchet in vitest.config.ts and README's quoted
numbers are recomputed against the rebased tree rather than the
pre-rebase measurement.

* test: hold the ratchet at main's floor after the AGENTS.md gate

Cover lint-agents.ts's own entrypoint guard and default output parameter
with an in-process module reload, the way
test/package-contents-contract.test.ts and
test/secret-scan-contract.test.ts already do for theirs, so the new gate
is the first to leave nothing uncovered. Rebased onto main again (#119,
docs-only); REQ-030 and RULE-069 remain free. Every metric now sits above
main's floor (99.06/96.17/100/98.97): statements 99.07%, branches 96.25%,
functions 100%, lines 98.99%, and vitest.config.ts and README.md carry the
measured values.

* fix(ci): claim only what closes, fixes or resolves a pull request

The claim workflow read every bare #NNN in a title or body as a claim,
labelling any issue merely mentioned ("follows #115", "blocked by #39",
"unlike #41"), the exact shape of the labelling incident this fixes. It
also released a claimed issue's parent on every child close, and depended
on gh issue view's --json parent field without confirming it exists on
the runner.

scripts/claim-issue.ts (test/claim-issue.test.ts, no gh call) now extracts
candidates from a GitHub closing keyword (closes, fixes, resolves, and
their inflections) immediately naming an issue, or the branch name's
leading number, stripping fenced code blocks first. The workflow calls it
for the candidate list and keeps every gh call in its own bash: on claim,
the parent is claimed too if unclaimed; on release, only the named issue
is released, never the parent, which stays a human's call or a later
workflow's. Dropped synchronize from the triggers, since a push re-runs
the same extraction over the same fields and can only repeat what already
holds.

Confirmed gh issue view --json parent is real and supported: added in gh
CLI 2.94.0 (2026-06-10 changelog), and the ubuntu-latest runner image
ships 2.100.0. A gh issue view failure is now logged with its real error
text rather than assumed to mean "not an issue".

Coverage rises again with the new file's own full coverage (statements
99.09%, branches 96.32%, functions 100%, lines 99.01%), still above
main's floor; vitest.config.ts and README.md carry the measured values.

* fix(test): keep agent configuration out of the mutation sandbox

Stryker copies the repository into its sandbox before running, and its
copyfile call dies on .claude/skills: a symlink to a directory, which
copyfile refuses outright (EISDIR). ignorePatterns (a real, documented
Stryker option, confirmed against node_modules/@stryker-mutator/core's own
stryker-schema.json for the pinned 10.0.0) now excludes /.claude and
/.agents, neither of which the mutation run needs: Claude Code
configuration and skill content, not src/. The symlink itself is
untouched; only the sandbox copy skips it.

* test: hold the ratchet at main's floor after rebasing onto #120

#120 (the Platform document reader) took REQ-030 for an unrelated
behaviour and added REQ-031; this branch's AGENTS.md script-list
requirement moves to REQ-032, the ledger now states 32 rows. #120 added no
npm script, so AGENTS.md's script list needed no change (lint:agents
confirms all 22 scripts still resolve). Coverage rises with #120's own
new, fully-tested code: statements 99.15%, branches 96.49%, functions
100%, lines 99.08%, still above main's floor (99.06/96.17/100/98.97, which
#120 left untouched); vitest.config.ts and README.md carry the measured
values.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: deploy homelab-deploy, deploy-v2, manifests, and rollout flow. component: docs Runbook, README, API docs, or documentation tooling. type: docs Documentation-only change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename the three-layer meta-model to the three-model pipeline Make the model chapters agree with themselves

1 participant