@piercebarney/whs-eleventy 2026.9.8 → 2026.9.13

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/_project.js CHANGED
@@ -40,12 +40,16 @@ function openRequests(root = ROOT) {
40
40
  // 1. WHS_STANDARD env var (the authoring repo points this at itself)
41
41
  // 2. the copy bundled into this package at publish time (standard/)
42
42
  // 3. the legacy ~/.claude/standards/web-house-style symlink
43
+ // With { preferLocal: true } the ~/.claude checkout is tried before the bundled
44
+ // copy (still behind WHS_STANDARD) — for `whs status`, which asks whether a
45
+ // newer standard exists on this machine rather than measuring against the pin.
43
46
  // Returns an absolute dir path, or null if none of them has a core.md.
44
- function resolveStandard() {
47
+ function resolveStandard({ preferLocal = false } = {}) {
48
+ const bundled = path.join(__dirname, "..", "standard");
49
+ const local = path.join(os.homedir(), ".claude", "standards", "web-house-style");
45
50
  const candidates = [
46
51
  process.env.WHS_STANDARD,
47
- path.join(__dirname, "..", "standard"),
48
- path.join(os.homedir(), ".claude", "standards", "web-house-style"),
52
+ ...(preferLocal ? [local, bundled] : [bundled, local]),
49
53
  ].filter(Boolean);
50
54
  for (const dir of candidates) {
51
55
  if (fs.existsSync(path.join(dir, "core.md"))) return dir;
package/lib/compliance.js CHANGED
@@ -717,9 +717,9 @@ function changelogSlugs(changelog, pin) {
717
717
  return [...slugs];
718
718
  }
719
719
 
720
- function versionDrift() {
720
+ function versionDrift(standardDir = STANDARD) {
721
721
  const pin = (claudeMd.match(/standard-version:\s*([0-9-]+)/) || [])[1];
722
- if (!STANDARD) {
722
+ if (!standardDir) {
723
723
  return {
724
724
  pin,
725
725
  current: null,
@@ -728,12 +728,12 @@ function versionDrift() {
728
728
  ],
729
729
  };
730
730
  }
731
- const coreMd = read("core.md", STANDARD) || "";
731
+ const coreMd = read("core.md", standardDir) || "";
732
732
  const current = (coreMd.match(/\*\*Version:\*\*\s*([0-9-]+)/) || [])[1];
733
733
  if (!pin || !current) return { pin, current, rows: [] };
734
734
  if (pin >= current) return { pin, current, rows: [] };
735
735
 
736
- const changelog = read("CHANGELOG.md", STANDARD) || "";
736
+ const changelog = read("CHANGELOG.md", standardDir) || "";
737
737
  // Worst-case severity per slug: one breaking touch marks it breaking for
738
738
  // good, even if a later (or earlier) entry touching the same slug since
739
739
  // the pin was non-breaking.
@@ -832,4 +832,11 @@ if (require.main === module) main();
832
832
 
833
833
  // CHECKS is exported for the "coverage" test — nothing else should read it as
834
834
  // data (call runCompliance() for results).
835
- module.exports = { runCompliance, summarize, changelogSlugs, changelogEntries, 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,103 @@
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 path = require("node:path");
15
+
16
+ const { versionDrift } = require("./compliance.js");
17
+ const { resolveStandard } = require("./_project.js");
18
+
19
+ // `status` answers "is there a newer standard available on this machine?" — so
20
+ // it resolves the standard text with { preferLocal: true }: the local canonical
21
+ // checkout (the ~/.claude symlink the README keeps) wins over the tooling's
22
+ // bundled snapshot when it exists. `whs compliance` deliberately measures the
23
+ // codebase against the bundled snapshot; `status` measures the pin against the
24
+ // freshest tree available, which is what a "you're behind" nudge should
25
+ // reflect. Machines without the symlink (CI, a fresh clone) fall back to the
26
+ // bundled snapshot unchanged. WHS_STANDARD still wins over both.
27
+
28
+ // Distinct CHANGELOG dates after the pin — the "N versions behind" count. The
29
+ // standard versions by date, and several entries can share one date (one
30
+ // release); this counts releases, not entries.
31
+ function versionsBehind(pin, std) {
32
+ if (!std || !pin) return 0;
33
+ try {
34
+ const changelog = fs.readFileSync(path.join(std, "CHANGELOG.md"), "utf8");
35
+ const dates = new Set();
36
+ for (const line of changelog.split("\n")) {
37
+ const m = line.match(/^##\s+([0-9]{4}-[0-9]{2}-[0-9]{2})\b/);
38
+ if (m && m[1] > pin) dates.add(m[1]);
39
+ }
40
+ return dates.size;
41
+ } catch {
42
+ return 0;
43
+ }
44
+ }
45
+
46
+ function main() {
47
+ const hook = process.argv.includes("--hook");
48
+ const std = resolveStandard({ preferLocal: true });
49
+ const { pin, current, rows } = versionDrift(std);
50
+
51
+ const unresolved =
52
+ rows.length === 1 && rows[0].startsWith("MANUAL: standard text not resolvable");
53
+ if (unresolved) {
54
+ if (!hook) console.log(rows[0]);
55
+ return;
56
+ }
57
+
58
+ if (!rows.length) {
59
+ if (!hook) {
60
+ console.log(
61
+ pin && current
62
+ ? `web house style: up to date (pinned ${pin}, standard at ${current})`
63
+ : "web house style: standard-version not pinned in CLAUDE.md",
64
+ );
65
+ }
66
+ return;
67
+ }
68
+
69
+ const breaking = rows.filter((r) => r.startsWith("BREAKING:")).length;
70
+ const versions = versionsBehind(pin, std);
71
+ const behind = `${versions} version${versions === 1 ? "" : "s"} behind`;
72
+ const recheck = `${rows.length} re-check${rows.length === 1 ? "" : "s"}${
73
+ breaking ? ` (${breaking} breaking)` : ""
74
+ }`;
75
+
76
+ if (hook) {
77
+ console.log(
78
+ `⚠ web house style: ${behind}, ${recheck} — pinned ${pin}, standard at ${current}. ` +
79
+ `Run /whs:upgrade to catch up.`,
80
+ );
81
+ return;
82
+ }
83
+
84
+ console.log(
85
+ `\nstandard-version: pinned ${pin}, standard at ${current} — ${behind}, ${recheck}\n`,
86
+ );
87
+ for (const row of rows) console.log(` ${row}`);
88
+
89
+ const hasUpgrade = fs.existsSync(
90
+ path.join(process.cwd(), ".claude", "commands", "whs", "upgrade.md"),
91
+ );
92
+ console.log(
93
+ hasUpgrade
94
+ ? "\nRun /whs:upgrade in a Claude Code session to work this backlog.\n"
95
+ : "\nThis project predates /whs:upgrade. Copy .claude/commands/whs/upgrade.md,\n" +
96
+ ".claude/hooks/standard-drift.sh, and the settings.json SessionStart entry\n" +
97
+ "from templates/eleventy-netlify/ in the standard, then run /whs:upgrade.\n",
98
+ );
99
+ }
100
+
101
+ if (require.main === module) main();
102
+
103
+ module.exports = { versionsBehind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piercebarney/whs-eleventy",
3
- "version": "2026.9.8",
3
+ "version": "2026.9.13",
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"
@@ -4,7 +4,10 @@ Slug-keyed. A change to a shared concept (a `core.md` chapter) lists the slug so
4
4
  "when did the deploy philosophy last change, and which docs moved" is answerable
5
5
  at a glance. Stack-only changes list the file.
6
6
 
7
- Format: `## <date>` → `### core: <slug>` / `### <stack>` entries.
7
+ Format: `## <date>` → `### core: <slug>` / `### <stack>` entries. Template
8
+ `.claude/` tooling (commands, agents, hooks) and repo-root routing files (e.g.
9
+ `COWORK.md`) are recorded here too, under `### templates/<stack>` / `### <file>`
10
+ — the same as any mechanism an adopting project consumes.
8
11
 
9
12
  **Severity.** Every `## <date>` heading also carries `[breaking]` or
10
13
  `[non-breaking]`. An entry is **breaking** if a project that was fully
@@ -18,6 +21,182 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
18
21
 
19
22
  ---
20
23
 
24
+ ## 2026-09-12 — A same-day-release suffix, two feedback fixes, and an /whs:upgrade false-negative [non-breaking]
25
+
26
+ ### templates/eleventy-netlify
27
+
28
+ `/whs:upgrade`'s "Before starting" guard stopped immediately whenever
29
+ `npm run status` reported the pin current — trusting a signal that itself
30
+ trusts whatever `@piercebarney/whs-eleventy` version happens to be
31
+ installed. Found today: that installed version's `resolveStandard()`
32
+ predates the `preferLocal` live-checkout preference entirely, so it always
33
+ reads its own frozen bundled snapshot — meaning `npm run status` can report
34
+ "up to date" while the real standard has moved well past it, and the guard
35
+ would stop before ever reaching the CHANGELOG read that would have caught
36
+ it. The guard now also checks `~/.claude/standards/web-house-style/
37
+ CHANGELOG.md` directly for any `## <date>` heading after the pin, and only
38
+ stops if that agrees with `npm run status`.
39
+
40
+ ### bin/check
41
+
42
+ `feedback/` is now excluded from check 4's real-adopter-name denylist.
43
+ Reported same-day: `feedback/README.md`'s own filing template asks for the
44
+ real adopter's directory name in `project:`, which the denylist rejected —
45
+ the documented feedback process couldn't pass its own gate as written. The
46
+ sterility grep (check 3) still applies to everything else in a report; only
47
+ the project-name denylist is exempted, and only for `feedback/`.
48
+
49
+ ### bin/set-version, CLAUDE.md
50
+
51
+ Same problem, a real fix instead of a workaround: `bin/set-version` now
52
+ accepts `<YYYY-MM-DD>-N` for a same-day re-release, and refuses a bare
53
+ re-run of a date already in use (telling you to add a suffix) instead of
54
+ silently no-opping. The doc pins (`core.md`, `stacks/*.md`, `index.json`,
55
+ every `standard-version:`) take the full suffixed string; `bin/check`'s
56
+ version-agreement check now compares base dates (suffix stripped) so a
57
+ suffixed pin still agrees with `packages/*/package.json`, which is left
58
+ untouched on purpose — no same-day-aware npm version scheme exists, so a
59
+ suffixed pin gets no git tag and no npm publish, a narrow, documented
60
+ exception to "a pin corresponds to both a git tag and the npm version."
61
+ This version bump (to 2026-09-12-2) is the suffix mechanism's own first
62
+ real use, closing out both feedback reports filed today.
63
+
64
+ ---
65
+
66
+ ## 2026-09-11 — Feature flags, with ads as the reference implementation [non-breaking]
67
+
68
+ ### core: config-idiom
69
+
70
+ Feature flags formalized as a named pattern: boolean env vars, one name per
71
+ concern (`FEATURE_<NAME>`), read through the same centralised env-access
72
+ module as build context. Additive — `#config-idiom` already required
73
+ centralised environment access; this names the flag shape so future features
74
+ follow one convention instead of inventing their own.
75
+
76
+ ### core: ads
77
+
78
+ Ad serving now also respects a kill-switch flag (`#config-idiom`), independent
79
+ of the `ads:` provider declaration — the declaration controls whether the
80
+ plumbing exists, the flag controls whether it's currently allowed to render.
81
+ Defaults to on, so an unset flag preserves prior behavior.
82
+
83
+ ### templates/eleventy-netlify
84
+
85
+ `_data/build.js` gains `flags.ads` (`FEATURE_ADS`, default on); `ad-slot.njk`
86
+ and `add-ons/ads/layout-head.njk` gate on it. `bin/deploy` and `deploy.yml`
87
+ read the flag's current value from Netlify's own stored environment variables
88
+ (`netlify env:get FEATURE_ADS --context production`) immediately before the
89
+ production build, so the toggle lives in Netlify's dashboard and takes effect
90
+ on the next deploy — consistent with `#ci-cd`'s "deploys are deliberate";
91
+ there is no live no-redeploy toggle for any switch in this standard, ads
92
+ included.
93
+
94
+ ## 2026-09-11 — Implementation work always runs through an agent [non-breaking]
95
+
96
+ ### templates/eleventy-netlify
97
+
98
+ `CLAUDE.md`'s House-style prose gains a standing convention: any request that
99
+ becomes an actual change is run through the same plan → delegate → verify →
100
+ record loop `/whs:build` already uses per stage — a named stage agent
101
+ (`brand`/`content`/`layout`/`pack`) when the work is its territory, the new
102
+ `infra` agent for config/CI-CD/deploy/security-header/secrets/feature-flag
103
+ work, a general-purpose agent otherwise — never edited inline in the main
104
+ session, except a single `Edit`/`Write` touching 5 lines or fewer in one file.
105
+ `/whs:upgrade`'s step 3 ("otherwise edit directly") is updated to match: it
106
+ now delegates (to `infra` or general-purpose) instead. The exception is
107
+ mechanically enforced, not just documented: `.claude/hooks/agent-only.js`, a
108
+ `PreToolUse` hook wired into `.claude/settings.json`, denies a main-session
109
+ `Edit`/`Write` over the 5-line threshold and any main-session `MultiEdit`,
110
+ telling the assistant to delegate instead — a subagent's own edits (identified
111
+ by the `agent_id` field the hook payload carries only inside a subagent call)
112
+ are never blocked. The "changed lines" check is a positional line compare, not
113
+ a real diff — a full-file rewrite that happens to keep every line in the same
114
+ position reads as small; documented as a known limitation in the hook's own
115
+ comments and `infra.md`, not silently assumed exact.
116
+
117
+ ## 2026-09-10 — COWORK.md backfilled, a sterility fix, and a tooling cleanup [non-breaking]
118
+
119
+ ### COWORK.md
120
+
121
+ Backfilled — landed the same day as the entry below but as its own commit with
122
+ no CHANGELOG entry. The router file a Cowork (Claude Desktop) Project's
123
+ instructions point at: routes between new-project setup (ideates, live-checks
124
+ whether the chosen stack actually has a working pipeline before committing to
125
+ it, stages the brief, scaffolds via `new-project.sh`, hands off to
126
+ `/whs:build`), content work on an existing project (already solved by
127
+ `CONTENT.md` — just recognizes and points at it, plus a standing
128
+ `standard-version` drift check), and converting an existing non-WHS project
129
+ (deliberately parked, same stance as the deferred stacks). Depended on two
130
+ small fixes, also unrecorded until now: `bin/init` seeds a given `--brief`
131
+ file into the new project's `.claude/brief.md`; `/whs:build` defaults to
132
+ reading it from there.
133
+
134
+ ### PROJECT-OVERVIEW.md, CHANGELOG.md
135
+
136
+ Closed a sterility leak: both named the one real project that has adopted the
137
+ standard in plain text, which `CLAUDE.md`'s "Keep it sterile" bans. Replaced
138
+ with an unnamed reference, substance preserved.
139
+
140
+ ### bin/check
141
+
142
+ Added a sub-check (4b): a maintained denylist of known real adopting-project
143
+ names/slugs, `git grep`'d over tracked files. The existing sterile grep (check
144
+ 4) only catches a leaked domain or AdSense publisher id by shape; a bare
145
+ project name has no such shape and was passing silently.
146
+
147
+ ### packages/whs-eleventy
148
+
149
+ Cleanup, no behavior change: `whs status` resolved the standard by mutating
150
+ `process.env.WHS_STANDARD` before requiring `compliance.js` — a side-channel.
151
+ `resolveStandard()` now takes a `{ preferLocal }` option instead;
152
+ `versionDrift()` takes the resolved standard dir as an optional argument.
153
+ Publishes to npm at the next dated release (no `package.json` version bump
154
+ here — nothing changes what a compliant project must do).
155
+
156
+ ## 2026-09-10 — a one-command standard-version catch-up, and drift that surfaces itself [non-breaking]
157
+
158
+ Catching an adopted project up to a moved-on standard was a hand-written prompt
159
+ every time. The detection existed (`whs compliance`'s severity-tagged
160
+ `re-check #<slug>` rows) but was passive — nothing said "you're behind" unless
161
+ someone ran it. `core.md#compliance` already expects the assistant to run
162
+ compliance "whenever `standard-version:` is behind"; this makes the Eleventy
163
+ stack actually do that, and adds the command that works the result. Non-breaking:
164
+ a project compliant before is still compliant — this is an opt-in reporter, a
165
+ session-start notice, and a new command; no requirement changes. Eleventy-only
166
+ pilot, same stance as `/whs:build`; Phoenix/SvelteKit keep the by-hand path
167
+ until they have a tooling package.
168
+
169
+ ### stacks/eleventy-netlify.md: compliance
170
+
171
+ - Documented `whs status` (the drift row alone — pinned vs. current version,
172
+ versions behind, the `re-check` list — no codebase sweep) and its
173
+ `SessionStart` wiring in the `compliance` section's **Wiring** bullet. Added a
174
+ migration-section note: an already-adopted project that has only fallen behind
175
+ runs `/whs:upgrade`; one scaffolded before the command shipped copies three
176
+ files from the template first.
177
+
178
+ ### templates/eleventy-netlify
179
+
180
+ - New `/whs:upgrade` command (`.claude/commands/whs/upgrade.md`) — reads the
181
+ CHANGELOG delta since the pin, bumps `@piercebarney/whs-eleventy`, works the
182
+ `compliance` backlog one `re-check` row at a time (delegating to the
183
+ `brand`/`content`/`layout`/`pack` stage agents where a slug is theirs), then
184
+ re-pins `standard-version`. Never deploys. Structured like `/whs:build`.
185
+ - New `.claude/hooks/standard-drift.sh` + a `SessionStart` entry in
186
+ `.claude/settings.json` — runs `whs status --hook` at the top of every Claude
187
+ Code session; one line when the pin is behind, silent when current, never
188
+ blocks.
189
+ - `package.json` gains a `status` script (`whs status`).
190
+ - `CLAUDE.md` points at `/whs:upgrade` for the behind-the-standard case.
191
+
192
+ ### packages/whs-eleventy
193
+
194
+ - New `whs status` subcommand (`lib/status.js`) — reuses `compliance.js`'s
195
+ `versionDrift()` (now exported) and reports the delta without running the
196
+ `CHECKS` sweep. `--hook` mode: one compact line when behind, nothing when
197
+ current, always exit 0. Registered in `cli.js`. Tests in `test/status.test.js`
198
+ plus a `versionDrift`-export assertion in `test/compliance.test.js`.
199
+
21
200
  ## 2026-09-08 — content-ops: reservation becomes a real opt-in, not a hardcoded default [non-breaking]
22
201
 
23
202
  Found while building `/whs:build`'s Content stage agent, which needs to know
@@ -50,6 +229,39 @@ true, contradicting the standard's own stated default.
50
229
  keeps `CONTENT.md` + `requests/` and the reserved paragraph; the default
51
230
  keeps neither and shows the in-repo paragraph instead.
52
231
 
232
+ ## 2026-09-08 — /whs:build, the four stage agents, and opt-in verify.sh [non-breaking]
233
+
234
+ Backfilled: this landed the same day as the content-ops entry above but had no
235
+ CHANGELOG entry of its own — a whole build-automation capability was visible
236
+ only in git log. Recorded now so the log is a complete account of what a
237
+ compliant project can rely on, not just what happened to get a dated entry.
238
+
239
+ ### templates/eleventy-netlify
240
+
241
+ - `/whs:build` (`.claude/commands/whs/build.md`) — a Claude Code command that
242
+ runs a scaffolded project's post-`bin/init` work as four fixed stages, always
243
+ in order: Brand → Content → Pages & Layout → Add-on packs. Each stage plans
244
+ first (asking only when the brief/standard doesn't already answer something
245
+ and getting it wrong would force rework), delegates execution to a dedicated
246
+ agent, verifies against `npm run check`, and commits before moving to the
247
+ next stage. Never deploys. Piloted on `eleventy-netlify` only — same "don't
248
+ build a path nothing needs yet" stance as Phoenix/SvelteKit's deferred
249
+ tooling (`TODO.md`).
250
+ - Four stage agents (`.claude/agents/{brand,content,layout,pack}.md`), each
251
+ anchored to a named authority: Brad Frost for Brand (tokens before
252
+ components), Karen McGrane for Content (structure before prose — checks
253
+ `CONTENT.md`'s reservation status before writing anything), Nielsen/Krug for
254
+ Pages & Layout (identify the primary friction point before designing), Kent
255
+ Beck for Add-on packs (one pack per invocation, its own commit, tests first).
256
+
257
+ ### templates/*
258
+
259
+ - Opt-in `.claude/verify.sh` in all three stack templates, wired to a global
260
+ Claude Code Stop hook that lives outside this repo (the operator's own
261
+ `~/.claude/hooks/verify-stop.sh`): a session can't report a build done while
262
+ the project's own gate is red. No-ops for a project that never creates the
263
+ script.
264
+
53
265
  ## 2026-09-04 — sveltekit-netlify's brand token layer, and a Pico gap that predates it [non-breaking]
54
266
 
55
267
  Third of the four functional gaps being closed ahead of the real project
@@ -411,7 +623,7 @@ tag, so `whs compliance --strict` actually gates on being behind on a
411
623
  breaking change rather than treating every changed slug — a typo fix and a
412
624
  broken-mechanism correction alike — as the same easy-to-miss `MANUAL` note.
413
625
  This is the concrete fix for the gap `TODO.md` #1's evidence section
414
- documented: `everydaymoneycalc` being 8 entries behind (several of them
626
+ documented: the one real adopting project being 8 entries behind (several of them
415
627
  breaking, per the backfill above) produced no signal beyond a note buried in
416
628
  a `MANUAL` list.
417
629
 
package/standard/core.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Web project house style — CORE (stack-agnostic)
2
2
 
3
- **Version:** 2026-09-08 · **Status:** active
3
+ **Version:** 2026-09-13 · **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
@@ -79,6 +79,12 @@ contents, and uses one mechanism per concern.
79
79
  sidecar files).
80
80
  - Environment access is centralised (see `#seo-urls` for the build-context
81
81
  module and `#secrets` for tokens).
82
+ - **Feature flags** are boolean environment variables, one named flag per
83
+ concern (`FEATURE_<NAME>`), read through the same centralised env-access
84
+ module as build context — never a scattered `process.env` read. Each flag
85
+ defaults to preserving current behaviour when unset, and is documented in
86
+ `.env.example` with what it gates (`#ads`'s visibility flag is the worked
87
+ example).
82
88
 
83
89
  **Verify.** Review.
84
90
 
@@ -704,6 +710,12 @@ ad directives, and documented.
704
710
  gated on the same `isProduction` flag as `noindex`. Serving ads on a preview /
705
711
  staging URL wastes impressions, risks an invalid-traffic policy strike, and
706
712
  pollutes analytics.
713
+ - **Kill switch.** Ad serving (loader, CMP, and units) also respects a feature
714
+ flag (`#config-idiom`) independent of the `ads:` provider declaration — the
715
+ declaration controls whether the plumbing (CSP, CMP, `ads.txt`) exists at
716
+ all; the flag controls whether it's currently allowed to render. Defaults to
717
+ on, so an unset flag preserves existing behaviour; flipping it off is the
718
+ fast path to pausing ads without a code change.
707
719
  - **Root files.** `ads.txt` (and `app-ads.txt` if the account has apps) at the
708
720
  site root, listing authorized sellers, is generated from data or is a reviewed
709
721
  file — not hand-edited over time. Excluded from the sitemap. Treated like
@@ -725,7 +737,8 @@ standard honest and makes the tradeoff explicit and reviewable.
725
737
  **Verify.** The audit page has an **Ads** panel (only when `ads:` ≠ `none`):
726
738
  `ads.txt` present and parseable; the CMP script present; the CSP grants exactly
727
739
  the declared ad origins; any `'unsafe-*'` exception noted; the loader gated on
728
- `isProduction`. The link/reference check (`#internal-links`) fails on any
740
+ `isProduction`; the kill switch's current state (active vs. paused). The
741
+ link/reference check (`#internal-links`) fails on any
729
742
  `adsbygoogle` / ad-provider reference in a non-production build and on a missing
730
743
  `ads.txt` in the production output. Manually, in a fresh browser on production:
731
744
  decline consent → no ad requests in the network panel; accept → ads load; check
@@ -1422,7 +1435,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
1422
1435
  - production-url: https://example.com
1423
1436
  - content-type: tool # tool | article (article => feeds)
1424
1437
  - publishing-rate: ~5 pages/week
1425
- - standard-version: 2026-09-08 # recommended — enables standard-version drift tracking (#compliance)
1438
+ - standard-version: 2026-09-13 # recommended — enables standard-version drift tracking (#compliance)
1426
1439
  ```
1427
1440
 
1428
1441
  If the section is absent, the assistant's first action is to run the flow above