@piercebarney/whs-eleventy 2026.9.13 → 2026.9.23
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/check-links.js +22 -10
- package/lib/compliance.js +19 -1
- package/lib/doctor.js +40 -21
- package/lib/status.js +32 -3
- package/package.json +1 -1
- package/standard/CHANGELOG.md +286 -0
- package/standard/core.md +53 -2
package/lib/check-links.js
CHANGED
|
@@ -14,20 +14,32 @@
|
|
|
14
14
|
const fs = require("node:fs");
|
|
15
15
|
const path = require("node:path");
|
|
16
16
|
const { parse } = require("node-html-parser");
|
|
17
|
-
const {
|
|
17
|
+
const { tryProjectRequire } = require("./_project.js");
|
|
18
18
|
|
|
19
19
|
const SITE = "_site";
|
|
20
|
-
const site =
|
|
21
|
-
const thirdparties =
|
|
20
|
+
const site = tryProjectRequire("src/_data/site.js", null);
|
|
21
|
+
const thirdparties = tryProjectRequire("src/_data/thirdparties.js", null);
|
|
22
|
+
|
|
23
|
+
const errors = [];
|
|
24
|
+
const err = (file, msg) => errors.push(`${file}: ${msg}`);
|
|
25
|
+
|
|
26
|
+
// Reported exactly once, here — every origin-dependent check below skips
|
|
27
|
+
// silently on a missing site/thirdparties, relying on these two lines to
|
|
28
|
+
// explain why, rather than one err() per guarded call site.
|
|
29
|
+
if (!site) err("src/_data/site.js", "missing — cannot verify links against the site's own origin");
|
|
30
|
+
if (!thirdparties) {
|
|
31
|
+
err("src/_data/thirdparties.js", "missing — cannot verify declared third-party origins");
|
|
32
|
+
}
|
|
33
|
+
const canCheckOrigins = !!(site && thirdparties);
|
|
22
34
|
|
|
23
35
|
// The /glossary/#slug check runs only when add-ons/glossary/ is applied
|
|
24
36
|
// (it ships src/_data/glossary.js). No file → no glossary → the check is inert.
|
|
25
37
|
const glossary = tryProjectRequire("src/_data/glossary.js");
|
|
26
38
|
const glossarySlugs = new Set(glossary ? glossary.map((t) => t.slug) : []);
|
|
27
39
|
const declaredOrigins = thirdparties
|
|
28
|
-
.flatMap((t) => [t.origin, ...(t.cspOrigins || [])])
|
|
29
|
-
|
|
30
|
-
const allowedOrigins = new Set([site.url, ...declaredOrigins]);
|
|
40
|
+
? thirdparties.flatMap((t) => [t.origin, ...(t.cspOrigins || [])]).filter(Boolean)
|
|
41
|
+
: [];
|
|
42
|
+
const allowedOrigins = new Set([...(site ? [site.url] : []), ...declaredOrigins]);
|
|
31
43
|
|
|
32
44
|
// Privacy/ad opt-out destinations that policy prose links to, plus the
|
|
33
45
|
// search-engine webmaster consoles the /audit/ page links to. All are
|
|
@@ -55,9 +67,6 @@ const REQUIRED_LD = {
|
|
|
55
67
|
Article: ["headline"],
|
|
56
68
|
};
|
|
57
69
|
|
|
58
|
-
const errors = [];
|
|
59
|
-
const err = (file, msg) => errors.push(`${file}: ${msg}`);
|
|
60
|
-
|
|
61
70
|
// ---- CSP <-> origin manifest agreement -------------------------------------
|
|
62
71
|
(function checkCsp() {
|
|
63
72
|
let toml;
|
|
@@ -78,6 +87,7 @@ const err = (file, msg) => errors.push(`${file}: ${msg}`);
|
|
|
78
87
|
}
|
|
79
88
|
}),
|
|
80
89
|
);
|
|
90
|
+
if (!thirdparties) return; // reported once, above, when the load failed
|
|
81
91
|
// Every origin the CSP grants must be a declared party — matched against each
|
|
82
92
|
// party's `origin` or its `cspOrigins` list. (The reverse isn't required: a
|
|
83
93
|
// host / form processor is in thirdparties.js but is not a browser-resource
|
|
@@ -132,6 +142,7 @@ function checkRef(file, pageUrl, raw, { external = true, isHyperlink = false } =
|
|
|
132
142
|
|
|
133
143
|
if (/^https?:\/\//i.test(v)) {
|
|
134
144
|
if (!external) return;
|
|
145
|
+
if (!canCheckOrigins) return; // reported once, above, when the load failed
|
|
135
146
|
let origin;
|
|
136
147
|
try {
|
|
137
148
|
origin = new URL(v).origin;
|
|
@@ -213,7 +224,7 @@ for (const f of htmlFiles) {
|
|
|
213
224
|
} catch {
|
|
214
225
|
origin = "";
|
|
215
226
|
}
|
|
216
|
-
if (origin && origin !== site.url) {
|
|
227
|
+
if (canCheckOrigins && origin && origin !== site.url) {
|
|
217
228
|
const rel2 = (a.getAttribute("rel") || "").toLowerCase();
|
|
218
229
|
if (!/\bnoopener\b|\bnoreferrer\b/.test(rel2)) {
|
|
219
230
|
err(f, `external <a> without rel="noopener": ${href}`);
|
|
@@ -256,6 +267,7 @@ for (const f of htmlFiles) {
|
|
|
256
267
|
// dynamically-built URL), but it catches the common case: a hardcoded
|
|
257
268
|
// external origin in client JS that was never added to the manifest.
|
|
258
269
|
(function checkJsOrigins() {
|
|
270
|
+
if (!canCheckOrigins) return; // reported once, above, when the load failed
|
|
259
271
|
const jsFiles = allFiles.filter((f) => f.endsWith(".js") && !f.endsWith("links-report.json"));
|
|
260
272
|
const ORIGIN_LITERAL = /(?:fetch|new\s+URL)\s*\(\s*[`'"](https?:\/\/[^`'"\s]+)[`'"]/g;
|
|
261
273
|
for (const f of jsFiles) {
|
package/lib/compliance.js
CHANGED
|
@@ -198,6 +198,13 @@ const CHECKS = {
|
|
|
198
198
|
},
|
|
199
199
|
|
|
200
200
|
styling: () => {
|
|
201
|
+
const njkRoots = ["src", "static", "scripts"].map((d) => path.join(ROOT, d));
|
|
202
|
+
const anyNjk = njkRoots.some((r) => walk(r).some((f) => f.endsWith(".njk")));
|
|
203
|
+
if (!anyNjk)
|
|
204
|
+
return {
|
|
205
|
+
status: MANUAL,
|
|
206
|
+
note: "no .njk files found — can't verify against a non-.njk layout",
|
|
207
|
+
};
|
|
201
208
|
const shift = grepSrc(/style=(["']).*?\1/, [".njk"]);
|
|
202
209
|
return shift.length
|
|
203
210
|
? { status: FAIL, note: `inline style= in ${shift.join(", ")}` }
|
|
@@ -350,8 +357,14 @@ const CHECKS = {
|
|
|
350
357
|
(f) =>
|
|
351
358
|
/\.(css|js|njk)$/.test(f) && f !== "src/_data/brand.js" && f !== "src/tokens.css.njk",
|
|
352
359
|
);
|
|
360
|
+
const existing = targets.filter((rel) => has(rel));
|
|
361
|
+
if (existing.length === 0)
|
|
362
|
+
return {
|
|
363
|
+
status: MANUAL,
|
|
364
|
+
note: "no static/style.css, scripts/og-card.js|icons.js, or src/ found — can't verify against a non-standard-layout project",
|
|
365
|
+
};
|
|
353
366
|
const bad = [];
|
|
354
|
-
for (const rel of
|
|
367
|
+
for (const rel of existing) {
|
|
355
368
|
const t = read(rel);
|
|
356
369
|
if (!t) continue;
|
|
357
370
|
for (const line of t.split("\n")) {
|
|
@@ -489,6 +502,11 @@ const CHECKS = {
|
|
|
489
502
|
},
|
|
490
503
|
|
|
491
504
|
"domain-tests": () => {
|
|
505
|
+
if (!has("static/calc"))
|
|
506
|
+
return {
|
|
507
|
+
status: MANUAL,
|
|
508
|
+
note: "no static/calc/ — can't verify against a non-standard-layout project",
|
|
509
|
+
};
|
|
492
510
|
const mods = walk(path.join(ROOT, "static/calc"))
|
|
493
511
|
.filter((f) => f.endsWith(".js"))
|
|
494
512
|
.map((f) => path.basename(f, ".js"));
|
package/lib/doctor.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const fs = require("node:fs");
|
|
22
22
|
const path = require("node:path");
|
|
23
23
|
const { execSync } = require("node:child_process");
|
|
24
|
-
const { ROOT, projectRequire, openRequests } = require("./_project.js");
|
|
24
|
+
const { ROOT, projectRequire, tryProjectRequire, openRequests } = require("./_project.js");
|
|
25
25
|
const { asRegexMap } = require("./header-expect.js");
|
|
26
26
|
|
|
27
27
|
const CACHE = path.join(ROOT, ".cache", "doctor.json");
|
|
@@ -29,7 +29,6 @@ const CACHE = path.join(ROOT, ".cache", "doctor.json");
|
|
|
29
29
|
// Loaded lazily (inside the functions) so requiring this module for its API
|
|
30
30
|
// doesn't fail in a dir without a project tree.
|
|
31
31
|
const loadSite = () => projectRequire("src/_data/site.js");
|
|
32
|
-
const loadThirdparties = () => projectRequire("src/_data/thirdparties.js");
|
|
33
32
|
|
|
34
33
|
// Expected header value patterns — the single source (header-expect.js) also
|
|
35
34
|
// feeds the /audit/ page's live-header check via _data/headerExpect.js.
|
|
@@ -51,8 +50,8 @@ function runRepoChecks({ preflight = false } = {}) {
|
|
|
51
50
|
const checks = [];
|
|
52
51
|
const add = (id, label, ok, severity, detail) =>
|
|
53
52
|
checks.push({ id, label, ok, severity: ok ? "ok" : severity, detail });
|
|
54
|
-
const site =
|
|
55
|
-
const thirdparties =
|
|
53
|
+
const site = tryProjectRequire("src/_data/site.js", null);
|
|
54
|
+
const thirdparties = tryProjectRequire("src/_data/thirdparties.js", null);
|
|
56
55
|
|
|
57
56
|
// 1. Security headers declared in netlify.toml
|
|
58
57
|
const toml = readText("netlify.toml");
|
|
@@ -74,21 +73,31 @@ function runRepoChecks({ preflight = false } = {}) {
|
|
|
74
73
|
);
|
|
75
74
|
|
|
76
75
|
// 2. Every origin the CSP grants is a declared third party
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
76
|
+
if (!thirdparties) {
|
|
77
|
+
add(
|
|
78
|
+
"csp-thirdparties",
|
|
79
|
+
"CSP origins all appear in src/_data/thirdparties.js",
|
|
80
|
+
false,
|
|
81
|
+
"blocker",
|
|
82
|
+
"src/_data/thirdparties.js missing",
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
const cspMatch = toml.match(/Content-Security-Policy\s*=\s*"([^"]+)"/);
|
|
86
|
+
const cspOrigins = cspMatch
|
|
87
|
+
? [...new Set((cspMatch[1].match(/https?:\/\/[^\s;'"]+/g) || []).map(originOf))]
|
|
88
|
+
: [];
|
|
89
|
+
const declaredOrigins = new Set(
|
|
90
|
+
thirdparties.flatMap((t) => [t.origin, ...(t.cspOrigins || [])]),
|
|
91
|
+
);
|
|
92
|
+
const undeclared = cspOrigins.filter((o) => !declaredOrigins.has(o));
|
|
93
|
+
add(
|
|
94
|
+
"csp-thirdparties",
|
|
95
|
+
"CSP origins all appear in src/_data/thirdparties.js",
|
|
96
|
+
undeclared.length === 0,
|
|
97
|
+
"warn",
|
|
98
|
+
undeclared.length ? `undeclared: ${undeclared.join(", ")}` : "in sync",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
// 3. .nvmrc matches the running Node major
|
|
@@ -190,8 +199,18 @@ function runRepoChecks({ preflight = false } = {}) {
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
// 8. site.url is a clean absolute origin
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
if (!site) {
|
|
203
|
+
add(
|
|
204
|
+
"site-url",
|
|
205
|
+
"src/_data/site.js url is a clean https origin",
|
|
206
|
+
false,
|
|
207
|
+
"blocker",
|
|
208
|
+
"src/_data/site.js missing",
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
const cleanUrl = /^https:\/\/[^/]+\.[a-z]{2,}$/i.test(site.url);
|
|
212
|
+
add("site-url", "src/_data/site.js url is a clean https origin", cleanUrl, "warn", site.url);
|
|
213
|
+
}
|
|
195
214
|
|
|
196
215
|
return checks;
|
|
197
216
|
}
|
package/lib/status.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
// full codebase sweep. Cheap enough to run on every Claude Code SessionStart.
|
|
5
5
|
//
|
|
6
6
|
// whs status print the version delta + the re-check rows
|
|
7
|
-
// whs status --hook silent when current; one line when behind
|
|
8
|
-
//
|
|
7
|
+
// whs status --hook silent when current; one line when behind, plus one
|
|
8
|
+
// more if the project's src/_data/dashboard.js
|
|
9
|
+
// (core.md#adopting's one-time setup checklist) has
|
|
10
|
+
// unconfirmed items (for the SessionStart hook —
|
|
11
|
+
// never non-zero, never noisy)
|
|
9
12
|
//
|
|
10
13
|
// Exit code is always 0: like `compliance` without --strict, drift is a
|
|
11
14
|
// backlog signal, not a build break.
|
|
@@ -14,7 +17,29 @@ const fs = require("node:fs");
|
|
|
14
17
|
const path = require("node:path");
|
|
15
18
|
|
|
16
19
|
const { versionDrift } = require("./compliance.js");
|
|
17
|
-
const { resolveStandard } = require("./_project.js");
|
|
20
|
+
const { resolveStandard, tryProjectRequire } = require("./_project.js");
|
|
21
|
+
|
|
22
|
+
// Best-effort: count src/_data/dashboard.js items still missing a verifiedOn
|
|
23
|
+
// date (core.md#adopting's one-time setup checklist, rendered on /audit/'s
|
|
24
|
+
// Infra tab). Never throws — a missing or malformed dashboard.js just means
|
|
25
|
+
// nothing to report, same as doctor.js's live tier treats "unavailable".
|
|
26
|
+
function pendingDashboardCount() {
|
|
27
|
+
const dashboard = tryProjectRequire("src/_data/dashboard.js", null);
|
|
28
|
+
if (!dashboard || !Array.isArray(dashboard.items)) return 0;
|
|
29
|
+
return dashboard.items.filter((i) => !(i && String(i.verifiedOn || "").trim())).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printDashboardPending() {
|
|
33
|
+
let pending;
|
|
34
|
+
try {
|
|
35
|
+
pending = pendingDashboardCount();
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (pending > 0) {
|
|
40
|
+
console.log(`${pending} manual checklist item(s) pending — see /audit/`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
18
43
|
|
|
19
44
|
// `status` answers "is there a newer standard available on this machine?" — so
|
|
20
45
|
// it resolves the standard text with { preferLocal: true }: the local canonical
|
|
@@ -52,6 +77,7 @@ function main() {
|
|
|
52
77
|
rows.length === 1 && rows[0].startsWith("MANUAL: standard text not resolvable");
|
|
53
78
|
if (unresolved) {
|
|
54
79
|
if (!hook) console.log(rows[0]);
|
|
80
|
+
else printDashboardPending();
|
|
55
81
|
return;
|
|
56
82
|
}
|
|
57
83
|
|
|
@@ -62,6 +88,8 @@ function main() {
|
|
|
62
88
|
? `web house style: up to date (pinned ${pin}, standard at ${current})`
|
|
63
89
|
: "web house style: standard-version not pinned in CLAUDE.md",
|
|
64
90
|
);
|
|
91
|
+
} else {
|
|
92
|
+
printDashboardPending();
|
|
65
93
|
}
|
|
66
94
|
return;
|
|
67
95
|
}
|
|
@@ -78,6 +106,7 @@ function main() {
|
|
|
78
106
|
`⚠ web house style: ${behind}, ${recheck} — pinned ${pin}, standard at ${current}. ` +
|
|
79
107
|
`Run /whs:upgrade to catch up.`,
|
|
80
108
|
);
|
|
109
|
+
printDashboardPending();
|
|
81
110
|
return;
|
|
82
111
|
}
|
|
83
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piercebarney/whs-eleventy",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.23",
|
|
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
|
@@ -21,6 +21,292 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## 2026-09-23 — Republish 2026-09-22-2 as a clean bare-date version [non-breaking]
|
|
25
|
+
|
|
26
|
+
### packages/whs-eleventy
|
|
27
|
+
|
|
28
|
+
No new content beyond what's already documented below. This bump republishes
|
|
29
|
+
everything already documented under the 2026-09-22-2 entry (the
|
|
30
|
+
`check-links.js` no-`src/`-layout crash fix) as a clean bare-date version,
|
|
31
|
+
specifically to trigger the npm publish of `@piercebarney/whs-eleventy` that
|
|
32
|
+
the 2026-09-22-2 same-day-suffix mechanism deliberately does not trigger on
|
|
33
|
+
its own (per that suffix's own documented exception in `CLAUDE.md`'s
|
|
34
|
+
Versions section: a suffixed pin gets no git tag and no npm publish).
|
|
35
|
+
|
|
36
|
+
This release also newly documents a second, separately-landed fix shipping
|
|
37
|
+
in the same publish: `styling`, `brand-source`, and `domain-tests` in
|
|
38
|
+
`lib/compliance.js` each built their scan-target list from hardcoded
|
|
39
|
+
standard-layout paths and treated "found zero files to scan" as "found zero
|
|
40
|
+
violations" — a false `PASS` on a project whose layout doesn't match the
|
|
41
|
+
standard yet, rather than the `MANUAL`/can't-verify result the other checks
|
|
42
|
+
correctly report in the same situation. Found on a real adoption and filed
|
|
43
|
+
as `feedback/2026-09-22-compliance-silent-pass-on-nonstandard-layout.md`;
|
|
44
|
+
fixed in `96aeac2` by checking whether each check's target(s) exist at all
|
|
45
|
+
before scanning — an existing-but-empty target (e.g. `static/calc/` with no
|
|
46
|
+
modules yet) still correctly reports `PASS`.
|
|
47
|
+
|
|
48
|
+
`core.md`, `stacks/*.md`, `index.json`, and every `standard-version:` pin
|
|
49
|
+
move from `2026-09-22-2` to `2026-09-23`; `packages/whs-eleventy/package.json`
|
|
50
|
+
moves to the corresponding npm version, `2026.9.23`. No `core.md` chapter,
|
|
51
|
+
`stacks/*.md`, or `index.json` content changed.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2026-09-22-2 [non-breaking]
|
|
56
|
+
|
|
57
|
+
### packages/whs-eleventy
|
|
58
|
+
|
|
59
|
+
`check-links.js` had the same crash class as `doctor.js`'s already-fixed
|
|
60
|
+
`MODULE_NOT_FOUND`: an unconditional module-top-level
|
|
61
|
+
`projectRequire("src/_data/site.js")` (and `thirdparties.js`) that throws on
|
|
62
|
+
a project with no `src/` tree, instead of reporting the missing layout as a
|
|
63
|
+
finding — found while reconciling a real adoption's migration backlog and
|
|
64
|
+
filed as `feedback/2026-09-22-check-links-same-crash-unfixed.md`. Fixed in
|
|
65
|
+
`e73de7d`: the two missing-file conditions are now reported exactly once,
|
|
66
|
+
and each dependent origin-validation check (internal-link/anchor/asset/
|
|
67
|
+
JSON-LD) skips silently rather than crashing or false-positiving. This is a
|
|
68
|
+
same-day suffix release — per `CLAUDE.md`'s Versions section it deliberately
|
|
69
|
+
does **not** get a git tag or npm publish; `packages/whs-eleventy/package.json`
|
|
70
|
+
stays at `2026.9.22`, which does not yet include this fix, until a future
|
|
71
|
+
bare-date promotion release republishes it. No `core.md` chapter,
|
|
72
|
+
`stacks/*.md`, or `index.json` content changed.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 2026-09-22 [non-breaking]
|
|
77
|
+
|
|
78
|
+
### packages/whs-eleventy
|
|
79
|
+
|
|
80
|
+
`doctor.js`'s `runRepoChecks()` crashed with an unhandled `MODULE_NOT_FOUND`
|
|
81
|
+
when a project had no `src/` tree at all, instead of reporting the missing
|
|
82
|
+
layout as a finding — found while adopting a real project via `/whs:adopt`
|
|
83
|
+
and filed as `feedback/2026-09-22-doctor-crashes-on-no-src-layout.md`. Fixed
|
|
84
|
+
in `7b43511`; this release publishes the fix. No `core.md` chapter,
|
|
85
|
+
`stacks/*.md`, or `index.json` content changed.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 2026-09-18 — Republish 2026-09-17-2 as a clean bare-date version [non-breaking]
|
|
90
|
+
|
|
91
|
+
No new content. This bump republishes everything already documented under
|
|
92
|
+
the 2026-09-17 entries below as a clean bare-date version, specifically to
|
|
93
|
+
trigger the npm publish of `@piercebarney/whs-eleventy` that the
|
|
94
|
+
2026-09-17-2 same-day-suffix mechanism deliberately does not trigger on its
|
|
95
|
+
own (per that suffix's own documented exception in `CLAUDE.md`'s Versions
|
|
96
|
+
section: a suffixed pin gets no git tag and no npm publish). `core.md`,
|
|
97
|
+
`stacks/*.md`, `index.json`, and every `standard-version:` pin move from
|
|
98
|
+
`2026-09-17-2` to `2026-09-18`; `packages/whs-eleventy/package.json` moves to
|
|
99
|
+
the corresponding npm version. Nothing else changes.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 2026-09-17 — /whs:new, and both commands go global [non-breaking]
|
|
104
|
+
|
|
105
|
+
### core: adopting
|
|
106
|
+
|
|
107
|
+
A new paragraph, "Mechanized new-project start," names `/whs:new` —
|
|
108
|
+
`templates/commands/whs/new.md` — as the pre-scaffold sibling to
|
|
109
|
+
`/whs:build`/`/whs:upgrade`/`/whs:adopt`: it mechanizes this chapter's
|
|
110
|
+
"Starting point" paragraph plus the six-question flow for a project that
|
|
111
|
+
doesn't exist yet. Before the user commits to a framework it reports each
|
|
112
|
+
stack's real readiness — derived live from `index.json`'s `status` field
|
|
113
|
+
plus an actual filesystem check for that stack's `bin/init` and stage
|
|
114
|
+
agents, not a static description that can drift — then applies this
|
|
115
|
+
paragraph's own fit test: an idea that doesn't honestly fit any offered
|
|
116
|
+
stack routes to the same `feedback/` mechanism `/whs:adopt` already uses,
|
|
117
|
+
filed before scaffolding rather than worked around. Knowingly choosing a
|
|
118
|
+
stack still marked `draft` is explicitly **not** that case — it's an
|
|
119
|
+
informed choice against an already-tracked, deliberately-deferred
|
|
120
|
+
limitation, not a new gap. Once a stack is chosen it scaffolds via the
|
|
121
|
+
existing, unmodified `templates/new-project.sh` and hands off to
|
|
122
|
+
`/whs:build` where a guided build actually exists (Eleventy today);
|
|
123
|
+
for a draft-stack scaffold it reports the partial result honestly and
|
|
124
|
+
points at that stack's own `TEMPLATE.md`.
|
|
125
|
+
|
|
126
|
+
### stacks/eleventy-netlify.md
|
|
127
|
+
|
|
128
|
+
A short cross-reference in its own `#adopting` section: for a brand-new
|
|
129
|
+
project, `/whs:new` will report Eleventy fully ready — scaffold, the full
|
|
130
|
+
`/whs:build` pipeline, and compliance tooling all already in place, the
|
|
131
|
+
one stack where the readiness table's top row is real today.
|
|
132
|
+
|
|
133
|
+
### stacks/phoenix.md, stacks/sveltekit-netlify.md
|
|
134
|
+
|
|
135
|
+
Each gets the matching honest counterpart: `/whs:new` reports them as
|
|
136
|
+
`draft` — scaffold-only via `templates/new-project.sh`, no `bin/init`
|
|
137
|
+
(manual placeholder fill per each stack's own `TEMPLATE.md`), no guided
|
|
138
|
+
`/whs:build` equivalent, no compliance tooling — the same gap list their
|
|
139
|
+
own `#adopting` sections already carry, not a new one.
|
|
140
|
+
|
|
141
|
+
### templates/commands
|
|
142
|
+
|
|
143
|
+
New file, `templates/commands/whs/new.md` — the fourth command in the
|
|
144
|
+
`/whs:build` / `/whs:upgrade` / `/whs:adopt` family, this one for a
|
|
145
|
+
destination that doesn't exist yet, not even a directory. It walks
|
|
146
|
+
`templates/concept-brief.md` conversationally (the same ideation
|
|
147
|
+
`COWORK.md`'s "New project" flow already does, run here instead), stops
|
|
148
|
+
before answering the framework question to show the live readiness table,
|
|
149
|
+
applies the fit test with its three outcomes (fits cleanly, including a
|
|
150
|
+
knowing `draft` choice / no offered stack fits, routing to `feedback/` /
|
|
151
|
+
abandon), then scaffolds via `templates/new-project.sh` unmodified and
|
|
152
|
+
reports one of three real outcomes matching the script's actual behavior:
|
|
153
|
+
`bin/init` ran (Eleventy), a partial scaffold with no `bin/init` (Phoenix,
|
|
154
|
+
SvelteKit today), or a genuine unexpected failure.
|
|
155
|
+
|
|
156
|
+
### README.md
|
|
157
|
+
|
|
158
|
+
Both `/whs:new` and `/whs:adopt` switch distribution model: instead of a
|
|
159
|
+
per-project `cp` of the command file into `.claude/commands/whs/`, the
|
|
160
|
+
`## Home` section now documents a single global symlink,
|
|
161
|
+
`~/.claude/commands/whs/` → this repo's `templates/commands/whs/`, set up
|
|
162
|
+
once per machine. This is a real behavior change for `/whs:adopt`, not
|
|
163
|
+
just new documentation for `/whs:new`: a machine that already did the old
|
|
164
|
+
per-project copy-in keeps working for that project, but every new project
|
|
165
|
+
now picks up both commands automatically, and editing a command's source
|
|
166
|
+
here updates what's "installed" everywhere with no copy-and-drift step.
|
|
167
|
+
The "Onboarding an existing project" subsection drops its old copy
|
|
168
|
+
instructions and now just points at `## Home`.
|
|
169
|
+
|
|
170
|
+
This bump moves the doc pins to `2026-09-17-2` (a same-day suffix — see
|
|
171
|
+
this repo's `CLAUDE.md`'s Versions section): `2026-09-17` was already used
|
|
172
|
+
today for the `/whs:adopt` entry below. `packages/*/package.json` is left
|
|
173
|
+
untouched on purpose; this release gets no git tag and no npm publish on
|
|
174
|
+
its own.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 2026-09-17 — /whs:adopt mechanizes onboarding for a preexisting project [non-breaking]
|
|
179
|
+
|
|
180
|
+
### core: adopting
|
|
181
|
+
|
|
182
|
+
A new paragraph, "Mechanized onboarding," names `/whs:adopt` — copied from
|
|
183
|
+
`templates/commands/whs/adopt.md` into a project's own
|
|
184
|
+
`.claude/commands/whs/adopt.md` — as the command that runs this entire
|
|
185
|
+
chapter end-to-end inside a project that has never adopted the standard,
|
|
186
|
+
including one on a stack this repo has no binding for. It detects the
|
|
187
|
+
stack, runs the six-question flow already documented here, and, before
|
|
188
|
+
writing anything, judges whether the project is even compatible with the
|
|
189
|
+
standard: a genuine out-of-scope project (native mobile, an embedded CLI,
|
|
190
|
+
anything the standard's domain never covered) gets a clean decline with
|
|
191
|
+
nothing written or filed; a real gap in the standard routes to the existing
|
|
192
|
+
`feedback/` mechanism, the same "Starting point" reasoning this chapter
|
|
193
|
+
already applies to a new-project brief that doesn't honestly fit an offered
|
|
194
|
+
stack. Once adoption proceeds it writes the `## House style` block, wires in
|
|
195
|
+
real tooling where it exists, and produces — but deliberately does not
|
|
196
|
+
work — an initial migration backlog, which stays incremental by hand or via
|
|
197
|
+
`/whs:upgrade` afterward. Nothing here changes what a compliant project
|
|
198
|
+
already had to do; this is a new, opt-in way to reach the same state
|
|
199
|
+
`core.md#adopting` already describes.
|
|
200
|
+
|
|
201
|
+
### stacks/eleventy-netlify.md
|
|
202
|
+
|
|
203
|
+
Its own `#adopting` section gets a short cross-reference: for a project
|
|
204
|
+
`/whs:adopt` onboards, it proposes `@piercebarney/whs-eleventy` as a
|
|
205
|
+
devDependency plus `check`/`compliance`/`status` npm scripts (asking first
|
|
206
|
+
if a conflicting script already exists or the build setup looks
|
|
207
|
+
non-trivial), then runs `npm run compliance` for the initial backlog — the
|
|
208
|
+
one stack where the mechanized path can actually run a real check.
|
|
209
|
+
|
|
210
|
+
### stacks/phoenix.md, stacks/sveltekit-netlify.md
|
|
211
|
+
|
|
212
|
+
Each gets the same kind of cross-reference, honestly scoped down: since
|
|
213
|
+
neither draft binding has compliance-sweep tooling yet (the same
|
|
214
|
+
`mix a11y`/`links.check`/`whs.check` gap their own `#adopting` sections
|
|
215
|
+
already note), `/whs:adopt` can still detect the stack and record the
|
|
216
|
+
`## House style` block with the gate command filled in from what the
|
|
217
|
+
project already runs (`mix check` / `npm run check`), but the migration
|
|
218
|
+
backlog for these two comes from walking `core.md`'s chapters by hand
|
|
219
|
+
rather than from an automated sweep.
|
|
220
|
+
|
|
221
|
+
### templates/commands
|
|
222
|
+
|
|
223
|
+
New file, `templates/commands/whs/adopt.md` — the command itself, the
|
|
224
|
+
third in the `/whs:build` / `/whs:upgrade` / `/whs:adopt` family, this one
|
|
225
|
+
for a project that's never touched the standard at all. Unlike the other
|
|
226
|
+
two it assumes nothing scaffolded yet (no stage agents, no `## House style`
|
|
227
|
+
block, possibly no stack binding), so it runs mostly inline: two guards
|
|
228
|
+
before starting (an already-adopted project gets pointed at `/whs:upgrade`
|
|
229
|
+
instead; a directory with no manifest at all gets pointed at
|
|
230
|
+
`templates/new-project.sh` instead), then stack detection, the
|
|
231
|
+
six-question flow, the compatibility judgment with its three outcomes
|
|
232
|
+
(compatible / out-of-scope-not-a-defect / genuine-gap-file-a-feedback-note),
|
|
233
|
+
recording the `## House style` block, tiered tooling wiring, and a
|
|
234
|
+
committed `.claude/whs-migration-backlog.md` as the durable artifact —
|
|
235
|
+
never a chat transcript — of what the project still owes.
|
|
236
|
+
|
|
237
|
+
### README.md
|
|
238
|
+
|
|
239
|
+
New "Onboarding an existing project" subsection under "How to use it,"
|
|
240
|
+
documenting the one-time manual copy-in (`cp
|
|
241
|
+
~/.claude/standards/web-house-style/templates/commands/whs/adopt.md
|
|
242
|
+
.claude/commands/whs/adopt.md`) and pointing at `/whs:adopt` — the same
|
|
243
|
+
"standard checked out at `~/.claude/standards/web-house-style/`"
|
|
244
|
+
assumption every adopted project's own `CLAUDE.md` directive already
|
|
245
|
+
makes, no new install mechanism.
|
|
246
|
+
|
|
247
|
+
### CLAUDE.md, .claude/agents/infra.md
|
|
248
|
+
|
|
249
|
+
Both the root `CLAUDE.md` agent-roster listing and `infra`'s own Scope
|
|
250
|
+
section are extended to name stack-agnostic content living directly at the
|
|
251
|
+
`templates/` root — not nested inside any single `templates/<stack>/`,
|
|
252
|
+
e.g. `templates/concept-brief.md`, `templates/new-project.sh`, and now
|
|
253
|
+
`templates/commands/` — as `infra`'s territory. `/whs:adopt` is the first
|
|
254
|
+
file to actually land there; without this the delegation rule this repo's
|
|
255
|
+
own `CLAUDE.md` requires had no answer for who owns it.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 2026-09-13 — One-time adopting setup becomes a tracked, dated confirmation [non-breaking]
|
|
260
|
+
|
|
261
|
+
### core: adopting
|
|
262
|
+
|
|
263
|
+
The one-time human setup a project owes after adopting/converting/upgrading
|
|
264
|
+
(secrets, host config, DNS) is now tracked the same way `#compliance` already
|
|
265
|
+
tracks its `MANUAL` rows: a committed, dated `verifiedOn` confirmation per
|
|
266
|
+
item, plus a best-effort, non-blocking session-start check that flags
|
|
267
|
+
unconfirmed items. This is not a gate — same reasoning `#compliance` gives
|
|
268
|
+
for keeping `MANUAL` rows out of `#the-gate` — so nothing that was compliant
|
|
269
|
+
before this change becomes non-compliant after it; the mechanism is purely
|
|
270
|
+
additive.
|
|
271
|
+
|
|
272
|
+
### templates/eleventy-netlify
|
|
273
|
+
|
|
274
|
+
Implements the contract for real: `bin/init` now seeds
|
|
275
|
+
`src/_data/dashboard.js` with the one-time setup items, committed as part of
|
|
276
|
+
the initial commit (a reviewer-caught ordering bug in the original build,
|
|
277
|
+
now fixed). `packages/whs-eleventy`'s `whs status --hook` (already
|
|
278
|
+
SessionStart-hooked) counts unconfirmed dashboard items and prints a
|
|
279
|
+
best-effort pending-count line. The dangling `BUILD-STATUS.md` reference —
|
|
280
|
+
never actually created by anything — is retired from `bin/init` and
|
|
281
|
+
`TEMPLATE.md`.
|
|
282
|
+
|
|
283
|
+
### stacks/sveltekit-netlify, stacks/phoenix
|
|
284
|
+
|
|
285
|
+
Each gets a short honest note in its `#adopting` mirror that this tracking
|
|
286
|
+
isn't built for that stack yet. No functional or template changes.
|
|
287
|
+
|
|
288
|
+
This bump moves the doc pins to `2026-09-13-2` (a same-day suffix — see this
|
|
289
|
+
repo's `CLAUDE.md`'s Versions section): `2026-09-13` was already used today
|
|
290
|
+
for the republish entry below. `packages/whs-eleventy/package.json` is left
|
|
291
|
+
untouched on purpose; this release gets no git tag and no npm publish on its
|
|
292
|
+
own.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 2026-09-13 — Republish 2026-09-12-2 as a clean bare-date version [non-breaking]
|
|
297
|
+
|
|
298
|
+
No new content. This bump republishes everything already documented under
|
|
299
|
+
the 2026-09-11 and 2026-09-12 entries below as a clean bare-date version,
|
|
300
|
+
specifically to trigger the npm publish of `@piercebarney/whs-eleventy` that
|
|
301
|
+
the 2026-09-12-2 same-day-suffix mechanism deliberately does not trigger on
|
|
302
|
+
its own (per that suffix's own documented exception in `CLAUDE.md`'s
|
|
303
|
+
Versions section: a suffixed pin gets no git tag and no npm publish). `core.md`,
|
|
304
|
+
`stacks/*.md`, `index.json`, and every `standard-version:` pin move from
|
|
305
|
+
`2026-09-12-2` to `2026-09-13`; `packages/whs-eleventy/package.json` moves to
|
|
306
|
+
the corresponding npm version. Nothing else changes.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
24
310
|
## 2026-09-12 — A same-day-release suffix, two feedback fixes, and an /whs:upgrade false-negative [non-breaking]
|
|
25
311
|
|
|
26
312
|
### templates/eleventy-netlify
|
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-23 · **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
|
|
@@ -1394,6 +1394,38 @@ brief cannot honestly fit an offered stack, that is a `feedback/` note (below)
|
|
|
1394
1394
|
filed *before* the build — resolved as a new binding, or the nearest stack with
|
|
1395
1395
|
the compromise recorded.
|
|
1396
1396
|
|
|
1397
|
+
**Mechanized new-project start.** `/whs:new` — `templates/commands/whs/new.md`,
|
|
1398
|
+
available the same way as `/whs:adopt` via the `~/.claude/commands/whs/`
|
|
1399
|
+
symlink (`README.md`'s `## Home` section) — mechanizes this "Starting point"
|
|
1400
|
+
paragraph plus the six-question flow above,
|
|
1401
|
+
for a project that doesn't exist yet. Before asking the user to commit to a
|
|
1402
|
+
framework, it reports each stack's actual readiness live — derived from
|
|
1403
|
+
`index.json`'s status plus a real filesystem check (does that stack's
|
|
1404
|
+
`bin/init` exist? do its stage agents exist?), not a static description that
|
|
1405
|
+
can drift out of date — naming explicitly which stacks carry a full pipeline
|
|
1406
|
+
(scaffold plus a guided build) versus a draft scaffold with known,
|
|
1407
|
+
already-tracked gaps. It then applies this paragraph's own test: if the
|
|
1408
|
+
described idea doesn't honestly fit any offered stack, that's the same
|
|
1409
|
+
`feedback/` path above, filed before scaffolding rather than worked around —
|
|
1410
|
+
knowingly choosing a stack still marked `draft` is *not* that case; it's an
|
|
1411
|
+
informed choice against an already-tracked, deliberately-deferred limitation,
|
|
1412
|
+
not a new gap. Once a stack is chosen, `/whs:new` hands off to `/whs:build`
|
|
1413
|
+
where a guided build actually exists (Eleventy today); for a draft-stack
|
|
1414
|
+
scaffold it reports the partial result honestly and points at that stack's
|
|
1415
|
+
own `TEMPLATE.md`.
|
|
1416
|
+
|
|
1417
|
+
**One-time setup.** Adopting, converting, or upgrading against the standard
|
|
1418
|
+
often leaves a project owing **one-time human setup** no script can finish for
|
|
1419
|
+
it — hosting/repo config, DNS, minting a secret (`#secrets`). That debt is
|
|
1420
|
+
tracked the same way `#compliance` already tracks a `MANUAL` row: a committed
|
|
1421
|
+
item carrying a dated `verifiedOn` confirmation, not a message printed once
|
|
1422
|
+
to a terminal and forgotten. A session-start check flags any item still
|
|
1423
|
+
unconfirmed so it can't go silently stale — **not a gate**; per
|
|
1424
|
+
`#compliance`'s own reasoning for staying out of `#the-gate`, visibility beats
|
|
1425
|
+
blocking so an unrelated pending item never freezes incremental adoption. The
|
|
1426
|
+
concrete shape — which items, how they're seeded, how the check runs — lives
|
|
1427
|
+
in each impl doc's own new-project checklist.
|
|
1428
|
+
|
|
1397
1429
|
**Record** the result near the top of the project's `.claude/CLAUDE.md` /
|
|
1398
1430
|
`AGENTS.md` as a `## House style` section — a directive paragraph plus a data
|
|
1399
1431
|
block. The recommended text:
|
|
@@ -1435,7 +1467,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
|
|
|
1435
1467
|
- production-url: https://example.com
|
|
1436
1468
|
- content-type: tool # tool | article (article => feeds)
|
|
1437
1469
|
- publishing-rate: ~5 pages/week
|
|
1438
|
-
- standard-version: 2026-09-
|
|
1470
|
+
- standard-version: 2026-09-23 # recommended — enables standard-version drift tracking (#compliance)
|
|
1439
1471
|
```
|
|
1440
1472
|
|
|
1441
1473
|
If the section is absent, the assistant's first action is to run the flow above
|
|
@@ -1464,6 +1496,25 @@ worked incrementally (the impl doc's migration path is the ordered version of
|
|
|
1464
1496
|
that backlog). The project is not expected to be all-`PASS` on day one; it is
|
|
1465
1497
|
expected to know exactly where it isn't.
|
|
1466
1498
|
|
|
1499
|
+
**Mechanized onboarding.** `/whs:adopt` — copied from
|
|
1500
|
+
`templates/commands/whs/adopt.md` into a project's own
|
|
1501
|
+
`.claude/commands/whs/adopt.md` — runs this entire section end-to-end inside a
|
|
1502
|
+
preexisting, never-adopted project: it detects the stack, runs the six-question
|
|
1503
|
+
flow above, then, before writing anything, judges whether the project is
|
|
1504
|
+
actually compatible with the standard. Two failure modes are distinct there:
|
|
1505
|
+
the project may simply be out of the standard's domain (not a defect — nothing
|
|
1506
|
+
gets filed or written), or the standard may have a genuine gap for a real need
|
|
1507
|
+
the project has, which routes to the same `feedback/` mechanism below
|
|
1508
|
+
(**Feedback to the standard**) before or alongside finalizing adoption, exactly
|
|
1509
|
+
as this section's own "Starting point" paragraph already does for a new-project
|
|
1510
|
+
brief that doesn't honestly fit an offered stack. Once adoption proceeds, it
|
|
1511
|
+
writes the `## House style` block above, then, where stack tooling exists to
|
|
1512
|
+
wire in, runs an initial compliance pass. It **produces** the initial
|
|
1513
|
+
migration backlog; it does not work it — working the backlog stays
|
|
1514
|
+
incremental, by hand or later via `/whs:upgrade` once the project is
|
|
1515
|
+
version-pinned and catching up to a newer standard release. `/whs:adopt` and
|
|
1516
|
+
`/whs:upgrade` are deliberately different commands with different scopes.
|
|
1517
|
+
|
|
1467
1518
|
**Feedback to the standard.** Adopting the house style is what surfaces its
|
|
1468
1519
|
bugs — a check that misfires, a `core.md` ↔ impl-doc contradiction, a Contract
|
|
1469
1520
|
that doesn't survive a real codebase, a need it never anticipated. That signal
|