@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 +2 -0
- package/lib/_project.js +7 -3
- package/lib/compliance.js +12 -5
- package/lib/status.js +103 -0
- package/package.json +1 -1
- package/standard/CHANGELOG.md +214 -2
- package/standard/core.md +16 -3
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
|
-
|
|
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 (!
|
|
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",
|
|
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",
|
|
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 = {
|
|
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.
|
|
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"
|
package/standard/CHANGELOG.md
CHANGED
|
@@ -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:
|
|
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-
|
|
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
|
|
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-
|
|
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
|