@piercebarney/whs-eleventy 2026.9.3 → 2026.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // whs — the web house style's Eleventy + Netlify tooling.
3
3
  //
4
+ // whs status [--hook] standard-version drift, at a glance
4
5
  // whs compliance [--strict] codebase-vs-standard conformance sweep
5
6
  // whs doctor [--live|--deploy-preflight] infrastructure drift check
6
7
  // whs links built-output link / CSP / JSON-LD integrity
@@ -16,6 +17,7 @@ const path = require("node:path");
16
17
  const { spawnSync } = require("node:child_process");
17
18
 
18
19
  const COMMANDS = {
20
+ status: "status.js",
19
21
  compliance: "compliance.js",
20
22
  doctor: "doctor.js",
21
23
  links: "check-links.js",
package/lib/compliance.js CHANGED
@@ -664,16 +664,26 @@ const CHECKS = {
664
664
 
665
665
  // ---- standard-version drift -----------------------------------------
666
666
 
667
- // Every chapter slug the CHANGELOG records as changed after `pin`. `### core: a
668
- // · b · c` yields every slug on the line, not just the first; a
669
- // `### stacks/eleventy-netlify.md` heading yields the binding marker.
670
- function changelogSlugs(changelog, pin) {
671
- const slugs = new Set();
672
- let inRange = false;
667
+ // Every CHANGELOG entry (`## <date> [breaking|non-breaking]`) dated after
668
+ // `pin`, with the chapter slugs it touched. `### core: a · b · c` yields
669
+ // every slug on the line, not just the first; a
670
+ // `### stacks/eleventy-netlify.md` heading yields the binding marker. An
671
+ // entry heading with no severity tag (a fixture, or a pre-2026-09-04
672
+ // CHANGELOG snapshot) defaults to non-breaking — the real standard's own
673
+ // CHANGELOG.md always carries the tag (bin/check's 6th check enforces it).
674
+ function changelogEntries(changelog, pin) {
675
+ const entries = [];
676
+ let current = null;
673
677
  for (const line of (changelog || "").split("\n")) {
674
- const dateM = line.match(/^##\s+([0-9]{4}-[0-9]{2}-[0-9]{2})/);
675
- if (dateM) inRange = dateM[1] > pin;
676
- if (!inRange) continue;
678
+ const dateM = line.match(
679
+ /^##\s+([0-9]{4}-[0-9]{2}-[0-9]{2}).*?(?:\[(breaking|non-breaking)\])?\s*$/,
680
+ );
681
+ if (dateM) {
682
+ current = dateM[1] > pin ? { severity: dateM[2] || "non-breaking", slugs: new Set() } : null;
683
+ if (current) entries.push(current);
684
+ continue;
685
+ }
686
+ if (!current) continue;
677
687
  const coreM = line.match(/^###\s+core:\s+(.+)/);
678
688
  if (coreM) {
679
689
  for (const part of coreM[1].split(/[·,]/)) {
@@ -687,15 +697,23 @@ function changelogSlugs(changelog, pin) {
687
697
  // not a literal slug named "all" — the registry has no such slug,
688
698
  // so emitting it as one would print a `MANUAL: re-check #all` row
689
699
  // that looks real but isn't.
690
- slugs.add("(all core chapters)");
700
+ current.slugs.add("(all core chapters)");
691
701
  continue;
692
702
  }
693
703
  const slug = (cleaned.match(/^[a-z0-9-]+/) || [])[0];
694
- if (slug) slugs.add(slug);
704
+ if (slug) current.slugs.add(slug);
695
705
  }
696
706
  }
697
- if (/^###\s+stacks\/eleventy-netlify\.md/.test(line)) slugs.add("(eleventy binding)");
707
+ if (/^###\s+stacks\/eleventy-netlify\.md/.test(line)) current.slugs.add("(eleventy binding)");
698
708
  }
709
+ return entries.map((e) => ({ severity: e.severity, slugs: [...e.slugs] }));
710
+ }
711
+
712
+ // Every chapter slug the CHANGELOG records as changed after `pin`, flattened
713
+ // (severity dropped) — kept for callers that only need the slug set.
714
+ function changelogSlugs(changelog, pin) {
715
+ const slugs = new Set();
716
+ for (const e of changelogEntries(changelog, pin)) for (const s of e.slugs) slugs.add(s);
699
717
  return [...slugs];
700
718
  }
701
719
 
@@ -716,11 +734,22 @@ function versionDrift() {
716
734
  if (pin >= current) return { pin, current, rows: [] };
717
735
 
718
736
  const changelog = read("CHANGELOG.md", STANDARD) || "";
737
+ // Worst-case severity per slug: one breaking touch marks it breaking for
738
+ // good, even if a later (or earlier) entry touching the same slug since
739
+ // the pin was non-breaking.
740
+ const severityBySlug = new Map();
741
+ for (const entry of changelogEntries(changelog, pin)) {
742
+ for (const slug of entry.slugs) {
743
+ if (severityBySlug.get(slug) !== "breaking") severityBySlug.set(slug, entry.severity);
744
+ }
745
+ }
719
746
  return {
720
747
  pin,
721
748
  current,
722
- rows: changelogSlugs(changelog, pin).map(
723
- (s) => `MANUAL: re-check #${s} — changed since the pinned ${pin}`,
749
+ rows: [...severityBySlug.entries()].map(([slug, severity]) =>
750
+ severity === "breaking"
751
+ ? `BREAKING: re-check #${slug} — changed since the pinned ${pin}`
752
+ : `MANUAL: re-check #${slug} — changed since the pinned ${pin}`,
724
753
  ),
725
754
  };
726
755
  }
@@ -751,10 +780,11 @@ function runCompliance() {
751
780
 
752
781
  function summarize({ results, drift }) {
753
782
  const n = (s) => results.filter((r) => r.status === s).length;
783
+ const breakingDrift = drift.rows.filter((r) => r.startsWith("BREAKING:")).length;
754
784
  return {
755
785
  pass: n(PASS),
756
- fail: n(FAIL),
757
- manual: n(MANUAL) + drift.rows.length,
786
+ fail: n(FAIL) + breakingDrift,
787
+ manual: n(MANUAL) + (drift.rows.length - breakingDrift),
758
788
  na: n(NA),
759
789
  };
760
790
  }
@@ -793,7 +823,7 @@ function main() {
793
823
  writeCache({ ...payload, summary: s });
794
824
 
795
825
  if (strict && s.fail) {
796
- console.error("compliance --strict: FAIL chapters present.");
826
+ console.error("compliance --strict: FAIL chapters or BREAKING standard-version drift present.");
797
827
  process.exit(1);
798
828
  }
799
829
  }
@@ -802,4 +832,11 @@ if (require.main === module) main();
802
832
 
803
833
  // CHECKS is exported for the "coverage" test — nothing else should read it as
804
834
  // data (call runCompliance() for results).
805
- module.exports = { runCompliance, summarize, changelogSlugs, CHECKS };
835
+ module.exports = {
836
+ runCompliance,
837
+ summarize,
838
+ changelogSlugs,
839
+ changelogEntries,
840
+ versionDrift,
841
+ CHECKS,
842
+ };
package/lib/status.js ADDED
@@ -0,0 +1,108 @@
1
+ // Standard-version drift, at a glance (core.md#compliance). The delta-only
2
+ // slice of `whs compliance` — how far the project's pinned standard-version is
3
+ // behind the standard, and which chapters that means re-checking — without the
4
+ // full codebase sweep. Cheap enough to run on every Claude Code SessionStart.
5
+ //
6
+ // whs status print the version delta + the re-check rows
7
+ // whs status --hook silent when current; one line when behind (for the
8
+ // SessionStart hook — never non-zero, never noisy)
9
+ //
10
+ // Exit code is always 0: like `compliance` without --strict, drift is a
11
+ // backlog signal, not a build break.
12
+
13
+ const fs = require("node:fs");
14
+ const os = require("node:os");
15
+ const path = require("node:path");
16
+
17
+ // `status` answers "is there a newer standard available on this machine?" — so
18
+ // before the compliance module resolves its standard text, prefer the local
19
+ // canonical checkout (the ~/.claude symlink the README keeps) when one exists
20
+ // and nothing more specific was set. `whs compliance` still measures the
21
+ // codebase against the tooling's own bundled snapshot; `status` measures the
22
+ // pin against the freshest tree available, which is what a "you're behind"
23
+ // nudge should reflect. Machines without the symlink (CI, a fresh clone) fall
24
+ // back to the bundled snapshot unchanged.
25
+ if (!process.env.WHS_STANDARD) {
26
+ const local = path.join(os.homedir(), ".claude", "standards", "web-house-style");
27
+ if (fs.existsSync(path.join(local, "core.md"))) process.env.WHS_STANDARD = local;
28
+ }
29
+
30
+ const { versionDrift } = require("./compliance.js");
31
+ const { resolveStandard } = require("./_project.js");
32
+
33
+ // Distinct CHANGELOG dates after the pin — the "N versions behind" count. The
34
+ // standard versions by date, and several entries can share one date (one
35
+ // release); this counts releases, not entries.
36
+ function versionsBehind(pin) {
37
+ const std = resolveStandard();
38
+ if (!std || !pin) return 0;
39
+ try {
40
+ const changelog = fs.readFileSync(path.join(std, "CHANGELOG.md"), "utf8");
41
+ const dates = new Set();
42
+ for (const line of changelog.split("\n")) {
43
+ const m = line.match(/^##\s+([0-9]{4}-[0-9]{2}-[0-9]{2})\b/);
44
+ if (m && m[1] > pin) dates.add(m[1]);
45
+ }
46
+ return dates.size;
47
+ } catch {
48
+ return 0;
49
+ }
50
+ }
51
+
52
+ function main() {
53
+ const hook = process.argv.includes("--hook");
54
+ const { pin, current, rows } = versionDrift();
55
+
56
+ const unresolved =
57
+ rows.length === 1 && rows[0].startsWith("MANUAL: standard text not resolvable");
58
+ if (unresolved) {
59
+ if (!hook) console.log(rows[0]);
60
+ return;
61
+ }
62
+
63
+ if (!rows.length) {
64
+ if (!hook) {
65
+ console.log(
66
+ pin && current
67
+ ? `web house style: up to date (pinned ${pin}, standard at ${current})`
68
+ : "web house style: standard-version not pinned in CLAUDE.md",
69
+ );
70
+ }
71
+ return;
72
+ }
73
+
74
+ const breaking = rows.filter((r) => r.startsWith("BREAKING:")).length;
75
+ const versions = versionsBehind(pin);
76
+ const behind = `${versions} version${versions === 1 ? "" : "s"} behind`;
77
+ const recheck = `${rows.length} re-check${rows.length === 1 ? "" : "s"}${
78
+ breaking ? ` (${breaking} breaking)` : ""
79
+ }`;
80
+
81
+ if (hook) {
82
+ console.log(
83
+ `⚠ web house style: ${behind}, ${recheck} — pinned ${pin}, standard at ${current}. ` +
84
+ `Run /whs:upgrade to catch up.`,
85
+ );
86
+ return;
87
+ }
88
+
89
+ console.log(
90
+ `\nstandard-version: pinned ${pin}, standard at ${current} — ${behind}, ${recheck}\n`,
91
+ );
92
+ for (const row of rows) console.log(` ${row}`);
93
+
94
+ const hasUpgrade = fs.existsSync(
95
+ path.join(process.cwd(), ".claude", "commands", "whs", "upgrade.md"),
96
+ );
97
+ console.log(
98
+ hasUpgrade
99
+ ? "\nRun /whs:upgrade in a Claude Code session to work this backlog.\n"
100
+ : "\nThis project predates /whs:upgrade. Copy .claude/commands/whs/upgrade.md,\n" +
101
+ ".claude/hooks/standard-drift.sh, and the settings.json SessionStart entry\n" +
102
+ "from templates/eleventy-netlify/ in the standard, then run /whs:upgrade.\n",
103
+ );
104
+ }
105
+
106
+ if (require.main === module) main();
107
+
108
+ module.exports = { versionsBehind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piercebarney/whs-eleventy",
3
- "version": "2026.9.3",
3
+ "version": "2026.9.10",
4
4
  "description": "The web house style's Eleventy + Netlify tooling — the compliance sweep, the infra doctor, the link/CSP integrity check, and the a11y scan, shared by every project on the stack.",
5
5
  "bin": {
6
6
  "whs": "cli.js"
@@ -36,8 +36,8 @@
36
36
  "sirv": "^3.0.2"
37
37
  },
38
38
  "devDependencies": {
39
- "@eslint/js": "^9.39.5",
40
- "eslint": "^9.39.5",
39
+ "@eslint/js": "^10.0.1",
40
+ "eslint": "^10.9.1",
41
41
  "globals": "^17.11.0",
42
42
  "prettier": "^3.9.6"
43
43
  }
@@ -6,9 +6,723 @@ at a glance. Stack-only changes list the file.
6
6
 
7
7
  Format: `## <date>` → `### core: <slug>` / `### <stack>` entries.
8
8
 
9
+ **Severity.** Every `## <date>` heading also carries `[breaking]` or
10
+ `[non-breaking]`. An entry is **breaking** if a project that was fully
11
+ compliant *before* the change is no longer compliant *after* it — the change
12
+ adds or tightens a requirement, removes a previously-sanctioned pattern, or
13
+ corrects a documented mechanism that didn't actually work (so a project
14
+ following the old doc has broken/non-compliant behavior it thought was fine).
15
+ Everything else — wording fixes, doc corrections that don't change what's
16
+ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
17
+ **non-breaking**. `bin/check` fails if a heading is missing the tag.
18
+
19
+ ---
20
+
21
+ ## 2026-09-10 — a one-command standard-version catch-up, and drift that surfaces itself [non-breaking]
22
+
23
+ Catching an adopted project up to a moved-on standard was a hand-written prompt
24
+ every time. The detection existed (`whs compliance`'s severity-tagged
25
+ `re-check #<slug>` rows) but was passive — nothing said "you're behind" unless
26
+ someone ran it. `core.md#compliance` already expects the assistant to run
27
+ compliance "whenever `standard-version:` is behind"; this makes the Eleventy
28
+ stack actually do that, and adds the command that works the result. Non-breaking:
29
+ a project compliant before is still compliant — this is an opt-in reporter, a
30
+ session-start notice, and a new command; no requirement changes. Eleventy-only
31
+ pilot, same stance as `/whs:build`; Phoenix/SvelteKit keep the by-hand path
32
+ until they have a tooling package.
33
+
34
+ ### stacks/eleventy-netlify.md: compliance
35
+
36
+ - Documented `whs status` (the drift row alone — pinned vs. current version,
37
+ versions behind, the `re-check` list — no codebase sweep) and its
38
+ `SessionStart` wiring in the `compliance` section's **Wiring** bullet. Added a
39
+ migration-section note: an already-adopted project that has only fallen behind
40
+ runs `/whs:upgrade`; one scaffolded before the command shipped copies three
41
+ files from the template first.
42
+
43
+ ### templates/eleventy-netlify
44
+
45
+ - New `/whs:upgrade` command (`.claude/commands/whs/upgrade.md`) — reads the
46
+ CHANGELOG delta since the pin, bumps `@piercebarney/whs-eleventy`, works the
47
+ `compliance` backlog one `re-check` row at a time (delegating to the
48
+ `brand`/`content`/`layout`/`pack` stage agents where a slug is theirs), then
49
+ re-pins `standard-version`. Never deploys. Structured like `/whs:build`.
50
+ - New `.claude/hooks/standard-drift.sh` + a `SessionStart` entry in
51
+ `.claude/settings.json` — runs `whs status --hook` at the top of every Claude
52
+ Code session; one line when the pin is behind, silent when current, never
53
+ blocks.
54
+ - `package.json` gains a `status` script (`whs status`).
55
+ - `CLAUDE.md` points at `/whs:upgrade` for the behind-the-standard case.
56
+
57
+ ### packages/whs-eleventy
58
+
59
+ - New `whs status` subcommand (`lib/status.js`) — reuses `compliance.js`'s
60
+ `versionDrift()` (now exported) and reports the delta without running the
61
+ `CHECKS` sweep. `--hook` mode: one compact line when behind, nothing when
62
+ current, always exit 0. Registered in `cli.js`. Tests in `test/status.test.js`
63
+ plus a `versionDrift`-export assertion in `test/compliance.test.js`.
64
+
65
+ ## 2026-09-08 — content-ops: reservation becomes a real opt-in, not a hardcoded default [non-breaking]
66
+
67
+ Found while building `/whs:build`'s Content stage agent, which needs to know
68
+ whether a project's content is reserved before touching anything.
69
+ `core.md#content-model`'s Ownership paragraph already documented the default
70
+ correctly ("by default the party that builds the site may also edit its
71
+ content") — but `templates/eleventy-netlify/` shipped `CONTENT.md` (declaring
72
+ "reserved for: Cowork") unconditionally, with no way to opt out. Every
73
+ project scaffolded from it started reserved regardless of whether that was
74
+ true, contradicting the standard's own stated default.
75
+
76
+ ### core: content-model
77
+
78
+ - New House-style field: `content-ops` (`in-repo` default | `reserved`).
79
+ Recorded as the sixth stack-selection question (`#adopting`). `CONTENT.md`
80
+ ships only when `reserved` is chosen; the voice guide ships either way —
81
+ clarified as not reservation-conditional (it was ambiguously grouped with
82
+ `CONTENT.md` before).
83
+
84
+ ### templates/eleventy-netlify
85
+
86
+ - `bin/init` reads `content-ops` from the brief (default `in-repo`): deletes
87
+ `CONTENT.md` and `requests/` when not reserved, keeps them when it is.
88
+ - `CLAUDE.md`'s Content section carries both branches of the reservation
89
+ paragraph behind a `<!-- content-ops:reserved -->` /
90
+ `<!-- content-ops:in-repo -->` marker pair — the same mechanism-marker
91
+ idiom `{ include: "…" }` already uses elsewhere — and `bin/init` resolves
92
+ to whichever branch applies, dropping the other.
93
+ - Verified both branches end-to-end in a scratch copy: `content-ops: reserved`
94
+ keeps `CONTENT.md` + `requests/` and the reserved paragraph; the default
95
+ keeps neither and shows the in-repo paragraph instead.
96
+
97
+ ## 2026-09-04 — sveltekit-netlify's brand token layer, and a Pico gap that predates it [non-breaking]
98
+
99
+ Third of the four functional gaps being closed ahead of the real project
100
+ adopting this stack. Scoping this one surfaced a bigger, previously
101
+ unnoticed gap: `AGENTS.md` and `stacks/sveltekit-netlify.md#styling` both
102
+ already asserted "Pico classless" as this template's CSS default, but Pico
103
+ was never actually installed, imported, or linked anywhere in the shipped
104
+ code — the "unstyled" state `TEMPLATE.md` flagged wasn't Pico-with-no-brand-
105
+ colors, it was no CSS framework at all. Fixing that was a real, if small,
106
+ scope expansion beyond "just add brand.ts" — confirmed with the user before
107
+ proceeding, on the grounds that the template's job is to satisfy
108
+ `core.md#styling` on day one the same as every other chapter, and the fix
109
+ doesn't foreclose Tailwind for the real project (still the documented
110
+ per-project alternative at adoption time, unaffected by this).
111
+
112
+ ### templates/sveltekit-netlify
113
+
114
+ - Added `@picocss/pico@2.1.1` (matching `templates/eleventy-netlify/`'s
115
+ pin) to `dependencies`.
116
+ - New `src/lib/brand.ts`: the token source (colors, fonts, logo), the exact
117
+ field shape of `templates/eleventy-netlify/src/_data/brand.js`.
118
+ - New `src/routes/pico.css/+server.ts`: serves Pico's classless jade build
119
+ self-hosted at `/pico.css`, re-exporting the npm package's CSS via Vite's
120
+ `?raw` import — no vendor-file copy step needed the way Eleventy's
121
+ passthrough-copy config requires.
122
+ - New `src/routes/tokens.css/+server.ts`: generates the `--pico-*` remap
123
+ from `brand.ts`, mirroring `tokens.css.njk`'s logic. `app.html` links
124
+ `/pico.css` then `/tokens.css`.
125
+ - **A real gotcha, found only by actually rendering a page:** load order
126
+ alone doesn't decide the cascade here. Pico 2.1.1's own light-mode block
127
+ is `:root:not([data-theme="dark"])`, which out-specifies a plain
128
+ `:root{}` override regardless of which stylesheet loads second — the
129
+ brand remap silently lost to Pico's stock teal-green palette until the
130
+ override's selector was changed to match Pico's specificity. Caught with
131
+ a real Playwright screenshot (installed for this session — the built
132
+ site had never been visually rendered before), not by reading the CSS.
133
+ `[data-theme="dark"]` and the `prefers-color-scheme: dark` media block
134
+ didn't have this problem — Pico's own selectors there are already the
135
+ same specificity as the override, so source order alone decides,
136
+ correctly. Verified both branches with real screenshots (light and
137
+ forced-dark) showing the brand magenta, not Pico's defaults.
138
+ - Full `npm run check` gate passes.
139
+
140
+ ### stacks/sveltekit-netlify.md: styling, brand-source
141
+
142
+ - `styling` now documents the shipped mechanism, including the specificity
143
+ gotcha above, so the next person touching this doesn't rediscover it the
144
+ hard way. `brand-source` now distinguishes the CSS consumer (built) from
145
+ the OG-card renderer and icon generator (still not built — `logo` sits in
146
+ `brand.ts` unused, waiting for either).
147
+
148
+ ### templates/sveltekit-netlify/TEMPLATE.md
149
+
150
+ - The brand-layer "still owns" bullet replaced with a "replace the
151
+ placeholder palette" note, since the layer itself now exists.
152
+
153
+ ---
154
+
155
+ ## 2026-09-04 — sveltekit-netlify's canonical tag, wired [non-breaking]
156
+
157
+ Second of the four functional gaps being closed ahead of the real project
158
+ adopting this stack (see the permalink-shape entry, below, for the full
159
+ context). `stacks/sveltekit-netlify.md#seo-urls` already named the exact
160
+ mechanism — `SITE_URL` from `src/lib/site.ts`, never `$page.url.origin` — but
161
+ no canonical tag actually shipped yet. This wires it.
162
+
163
+ ### templates/sveltekit-netlify
164
+
165
+ - New `src/lib/components/Canonical.svelte`: reads `page.url.pathname` from
166
+ `$app/state` (the path only — never `.url.origin`, which is the
167
+ request/preview host, not necessarily production) and `SITE_URL` from
168
+ `src/lib/site.ts`, emitting `<link rel="canonical" href={SITE_URL +
169
+ pathname}>` from its own `<svelte:head>`.
170
+ - Mounted once in the root `+layout.svelte` — Svelte merges `<svelte:head>`
171
+ blocks from anywhere in the component tree, so every route gets a
172
+ canonical tag without a per-page component instance.
173
+ - Verified with a real build: every page's canonical trailing slash matches
174
+ its own permalink exactly — home (`https://example.com/`), a static route
175
+ (`.../about/`), the guides index, a guide detail page, and the audit page
176
+ all checked directly in the built HTML. Full `npm run check` gate passes.
177
+
178
+ ### stacks/sveltekit-netlify.md: seo-urls, seo-meta, noindex
179
+
180
+ - `seo-urls`'s "not yet wired" note replaced with the actual mechanism and
181
+ verification. `seo-meta` now distinguishes the canonical tag (shared
182
+ component, shipped) from the still-deferred full OG/Twitter `<Seo>`
183
+ component (needs per-page `load` data, a bigger job — unchanged scope).
184
+ `noindex`'s Layer 3 note updated from "not yet wired" to a pointer at
185
+ `seo-urls`.
186
+
187
+ ---
188
+
189
+ ## 2026-09-04 — sveltekit-netlify: fixes from an independent code review [non-breaking]
190
+
191
+ The four functional-gap fixes above were self-verified (real builds, real
192
+ screenshots) but not independently reviewed before landing. An `/code-review
193
+ high` pass across the full diff, run afterward, surfaced one real regression
194
+ and several smaller gaps the self-verification didn't check for. All fixed
195
+ and re-verified against real builds/tests, not just read over.
196
+
197
+ ### netlify.toml
198
+
199
+ - **The regression.** The permalink-shape fix's `trailingSlash: 'always'`
200
+ moved the audit page's served/canonical URL to `/audit/`, but the
201
+ `X-Robots-Tag: noindex` header rule still targeted the bare `/audit` path.
202
+ Netlify's `for` header-path matching is exact-string outside an explicit
203
+ wildcard (confirmed against Netlify's own docs) — the rule silently
204
+ stopped matching the page it was written for. The in-HTML `<meta
205
+ robots>` tag still fired, so this wasn't a total exposure, but a
206
+ documented defense-in-depth layer quietly broke as an unaddressed side
207
+ effect of an unrelated change. Fixed with two explicit `[[headers]]`
208
+ rules (`/audit` and `/audit/`) rather than a wildcard.
209
+
210
+ ### templates/sveltekit-netlify
211
+
212
+ - `.claude/hooks/og-guard.sh`: the rebuild's exit code was discarded (`||
213
+ true`, inherited verbatim from `templates/eleventy-netlify/`'s own hook,
214
+ which has the same bug) and the hook printed "rebuilt" unconditionally.
215
+ Fixed to print a different message on failure, naming the output files as
216
+ possibly stale rather than asserting they're current — verified by
217
+ actually breaking `brand.ts` and running the hook, not just reading the
218
+ diff.
219
+ - `src/lib/components/Canonical.svelte`: now wraps `page.url.pathname` in
220
+ `withTrailingSlash()` like every other internal href in this pass,
221
+ instead of trusting it already normalized — covers an error/404 render or
222
+ a future route opting out of the layout's `trailingSlash`.
223
+ `withTrailingSlash()`'s parameter type loosened from `ResolvedPathname` to
224
+ plain `string` so it accepts a raw pathname too; its return type stays
225
+ `ResolvedPathname` so the two existing `<a href>` call sites still satisfy
226
+ `eslint-plugin-svelte`'s navigation rule (a `<link rel="canonical">` was
227
+ never a target of that rule regardless).
228
+ - `src/lib/site.test.ts` (new): unit tests for `withTrailingSlash()` — pure,
229
+ trivially testable logic that shipped with none, despite vitest already
230
+ being wired into this template's gate and a direct precedent
231
+ (`content/validate.test.ts`) for testing exactly this kind of logic.
232
+ - `src/lib/staticRoutes.ts` (new, moved out of `sitemap.xml/+server.ts`) and
233
+ `src/lib/tokensCss.ts` (new, moved out of `tokens.css/+server.ts`): both
234
+ were flagged as hand-synced/duplicated logic with no drift check.
235
+ Factoring them out of their `+server.ts` files was **required**, not just
236
+ tidiness — SvelteKit restricts `+server.ts` exports to a fixed list
237
+ (`GET`, `prerender`, etc.) and rejects any other named export at build
238
+ time, caught only by actually running `vite build` after first trying to
239
+ export them inline. `staticRoutes.test.ts` checks `STATIC_ROUTES` against
240
+ the real `+page.svelte` route tree; `tokensCss.test.ts` checks the
241
+ generated CSS's light-mode selector against the *installed*
242
+ `@picocss/pico` package's own CSS, so a future Pico version bump that
243
+ changes that selector fails a test instead of silently losing the cascade
244
+ again (the exact bug the permalink-shape entry above found and fixed by
245
+ hand). `tokens.css/+server.ts`'s dark-mode block, previously written out
246
+ twice (once for `[data-theme="dark"]`, once for `prefers-color-scheme`),
247
+ is now built once and reused by both.
248
+ - `src/lib/brand.ts`: `color.muted` had zero consumers anywhere in this
249
+ template (unlike Eleventy's, where it feeds the OG card's subtitle — this
250
+ template's simpler single-line card has none) and, unlike `logo`, carried
251
+ no comment saying so. Commented as intentionally unused rather than left
252
+ to look like dead code.
253
+
254
+ ### templates/eleventy-netlify/src/tokens.css.njk
255
+
256
+ - Added a comment cross-referencing `templates/sveltekit-netlify/src/lib/
257
+ tokensCss.ts` as the same mechanism for that stack, stating explicitly
258
+ that the two `--pico-*` remap blocks must be kept in sync by hand (not
259
+ factored into a shared module — the two templates don't share a runtime).
260
+ Verified `templates/verify.sh`'s eleventy-netlify branch still passes
261
+ (28 pass · 0 fail · 5 manual · 6 n/a) after this comment-only change.
262
+
263
+ ### stacks/sveltekit-netlify.md: seo-urls, brand-source, audit-page, generated-asset-freshness
264
+
265
+ - Updated to describe the fixes above and their verification.
266
+ `brand-source` also had a stale paragraph from before the og-image entry
267
+ above — still said the OG-card renderer was "not yet built" after it had
268
+ been; corrected in the same pass since it was found while touching this
269
+ section anyway.
270
+
271
+ Last of the four functional gaps closed ahead of the real project adopting
272
+ this stack. `og-image`'s own note already named the front-runner mechanism
273
+ (a prerendered `+server.ts` route, `satori` + `@resvg/resvg-js`) without
274
+ committing to it; this takes it, and the same trick already proven for
275
+ `tokens.css` (item 3) turns out to solve
276
+ `core.md#generated-asset-freshness`'s tier 1 for free — a genuinely better
277
+ answer than Eleventy's `eleventy.after` hook, not merely an adequate
278
+ substitute for it, since there's no persisted file that can go stale between
279
+ builds at all.
280
+
281
+ ### templates/sveltekit-netlify
282
+
283
+ - Added `satori@0.33.4`, `@resvg/resvg-js@2.6.2`, `@fontsource/inter@5.3.0`
284
+ (same versions as `templates/eleventy-netlify/scripts/og.js`) to
285
+ `dependencies`.
286
+ - New `src/routes/og/default.png/+server.ts`: renders the site-wide default
287
+ 1200×630 OG card from `src/lib/brand.ts` (a primary-color bar + the site
288
+ name, Inter regardless of the live site's font — an OG card is a designed
289
+ asset). Deleted `static/og/default.png`, the hand-placed file this route
290
+ now replaces at the same path.
291
+ - **Scope check, not scope creep:** no page emits an `og:image` meta tag
292
+ anywhere in this template yet (that's `seo-meta`'s separately-deferred
293
+ full OG/Twitter table). So this generates the one site-wide default only,
294
+ not per-page cards — building those now would be output nothing
295
+ references. Flagged explicitly in `TEMPLATE.md` and `stacks/
296
+ sveltekit-netlify.md#og-image` so it isn't mistaken for full OG support.
297
+ - Verified beyond a successful build: `build/og/default.png` is a real
298
+ 1200×630 8-bit PNG, ~10 KB (well under the ~1 MB ceiling), and was
299
+ actually viewed (Playwright's Chromium, installed last session, made this
300
+ possible) to confirm the brand color and text render correctly, not just
301
+ that `satori`/`resvg` didn't throw.
302
+ - `core.md#generated-asset-freshness`, all three tiers: (1) the build step —
303
+ free, as above; (2) `.claude/hooks/og-guard.sh` + `.claude/settings.json`,
304
+ the same `PostToolUse` pattern as `templates/eleventy-netlify/`'s
305
+ `og-guard.sh`, watching `brand.ts` and both generator `+server.ts` files,
306
+ rebuilding, and pointing the operator at the two output files directly
307
+ (this stack's audit page has no OG-gallery tab yet) — tested directly with
308
+ matching and non-matching `tool_input.file_path` values, not just read
309
+ over; (3) a new "Generated visual assets" paragraph in `AGENTS.md` (this
310
+ stack's actual instructions file — see below).
311
+ - Full `npm run check` gate passes.
312
+
313
+ ### stacks/sveltekit-netlify.md: og-image, generated-asset-freshness, where-sveltekit-fights-the-grain
314
+
315
+ - `og-image` and `generated-asset-freshness` now describe what's built and
316
+ what verified it, including the explicit per-page-cards non-scope.
317
+ `where-sveltekit-fights-the-grain`#5 retitled from "still deferred" to
318
+ "resolved differently" — the `+server.ts` route isn't a lesser substitute
319
+ for `eleventy.after`, it has no staleness window at all, which Eleventy's
320
+ own mechanism can't claim.
321
+
322
+ ### templates/sveltekit-netlify/TEMPLATE.md
323
+
324
+ - The "No OG-image generation" bullet replaced with what's actually built
325
+ and what's still missing (per-page cards, the meta tag). Noted `AGENTS.md`
326
+ uses this stack's real instructions filename, not `CLAUDE.md` — a
327
+ pre-existing naming difference from `templates/eleventy-netlify/`, not
328
+ something this change introduced or is fixing.
329
+
330
+ ---
331
+
332
+ ## 2026-09-04 — sveltekit-netlify's permalink shape, actually resolved [non-breaking]
333
+
334
+ A real project is about to adopt `templates/sveltekit-netlify/` (closing the
335
+ "deliberately deferred" item in `TODO.md`), which turns the binding's four
336
+ self-documented gaps from theoretical to load-bearing. First of four: the
337
+ permalink shape. `stacks/sveltekit-netlify.md#seo-urls` had already found and
338
+ recorded that the naive fix — `trailingSlash: 'always'` alone — trades one
339
+ gap for a subtler one, since `resolve()` and a hand-built `sitemap.xml` don't
340
+ pick the setting up on their own. This closes it for real, verified against
341
+ an actual build's output files, rendered `href`s, and `sitemap.xml` content,
342
+ not just an exit code.
343
+
344
+ ### templates/sveltekit-netlify
345
+
346
+ - `src/routes/+layout.ts`: added `export const trailingSlash = 'always';`.
347
+ - `src/lib/site.ts`: added `withTrailingSlash()`, typed `(path:
348
+ ResolvedPathname) => ResolvedPathname` rather than `string` — plain
349
+ `string` would make `eslint-plugin-svelte`'s
350
+ `svelte/no-navigation-without-resolve` rule stop recognizing the wrapped
351
+ `resolve()` call as safe, since that rule accepts an expression either by
352
+ syntax (a literal `resolve()` call) or by matching `$app/types`'s
353
+ `ResolvedPathname` type structurally — a generic wrapper satisfies neither
354
+ unless typed this way.
355
+ - `src/routes/guides/+page.svelte` and `guides/[slug]/+page.svelte`: every
356
+ `<a href>` built from `resolve('/guides/[slug]', { slug })` now wraps it in
357
+ `withTrailingSlash(...)`.
358
+ - `src/routes/sitemap.xml/+server.ts`: the static route list and guide-slug
359
+ URLs are plain literals under our control (not `resolve()` output), so
360
+ they're simply written with the trailing slash already baked in, rather
361
+ than routed through the same helper.
362
+ - Verified with a real `npm run build`: `build/about/index.html` (not
363
+ `about.html`); guide-list and guide-detail pages' rendered `href`s end in
364
+ `/`; `sitemap.xml`'s 8 entries (5 static + 3 guides) all end in `/`. Full
365
+ `npm run check` gate passes.
366
+
367
+ ### stacks/sveltekit-netlify.md: seo-urls
368
+
369
+ - The permalink-shape note updated from "a real gap, not yet resolved" to
370
+ "resolved," describing the actual fix and the `ResolvedPathname`-typing
371
+ wrinkle the lint rule forced.
372
+
373
+ ---
374
+
375
+ ## 2026-09-04 — the standalone-page shape divergence, accepted and documented [non-breaking]
376
+
377
+ Triages `feedback/2026-09-03-pages-model-diverges-across-stacks.md`
378
+ (deleted, per this file's own triage convention). The note found that
379
+ "standalone pages are data" is implemented three different ways across the
380
+ templates — `eleventy-netlify`'s `pages.json` (an array, structured `body`
381
+ sections, `{ include }` markers, layout blocks), `sveltekit-netlify`'s (an
382
+ object keyed by page name, a flat `body: string[]`), `phoenix`'s (no
383
+ `pages.json` at all — markdown or a reviewed component) — and asked for a
384
+ decision: converge on one shape, or document the difference. The note's own
385
+ read was right: convergence "wants a second real adopting project on a
386
+ non-eleventy stack to justify the work" (`TODO.md`'s already-recorded
387
+ decision not to invest further in Phoenix/SvelteKit until one exists says
388
+ the same thing), so this takes the documented-divergence option.
389
+
390
+ ### core: content-model
391
+
392
+ - New Spec: the standalone-page entry shape is a per-stack choice, named by
393
+ each stack's own impl doc (and, where one ships, the project's own
394
+ `CONTENT.md` "entry shape" section — the one an editing session actually
395
+ needs, read fresh each session rather than assumed from another project).
396
+
397
+ ### stacks/sveltekit-netlify.md: content-model
398
+
399
+ - A real, previously-undocumented gap fixed while here: this section named
400
+ the files (`{guides,pages}.json` + zod) but never their field shape,
401
+ unlike the Eleventy and Phoenix bindings, which both already document
402
+ theirs. Added, verified against the real `*.schema.ts` files: `guides.json`
403
+ mirrors `eleventy-netlify`'s field set structurally; `pages.json` is
404
+ genuinely simpler (an object keyed by page name, a flat `body: string[]`,
405
+ no markers or blocks) — not just smaller, a different shape, and now
406
+ stated as a deliberate one.
407
+
408
+ ### templates/sveltekit-netlify/CONTENT.md
409
+
410
+ - Gains "The entry shape — `guides.json`" / "`pages.json`" sections mirroring
411
+ `templates/eleventy-netlify/CONTENT.md`'s existing pattern — previously
412
+ named the content set files with no field-level reference at all, so an
413
+ editorial agent had nothing to read for this stack's shape short of the
414
+ binding doc (which the editorial-project instruction doesn't point it at —
415
+ `CONTENT.md` is the one per-session read).
416
+
417
+ ## 2026-09-04 — Q1's SvelteKit criterion stops being circular [non-breaking]
418
+
419
+ Flagged in a review of the stack-selection guidance (`TODO.md`, prompted by
420
+ `PROJECT-OVERVIEW.md`'s planning conversation): Q1 told an adopter to pick
421
+ `sveltekit-netlify` "when a project specifically needs Svelte components"
422
+ without ever saying what would make that true, so it gave no real signal
423
+ against the default. Meanwhile `stacks/sveltekit-netlify.md`'s own
424
+ `where-sveltekit-fights-the-grain` #2 had already found, and buried, the
425
+ actual answer: SvelteKit hydrates every page by default, so picking it ships
426
+ the framework runtime site-wide, not only on the interactive page — a real
427
+ cost `core.md#client-logic`'s pure-function-plus-wiring pattern avoids
428
+ entirely for the common case (one calculator, one widget). Phoenix's
429
+ criterion ("a database or server-side state is in scope") was already crisp
430
+ by comparison — only the SvelteKit clause needed the fix. No project's
431
+ existing stack choice is invalidated — this sharpens guidance for a *future*
432
+ choice, it doesn't reopen a past one.
433
+
434
+ ### core: adopting
435
+
436
+ - Q1's SvelteKit clause restated around the real trigger (several
437
+ interactive pieces genuinely sharing state, not one calculation) plus the
438
+ hydration-cost tradeoff, with a pointer to
439
+ `stacks/sveltekit-netlify.md#where-sveltekit-fights-the-grain` #2. Phoenix's
440
+ and the "other" bucket's wording untouched — re-read, no knock-on issue
441
+ found. All three stacks' own mirrored `adopting` sections re-read too
442
+ (none restate Q1's selection criteria — nothing to change there).
443
+
444
+ ### README.md
445
+
446
+ - Q1's framework-question mirror gets the same tightened SvelteKit clause,
447
+ condensed to match this doc's shorter style.
448
+
449
+ ## 2026-09-04 — the standard-version drift row is severity-tagged [breaking]
450
+
451
+ The other half of the severity-signal work (`TODO.md` #1 depended on this, not
452
+ just the tag itself): the `#compliance` drift row now distinguishes
453
+ `BREAKING` from `MANUAL` per slug, using each changed CHANGELOG entry's own
454
+ tag, so `whs compliance --strict` actually gates on being behind on a
455
+ breaking change rather than treating every changed slug — a typo fix and a
456
+ broken-mechanism correction alike — as the same easy-to-miss `MANUAL` note.
457
+ This is the concrete fix for the gap `TODO.md` #1's evidence section
458
+ documented: `everydaymoneycalc` being 8 entries behind (several of them
459
+ breaking, per the backfill above) produced no signal beyond a note buried in
460
+ a `MANUAL` list.
461
+
462
+ ### core: compliance
463
+
464
+ - `#compliance`'s drift-row Spec: a slug from a `breaking` CHANGELOG entry
465
+ now emits `BREAKING: re-check #<slug>` and folds into the fail count
466
+ (gates `--strict`); a `non-breaking` one still emits the softer
467
+ `MANUAL: re-check #<slug>`. A slug touched by more than one entry since the
468
+ pin takes the worst severity seen.
469
+
470
+ ### stacks/eleventy-netlify.md
471
+
472
+ - `packages/whs-eleventy/lib/compliance.js`'s `versionDrift()` reads each
473
+ entry's `[breaking]`/`[non-breaking]` tag via a new `changelogEntries()`
474
+ (`changelogSlugs()` kept, now a flattened view over it, for backward
475
+ compatibility) and emits the tagged row; `summarize()` folds `BREAKING`
476
+ rows into the fail count, `main()`'s `--strict` message updated to match.
477
+ `src/audit.njk`'s drift list bolds `BREAKING` rows. Verified end to end:
478
+ rolled `templates/eleventy-netlify/`'s pin back to `2026-08-27` and
479
+ confirmed `whs compliance` printed exactly 8 `BREAKING` + 8 `MANUAL` rows
480
+ matching the backfilled tags (`28 pass · 8 fail · 13 manual · 6 n/a` — 0
481
+ real chapter `FAIL`s, all 8 from drift), the built `/audit/` page bolding
482
+ only the `BREAKING` `<li>`s, then restored the pin. `npm run
483
+ check`/`compliance` re-verified clean afterward. 5 new tests added to
484
+ `packages/whs-eleventy/test/compliance.test.js` (54 total, all green).
485
+
486
+ ### stacks/phoenix.md
487
+
488
+ - The still-unshipped `#compliance` design doc's drift-row description
489
+ updated to match — the same `BREAKING`/`MANUAL` split, once built, per
490
+ `versionDrift()`, "the pattern to port."
491
+
492
+ ## 2026-09-04 — a severity signal on CHANGELOG entries [non-breaking]
493
+
494
+ Flagged in a planning conversation (`PROJECT-OVERVIEW.md`): entries were dated
495
+ but not marked as breaking vs. non-breaking, so a project couldn't tell from
496
+ the log alone whether it was safe to ignore an update or urgently behind — and
497
+ without that signal, an eventual upgrade path for already-adopted projects
498
+ (tracked in `TODO.md`) would have nothing to prioritize on. Every `## <date>`
499
+ heading, past and present, now carries `[breaking]` or `[non-breaking]`, per
500
+ the rule stated in this file's own header note. Not a `core.md`/`stacks/*.md`
501
+ content change — process/tooling only, no version bump (same precedent as the
502
+ 2026-08-30 "the standard is a git repo" entry).
503
+
504
+ ### tooling
505
+
506
+ - `bin/check` gains a sixth check: every `## <date>` CHANGELOG heading must
507
+ carry a severity tag. `CLAUDE.md` and `README.md`'s Consistency checks
508
+ section updated to match (six checks; `templates/verify.sh` is now the
509
+ *seventh*, not sixth).
510
+
511
+ ### CHANGELOG.md
512
+
513
+ - All 20 existing entries backfilled with a severity tag, judged against the
514
+ rule stated in the new header note (breaking = a previously-compliant
515
+ project is no longer compliant after the change).
516
+
517
+ ## 2026-09-04 — name the deploy hand-off in the content-ops protocol [breaking]
518
+
519
+ The content-arm interface (2026-09-03, below) defined how an LLM agent (Cowork)
520
+ edits reserved content, but left the last step implicit: the agent commits to
521
+ `main` and cannot deploy, and the only cue to the operator was "if it's
522
+ time-sensitive, say so in the commit message" — passive, no addressee. A real
523
+ adopter workflow (Cowork owning content post-launch) surfaced the gap. The
524
+ deploy boundary is unchanged — a person still runs the press — but the signal is
525
+ now named: a `DEPLOY:` line closing the agent's reply, and `[deploy by <date>]`
526
+ in the `content:` subject when there's a deadline.
527
+
528
+ ### core: content-model
529
+
530
+ - The LLM-agent content-ops protocol gains a fifth element: the agent does not
531
+ deploy — it commits a finished change to the default branch and hands it to
532
+ the human deploy cadence (`#ci-cd`) with a session-closing report, deadline in
533
+ the commit subject. `#adopting`'s "Editorial operation" stable instruction
534
+ reworded to match ("never deploy — handed off, not shipped").
535
+
536
+ ### stacks/eleventy-netlify.md: content-model
537
+
538
+ - New **Deploy hand-off** bullet: the `DEPLOY: awaiting …` / `DEPLOY: by <date>
539
+ …` closing line, the `[deploy by <date>]` commit-subject tag for the
540
+ operator's `git log <last-deploy>..main` batch review, and the two deploy
541
+ doorways (`deploy.yml` Run workflow / `npm run deploy`) — nothing in the
542
+ pre-flight or tooling changes.
543
+ - The representative `#compliance` `content-model` row in the mechanical-checks
544
+ table now spells out the protocol-presence note (`CONTENT.md`,
545
+ `content-check`, `requests/`) the sweep already emits — the row had listed
546
+ only the data + schema + validator half.
547
+ - `stacks/phoenix.md` and `stacks/sveltekit-netlify.md` content-model sections
548
+ and the three template `CONTENT.md`s (+
549
+ `templates/eleventy-netlify/CLAUDE.md`) mirror the hand-off wording.
550
+
551
+ ## 2026-09-04 — the SvelteKit binding renamed to sveltekit-netlify, honestly scoped [non-breaking]
552
+
553
+ Caught in conversation, not tooling: `stacks/sveltekit.md` / `templates/sveltekit/`
554
+ were named as if this were a general-purpose SvelteKit binding, when its actual
555
+ origin and entire scope is narrower — it exists specifically to answer "Svelte,
556
+ static, on Netlify," because plain Svelte has no durable static-site tooling of
557
+ its own. The naming didn't say so, and that's an inconsistency the repo's own
558
+ implicit rule already flags: a binding scoped to exactly one host with no
559
+ alternative documented (`eleventy-netlify` is the model) gets the host in its
560
+ name; a binding documenting multiple real hosts (`phoenix` — Render default,
561
+ Gigalixir/Fly as the graduate path) correctly doesn't. `sveltekit` documented
562
+ only Netlify, so by that same rule it should have carried the host in its name
563
+ too — it didn't, and nothing explained why not.
564
+
565
+ ### stacks/sveltekit-netlify.md (renamed from stacks/sveltekit.md)
566
+
567
+ - Retitled **SvelteKit + Netlify**. The intro now states the binding's real
568
+ origin up front — "why SvelteKit, if the actual need is just Svelte, static,
569
+ on Netlify" — rather than reading as a general SvelteKit offering that
570
+ happens to default to Netlify. A project that needs SvelteKit's actual
571
+ server capabilities (real SSR, live `+server.ts` endpoints, a database) is
572
+ explicitly called out as a different, larger binding this repo does not
573
+ have — not something this doc quietly under-scopes.
574
+ - Section-header labels (`How (SvelteKit)`, `Adopting this standard
575
+ (SvelteKit)`, etc.) deliberately keep the framework-only wording, matching
576
+ `stacks/eleventy-netlify.md`'s own convention of naming the binding
577
+ file/directory with the host but labeling in-body sections by framework
578
+ alone.
579
+
580
+ ### templates/sveltekit-netlify (renamed from templates/sveltekit)
581
+
582
+ - Directory, all internal doc cross-references (`AGENTS.md`, `README.md`,
583
+ `TEMPLATE.md`), and the CI job (`templates-sveltekit-netlify`) renamed to
584
+ match. `AGENTS.md`'s `framework:` data-block value now reads
585
+ `sveltekit-netlify`. Along the way, fixed a real, unrelated inaccuracy found
586
+ in the same file: `TEMPLATE.md` told an adopter to rename `whs_sveltekit` in
587
+ `package.json` — the actual placeholder there is `__PKG_NAME__`
588
+ (`whs_sveltekit` was a leftover copy-paste from the Phoenix template's
589
+ equivalent instruction, which is accurate for Phoenix's real OTP app name
590
+ but was never true here).
591
+
592
+ ### core: adopting
593
+
594
+ - Q1's framework list: `SvelteKit` → `SvelteKit + Netlify`, with the same
595
+ origin-story framing as the doc's own intro — a project with no Svelte
596
+ requirement should still default to Eleventy + Netlify; a project that
597
+ needs SvelteKit's server features has no binding here yet (falls to
598
+ "other").
599
+
600
+ ### tooling
601
+
602
+ - `index.json`'s `sveltekit` key → `sveltekit-netlify` (file, template path,
603
+ and summary all updated to state the narrower scope explicitly).
604
+ `README.md`'s doc-map table, framework-question wording, and
605
+ `templates/verify.sh` description updated to match.
606
+ `templates/concept-brief.md`'s `framework:` comment and the one open
607
+ `feedback/` report referencing the old name updated for accuracy while
608
+ still open.
609
+
9
610
  ---
10
611
 
11
- ## 2026-09-03 — the content-arm interface: a project and its coding arm
612
+ ## 2026-09-03 — a self-audit: gate/gitignore/doc gaps found by external review [non-breaking]
613
+
614
+ A skeptical outside audit of the standard's own repo (docs, templates, CI,
615
+ tooling) turned up a cluster of small-but-real defects — none a contract
616
+ violation, all either a check that couldn't catch its own failure mode, a
617
+ tooling side-effect leaking outside its own project, or a doc claim that had
618
+ drifted from what the repo actually contains. Fixed together as one pass;
619
+ see `git log` for the individual verification each one got (cold installs,
620
+ mutation tests, real builds, fetched external docs).
621
+
622
+ ### core: adopting
623
+
624
+ - **Stale template-count claim.** The stack-selection flow's "Starting
625
+ point" paragraph said "Only `eleventy-netlify` has a template today; the
626
+ others follow the checklist" — both `templates/phoenix/` and
627
+ `templates/sveltekit/` have existed (with real, passing gates) since
628
+ 2026-09-02. Corrected to point at `index.json`'s `active`/`draft` status
629
+ instead of a hardcoded, now-wrong count. Re-read each `stacks/*.md`
630
+ mirrored `adopting` section — all three already stated their own template's
631
+ real status correctly; only this file's flow-level prose had drifted.
632
+
633
+ ### bin/check
634
+
635
+ - **Check #5 (version strings agree) couldn't detect a missing pin** — it
636
+ compared only the *distinct values* found, so a file missing its
637
+ `standard-version:`/`**Version:**` line entirely just vanished from the
638
+ comparison instead of failing it (confirmed: deleting
639
+ `templates/sveltekit/AGENTS.md`'s pin left the check green). Now checks
640
+ pin *presence* per expected file first, and names the file if one is
641
+ missing.
642
+
643
+ ### templates/phoenix
644
+
645
+ - **The `git_hooks` monorepo guard was incomplete.** `config/dev.exs`
646
+ guarded the `hooks:` config behind `File.dir?(".git")` so the template
647
+ wouldn't install hooks into the standard's own repo when compiled in
648
+ place — but `git_hooks`'s `auto_install` (default `true`) runs before that
649
+ config is ever consulted, and unconditionally writes a `git_hooks.db`
650
+ marker into whatever `.git/hooks` it finds via `git rev-parse
651
+ --show-toplevel`. Confirmed landing in the standard's own `.git/hooks/`
652
+ a second time, guard already in place. Fixed with an explicit
653
+ `auto_install: false` in the guard's `else` branch — verified with a full
654
+ `rm -rf _build deps && mix deps.get && mix compile`, the exact sequence
655
+ that reproduced the bug, no longer touching the main checkout.
656
+
657
+ ### repo-hygiene (root .gitignore)
658
+
659
+ - Added `erl_crash.dump` — the root `.gitignore` covered only Node/JS build
660
+ output; a BEAM crash dump landing at the repo root (any `mix` command run
661
+ from there, not inside `templates/phoenix/`) was untracked and therefore
662
+ invisible to `bin/check`'s `git grep`-based sterility scan despite
663
+ containing a personal filesystem path. `templates/phoenix/.gitignore`
664
+ already covered its own case; this was the repo-root gap.
665
+
666
+ ### stacks/sveltekit.md, templates/*/netlify.toml
667
+
668
+ - Corrected the stated root cause of `#noindex` layer 2 not applying: the
669
+ doc said per-context header scoping "needs th[e] build pipeline to run"
670
+ (implying it would work if Netlify's build system did run) — Netlify's
671
+ docs are explicit that `[[headers]]` is never context-aware on *any*
672
+ deploy path; the pre-built-artifact deploy model is a second, independent
673
+ reason the documented workaround doesn't apply here, not the cause of the
674
+ first problem. `stacks/eleventy-netlify.md` and both templates'
675
+ `netlify.toml` already stated this correctly; only this file's prose had
676
+ drifted. Also dropped an unverifiable direct quotation of Netlify's docs
677
+ (substance was correct, exact wording wasn't confirmed) from this file and
678
+ `templates/eleventy-netlify/netlify.toml`.
679
+ - Corrected two "previously-undocumented" claims (the `vite.config.ts`
680
+ config-consolidation and `resolve()` from `$app/paths`) — both are real,
681
+ and both are documented on svelte.dev; what was actually true is that this
682
+ binding hadn't previously accounted for them.
683
+ - Documented a real, previously-unstated gap: the template's flat
684
+ `about.html`-shaped build output doesn't meet `core.md#adopting`'s
685
+ directory-style-permalink item. Tried the one-line fix
686
+ (`trailingSlash = 'always'`) and verified in a real build that it's not
687
+ sufficient alone — `resolve()` from `$app/paths` doesn't append the
688
+ trailing slash to match, nor does the hand-built sitemap route list, so it
689
+ trades one gap for a file-layout/link/sitemap mismatch. Left unfixed,
690
+ documented precisely instead.
691
+
692
+ ### stacks/phoenix.md
693
+
694
+ - Tightened Render free-Postgres wording: "deleted 30 days after creation"
695
+ → "expires 30 days after creation" (inaccessible immediately, permanently
696
+ deleted after a further 14-day grace period) — matches Render's own docs.
697
+
698
+ ### .github/workflows/standard.yml
699
+
700
+ - Added a comment at the `templates` job's matrix declaration explaining
701
+ that the `[eleventy-netlify]` list is manually curated (not discovered
702
+ from `templates/*/` the way `templates/verify.sh` is) and when a new
703
+ template should be added to it — the file's top-of-file comment already
704
+ explained this design, but not at the point someone would actually edit.
705
+
706
+ ### templates/new-project.sh
707
+
708
+ - The no-`bin/init`-yet branch's own comment said "fail loudly" but the
709
+ script exited 0 regardless — nothing after the `echo` produced a non-zero
710
+ status. Added the missing `exit 1`; verified against `sveltekit` (no
711
+ `bin/init` yet) going from exit 0 to exit 1, `eleventy-netlify`'s
712
+ (`bin/init`-having) path unaffected.
713
+
714
+ ### packages/whs-eleventy, templates/eleventy-netlify
715
+
716
+ - Bumped `eslint`/`@eslint/js` off the EOL `9.39.5` pin (`npm warn
717
+ deprecated eslint@9.39.5: This version is no longer supported`) to
718
+ `10.9.1`/`10.0.1` in both — `templates/sveltekit/` was already on the
719
+ current major. Verified with a cold `rm -rf node_modules package-lock.json
720
+ && npm install` plus the full gate for each: `whs-eleventy`'s `npm run
721
+ check` (49 tests), `eleventy-netlify`'s `npm run check` (lint → validate →
722
+ test → build → links → a11y), both green, no deprecation warning on
723
+ install.
724
+
725
+ ## 2026-09-03 — the content-arm interface: a project and its coding arm [breaking]
12
726
 
13
727
  A project built to the house style is often only part of a larger operation —
14
728
  an editor-in-chief (a Claude Project, ideation + editorial context) that hands
@@ -133,7 +847,7 @@ for the common small-site case); a **`schedule` trigger** on the deploy workflow
133
847
  sveltekit flat `string[]`, phoenix none) — converge or document the split, at
134
848
  triage.
135
849
 
136
- ## 2026-09-02 — a starter template for the SvelteKit binding, moved out of parked
850
+ ## 2026-09-02 — a starter template for the SvelteKit binding, moved out of parked [non-breaking]
137
851
 
138
852
  `stacks/sveltekit.md` was a parked stub — every mirrored section's "How
139
853
  (SvelteKit)" was `TODO`, not offered by the `#adopting` flow. A real need
@@ -255,7 +969,7 @@ and real `vite build` output, not written from the binding's prose alone.
255
969
 
256
970
  ---
257
971
 
258
- ## 2026-09-02 — Render as the Phoenix binding's default free-launch host
972
+ ## 2026-09-02 — Render as the Phoenix binding's default free-launch host [non-breaking]
259
973
 
260
974
  Netlify can't run this stack at all — LiveView needs a persistent BEAM node
261
975
  (long-lived WebSocket connections, a warm Postgres pool), not a static
@@ -330,7 +1044,7 @@ isn't trustworthy here.
330
1044
 
331
1045
  ---
332
1046
 
333
- ## 2026-09-02 — a starter template for the Phoenix binding, and hardening the doc it's built from
1047
+ ## 2026-09-02 — a starter template for the Phoenix binding, and hardening the doc it's built from [breaking]
334
1048
 
335
1049
  Built `templates/phoenix/` the same way `templates/eleventy-netlify/` was
336
1050
  proven out: generic placeholder content, independent of any real business,
@@ -465,7 +1179,7 @@ fabricated or imprecise APIs the same way the earlier `Plug.CSP` finding did.
465
1179
 
466
1180
  ---
467
1181
 
468
- ## 2026-09-02 — closing the gap between the compliance grid and the Contracts it checks
1182
+ ## 2026-09-02 — closing the gap between the compliance grid and the Contracts it checks [non-breaking]
469
1183
 
470
1184
  A pass through every chapter's actual enforcement, prompted by an outside
471
1185
  evaluation that traced specific checks to specific failure modes they
@@ -624,7 +1338,7 @@ consistency defects the same pass turned up.
624
1338
 
625
1339
  ---
626
1340
 
627
- ## 2026-09-01 — the shared tooling package (#compliance mechanism, eleventy)
1341
+ ## 2026-09-01 — the shared tooling package (#compliance mechanism, eleventy) [non-breaking]
628
1342
 
629
1343
  ### stacks/eleventy-netlify.md
630
1344
 
@@ -662,7 +1376,7 @@ consistency defects the same pass turned up.
662
1376
 
663
1377
  ---
664
1378
 
665
- ## 2026-08-31 — the Cowork ↔ Claude Code loop: concept brief + content ops (#content-model)
1379
+ ## 2026-08-31 — the Cowork ↔ Claude Code loop: concept brief + content ops (#content-model) [breaking]
666
1380
 
667
1381
  ### core: adopting
668
1382
 
@@ -741,7 +1455,7 @@ consistency defects the same pass turned up.
741
1455
 
742
1456
  ---
743
1457
 
744
- ## 2026-08-30 — a copy-and-modify template for Eleventy + Netlify (#adopting)
1458
+ ## 2026-08-30 — a copy-and-modify template for Eleventy + Netlify (#adopting) [non-breaking]
745
1459
 
746
1460
  ### templates/eleventy-netlify (new)
747
1461
 
@@ -838,7 +1552,7 @@ consistency defects the same pass turned up.
838
1552
 
839
1553
  ---
840
1554
 
841
- ## 2026-08-30 — the standard is a git repo
1555
+ ## 2026-08-30 — the standard is a git repo [non-breaking]
842
1556
 
843
1557
  ### repo
844
1558
 
@@ -854,7 +1568,7 @@ consistency defects the same pass turned up.
854
1568
 
855
1569
  ---
856
1570
 
857
- ## 2026-08-29 — AI & agent capabilities, opt-in (#llm-integration, #agent-artifacts, #usage-metering)
1571
+ ## 2026-08-29 — AI & agent capabilities, opt-in (#llm-integration, #agent-artifacts, #usage-metering) [non-breaking]
858
1572
 
859
1573
  ### core: llm-integration · agent-artifacts · usage-metering (new chapters)
860
1574
 
@@ -935,7 +1649,7 @@ consistency defects the same pass turned up.
935
1649
 
936
1650
  ---
937
1651
 
938
- ## 2026-08-28 — codebase-vs-standard conformance (#compliance)
1652
+ ## 2026-08-28 — codebase-vs-standard conformance (#compliance) [non-breaking]
939
1653
 
940
1654
  ### core: compliance (new chapter)
941
1655
 
@@ -992,7 +1706,7 @@ consistency defects the same pass turned up.
992
1706
 
993
1707
  ---
994
1708
 
995
- ## 2026-08-28 — the `CLAUDE.md` House-style directive
1709
+ ## 2026-08-28 — the `CLAUDE.md` House-style directive [breaking]
996
1710
 
997
1711
  ### core: adopting
998
1712
 
@@ -1015,7 +1729,7 @@ consistency defects the same pass turned up.
1015
1729
 
1016
1730
  ---
1017
1731
 
1018
- ## 2026-08-28 — display advertising (AdSense)
1732
+ ## 2026-08-28 — display advertising (AdSense) [non-breaking]
1019
1733
 
1020
1734
  ### core: ads (new chapter)
1021
1735
 
@@ -1066,7 +1780,7 @@ consistency defects the same pass turned up.
1066
1780
 
1067
1781
  ---
1068
1782
 
1069
- ## 2026-08-28 — hardening from the first full Eleventy migration
1783
+ ## 2026-08-28 — hardening from the first full Eleventy migration [breaking]
1070
1784
 
1071
1785
  Fixes and gaps surfaced running the first full Eleventy migration end to end.
1072
1786
 
@@ -1117,7 +1831,7 @@ Fixes and gaps surfaced running the first full Eleventy migration end to end.
1117
1831
 
1118
1832
  ---
1119
1833
 
1120
- ## 2026-08-28 — solo is the default working mode
1834
+ ## 2026-08-28 — solo is the default working mode [non-breaking]
1121
1835
 
1122
1836
  ### core: ci-cd
1123
1837
 
@@ -1150,7 +1864,7 @@ Fixes and gaps surfaced running the first full Eleventy migration end to end.
1150
1864
  across all three; migration `[one PR:]` markers → `[together:]`; the
1151
1865
  "busy PR flow" build-minutes framing dropped.
1152
1866
 
1153
- ## 2026-08-27 — netlify deploy `--no-build`
1867
+ ## 2026-08-27 — netlify deploy `--no-build` [breaking]
1154
1868
 
1155
1869
  ### stacks/eleventy-netlify.md
1156
1870
 
@@ -1168,7 +1882,7 @@ Fixes and gaps surfaced running the first full Eleventy migration end to end.
1168
1882
 
1169
1883
  - `ci-cd` hint — same `--no-build` fix in the `adapter-static` + Netlify note.
1170
1884
 
1171
- ## 2026-08-27 — initial split
1885
+ ## 2026-08-27 — initial split [non-breaking]
1172
1886
 
1173
1887
  The single 1518-line "Eleventy + Netlify static-site house style" document was
1174
1888
  split into a stack-agnostic CORE contract plus per-stack implementation
package/standard/core.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Web project house style — CORE (stack-agnostic)
2
2
 
3
- **Version:** 2026-09-03 · **Status:** active
3
+ **Version:** 2026-09-10 · **Status:** active
4
4
 
5
5
  This is the stack-agnostic contract every web project follows, regardless of
6
6
  framework, host, or CSS system. It says **what** must be true, with concrete
7
7
  specs. It never says **how** — that lives in one implementation doc per stack
8
- (`stacks/eleventy-netlify.md`, `stacks/phoenix.md`, `stacks/sveltekit.md`).
8
+ (`stacks/eleventy-netlify.md`, `stacks/phoenix.md`, `stacks/sveltekit-netlify.md`).
9
9
 
10
10
  **How to use this:** at project start, run the stack-selection flow
11
11
  (`#adopting`). Then load **this file plus the one `stacks/*.md` file** matching
@@ -114,25 +114,43 @@ a build-time schema gate hard-fails the build on invalid content.
114
114
  the same data files is acceptable — a separate database for site content is
115
115
  not.
116
116
  - **Ownership.** By default the party that builds the site may also edit its
117
- content. A project that runs a **separate content operation** — a non-developer,
118
- or an LLM agent editing the data files directly — **reserves** that content:
119
- one file at the repo root names the reserving party and the exact set it owns,
120
- and is authoritative for every party. The builder treats the reserved set as
121
- read-only and routes any change it needs there through the project's escalation
122
- channel, never a direct edit. The reserved set is enumerated at whatever
123
- granularity the project needs — all content, or named collections/paths.
117
+ content — recorded as `content-ops: in-repo` in `CLAUDE.md` (`#adopting`),
118
+ and no reservation file ships. A project that runs a **separate content
119
+ operation** — a non-developer, or an LLM agent editing the data files
120
+ directly — **reserves** that content: `content-ops: reserved` in `CLAUDE.md`,
121
+ and one file at the repo root (`CONTENT.md`) names the reserving party and
122
+ the exact set it owns, and is authoritative for every party. The builder
123
+ treats the reserved set as read-only and routes any change it needs there
124
+ through the project's escalation channel, never a direct edit. The reserved
125
+ set is enumerated at whatever granularity the project needs — all content,
126
+ or named collections/paths.
124
127
  - An **LLM agent** editing reserved content follows a documented per-project
125
128
  content-ops protocol: it changes only the reserved set; a pre-commit content
126
129
  check (the content-relevant slice of the gate) **errors** on any staged change
127
130
  outside that set; a `content:` commit convention; and an escalation channel for
128
- anything structural, so a blocked request is recorded, not lost. See your
129
- stack's impl doc.
131
+ anything structural, so a blocked request is recorded, not lost. The agent does
132
+ not deploy: it commits a finished change to the default branch and hands it to
133
+ the human deploy cadence (`#ci-cd`) — a session-closing report that names what
134
+ landed and whether it is time-sensitive, with any deadline also carried in the
135
+ commit subject. See your stack's impl doc.
130
136
  - **Drafts** are a data flag. A draft renders in local and preview builds
131
137
  (reviewable) but is excluded from the production build and the sitemap.
132
138
  Publishing is removing the flag.
133
139
  - The project records its target publishing rate for new content pages in
134
140
  `CLAUDE.md`. Bulk-publishing many pages at once depresses indexing; pace
135
141
  deliberately.
142
+ - **The standalone-page entry shape is a per-stack choice, made explicitly.**
143
+ This chapter requires only that page content *is* data (above) — the
144
+ concrete fields, whether entries are an array or a simpler keyed object,
145
+ and how a mechanism-bound section is marked, is named by each stack's own
146
+ impl doc, and may legitimately differ between stacks. This is a documented
147
+ choice, not an inconsistency: forcing one shape onto every stack would cost
148
+ more (a rewrite of a working, unproven template) than it buys (one less
149
+ thing for a cross-stack content agent to learn) while only one stack has a
150
+ real adopting project. Where a project ships a `CONTENT.md` for agent
151
+ editing (above), that file's own "entry shape" section is the one an
152
+ editing session actually needs — read it fresh each session, don't assume
153
+ another project's stack taught you this one's shape.
136
154
 
137
155
  **Rationale.** Content that lives in templates can't be validated, can't be
138
156
  edited safely by non-developers, and drifts from its own stated facts (a
@@ -898,10 +916,17 @@ and it is **not** part of the gate.
898
916
  page's Compliance panel (`#audit-page`); and the **deploy pre-flight**
899
917
  (`#ci-cd`) **warns** on any chapter that went `PASS → FAIL` since the last
900
918
  deploy (a real regression of something previously compliant).
901
- - **Standard-version drift is a row.** The command compares the project's pinned
902
- `standard-version:` (`#adopting`) to the standard's own version header; if the
903
- pin is behind, each `CHANGELOG` slug changed since becomes a
904
- `MANUAL: re-check #<slug>` line.
919
+ - **Standard-version drift is a row, severity-tagged.** The command compares
920
+ the project's pinned `standard-version:` (`#adopting`) to the standard's own
921
+ version header; if the pin is behind, each `CHANGELOG` slug changed since
922
+ becomes a re-check line, carrying the severity of the entry it came from
923
+ (`CHANGELOG.md`'s own `[breaking]` / `[non-breaking]` tag on the `## <date>`
924
+ heading — see its header note for the rule): `BREAKING: re-check #<slug>`
925
+ folds into the fail count and gates `--strict`; `MANUAL: re-check #<slug>`
926
+ stays a judgment call, same as any other `MANUAL` chapter result. A slug
927
+ touched by more than one entry since the pin takes the worst (a single
928
+ breaking touch marks it `BREAKING`, even if a later entry on the same slug
929
+ was non-breaking).
905
930
  - The mechanical tier **reuses existing machinery** — the repo-config
906
931
  introspection already in the audit doctor and the built-output scan already in
907
932
  the link/reference check — not a parallel implementation.
@@ -1297,17 +1322,25 @@ records the result. "The gate passing" is the definition of compliant.
1297
1322
  **Specs.**
1298
1323
 
1299
1324
  **Stack-selection flow** — run at project start (or when onboarding an existing
1300
- project), before non-trivial work. Ask five questions with defaults:
1325
+ project), before non-trivial work. Ask six questions with defaults:
1301
1326
 
1302
1327
  1. **Framework?** Eleventy + Netlify *(default)* · Elixir/Phoenix *(when a
1303
1328
  database or server-side state is in scope — `#beyond-this-standard`)* ·
1304
- SvelteKit *(only when a project specifically needs it — Svelte components
1305
- without SvelteKit has no durable static-site tooling, so SvelteKit +
1306
- `adapter-static`, used minimally as a build tool with no server, is the
1307
- fallback even for a "just static Svelte" need; a project with no Svelte
1308
- requirement should still default to Eleventy + Netlify)* · other *(→ no
1309
- binding exists; use this file as principles and do what is idiomatic for
1310
- the technology)*.
1329
+ SvelteKit + Netlify *(only when the client-side need is genuinely
1330
+ component-shaped — several interactive pieces sharing state, not a single
1331
+ calculation `#client-logic`'s pure-function-plus-wiring pattern already
1332
+ covers; SvelteKit hydrates every page by default, so picking it means the
1333
+ framework runtime ships site-wide, not just where it's interactive — a
1334
+ real cost, not a free upgrade
1335
+ (`stacks/sveltekit-netlify.md#where-sveltekit-fights-the-grain` #2). Plain
1336
+ Svelte has no durable static-site tooling of its own, so this binding is
1337
+ SvelteKit + `adapter-static`, used minimally as a build tool with no
1338
+ server — a "static Svelte, on Netlify" need specifically, not a
1339
+ general-purpose SvelteKit offering; a project whose interactivity is one
1340
+ widget should still default to Eleventy + Netlify)* · other *(→ no binding
1341
+ exists; use this file as principles and do what is idiomatic for the
1342
+ technology — this covers SvelteKit with real server features too, which
1343
+ this standard does not have a binding for yet)*.
1311
1344
  2. **CSS system?** Pico classless *(default)* · Pico class-based · Tailwind ·
1312
1345
  Tailwind + component library · vanilla tokens. (Options are bound per stack
1313
1346
  in the impl doc's `styling` section.)
@@ -1323,6 +1356,13 @@ project), before non-trivial work. Ask five questions with defaults:
1323
1356
  `#agent-artifacts`); is pricing usage-based rather than flat? (→ add
1324
1357
  `+metering`, `#usage-metering`). This gate is by real need, not by framework —
1325
1358
  a plain CRUD app that calls no model is unaffected.
1359
+ 6. **Who edits content after launch?** *Default: the builder* — record
1360
+ `content-ops: in-repo`; no reservation file ships, no protocol to follow.
1361
+ A **separate content operation** (a Claude Project / Cowork, a
1362
+ non-developer editing the data files directly) → record
1363
+ `content-ops: reserved`, `#content-model` Ownership — ships `CONTENT.md`
1364
+ (the reserved set + escalation protocol) at the project root. The voice
1365
+ guide ships either way, read by whichever party writes copy.
1326
1366
 
1327
1367
  Follow-ups when relevant: production domain; content type (`tool` vs `article` —
1328
1368
  `article` implies feeds); publishing rate for content sites.
@@ -1333,8 +1373,8 @@ the new-project path: the template is already compliant, so a new project begins
1333
1373
  at "fill in brand + content" instead of "assemble the machinery". The impl doc's
1334
1374
  numbered new-project checklist is then two things: the by-hand equivalent for a
1335
1375
  stack with no template, and the annotated inventory of what the template
1336
- contains. Only `eleventy-netlify` has a template today; the others follow the
1337
- checklist. The recommended input to the flow is a filled
1376
+ contains. A template exists for every stack today (`templates/<stack>/`); `index.json`
1377
+ records which are `active` vs `draft`. The recommended input to the flow is a filled
1338
1378
  `templates/concept-brief.md` (the idea, the answers above, a concept-level
1339
1379
  brand, the content model, layout notes) — `README.md` has the hand-off. If the
1340
1380
  brief cannot honestly fit an offered stack, that is a `feedback/` note (below)
@@ -1378,10 +1418,11 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
1378
1418
  - scope: static+forms # or: +db, +auth, +ssr — see core.md#beyond-this-standard
1379
1419
  - ads: none # or: adsense — see core.md#ads
1380
1420
  - ai: none # or: llm-api (+artifacts if model-driven file downloads, +metering if usage-priced) — see core.md#llm-integration
1421
+ - content-ops: in-repo # or: reserved — a separate content operation owns content, see core.md#content-model
1381
1422
  - production-url: https://example.com
1382
1423
  - content-type: tool # tool | article (article => feeds)
1383
1424
  - publishing-rate: ~5 pages/week
1384
- - standard-version: 2026-09-03 # recommended — enables standard-version drift tracking (#compliance)
1425
+ - standard-version: 2026-09-10 # recommended — enables standard-version drift tracking (#compliance)
1385
1426
  ```
1386
1427
 
1387
1428
  If the section is absent, the assistant's first action is to run the flow above
@@ -1389,14 +1430,17 @@ and propose it. Each impl doc's `adopting` section fills in the gate command and
1389
1430
  adds any stack-specific `CLAUDE.md` prose (e.g. the generated-assets note).
1390
1431
 
1391
1432
  **Editorial operation.** Where content is maintained after launch by an LLM
1392
- agent rather than by in-repo development (`#content-model` — a *reserved*
1393
- content set), two files ship at the project root: `CONTENT.md` (the reserved set
1394
- plus the per-project content-ops protocol) and a voice guide (see the impl
1395
- doc). The operation's editor-in-chief — the human's standing ideation/editorial
1433
+ agent rather than by in-repo development (`content-ops: reserved`,
1434
+ `#content-model` — a *reserved* content set), `CONTENT.md` ships at the
1435
+ project root — the reserved set plus the per-project content-ops protocol.
1436
+ The voice guide ships regardless of `content-ops` (see the impl doc) — it's
1437
+ read by whichever party writes copy, builder or editorial operation alike.
1438
+ The operation's editor-in-chief — the human's standing ideation/editorial
1396
1439
  context, typically a Claude Project — connects to the repo through a **thin,
1397
1440
  stable instruction**: check out the repo; read `CONTENT.md` and the voice guide
1398
1441
  at the start of each session; follow `CONTENT.md`; escalate through its channel;
1399
- never deploy. The detail stays in the repo, re-read each session — not in the
1442
+ never deploy — a finished change is handed off for the human deploy cadence, not
1443
+ shipped. The detail stays in the repo, re-read each session — not in the
1400
1444
  instruction — and `CONTENT.md` carries a paste-once "setting up" section for
1401
1445
  exactly this. The build arm treats the reserved set as read-only and escalates
1402
1446
  into the same channel.