@piercebarney/whs-eleventy 2026.9.3 → 2026.9.8

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/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,4 @@ 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 = { runCompliance, summarize, changelogSlugs, changelogEntries, CHECKS };
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.8",
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,679 @@ 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-08 — content-ops: reservation becomes a real opt-in, not a hardcoded default [non-breaking]
22
+
23
+ Found while building `/whs:build`'s Content stage agent, which needs to know
24
+ whether a project's content is reserved before touching anything.
25
+ `core.md#content-model`'s Ownership paragraph already documented the default
26
+ correctly ("by default the party that builds the site may also edit its
27
+ content") — but `templates/eleventy-netlify/` shipped `CONTENT.md` (declaring
28
+ "reserved for: Cowork") unconditionally, with no way to opt out. Every
29
+ project scaffolded from it started reserved regardless of whether that was
30
+ true, contradicting the standard's own stated default.
31
+
32
+ ### core: content-model
33
+
34
+ - New House-style field: `content-ops` (`in-repo` default | `reserved`).
35
+ Recorded as the sixth stack-selection question (`#adopting`). `CONTENT.md`
36
+ ships only when `reserved` is chosen; the voice guide ships either way —
37
+ clarified as not reservation-conditional (it was ambiguously grouped with
38
+ `CONTENT.md` before).
39
+
40
+ ### templates/eleventy-netlify
41
+
42
+ - `bin/init` reads `content-ops` from the brief (default `in-repo`): deletes
43
+ `CONTENT.md` and `requests/` when not reserved, keeps them when it is.
44
+ - `CLAUDE.md`'s Content section carries both branches of the reservation
45
+ paragraph behind a `<!-- content-ops:reserved -->` /
46
+ `<!-- content-ops:in-repo -->` marker pair — the same mechanism-marker
47
+ idiom `{ include: "…" }` already uses elsewhere — and `bin/init` resolves
48
+ to whichever branch applies, dropping the other.
49
+ - Verified both branches end-to-end in a scratch copy: `content-ops: reserved`
50
+ keeps `CONTENT.md` + `requests/` and the reserved paragraph; the default
51
+ keeps neither and shows the in-repo paragraph instead.
52
+
53
+ ## 2026-09-04 — sveltekit-netlify's brand token layer, and a Pico gap that predates it [non-breaking]
54
+
55
+ Third of the four functional gaps being closed ahead of the real project
56
+ adopting this stack. Scoping this one surfaced a bigger, previously
57
+ unnoticed gap: `AGENTS.md` and `stacks/sveltekit-netlify.md#styling` both
58
+ already asserted "Pico classless" as this template's CSS default, but Pico
59
+ was never actually installed, imported, or linked anywhere in the shipped
60
+ code — the "unstyled" state `TEMPLATE.md` flagged wasn't Pico-with-no-brand-
61
+ colors, it was no CSS framework at all. Fixing that was a real, if small,
62
+ scope expansion beyond "just add brand.ts" — confirmed with the user before
63
+ proceeding, on the grounds that the template's job is to satisfy
64
+ `core.md#styling` on day one the same as every other chapter, and the fix
65
+ doesn't foreclose Tailwind for the real project (still the documented
66
+ per-project alternative at adoption time, unaffected by this).
67
+
68
+ ### templates/sveltekit-netlify
69
+
70
+ - Added `@picocss/pico@2.1.1` (matching `templates/eleventy-netlify/`'s
71
+ pin) to `dependencies`.
72
+ - New `src/lib/brand.ts`: the token source (colors, fonts, logo), the exact
73
+ field shape of `templates/eleventy-netlify/src/_data/brand.js`.
74
+ - New `src/routes/pico.css/+server.ts`: serves Pico's classless jade build
75
+ self-hosted at `/pico.css`, re-exporting the npm package's CSS via Vite's
76
+ `?raw` import — no vendor-file copy step needed the way Eleventy's
77
+ passthrough-copy config requires.
78
+ - New `src/routes/tokens.css/+server.ts`: generates the `--pico-*` remap
79
+ from `brand.ts`, mirroring `tokens.css.njk`'s logic. `app.html` links
80
+ `/pico.css` then `/tokens.css`.
81
+ - **A real gotcha, found only by actually rendering a page:** load order
82
+ alone doesn't decide the cascade here. Pico 2.1.1's own light-mode block
83
+ is `:root:not([data-theme="dark"])`, which out-specifies a plain
84
+ `:root{}` override regardless of which stylesheet loads second — the
85
+ brand remap silently lost to Pico's stock teal-green palette until the
86
+ override's selector was changed to match Pico's specificity. Caught with
87
+ a real Playwright screenshot (installed for this session — the built
88
+ site had never been visually rendered before), not by reading the CSS.
89
+ `[data-theme="dark"]` and the `prefers-color-scheme: dark` media block
90
+ didn't have this problem — Pico's own selectors there are already the
91
+ same specificity as the override, so source order alone decides,
92
+ correctly. Verified both branches with real screenshots (light and
93
+ forced-dark) showing the brand magenta, not Pico's defaults.
94
+ - Full `npm run check` gate passes.
95
+
96
+ ### stacks/sveltekit-netlify.md: styling, brand-source
97
+
98
+ - `styling` now documents the shipped mechanism, including the specificity
99
+ gotcha above, so the next person touching this doesn't rediscover it the
100
+ hard way. `brand-source` now distinguishes the CSS consumer (built) from
101
+ the OG-card renderer and icon generator (still not built — `logo` sits in
102
+ `brand.ts` unused, waiting for either).
103
+
104
+ ### templates/sveltekit-netlify/TEMPLATE.md
105
+
106
+ - The brand-layer "still owns" bullet replaced with a "replace the
107
+ placeholder palette" note, since the layer itself now exists.
108
+
109
+ ---
110
+
111
+ ## 2026-09-04 — sveltekit-netlify's canonical tag, wired [non-breaking]
112
+
113
+ Second of the four functional gaps being closed ahead of the real project
114
+ adopting this stack (see the permalink-shape entry, below, for the full
115
+ context). `stacks/sveltekit-netlify.md#seo-urls` already named the exact
116
+ mechanism — `SITE_URL` from `src/lib/site.ts`, never `$page.url.origin` — but
117
+ no canonical tag actually shipped yet. This wires it.
118
+
119
+ ### templates/sveltekit-netlify
120
+
121
+ - New `src/lib/components/Canonical.svelte`: reads `page.url.pathname` from
122
+ `$app/state` (the path only — never `.url.origin`, which is the
123
+ request/preview host, not necessarily production) and `SITE_URL` from
124
+ `src/lib/site.ts`, emitting `<link rel="canonical" href={SITE_URL +
125
+ pathname}>` from its own `<svelte:head>`.
126
+ - Mounted once in the root `+layout.svelte` — Svelte merges `<svelte:head>`
127
+ blocks from anywhere in the component tree, so every route gets a
128
+ canonical tag without a per-page component instance.
129
+ - Verified with a real build: every page's canonical trailing slash matches
130
+ its own permalink exactly — home (`https://example.com/`), a static route
131
+ (`.../about/`), the guides index, a guide detail page, and the audit page
132
+ all checked directly in the built HTML. Full `npm run check` gate passes.
133
+
134
+ ### stacks/sveltekit-netlify.md: seo-urls, seo-meta, noindex
135
+
136
+ - `seo-urls`'s "not yet wired" note replaced with the actual mechanism and
137
+ verification. `seo-meta` now distinguishes the canonical tag (shared
138
+ component, shipped) from the still-deferred full OG/Twitter `<Seo>`
139
+ component (needs per-page `load` data, a bigger job — unchanged scope).
140
+ `noindex`'s Layer 3 note updated from "not yet wired" to a pointer at
141
+ `seo-urls`.
142
+
143
+ ---
144
+
145
+ ## 2026-09-04 — sveltekit-netlify: fixes from an independent code review [non-breaking]
146
+
147
+ The four functional-gap fixes above were self-verified (real builds, real
148
+ screenshots) but not independently reviewed before landing. An `/code-review
149
+ high` pass across the full diff, run afterward, surfaced one real regression
150
+ and several smaller gaps the self-verification didn't check for. All fixed
151
+ and re-verified against real builds/tests, not just read over.
152
+
153
+ ### netlify.toml
154
+
155
+ - **The regression.** The permalink-shape fix's `trailingSlash: 'always'`
156
+ moved the audit page's served/canonical URL to `/audit/`, but the
157
+ `X-Robots-Tag: noindex` header rule still targeted the bare `/audit` path.
158
+ Netlify's `for` header-path matching is exact-string outside an explicit
159
+ wildcard (confirmed against Netlify's own docs) — the rule silently
160
+ stopped matching the page it was written for. The in-HTML `<meta
161
+ robots>` tag still fired, so this wasn't a total exposure, but a
162
+ documented defense-in-depth layer quietly broke as an unaddressed side
163
+ effect of an unrelated change. Fixed with two explicit `[[headers]]`
164
+ rules (`/audit` and `/audit/`) rather than a wildcard.
165
+
166
+ ### templates/sveltekit-netlify
167
+
168
+ - `.claude/hooks/og-guard.sh`: the rebuild's exit code was discarded (`||
169
+ true`, inherited verbatim from `templates/eleventy-netlify/`'s own hook,
170
+ which has the same bug) and the hook printed "rebuilt" unconditionally.
171
+ Fixed to print a different message on failure, naming the output files as
172
+ possibly stale rather than asserting they're current — verified by
173
+ actually breaking `brand.ts` and running the hook, not just reading the
174
+ diff.
175
+ - `src/lib/components/Canonical.svelte`: now wraps `page.url.pathname` in
176
+ `withTrailingSlash()` like every other internal href in this pass,
177
+ instead of trusting it already normalized — covers an error/404 render or
178
+ a future route opting out of the layout's `trailingSlash`.
179
+ `withTrailingSlash()`'s parameter type loosened from `ResolvedPathname` to
180
+ plain `string` so it accepts a raw pathname too; its return type stays
181
+ `ResolvedPathname` so the two existing `<a href>` call sites still satisfy
182
+ `eslint-plugin-svelte`'s navigation rule (a `<link rel="canonical">` was
183
+ never a target of that rule regardless).
184
+ - `src/lib/site.test.ts` (new): unit tests for `withTrailingSlash()` — pure,
185
+ trivially testable logic that shipped with none, despite vitest already
186
+ being wired into this template's gate and a direct precedent
187
+ (`content/validate.test.ts`) for testing exactly this kind of logic.
188
+ - `src/lib/staticRoutes.ts` (new, moved out of `sitemap.xml/+server.ts`) and
189
+ `src/lib/tokensCss.ts` (new, moved out of `tokens.css/+server.ts`): both
190
+ were flagged as hand-synced/duplicated logic with no drift check.
191
+ Factoring them out of their `+server.ts` files was **required**, not just
192
+ tidiness — SvelteKit restricts `+server.ts` exports to a fixed list
193
+ (`GET`, `prerender`, etc.) and rejects any other named export at build
194
+ time, caught only by actually running `vite build` after first trying to
195
+ export them inline. `staticRoutes.test.ts` checks `STATIC_ROUTES` against
196
+ the real `+page.svelte` route tree; `tokensCss.test.ts` checks the
197
+ generated CSS's light-mode selector against the *installed*
198
+ `@picocss/pico` package's own CSS, so a future Pico version bump that
199
+ changes that selector fails a test instead of silently losing the cascade
200
+ again (the exact bug the permalink-shape entry above found and fixed by
201
+ hand). `tokens.css/+server.ts`'s dark-mode block, previously written out
202
+ twice (once for `[data-theme="dark"]`, once for `prefers-color-scheme`),
203
+ is now built once and reused by both.
204
+ - `src/lib/brand.ts`: `color.muted` had zero consumers anywhere in this
205
+ template (unlike Eleventy's, where it feeds the OG card's subtitle — this
206
+ template's simpler single-line card has none) and, unlike `logo`, carried
207
+ no comment saying so. Commented as intentionally unused rather than left
208
+ to look like dead code.
209
+
210
+ ### templates/eleventy-netlify/src/tokens.css.njk
211
+
212
+ - Added a comment cross-referencing `templates/sveltekit-netlify/src/lib/
213
+ tokensCss.ts` as the same mechanism for that stack, stating explicitly
214
+ that the two `--pico-*` remap blocks must be kept in sync by hand (not
215
+ factored into a shared module — the two templates don't share a runtime).
216
+ Verified `templates/verify.sh`'s eleventy-netlify branch still passes
217
+ (28 pass · 0 fail · 5 manual · 6 n/a) after this comment-only change.
218
+
219
+ ### stacks/sveltekit-netlify.md: seo-urls, brand-source, audit-page, generated-asset-freshness
220
+
221
+ - Updated to describe the fixes above and their verification.
222
+ `brand-source` also had a stale paragraph from before the og-image entry
223
+ above — still said the OG-card renderer was "not yet built" after it had
224
+ been; corrected in the same pass since it was found while touching this
225
+ section anyway.
226
+
227
+ Last of the four functional gaps closed ahead of the real project adopting
228
+ this stack. `og-image`'s own note already named the front-runner mechanism
229
+ (a prerendered `+server.ts` route, `satori` + `@resvg/resvg-js`) without
230
+ committing to it; this takes it, and the same trick already proven for
231
+ `tokens.css` (item 3) turns out to solve
232
+ `core.md#generated-asset-freshness`'s tier 1 for free — a genuinely better
233
+ answer than Eleventy's `eleventy.after` hook, not merely an adequate
234
+ substitute for it, since there's no persisted file that can go stale between
235
+ builds at all.
236
+
237
+ ### templates/sveltekit-netlify
238
+
239
+ - Added `satori@0.33.4`, `@resvg/resvg-js@2.6.2`, `@fontsource/inter@5.3.0`
240
+ (same versions as `templates/eleventy-netlify/scripts/og.js`) to
241
+ `dependencies`.
242
+ - New `src/routes/og/default.png/+server.ts`: renders the site-wide default
243
+ 1200×630 OG card from `src/lib/brand.ts` (a primary-color bar + the site
244
+ name, Inter regardless of the live site's font — an OG card is a designed
245
+ asset). Deleted `static/og/default.png`, the hand-placed file this route
246
+ now replaces at the same path.
247
+ - **Scope check, not scope creep:** no page emits an `og:image` meta tag
248
+ anywhere in this template yet (that's `seo-meta`'s separately-deferred
249
+ full OG/Twitter table). So this generates the one site-wide default only,
250
+ not per-page cards — building those now would be output nothing
251
+ references. Flagged explicitly in `TEMPLATE.md` and `stacks/
252
+ sveltekit-netlify.md#og-image` so it isn't mistaken for full OG support.
253
+ - Verified beyond a successful build: `build/og/default.png` is a real
254
+ 1200×630 8-bit PNG, ~10 KB (well under the ~1 MB ceiling), and was
255
+ actually viewed (Playwright's Chromium, installed last session, made this
256
+ possible) to confirm the brand color and text render correctly, not just
257
+ that `satori`/`resvg` didn't throw.
258
+ - `core.md#generated-asset-freshness`, all three tiers: (1) the build step —
259
+ free, as above; (2) `.claude/hooks/og-guard.sh` + `.claude/settings.json`,
260
+ the same `PostToolUse` pattern as `templates/eleventy-netlify/`'s
261
+ `og-guard.sh`, watching `brand.ts` and both generator `+server.ts` files,
262
+ rebuilding, and pointing the operator at the two output files directly
263
+ (this stack's audit page has no OG-gallery tab yet) — tested directly with
264
+ matching and non-matching `tool_input.file_path` values, not just read
265
+ over; (3) a new "Generated visual assets" paragraph in `AGENTS.md` (this
266
+ stack's actual instructions file — see below).
267
+ - Full `npm run check` gate passes.
268
+
269
+ ### stacks/sveltekit-netlify.md: og-image, generated-asset-freshness, where-sveltekit-fights-the-grain
270
+
271
+ - `og-image` and `generated-asset-freshness` now describe what's built and
272
+ what verified it, including the explicit per-page-cards non-scope.
273
+ `where-sveltekit-fights-the-grain`#5 retitled from "still deferred" to
274
+ "resolved differently" — the `+server.ts` route isn't a lesser substitute
275
+ for `eleventy.after`, it has no staleness window at all, which Eleventy's
276
+ own mechanism can't claim.
277
+
278
+ ### templates/sveltekit-netlify/TEMPLATE.md
279
+
280
+ - The "No OG-image generation" bullet replaced with what's actually built
281
+ and what's still missing (per-page cards, the meta tag). Noted `AGENTS.md`
282
+ uses this stack's real instructions filename, not `CLAUDE.md` — a
283
+ pre-existing naming difference from `templates/eleventy-netlify/`, not
284
+ something this change introduced or is fixing.
285
+
286
+ ---
287
+
288
+ ## 2026-09-04 — sveltekit-netlify's permalink shape, actually resolved [non-breaking]
289
+
290
+ A real project is about to adopt `templates/sveltekit-netlify/` (closing the
291
+ "deliberately deferred" item in `TODO.md`), which turns the binding's four
292
+ self-documented gaps from theoretical to load-bearing. First of four: the
293
+ permalink shape. `stacks/sveltekit-netlify.md#seo-urls` had already found and
294
+ recorded that the naive fix — `trailingSlash: 'always'` alone — trades one
295
+ gap for a subtler one, since `resolve()` and a hand-built `sitemap.xml` don't
296
+ pick the setting up on their own. This closes it for real, verified against
297
+ an actual build's output files, rendered `href`s, and `sitemap.xml` content,
298
+ not just an exit code.
299
+
300
+ ### templates/sveltekit-netlify
301
+
302
+ - `src/routes/+layout.ts`: added `export const trailingSlash = 'always';`.
303
+ - `src/lib/site.ts`: added `withTrailingSlash()`, typed `(path:
304
+ ResolvedPathname) => ResolvedPathname` rather than `string` — plain
305
+ `string` would make `eslint-plugin-svelte`'s
306
+ `svelte/no-navigation-without-resolve` rule stop recognizing the wrapped
307
+ `resolve()` call as safe, since that rule accepts an expression either by
308
+ syntax (a literal `resolve()` call) or by matching `$app/types`'s
309
+ `ResolvedPathname` type structurally — a generic wrapper satisfies neither
310
+ unless typed this way.
311
+ - `src/routes/guides/+page.svelte` and `guides/[slug]/+page.svelte`: every
312
+ `<a href>` built from `resolve('/guides/[slug]', { slug })` now wraps it in
313
+ `withTrailingSlash(...)`.
314
+ - `src/routes/sitemap.xml/+server.ts`: the static route list and guide-slug
315
+ URLs are plain literals under our control (not `resolve()` output), so
316
+ they're simply written with the trailing slash already baked in, rather
317
+ than routed through the same helper.
318
+ - Verified with a real `npm run build`: `build/about/index.html` (not
319
+ `about.html`); guide-list and guide-detail pages' rendered `href`s end in
320
+ `/`; `sitemap.xml`'s 8 entries (5 static + 3 guides) all end in `/`. Full
321
+ `npm run check` gate passes.
322
+
323
+ ### stacks/sveltekit-netlify.md: seo-urls
324
+
325
+ - The permalink-shape note updated from "a real gap, not yet resolved" to
326
+ "resolved," describing the actual fix and the `ResolvedPathname`-typing
327
+ wrinkle the lint rule forced.
328
+
9
329
  ---
10
330
 
11
- ## 2026-09-03 — the content-arm interface: a project and its coding arm
331
+ ## 2026-09-04 — the standalone-page shape divergence, accepted and documented [non-breaking]
332
+
333
+ Triages `feedback/2026-09-03-pages-model-diverges-across-stacks.md`
334
+ (deleted, per this file's own triage convention). The note found that
335
+ "standalone pages are data" is implemented three different ways across the
336
+ templates — `eleventy-netlify`'s `pages.json` (an array, structured `body`
337
+ sections, `{ include }` markers, layout blocks), `sveltekit-netlify`'s (an
338
+ object keyed by page name, a flat `body: string[]`), `phoenix`'s (no
339
+ `pages.json` at all — markdown or a reviewed component) — and asked for a
340
+ decision: converge on one shape, or document the difference. The note's own
341
+ read was right: convergence "wants a second real adopting project on a
342
+ non-eleventy stack to justify the work" (`TODO.md`'s already-recorded
343
+ decision not to invest further in Phoenix/SvelteKit until one exists says
344
+ the same thing), so this takes the documented-divergence option.
345
+
346
+ ### core: content-model
347
+
348
+ - New Spec: the standalone-page entry shape is a per-stack choice, named by
349
+ each stack's own impl doc (and, where one ships, the project's own
350
+ `CONTENT.md` "entry shape" section — the one an editing session actually
351
+ needs, read fresh each session rather than assumed from another project).
352
+
353
+ ### stacks/sveltekit-netlify.md: content-model
354
+
355
+ - A real, previously-undocumented gap fixed while here: this section named
356
+ the files (`{guides,pages}.json` + zod) but never their field shape,
357
+ unlike the Eleventy and Phoenix bindings, which both already document
358
+ theirs. Added, verified against the real `*.schema.ts` files: `guides.json`
359
+ mirrors `eleventy-netlify`'s field set structurally; `pages.json` is
360
+ genuinely simpler (an object keyed by page name, a flat `body: string[]`,
361
+ no markers or blocks) — not just smaller, a different shape, and now
362
+ stated as a deliberate one.
363
+
364
+ ### templates/sveltekit-netlify/CONTENT.md
365
+
366
+ - Gains "The entry shape — `guides.json`" / "`pages.json`" sections mirroring
367
+ `templates/eleventy-netlify/CONTENT.md`'s existing pattern — previously
368
+ named the content set files with no field-level reference at all, so an
369
+ editorial agent had nothing to read for this stack's shape short of the
370
+ binding doc (which the editorial-project instruction doesn't point it at —
371
+ `CONTENT.md` is the one per-session read).
372
+
373
+ ## 2026-09-04 — Q1's SvelteKit criterion stops being circular [non-breaking]
374
+
375
+ Flagged in a review of the stack-selection guidance (`TODO.md`, prompted by
376
+ `PROJECT-OVERVIEW.md`'s planning conversation): Q1 told an adopter to pick
377
+ `sveltekit-netlify` "when a project specifically needs Svelte components"
378
+ without ever saying what would make that true, so it gave no real signal
379
+ against the default. Meanwhile `stacks/sveltekit-netlify.md`'s own
380
+ `where-sveltekit-fights-the-grain` #2 had already found, and buried, the
381
+ actual answer: SvelteKit hydrates every page by default, so picking it ships
382
+ the framework runtime site-wide, not only on the interactive page — a real
383
+ cost `core.md#client-logic`'s pure-function-plus-wiring pattern avoids
384
+ entirely for the common case (one calculator, one widget). Phoenix's
385
+ criterion ("a database or server-side state is in scope") was already crisp
386
+ by comparison — only the SvelteKit clause needed the fix. No project's
387
+ existing stack choice is invalidated — this sharpens guidance for a *future*
388
+ choice, it doesn't reopen a past one.
389
+
390
+ ### core: adopting
391
+
392
+ - Q1's SvelteKit clause restated around the real trigger (several
393
+ interactive pieces genuinely sharing state, not one calculation) plus the
394
+ hydration-cost tradeoff, with a pointer to
395
+ `stacks/sveltekit-netlify.md#where-sveltekit-fights-the-grain` #2. Phoenix's
396
+ and the "other" bucket's wording untouched — re-read, no knock-on issue
397
+ found. All three stacks' own mirrored `adopting` sections re-read too
398
+ (none restate Q1's selection criteria — nothing to change there).
399
+
400
+ ### README.md
401
+
402
+ - Q1's framework-question mirror gets the same tightened SvelteKit clause,
403
+ condensed to match this doc's shorter style.
404
+
405
+ ## 2026-09-04 — the standard-version drift row is severity-tagged [breaking]
406
+
407
+ The other half of the severity-signal work (`TODO.md` #1 depended on this, not
408
+ just the tag itself): the `#compliance` drift row now distinguishes
409
+ `BREAKING` from `MANUAL` per slug, using each changed CHANGELOG entry's own
410
+ tag, so `whs compliance --strict` actually gates on being behind on a
411
+ breaking change rather than treating every changed slug — a typo fix and a
412
+ broken-mechanism correction alike — as the same easy-to-miss `MANUAL` note.
413
+ This is the concrete fix for the gap `TODO.md` #1's evidence section
414
+ documented: `everydaymoneycalc` being 8 entries behind (several of them
415
+ breaking, per the backfill above) produced no signal beyond a note buried in
416
+ a `MANUAL` list.
417
+
418
+ ### core: compliance
419
+
420
+ - `#compliance`'s drift-row Spec: a slug from a `breaking` CHANGELOG entry
421
+ now emits `BREAKING: re-check #<slug>` and folds into the fail count
422
+ (gates `--strict`); a `non-breaking` one still emits the softer
423
+ `MANUAL: re-check #<slug>`. A slug touched by more than one entry since the
424
+ pin takes the worst severity seen.
425
+
426
+ ### stacks/eleventy-netlify.md
427
+
428
+ - `packages/whs-eleventy/lib/compliance.js`'s `versionDrift()` reads each
429
+ entry's `[breaking]`/`[non-breaking]` tag via a new `changelogEntries()`
430
+ (`changelogSlugs()` kept, now a flattened view over it, for backward
431
+ compatibility) and emits the tagged row; `summarize()` folds `BREAKING`
432
+ rows into the fail count, `main()`'s `--strict` message updated to match.
433
+ `src/audit.njk`'s drift list bolds `BREAKING` rows. Verified end to end:
434
+ rolled `templates/eleventy-netlify/`'s pin back to `2026-08-27` and
435
+ confirmed `whs compliance` printed exactly 8 `BREAKING` + 8 `MANUAL` rows
436
+ matching the backfilled tags (`28 pass · 8 fail · 13 manual · 6 n/a` — 0
437
+ real chapter `FAIL`s, all 8 from drift), the built `/audit/` page bolding
438
+ only the `BREAKING` `<li>`s, then restored the pin. `npm run
439
+ check`/`compliance` re-verified clean afterward. 5 new tests added to
440
+ `packages/whs-eleventy/test/compliance.test.js` (54 total, all green).
441
+
442
+ ### stacks/phoenix.md
443
+
444
+ - The still-unshipped `#compliance` design doc's drift-row description
445
+ updated to match — the same `BREAKING`/`MANUAL` split, once built, per
446
+ `versionDrift()`, "the pattern to port."
447
+
448
+ ## 2026-09-04 — a severity signal on CHANGELOG entries [non-breaking]
449
+
450
+ Flagged in a planning conversation (`PROJECT-OVERVIEW.md`): entries were dated
451
+ but not marked as breaking vs. non-breaking, so a project couldn't tell from
452
+ the log alone whether it was safe to ignore an update or urgently behind — and
453
+ without that signal, an eventual upgrade path for already-adopted projects
454
+ (tracked in `TODO.md`) would have nothing to prioritize on. Every `## <date>`
455
+ heading, past and present, now carries `[breaking]` or `[non-breaking]`, per
456
+ the rule stated in this file's own header note. Not a `core.md`/`stacks/*.md`
457
+ content change — process/tooling only, no version bump (same precedent as the
458
+ 2026-08-30 "the standard is a git repo" entry).
459
+
460
+ ### tooling
461
+
462
+ - `bin/check` gains a sixth check: every `## <date>` CHANGELOG heading must
463
+ carry a severity tag. `CLAUDE.md` and `README.md`'s Consistency checks
464
+ section updated to match (six checks; `templates/verify.sh` is now the
465
+ *seventh*, not sixth).
466
+
467
+ ### CHANGELOG.md
468
+
469
+ - All 20 existing entries backfilled with a severity tag, judged against the
470
+ rule stated in the new header note (breaking = a previously-compliant
471
+ project is no longer compliant after the change).
472
+
473
+ ## 2026-09-04 — name the deploy hand-off in the content-ops protocol [breaking]
474
+
475
+ The content-arm interface (2026-09-03, below) defined how an LLM agent (Cowork)
476
+ edits reserved content, but left the last step implicit: the agent commits to
477
+ `main` and cannot deploy, and the only cue to the operator was "if it's
478
+ time-sensitive, say so in the commit message" — passive, no addressee. A real
479
+ adopter workflow (Cowork owning content post-launch) surfaced the gap. The
480
+ deploy boundary is unchanged — a person still runs the press — but the signal is
481
+ now named: a `DEPLOY:` line closing the agent's reply, and `[deploy by <date>]`
482
+ in the `content:` subject when there's a deadline.
483
+
484
+ ### core: content-model
485
+
486
+ - The LLM-agent content-ops protocol gains a fifth element: the agent does not
487
+ deploy — it commits a finished change to the default branch and hands it to
488
+ the human deploy cadence (`#ci-cd`) with a session-closing report, deadline in
489
+ the commit subject. `#adopting`'s "Editorial operation" stable instruction
490
+ reworded to match ("never deploy — handed off, not shipped").
491
+
492
+ ### stacks/eleventy-netlify.md: content-model
493
+
494
+ - New **Deploy hand-off** bullet: the `DEPLOY: awaiting …` / `DEPLOY: by <date>
495
+ …` closing line, the `[deploy by <date>]` commit-subject tag for the
496
+ operator's `git log <last-deploy>..main` batch review, and the two deploy
497
+ doorways (`deploy.yml` Run workflow / `npm run deploy`) — nothing in the
498
+ pre-flight or tooling changes.
499
+ - The representative `#compliance` `content-model` row in the mechanical-checks
500
+ table now spells out the protocol-presence note (`CONTENT.md`,
501
+ `content-check`, `requests/`) the sweep already emits — the row had listed
502
+ only the data + schema + validator half.
503
+ - `stacks/phoenix.md` and `stacks/sveltekit-netlify.md` content-model sections
504
+ and the three template `CONTENT.md`s (+
505
+ `templates/eleventy-netlify/CLAUDE.md`) mirror the hand-off wording.
506
+
507
+ ## 2026-09-04 — the SvelteKit binding renamed to sveltekit-netlify, honestly scoped [non-breaking]
508
+
509
+ Caught in conversation, not tooling: `stacks/sveltekit.md` / `templates/sveltekit/`
510
+ were named as if this were a general-purpose SvelteKit binding, when its actual
511
+ origin and entire scope is narrower — it exists specifically to answer "Svelte,
512
+ static, on Netlify," because plain Svelte has no durable static-site tooling of
513
+ its own. The naming didn't say so, and that's an inconsistency the repo's own
514
+ implicit rule already flags: a binding scoped to exactly one host with no
515
+ alternative documented (`eleventy-netlify` is the model) gets the host in its
516
+ name; a binding documenting multiple real hosts (`phoenix` — Render default,
517
+ Gigalixir/Fly as the graduate path) correctly doesn't. `sveltekit` documented
518
+ only Netlify, so by that same rule it should have carried the host in its name
519
+ too — it didn't, and nothing explained why not.
520
+
521
+ ### stacks/sveltekit-netlify.md (renamed from stacks/sveltekit.md)
522
+
523
+ - Retitled **SvelteKit + Netlify**. The intro now states the binding's real
524
+ origin up front — "why SvelteKit, if the actual need is just Svelte, static,
525
+ on Netlify" — rather than reading as a general SvelteKit offering that
526
+ happens to default to Netlify. A project that needs SvelteKit's actual
527
+ server capabilities (real SSR, live `+server.ts` endpoints, a database) is
528
+ explicitly called out as a different, larger binding this repo does not
529
+ have — not something this doc quietly under-scopes.
530
+ - Section-header labels (`How (SvelteKit)`, `Adopting this standard
531
+ (SvelteKit)`, etc.) deliberately keep the framework-only wording, matching
532
+ `stacks/eleventy-netlify.md`'s own convention of naming the binding
533
+ file/directory with the host but labeling in-body sections by framework
534
+ alone.
535
+
536
+ ### templates/sveltekit-netlify (renamed from templates/sveltekit)
537
+
538
+ - Directory, all internal doc cross-references (`AGENTS.md`, `README.md`,
539
+ `TEMPLATE.md`), and the CI job (`templates-sveltekit-netlify`) renamed to
540
+ match. `AGENTS.md`'s `framework:` data-block value now reads
541
+ `sveltekit-netlify`. Along the way, fixed a real, unrelated inaccuracy found
542
+ in the same file: `TEMPLATE.md` told an adopter to rename `whs_sveltekit` in
543
+ `package.json` — the actual placeholder there is `__PKG_NAME__`
544
+ (`whs_sveltekit` was a leftover copy-paste from the Phoenix template's
545
+ equivalent instruction, which is accurate for Phoenix's real OTP app name
546
+ but was never true here).
547
+
548
+ ### core: adopting
549
+
550
+ - Q1's framework list: `SvelteKit` → `SvelteKit + Netlify`, with the same
551
+ origin-story framing as the doc's own intro — a project with no Svelte
552
+ requirement should still default to Eleventy + Netlify; a project that
553
+ needs SvelteKit's server features has no binding here yet (falls to
554
+ "other").
555
+
556
+ ### tooling
557
+
558
+ - `index.json`'s `sveltekit` key → `sveltekit-netlify` (file, template path,
559
+ and summary all updated to state the narrower scope explicitly).
560
+ `README.md`'s doc-map table, framework-question wording, and
561
+ `templates/verify.sh` description updated to match.
562
+ `templates/concept-brief.md`'s `framework:` comment and the one open
563
+ `feedback/` report referencing the old name updated for accuracy while
564
+ still open.
565
+
566
+ ---
567
+
568
+ ## 2026-09-03 — a self-audit: gate/gitignore/doc gaps found by external review [non-breaking]
569
+
570
+ A skeptical outside audit of the standard's own repo (docs, templates, CI,
571
+ tooling) turned up a cluster of small-but-real defects — none a contract
572
+ violation, all either a check that couldn't catch its own failure mode, a
573
+ tooling side-effect leaking outside its own project, or a doc claim that had
574
+ drifted from what the repo actually contains. Fixed together as one pass;
575
+ see `git log` for the individual verification each one got (cold installs,
576
+ mutation tests, real builds, fetched external docs).
577
+
578
+ ### core: adopting
579
+
580
+ - **Stale template-count claim.** The stack-selection flow's "Starting
581
+ point" paragraph said "Only `eleventy-netlify` has a template today; the
582
+ others follow the checklist" — both `templates/phoenix/` and
583
+ `templates/sveltekit/` have existed (with real, passing gates) since
584
+ 2026-09-02. Corrected to point at `index.json`'s `active`/`draft` status
585
+ instead of a hardcoded, now-wrong count. Re-read each `stacks/*.md`
586
+ mirrored `adopting` section — all three already stated their own template's
587
+ real status correctly; only this file's flow-level prose had drifted.
588
+
589
+ ### bin/check
590
+
591
+ - **Check #5 (version strings agree) couldn't detect a missing pin** — it
592
+ compared only the *distinct values* found, so a file missing its
593
+ `standard-version:`/`**Version:**` line entirely just vanished from the
594
+ comparison instead of failing it (confirmed: deleting
595
+ `templates/sveltekit/AGENTS.md`'s pin left the check green). Now checks
596
+ pin *presence* per expected file first, and names the file if one is
597
+ missing.
598
+
599
+ ### templates/phoenix
600
+
601
+ - **The `git_hooks` monorepo guard was incomplete.** `config/dev.exs`
602
+ guarded the `hooks:` config behind `File.dir?(".git")` so the template
603
+ wouldn't install hooks into the standard's own repo when compiled in
604
+ place — but `git_hooks`'s `auto_install` (default `true`) runs before that
605
+ config is ever consulted, and unconditionally writes a `git_hooks.db`
606
+ marker into whatever `.git/hooks` it finds via `git rev-parse
607
+ --show-toplevel`. Confirmed landing in the standard's own `.git/hooks/`
608
+ a second time, guard already in place. Fixed with an explicit
609
+ `auto_install: false` in the guard's `else` branch — verified with a full
610
+ `rm -rf _build deps && mix deps.get && mix compile`, the exact sequence
611
+ that reproduced the bug, no longer touching the main checkout.
612
+
613
+ ### repo-hygiene (root .gitignore)
614
+
615
+ - Added `erl_crash.dump` — the root `.gitignore` covered only Node/JS build
616
+ output; a BEAM crash dump landing at the repo root (any `mix` command run
617
+ from there, not inside `templates/phoenix/`) was untracked and therefore
618
+ invisible to `bin/check`'s `git grep`-based sterility scan despite
619
+ containing a personal filesystem path. `templates/phoenix/.gitignore`
620
+ already covered its own case; this was the repo-root gap.
621
+
622
+ ### stacks/sveltekit.md, templates/*/netlify.toml
623
+
624
+ - Corrected the stated root cause of `#noindex` layer 2 not applying: the
625
+ doc said per-context header scoping "needs th[e] build pipeline to run"
626
+ (implying it would work if Netlify's build system did run) — Netlify's
627
+ docs are explicit that `[[headers]]` is never context-aware on *any*
628
+ deploy path; the pre-built-artifact deploy model is a second, independent
629
+ reason the documented workaround doesn't apply here, not the cause of the
630
+ first problem. `stacks/eleventy-netlify.md` and both templates'
631
+ `netlify.toml` already stated this correctly; only this file's prose had
632
+ drifted. Also dropped an unverifiable direct quotation of Netlify's docs
633
+ (substance was correct, exact wording wasn't confirmed) from this file and
634
+ `templates/eleventy-netlify/netlify.toml`.
635
+ - Corrected two "previously-undocumented" claims (the `vite.config.ts`
636
+ config-consolidation and `resolve()` from `$app/paths`) — both are real,
637
+ and both are documented on svelte.dev; what was actually true is that this
638
+ binding hadn't previously accounted for them.
639
+ - Documented a real, previously-unstated gap: the template's flat
640
+ `about.html`-shaped build output doesn't meet `core.md#adopting`'s
641
+ directory-style-permalink item. Tried the one-line fix
642
+ (`trailingSlash = 'always'`) and verified in a real build that it's not
643
+ sufficient alone — `resolve()` from `$app/paths` doesn't append the
644
+ trailing slash to match, nor does the hand-built sitemap route list, so it
645
+ trades one gap for a file-layout/link/sitemap mismatch. Left unfixed,
646
+ documented precisely instead.
647
+
648
+ ### stacks/phoenix.md
649
+
650
+ - Tightened Render free-Postgres wording: "deleted 30 days after creation"
651
+ → "expires 30 days after creation" (inaccessible immediately, permanently
652
+ deleted after a further 14-day grace period) — matches Render's own docs.
653
+
654
+ ### .github/workflows/standard.yml
655
+
656
+ - Added a comment at the `templates` job's matrix declaration explaining
657
+ that the `[eleventy-netlify]` list is manually curated (not discovered
658
+ from `templates/*/` the way `templates/verify.sh` is) and when a new
659
+ template should be added to it — the file's top-of-file comment already
660
+ explained this design, but not at the point someone would actually edit.
661
+
662
+ ### templates/new-project.sh
663
+
664
+ - The no-`bin/init`-yet branch's own comment said "fail loudly" but the
665
+ script exited 0 regardless — nothing after the `echo` produced a non-zero
666
+ status. Added the missing `exit 1`; verified against `sveltekit` (no
667
+ `bin/init` yet) going from exit 0 to exit 1, `eleventy-netlify`'s
668
+ (`bin/init`-having) path unaffected.
669
+
670
+ ### packages/whs-eleventy, templates/eleventy-netlify
671
+
672
+ - Bumped `eslint`/`@eslint/js` off the EOL `9.39.5` pin (`npm warn
673
+ deprecated eslint@9.39.5: This version is no longer supported`) to
674
+ `10.9.1`/`10.0.1` in both — `templates/sveltekit/` was already on the
675
+ current major. Verified with a cold `rm -rf node_modules package-lock.json
676
+ && npm install` plus the full gate for each: `whs-eleventy`'s `npm run
677
+ check` (49 tests), `eleventy-netlify`'s `npm run check` (lint → validate →
678
+ test → build → links → a11y), both green, no deprecation warning on
679
+ install.
680
+
681
+ ## 2026-09-03 — the content-arm interface: a project and its coding arm [breaking]
12
682
 
13
683
  A project built to the house style is often only part of a larger operation —
14
684
  an editor-in-chief (a Claude Project, ideation + editorial context) that hands
@@ -133,7 +803,7 @@ for the common small-site case); a **`schedule` trigger** on the deploy workflow
133
803
  sveltekit flat `string[]`, phoenix none) — converge or document the split, at
134
804
  triage.
135
805
 
136
- ## 2026-09-02 — a starter template for the SvelteKit binding, moved out of parked
806
+ ## 2026-09-02 — a starter template for the SvelteKit binding, moved out of parked [non-breaking]
137
807
 
138
808
  `stacks/sveltekit.md` was a parked stub — every mirrored section's "How
139
809
  (SvelteKit)" was `TODO`, not offered by the `#adopting` flow. A real need
@@ -255,7 +925,7 @@ and real `vite build` output, not written from the binding's prose alone.
255
925
 
256
926
  ---
257
927
 
258
- ## 2026-09-02 — Render as the Phoenix binding's default free-launch host
928
+ ## 2026-09-02 — Render as the Phoenix binding's default free-launch host [non-breaking]
259
929
 
260
930
  Netlify can't run this stack at all — LiveView needs a persistent BEAM node
261
931
  (long-lived WebSocket connections, a warm Postgres pool), not a static
@@ -330,7 +1000,7 @@ isn't trustworthy here.
330
1000
 
331
1001
  ---
332
1002
 
333
- ## 2026-09-02 — a starter template for the Phoenix binding, and hardening the doc it's built from
1003
+ ## 2026-09-02 — a starter template for the Phoenix binding, and hardening the doc it's built from [breaking]
334
1004
 
335
1005
  Built `templates/phoenix/` the same way `templates/eleventy-netlify/` was
336
1006
  proven out: generic placeholder content, independent of any real business,
@@ -465,7 +1135,7 @@ fabricated or imprecise APIs the same way the earlier `Plug.CSP` finding did.
465
1135
 
466
1136
  ---
467
1137
 
468
- ## 2026-09-02 — closing the gap between the compliance grid and the Contracts it checks
1138
+ ## 2026-09-02 — closing the gap between the compliance grid and the Contracts it checks [non-breaking]
469
1139
 
470
1140
  A pass through every chapter's actual enforcement, prompted by an outside
471
1141
  evaluation that traced specific checks to specific failure modes they
@@ -624,7 +1294,7 @@ consistency defects the same pass turned up.
624
1294
 
625
1295
  ---
626
1296
 
627
- ## 2026-09-01 — the shared tooling package (#compliance mechanism, eleventy)
1297
+ ## 2026-09-01 — the shared tooling package (#compliance mechanism, eleventy) [non-breaking]
628
1298
 
629
1299
  ### stacks/eleventy-netlify.md
630
1300
 
@@ -662,7 +1332,7 @@ consistency defects the same pass turned up.
662
1332
 
663
1333
  ---
664
1334
 
665
- ## 2026-08-31 — the Cowork ↔ Claude Code loop: concept brief + content ops (#content-model)
1335
+ ## 2026-08-31 — the Cowork ↔ Claude Code loop: concept brief + content ops (#content-model) [breaking]
666
1336
 
667
1337
  ### core: adopting
668
1338
 
@@ -741,7 +1411,7 @@ consistency defects the same pass turned up.
741
1411
 
742
1412
  ---
743
1413
 
744
- ## 2026-08-30 — a copy-and-modify template for Eleventy + Netlify (#adopting)
1414
+ ## 2026-08-30 — a copy-and-modify template for Eleventy + Netlify (#adopting) [non-breaking]
745
1415
 
746
1416
  ### templates/eleventy-netlify (new)
747
1417
 
@@ -838,7 +1508,7 @@ consistency defects the same pass turned up.
838
1508
 
839
1509
  ---
840
1510
 
841
- ## 2026-08-30 — the standard is a git repo
1511
+ ## 2026-08-30 — the standard is a git repo [non-breaking]
842
1512
 
843
1513
  ### repo
844
1514
 
@@ -854,7 +1524,7 @@ consistency defects the same pass turned up.
854
1524
 
855
1525
  ---
856
1526
 
857
- ## 2026-08-29 — AI & agent capabilities, opt-in (#llm-integration, #agent-artifacts, #usage-metering)
1527
+ ## 2026-08-29 — AI & agent capabilities, opt-in (#llm-integration, #agent-artifacts, #usage-metering) [non-breaking]
858
1528
 
859
1529
  ### core: llm-integration · agent-artifacts · usage-metering (new chapters)
860
1530
 
@@ -935,7 +1605,7 @@ consistency defects the same pass turned up.
935
1605
 
936
1606
  ---
937
1607
 
938
- ## 2026-08-28 — codebase-vs-standard conformance (#compliance)
1608
+ ## 2026-08-28 — codebase-vs-standard conformance (#compliance) [non-breaking]
939
1609
 
940
1610
  ### core: compliance (new chapter)
941
1611
 
@@ -992,7 +1662,7 @@ consistency defects the same pass turned up.
992
1662
 
993
1663
  ---
994
1664
 
995
- ## 2026-08-28 — the `CLAUDE.md` House-style directive
1665
+ ## 2026-08-28 — the `CLAUDE.md` House-style directive [breaking]
996
1666
 
997
1667
  ### core: adopting
998
1668
 
@@ -1015,7 +1685,7 @@ consistency defects the same pass turned up.
1015
1685
 
1016
1686
  ---
1017
1687
 
1018
- ## 2026-08-28 — display advertising (AdSense)
1688
+ ## 2026-08-28 — display advertising (AdSense) [non-breaking]
1019
1689
 
1020
1690
  ### core: ads (new chapter)
1021
1691
 
@@ -1066,7 +1736,7 @@ consistency defects the same pass turned up.
1066
1736
 
1067
1737
  ---
1068
1738
 
1069
- ## 2026-08-28 — hardening from the first full Eleventy migration
1739
+ ## 2026-08-28 — hardening from the first full Eleventy migration [breaking]
1070
1740
 
1071
1741
  Fixes and gaps surfaced running the first full Eleventy migration end to end.
1072
1742
 
@@ -1117,7 +1787,7 @@ Fixes and gaps surfaced running the first full Eleventy migration end to end.
1117
1787
 
1118
1788
  ---
1119
1789
 
1120
- ## 2026-08-28 — solo is the default working mode
1790
+ ## 2026-08-28 — solo is the default working mode [non-breaking]
1121
1791
 
1122
1792
  ### core: ci-cd
1123
1793
 
@@ -1150,7 +1820,7 @@ Fixes and gaps surfaced running the first full Eleventy migration end to end.
1150
1820
  across all three; migration `[one PR:]` markers → `[together:]`; the
1151
1821
  "busy PR flow" build-minutes framing dropped.
1152
1822
 
1153
- ## 2026-08-27 — netlify deploy `--no-build`
1823
+ ## 2026-08-27 — netlify deploy `--no-build` [breaking]
1154
1824
 
1155
1825
  ### stacks/eleventy-netlify.md
1156
1826
 
@@ -1168,7 +1838,7 @@ Fixes and gaps surfaced running the first full Eleventy migration end to end.
1168
1838
 
1169
1839
  - `ci-cd` hint — same `--no-build` fix in the `adapter-static` + Netlify note.
1170
1840
 
1171
- ## 2026-08-27 — initial split
1841
+ ## 2026-08-27 — initial split [non-breaking]
1172
1842
 
1173
1843
  The single 1518-line "Eleventy + Netlify static-site house style" document was
1174
1844
  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-08 · **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-08 # 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.