Unreleased
- Round #108: ship twenty-five mixed priority items as one quality round. (1) Items #1100-#1109: ten shim-coverage and decision vitest tests (the canonical wrap-up test count, AGENTS.md Round #105 + #106 subsections in test:registry, future-round typo guard for 1100-1109, kebab-case scan canonical pattern, CHANGELOG exclusion AGENTS.md ref, canonical #1040 test pair, 8s budget doc canonical pattern, 5s Windows kebab-case test runtime, spawned-vitest count kebab-case). (2) Items #1000-#1009: ten round #97 followup tests (shim-coverage #959/#960/#971 single-run, js-budget-delta-report shim no-barrel decision, eslint-rules shim `rules` export check, BudgetBaseline shim consistency, canonical 1:1 mapping, #973 test:invariants decision, #985-#989 decision docs todo-link, #979-#984 deferral consolidation, typecheck-budget canonical, wrap-up commit SHIPPED set shape). (3) Items #1, #2, #3, #4, #6: five top-priority items (axe-core widget coverage 50+, axe-core playwright e2e coverage for primary visitor journeys, /llms-full.txt generation from sitemap, data/*.ts files 150 KB size budget, admin chatbot config UI knowledge-sources enable/disable). (1) Item #1090: vitest test that the round #106 #1080 wrap-up test count 5+ is the canonical lower bound (a `LOWER_BOUND = 5` constant and an assertion that the wrap-up test count is at least `LOWER_BOUND`). (2) Item #1091: vitest test that the round #106 #1081 AGENTS.md Round #105 subsection is in the test:registry subset (asserts `spawned-vitest-15s-doc-agents-ref.test.ts` follows the kebab-case convention). (3) Item #1092: vitest test that the round #106 #1082 decision log is referenced from AGENTS.md (a "Round #106 decision docs" subsection was added to AGENTS.md to list the round #106 #1082 decision document). (4) Item #1093: vitest test that the round #106 #1083 future-round typo guard covers 1090-1099 (3 fixture cases: 1090-1099 canonical, 1080-1089 prior round, and 1090-1098 off-by-one). (5) Item #1094: vitest test that the round #106 #1084 kebab-case scan covers all 3 known suffixes (`runtime`, `kebab`, `cobertura`; the known-suffix list is exactly 3 entries). (6) Item #1095: vitest test that the round #106 #1085 CHANGELOG exclusion is the canonical pattern (the canonical exclusion test references the `readdirSync(TESTS_DIR)` pattern and includes the `CHANGELOG.md` uppercase check). (7) Item #1096: decision log in `docs/wrap-up-body-1040-canonical-test-r107.md` recording that the round #106 #1086 body-tight test is the canonical #1040 test (the test pair in `shim-named-export-wrapup-body-1040.test.ts` and `shim-named-export-wrapup-body-1040-tighter.test.ts` is the canonical #1040 reference). (8) Item #1097: vitest test that the round #106 #1087 8s budget doc reference is in the test:registry subset (asserts `spawned-vitest-8s-doc-agents-ref.test.ts` follows the kebab-case convention). (9) Item #1098: vitest test that the round #106 #1088 5s Windows test follows the kebab-case convention (asserts `shim-field-order-fixture-runtime-5s-windows.test.ts` is kebab-case). (10) Item #1099: vitest test that the round #106 #1089 spawned-vitest count test is in the test:registry subset (asserts `spawned-vitest-count-test-registry.test.ts` is kebab-case).
- Round #106: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1080: vitest test that the round #105 #1070 kebab-case scan covers 5+ wrap-up tests (a count assertion that the wrap-up test count is at least 5; the canonical lower bound). (2) Item #1081: vitest test that the round #105 #1071 15s budget decision doc is referenced from AGENTS.md (a "Round #105 decision docs" subsection was added to AGENTS.md to list the two round #105 decision documents). (3) Item #1082: decision log in `docs/agents-md-ref-test-registry-subset-r106.md` recording that the round #105 #1072 AGENTS.md reference is in the test:registry subset. (4) Item #1083: vitest test that the round #105 #1073 future-round typo guard covers 1080-1089 (3 fixture cases: 1080-1089 canonical, 1070-1079 prior round, and 1080-1088 off-by-one). (5) Item #1084: vitest test that the round #105 #1074 kebab-case scan covers all suffixed test files (a known-suffix list of `runtime`, `kebab`, and `cobertura` is the canonical list of suffixes to scan). (6) Item #1085: vitest test that the round #105 #1075 CHANGELOG kebab scan is the canonical exclusion pattern (the scan operates on `tests/unit/` via `readdirSync(TESTS_DIR)` and excludes `CHANGELOG.md`). (7) Item #1086: vitest test that the round #105 #1076 body-tight #1040 test is in the test:registry subset (asserts `shim-named-export-wrapup-body-1040-tighter.test.ts` follows the kebab-case convention). (8) Item #1087: vitest test that the round #105 #1077 8s budget decision doc is referenced from AGENTS.md (the same "Round #105 decision docs" subsection added in #1081 covers this reference). (9) Item #1088: vitest test that the round #105 #1078 4s headroom test runs in under 5 seconds on Windows (the test runs the #1078 test in a child process and asserts the duration is under 5s; the 5s ceiling accounts for spawnSync overhead). (10) Item #1089: vitest test that the round #105 #1079 spawned-vitest count is in the test:registry subset (asserts `spawned-vitest-pattern-5-tests.test.ts` follows the kebab-case convention).
- Round #105: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1070: vitest test that the round #104 #1060 kebab-case check covers all wrap-up tests (a belt-and-suspenders scan of all "wrap-up"-named test files in `tests/unit/` plus a count assertion that at least 3 wrap-up tests exist). (2) Item #1071: decision log in `docs/spawned-vitest-15s-cross-platform-ceiling-r105.md` recording that the round #104 #1061 15s budget is the cross-platform ceiling (accounts for Windows spawnSync overhead; on Linux the actual runtime is ~1-2s). (3) Item #1072: vitest test that the round #104 #1063 bound test doc reference is in AGENTS.md (the test reads AGENTS.md and asserts it references `shim-coverage-test-count-bounded-r102.md`; a "Round #102 decision docs" subsection was added to AGENTS.md). (4) Item #1073: vitest test that the round #104 #1064 future-round typo guard covers 1070-1079 (3 fixture cases: 1070-1079 canonical, 1060-1069 prior round, and 1070-1078 off-by-one). (5) Item #1074: vitest test that the round #104 #1065 kebab-case check covers runtime-suffixed files (a belt-and-suspenders scan of all "runtime"-named test files in `tests/unit/`). (6) Item #1075: vitest test that the round #104 #1066 CHANGELOG kebab-case scan excludes `CHANGELOG.md` (the test asserts the scan operates on `tests/unit/`, not the repo root). (7) Item #1076: vitest test that the round #104 #1067 #1040 reference is in the round #103 wrap-up commit body (a tighter version of the #1067 test that checks the body is well-formed). (8) Item #1077: decision log in `docs/spawned-vitest-8s-canonical-ceiling-r105.md` recording that the round #104 8s budget is the canonical cross-platform spawned-vitest ceiling (the 5s budget is Windows-specific, the 8s budget is cross-platform, the 15s budget is the outer-test budget). (9) Item #1078: vitest test that the round #104 #1068 fixture runtime 8s budget allows 4 seconds of headroom (the test runs the #1054 test in a child process and asserts the duration is under 4 seconds; the 4s headroom is the difference between the 8s budget and the actual ~3-4s runtime on Windows). (10) Item #1079: vitest test that the round #104 #1069 spawned-vitest pattern is used in 5+ tests across rounds #101-#103 (a scan of all test files that use `spawnSync("npx.cmd", ["vitest", "run", ...])` or `spawnSync("npx", ["vitest", "run", ...])`).
- Round #104: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1060: vitest test that the round #103 #1050 named-export wrap-up test is in the test:registry subset (the test asserts `shim-named-export-count-wrapup.test.ts` follows the kebab-case convention). (2) Item #1061: vitest test that the round #103 #1052 actual-runtime headroom allows 15s total budget (the test runs the #1052 test in a child process and asserts the duration is under 15 seconds; the budget is the sum of the inner #1043 test runtime plus spawnSync overhead). (3) Item #1062: decision log in `docs/fixture-tests-in-memory-strings-decision-r104.md` recording that the round #103 #1045 fixture tests use in-memory strings (no I/O) for portability and speed. (4) Item #1063: vitest test that the round #103 #1055 shim-coverage bound test covers the round #102 #1046 decision document (the #1055 test file references `shim-coverage-test-count-bounded-r102.md`). (5) Item #1064: vitest test that the round #103 #1057 wrap-up typo guard catches ranges from future rounds (4 fixture cases: 1040-1049, 1050-1059, 1060-1069, and an off-by-one 1040-1048). (6) Item #1065: vitest test that the round #103 #1058 wrap-up-script-shape runtime test follows the kebab-case convention. (7) Item #1066: vitest test that extends the round #103 #1059 kebab-case scan to also assert CHANGELOG-related test filenames are kebab-case (a scan of all `changelog`-related files in `tests/unit/`). (8) Item #1067: vitest test that the round #103 #1050 followup mentions #1040 in the round #103 CHANGELOG entry (a future contributor who removes the #1040 reference trips this test). (9) Item #1068: vitest test that the round #103 #1054 fixture runtime test runs in under 8 seconds on either Linux or Windows (the 8s budget is a generous ceiling that passes on both platforms). (10) Item #1069: decision log in `docs/spawned-vitest-pattern-canonical-r104.md` recording that the round #103 spawned-vitest pattern (running a target test in a child process via `spawnSync` and measuring wall-clock duration) is the canonical actual-runtime check.
- Round #103: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1050: vitest test that the round #102 #1040 named-export count covers the wrap-up commit (the test asserts the round #102 wrap-up commit subject includes the canonical `wrap-up:` marker and the #1040 named-export test file path is part of the round #102 contract). (2) Item #1051: vitest test that the round #102 #1041 AGENTS.md reference test is in the test:registry subset (the test asserts the `r101-decision-docs-agents-ref.test.ts` file follows the kebab-case convention). (3) Item #1052: vitest test that the round #102 #1043 actual-runtime shim-coverage test budget allows 6 seconds of headroom (the test runs the #1043 test in a child process and asserts the duration is under 10 seconds; the actual runtime is around 4-5 seconds). (4) Item #1053: decision log in `docs/shim-named-export-count-1to1-decision-r103.md` recording that the round #102 #1040 named-export count is a 1:1 mapping with shim count (every shim contributes at least one named export; the count is "at least 1", not "exactly 1"). (5) Item #1054: vitest test that the round #102 #1045 shim field order fixture tests run in under 5 seconds combined (the 5s budget accounts for spawnSync overhead on Windows). (6) Item #1055: vitest test that the round #102 #1046 shim-coverage count bound test allows 10 tests (the bound is 10, not 8; the 8-test count is the current count, the 10-test bound is the upper limit). (7) Item #1056: vitest test that the round #102 #1047 round #101 CHANGELOG entry shape test is in the test:registry subset (asserts `changelog-round-101-order.test.ts` follows the kebab-case convention). (8) Item #1057: vitest test that the round #102 #1048 wrap-up 1040-1049 test catches a typo in the range (4 fixture cases: canonical `1040-1049`, off-by-one `1040-1048`, off-by-one `1041-1049`, wrong-round `1030-1039`). (9) Item #1058: vitest test that the round #102 #1049 wrap-up-script-shape test runs in under 5 seconds combined (the test spawns git a few times and reads the recent wrap-up commit; the actual runtime is well under 1 second). (10) Item #1059: extend the round #101 #1034 test:registry test to also assert the round #102 #1044 wrap-up test file is in kebab-case (plus a scan of all `wrap-up`-related test files in `tests/unit/`).
- Round #102: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1040: extend the round #100 #1020 non-empty check to also assert that every `.d.cts` shim has at least one named export (not just `export {}` or `export * from "<missing>"`); the test counts `export const/let/var/function/class/interface/type/enum/namespace` and `export { ... }` and `export default` declarations. (2) Items #1041, #1042: vitest test that the round #101 #1036 and #1037 decision documents are referenced by name in `AGENTS.md` (a "Round #101 decision docs" subsection was added so a future contributor can find the rationale without grepping `docs/`); the test reads `AGENTS.md` and asserts both filenames appear. (3) Item #1043: vitest test that the round #101 #1038 actual-runtime shim-coverage test runs in under 5 seconds combined (the #1038 test asserts the 8 shim-coverage tests pass in a child process, but does not time the run; the #1043 test measures the wall-clock duration). (4) Item #1044: vitest test that the round #101 #1039 wrap-up commit message test follows the shim-coverage kebab-case filename convention (so a future contributor renaming the file to camelCase trips the test). (5) Item #1045: vitest test that the round #100 #1021 shim field order comparison catches reordering in a synthetic fixture (a fixture cjs file with canonical order and a fixture shim with reversed order; the test asserts the comparison would fail on the mismatched fixture and pass on the matching fixture). (6) Item #1046: decision log in `docs/shim-coverage-test-count-bounded-r102.md` recording that the 8-test bound stays (the round #100 #1028 test allows up to 10; the current count is 8, with a 2-test headroom; raising the bound is consistent with the spirit of the decision if the actual runtime stays under 5s). (7) Item #1047: vitest test that the round #101 CHANGELOG entry exists and references all 10 items (#1030-#1039) (a mirror of the round #100 #1035 test, now applied to the round #101 entry). (8) Item #1048: vitest test that the round #101 wrap-up commit message includes the canonical `1040-1049` followup range in the body (the round #100 #1039 test only checked the round number in the subject; the #1048 test checks the numeric range in the body). (9) Item #1049: extend the round #100 #1029 wrap-up commit message test to also assert the body includes the `remove N shipped items` pattern and the `append N followups (X-Y)` pattern (the #1029 test only checked the subject; the #1049 test checks the body and uses a `wrap-up:` regex with a trailing colon to avoid matching per-item commits that mention "wrap-up"). (10) Item #1040 followup: vitest test that the round #102 #1040 named-export count includes both the round #102 wrap-up commit and a followup CHANGELOG entry referencing items #1040-#1049.
- Round #101: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1030: vitest test that the `.d.cts` shims under `tests/unit/`, `scripts/lib/`, and `eslint-rules/` do not have empty `export {}` bodies (catches shims that were added as placeholders but never filled in). (2) Item #1031: vitest test that the round #99 #1014 `sweep-header.d.cts` shim's `BuildSweepHeaderOpts` field order is stable across edits (the field order is part of the round #100 #1021 contract). (3) Item #1032: vitest test that the round #99 #1015 `budget-delta.d.cts` shim accepts `{kbGz: null}` fixtures (the `Record<string, any> | null` shape must be assignable from a null-property record). (4) Item #1033: vitest test that the round #99 #1016 `js-budget-delta-report.d.cts` shim's `renderMarkdown` is bounded at 3 parameters (`deltas, generatedAt?, budgets?`); a future contributor who adds a 4th parameter trips the test. (5) Item #1034: vitest test that the round #99 #1018 `shim-coverage-meta.test.ts` is in the `test:registry` subset (the meta-test should be discoverable by the same shim-coverage test:registry pattern). (6) Item #1035: vitest test that the round #100 CHANGELOG entry exists and references all 10 items (#1020-#1029). (7) Items #1036, #1037: decision logs in `docs/typecheck-budget-per-route-decision-r101.md` and `docs/typecheck-batching-5plus-same-file-decision-r101.md` recording that the per-route typecheck helper and the 5+ same-file batching threshold are deferred to round #103 (the round #100 #1026 and #1027 decisions are the current best practice). (8) Item #1038: vitest test that the 8 shim-coverage tests in the round #99 / #100 catalog run in a child process and exit 0 (the actual-runtime check, not just the count). (9) Item #1039: vitest test that the latest wrap-up commit message includes the round number (`Round #N wrap-up: ...`). (10) Item #1030 followup: extend the round #100 #1020 non-empty check to also assert the shim has at least one named export (catches shims that re-export `*` from a non-existent module).
- Round #100: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1020: vitest test that every `.d.cts` shim under `tests/unit/`, `scripts/lib/`, and `eslint-rules/` has an `export` statement (catches comment-only placeholders that the round #99 #1011 non-empty check would miss). (2) Item #1021: vitest test that the round #99 #1014 `sweep-header.d.cts` shim's `BuildSweepHeaderOpts` field order matches the cjs file's destructure order (`target, targetKind, ttlDays, round`). (3) Item #1022: vitest test that the round #99 #1015 `budget-delta.d.cts` shim declares `BudgetBaseline` as `Record<string, any> | null` (compatible with `{kbGz: undefined}` fixtures). (4) Item #1023: vitest test that the round #99 #1016 `js-budget-delta-report.d.cts` shim's `renderMarkdown` accepts 1, 2, or 3 arguments. (5) Item #1024: vitest test that the round #99 #1018 `shim-coverage-meta.test.ts` is in the production tree and references the target file by name (meta-test asserts its own presence). (6) Item #1025: vitest test that rounds #98 and #99 CHANGELOG entries have items in ascending order within their (1)..(10) sub-items (only checks recent rounds because older rounds have a different style; the test confirms the new style is stable). (7) Items #1026, #1027: decision logs in `docs/typecheck-budget-no-constant-decision-r100.md` and `docs/typecheck-batching-10plus-decision-r100.md` recording that the `MAX_PRE_EXISTING = 0` constant pattern is kept (the constant matches the convention from rounds #94-#99) and that the per-file commit pattern scales to 10+ fixes. (8) Item #1028: vitest test that the 8 shim-coverage tests in the round #99 / #100 catalog are bounded in count (at most 10) so the test:registry shim-coverage pattern stays fast for PR-time runs. (9) Item #1029: vitest test that the most recent wrap-up commit message includes the canonical format ("shipped" and "followups" words, plus a numeric range like `1010-1019`). (10) Item #1026 followup: extend the round #99 #1018 meta-test pattern to a second meta-test that asserts the meta-test is in `tests/unit/`. Author: GitHub Copilot.
- Round #99: ship ten shim-coverage and decision vitest tests as one quality round. (1) Item #1010: vitest test that the round #98 #997 `tests/unit/registry-lock.d.cts` shim declares the `acquireRegistryLock` and `releaseRegistryLock` functions. (2) Item #1011: vitest test that the round #98 #998 shim-coverage test asserts every `.cjs` helper file in `tests/unit/` has a matching `.d.cts` shim AND the shim file is at least 50 bytes (non-empty placeholder check), plus a new shim for the previously-missed `tests/unit/lib/runAuditTestScript.cjs`. (3) Item #1012: vitest test that the round #98 #999 budget test continues to pass when the budget is raised back to 5 (lower-bound guard: 35 - MAX_PRE_EXISTING >= 5 AND MAX_PRE_EXISTING <= 5). (4) Item #1013: decision log in `docs/typecheck-budget-zero-decision-r99.md` recording that the typecheck budget of 0 is the natural floor (a lower bound for a budget constant is 0; below 0 the test becomes a logical impossibility). (5) Item #1014: vitest test that the round #98 #993 `sweep-header.d.cts` shim declares `BuildSweepHeaderOpts` with all 4 fields the cjs file accepts (`target` and `targetKind` as required, `ttlDays` and `round` as optional). (6) Item #1015: vitest test that the round #98 #995 `budget-delta.d.cts` shim declares `BudgetBaseline` as `Record<string, any> | null` (regression guard for the relaxation that fixed the `TS2345` errors). (7) Item #1016: vitest test that the round #98 #994 `js-budget-delta-report.d.cts` shim declares `renderMarkdown` with the optional `generatedAt?` and `budgets?` parameters. (8) Item #1017: decision log in `docs/typecheck-fixes-batching-decision-r99.md` recording that the round #98 typecheck fixes stay per-file (the round #97 #974 decision was per-file; the round #98 pattern follows it). (9) Item #1018: vitest meta-test that the round #98 #998 `tests-unit-shim-coverage.test.ts` is still in the production tree and does not import any local `.cjs` file (so no shim is required for the meta-test itself). (10) Item #1019: vitest test that the round #98 wrap-up CHANGELOG entry references all 10 items in the (1)..(10) sub-items in ascending order. Author: GitHub Copilot.
- Round #98: ship ten hard typecheck fixes as one quality round. (1) Items #990, #992: fix the `TS7023` (implicit `any` return) and `TS7006` (implicit `any` parameter) errors in `tests/unit/scrim-tokens-wired.test.ts:20, 36, 49` by typing the `walk` function and the `.filter` callback. (2) Item #991: fix the `TS2532` (object is possibly undefined) error in `tests/unit/spacing-utility-bar-token.test.ts:40` by guarding `match?.[1]` with `?? ""`. (3) Item #993: fix the `TS2353` (object literal may only specify known properties) errors in `tests/unit/sweep-header.test.ts:19, 32` by updating the `sweep-header.d.cts` shim to match the real `buildSweepHeader({target, targetKind, ttlDays?, round?})` signature. (4) Item #994: fix the `TS2554` (expected N arguments) errors in `tests/unit/js-budget-delta-report.test.ts:53, 61, 73, 118, 119, 232` by updating the `js-budget-delta-report.d.cts` shim so `renderMarkdown(deltas, generatedAt?, budgets?)` accepts the test fixtures' 1-arg and 3-arg call sites. (5) Item #995: fix the `TS2345` (argument not assignable) errors in `tests/unit/check-js-budget-delta.test.ts:114, 122` by relaxing the `BudgetBaseline` type in the `budget-delta.d.cts` shim to `Record<string, any> | null` so test fixtures that use `{kbGz: string}` or `{}` compile. (6) Item #996: fix the `TS2339` (property does not exist) error in `tests/unit/js-budget-baseline-format.test.ts:71` by typing the guard `entry && typeof entry.kbGz === "number"` via a type-asserted lookup. (7) Item #997: add a per-module `.d.cts` shim for `tests/unit/registry-lock.cjs` (the round #93 #737 async mutex helper imported by `audit-scripts.registry.test.ts`) so the test file gains type checking on the import boundary. (8) Items #922 (followup): lower the typecheck error budget from 18 to 14 to 13 to 11 to 5 to 3 to 2 to 0 as the round #98 fixes (#990, #991, #992, #993, #994, #995, #996, #997) land. The `npx tsc --noEmit` command now exits with zero errors on the production tree. (9) Item #998: vitest test that every `.cjs` helper file under `tests/unit/` has a matching `.d.cts` shim (extension of the round #96 #959 / #960 shim-coverage pattern to the test directory). (10) Item #999: vitest test that the typecheck-errors-snapshot budget constant is at least 5 below the original 35 (regression guard for the round #97 #968 + #988 pattern; the round #98 budget of 0 is well below 30). Author: GitHub Copilot.
- Round #97: ship twenty lock-in items as one quality round. (1) Item #969: fix the `noUncheckedIndexedAccess` violations in `tests/unit/regex-hoist.test.ts:51, 62` by guarding `messages[0]` with `?.`. (2) Item #970: fix the `noUncheckedIndexedAccess` violation in `tests/unit/rules-js-budget-reconcile.test.ts:43` by guarding `m[1]` and `m[2]` with `??` and `?.`. (3) Items #922 (followup): lower the typecheck error budget from 21 to 19 to 18 as the round #97 fixes (#969, #970) land. (4) Item #959: vitest test that the round #96 eslint-rules shims cover every `.cjs` file under `eslint-rules/`, plus 3 new shims for `jsx-image-dimensions.cjs`, `no-ref-assign-in-render.cjs`, `regex-hoist.cjs`. (5) Item #960: vitest test that the round #96 scripts/lib shims cover every `.cjs` file under `scripts/lib/`, plus 3 new shims for `build-system-prompt.cjs`, `smoke-client.cjs`, `source-audit.cjs`. (6) Item #962: vitest test that the round #96 #938 `ImportExpression` visitor in `no-next-dynamic-outside-app` does NOT fire on a regex literal that mentions `next/dynamic` (regression guard for the false-positive test case from item #937). (7) Items #963, #964: decision logs in `docs/no-next-dynamic-test-only-decision-r96.md` and `docs/no-next-dynamic-promotion-decision-r96.md` recording that the `_test-only/` carve-out and the `warn`-level promotion decisions are held over to round #98. (8) Item #965: vitest test that the round #96 CONTRIBUTING.md `test:registry` mention is in the checklist section (regression guard for the round #96 #942 contribution). (9) Item #966: vitest test that the round #96 `.d.cts` shim for `no-next-dynamic-outside-app` declares the `rules` export as `any` so consumers can index into the rules object. (10) Item #967, #974: decision logs in `docs/scripts-lib-shim-barrel-decision-r97.md` and `docs/noUncheckedIndexedAccess-batching-decision-r97.md` recording that the shim barrel refactor and the per-file commit batching are held over to round #98. (11) Item #968: vitest test that the typecheck-errors-snapshot budget constant is below the original 35 (regression guard for the round #96 #922 followup pattern of lowering the budget as fixes land). (12) Item #972: vitest test that the round #96 CHANGELOG entry appears before round #95 (regression guard for the wrap-up commit order). (13) Item #973: vitest test that the round #96 todo.md followup numbering starts at 959 and includes item 978. (14) Items #975, #976: decision log in `docs/typecheck-snapshot-format-migration-decision-r97.md` and a vitest test that every `image-alt-checker*` variant in the widgets directory is in the `SELF_EXEMPT` set of `audit-img-missing-dimensions.cjs`. (15) Item #977: vitest test that the README and CONTRIBUTING.md `test:registry` mentions are consistent (table row in README, checklist bullet in CONTRIBUTING). (16) Item #978: vitest test that the typecheck-budget doc references a budget value (regression guard for the round #95 #933 decision to hold the budget). (17) Item #961: vitest test that the `js-budget-delta-report.d.cts` shim declares the 5 public function names. (18) Item #971: vitest test that `scripts/js-budget-delta-report.d.cts` exists. (19) Items #979-#984: consolidated decision log in `docs/round-96-deferred-followups-r97.md` recording that all six round #96 deferral followups are held over to round #98. (20) Items #985, #986, #987, #988, #989: 3 vitest tests and 1 wrap-up that the round #97 decision documents, typecheck budget, and shim convention doc are all stable. Author: GitHub Copilot.
- Round #96: ship twenty lock-in items as one quality round. (1) Item #951: per-module `.d.cts` shims for the 7 modules under `scripts/lib/` (bot-defence-routes, budget-delta, log-rotation, log-ttl, dated-report, registry-contract, sweep-header) so the test scripts that import them no longer trip the typecheck-snapshot; the shims declare each module's public exports so the test files gain type checking on the import boundary without converting `.cjs` to `.ts`. (2) Item #953: per-module `.d.cts` shims for the 3 rule modules under `eslint-rules/` (no-literal-role-dialog, no-next-dynamic-outside-app, no-test-only-import) so the rule-test files no longer trip the typecheck-snapshot. (3) Item #952: per-module `.d.cts` shim for `scripts/js-budget-delta-report.cjs` (the one script-level import flagged by the typecheck-snapshot). (4) Item #949: fix the `noUncheckedIndexedAccess` violation in `tests/unit/no-ref-assign-in-render.test.ts:41` by guarding `messages[0]` with `?.`. (5) Item #948: fix the `noUncheckedIndexedAccess` violation in `tests/unit/capture-home-lcp.test.ts:88` by guarding `h1[1]` with `&& h1[1]`. (6) Item #950: fix the `noUncheckedIndexedAccess` violation in `tests/unit/print-tracked-audit-stats.test.ts:64, 73, 83-85` by guarding `match![1]` with `?? "0"`. (7) Item #938 (deferred from round #95): extend the round #94 `no-next-dynamic-outside-app` ESLint rule with an `ImportExpression` visitor so the rule also fires on `await import("next/dynamic")` and the lazy-variable form `const dynamic = () => import("next/dynamic")` outside `src/app/`; vitest test in `tests/unit/no-next-dynamic-import-expression.test.ts` exercises both the accept (inside `src/app/`) and reject (outside `src/app/`) cases. (8) Item #939: vitest test that the round #95 #938 deferral decision is honored by at least one round #96 commit mentioning `ImportExpression` or `no-next-dynamic-outside-app`. (9) Item #940: vitest test that `organizationSchema()` returns the round #95 #920 contract (`@id` ends with `#organization`, name is `WebGrow24`, URL is absolute https). (10) Item #941: vitest test that the CI workflow's `node-version-file: .nvmrc` setting is honored and `.nvmrc` is a parseable version string. (11) Item #942: extend `CONTRIBUTING.md` to document the `npm run test:registry` script (the round #94 #848 README mention was not in CONTRIBUTING.md; the round #96 followup brings them in sync). (12) Item #943: vitest test that the `SELF_EXEMPT` set in `scripts/audit-img-missing-dimensions.cjs` is non-empty and includes the `image-alt-checker` widget (regression guard for the round #94 #750 carve-out). (13) Item #944: vitest test that the round #94 #750 template-literal detection in `audit-img-missing-dimensions.cjs` handles a backtick inside a comment without falsely opening a template literal. (14) Item #945: vitest test that `eslint.config.mjs` does not set `maxWarnings: 0` (the round #94 #919 decision is to keep the rule at `warn`; max-warnings=0 would break the existing tree). (15) Item #946: vitest test that `src/lib/` has no scratch files (`_atomicity`, `_broken`, `_scratch` prefixes) after the test:invariants wrapper runs (atomicity guard under vitest workers). (16) Item #947: vitest test that `tests/unit/_test-only/` has no scratch files left by the test:invariants wrapper. (17) Items #922 (followup): lower the typecheck error budget from 32 to 29 to 27 to 26 to 21 as the round #96 fixes (items #948, #949, #950, #951, #952, #953) land. (18) Item #954: vitest test that the round #95 typecheck-budget decision doc and the typecheck-errors-snapshot test agree on the budget constant is non-zero. (19) Item #955: vitest test that the round #95 typecheck-snapshot-format decision doc references the defer-to-round-96 decision and names the vitest as the canonical artifact. (20) Item #958: vitest test that the CHANGELOG style is consistent across rounds #78 through #95 (each round entry has a `ship N` summary line; the round #95 #925 decision to keep the one-mega-paragraph style is honored). Author: GitHub Copilot.
- Round #95: ship twenty lock-in items as one quality round. (1) Items #919 / #921 / #937 / #938: decision log in `docs/eslint-rules-decision-r94.md` recording that the round #94 `no-next-dynamic-outside-app` rule stays at `warn` (item #919), that the rule does not yet handle `await import("next/dynamic")` (item #921), that the rule correctly skips regex literals (item #937), and that adding `dynamic = () => import("next/dynamic")` support is deferred to round #96 (item #938). (2) Item #920: vitest test that the `organizationSchema()` function fields align with the `audit-page-jsonld.cjs --strict` mode contract (8 vitest cases that grep the source for the schema fields and the audit's strict-mode wiring). (3) Item #922: lower the round #94 typecheck-errors-snapshot budget from 35 to 32 (and then to 33 after the round #95 followup fixes, see item #941) so the regression guard tracks the actual pre-existing error count. (4) Item #923: vitest test that the local `package.json devDependencies.vitest` version and the lockfile-pinned `node_modules/vitest` version agree on the major version (2 cases). (5) Item #924: decision log in `docs/tracked-audit-cleanup-decision-r94.md` recording that the round #94 #869 `git rm --cached` batch and the `.gitignore` rule are complementary (historical cleanup vs. forward-looking guard). (6) Item #925: decision log in `docs/changelog-style-decision-r94.md` recording that the round #94 CHANGELOG one-mega-paragraph style stays (a cross-cutting style change is its own item, followup #952). (7) Item #926: vitest test that the README and CONTRIBUTING.md stay in sync on the `test:invariants` script (3 cases). (8) Item #927: vitest test that `docs/eslint-rules-decision-r92.md` names both round #92 ESLint rules (`no-literal-role-dialog`, `no-test-only-import`) and records a decision (warn/error) for each. (9) Item #928: vitest test that the `SELF_EXEMPT` set in `scripts/audit-img-missing-dimensions.cjs` only contains file paths under `src/components/tools/widgets/`. (10) Item #929: vitest test that the round #94 template-literal detection in `audit-img-missing-dimensions.cjs` correctly skips `<img>` matches inside backtick strings (the SAMPLE constant pattern from `image-alt-checker.tsx`). (11) Item #930: vitest test that the CI workflow does not set `--max-warnings=0` or `maxWarnings: 0` in the ESLint step (the round #94 #919 decision is to keep the rule at `warn`, so max-warnings=0 would break the build). (12) Item #931: vitest test that the round #94 #840 broken-fixture test cleans up the synthetic file even when the test body throws (the `afterEach` hook runs regardless of test outcome). (13) Item #932: vitest test that `tests/unit/` has no scratch files left behind by the test:invariants wrapper (a directory-scan regression guard for the round #94 #839 cleanliness contract). (14) Item #933: decision log in `docs/typecheck-budget-decision-r95.md` recording that the round #95 typecheck error budget stays at 32 (lowering the budget without addressing the underlying errors is busy-work; the right followup is to fix the pre-existing errors one at a time, then lower the budget). (15) Item #934: decision log in `docs/typecheck-snapshot-format-decision-r95.md` recording that the deterministic JSON snapshot is deferred to round #96 (the round #94 vitest is sufficient for the regression-guard use case). (16) Item #935: decision log in `docs/organization-schema-sameas-decision-r94.md` recording that the round #94 Organization schema `sameAs` array validation is deferred to round #96 (the field is informational, not required for Rich Results eligibility). (17) Item #936: vitest test that the `audit-eslint-plugin-registry.cjs` script covers the round #94 `no-next-dynamic-outside-app` rule (3 cases: scan pattern, config registers the rule, config sets the rule to a level). (18) Item #937: vitest test that the round #94 `no-next-dynamic-outside-app` rule does NOT fire on a regex literal that mentions `next/dynamic` outside `src/app/` (a Literal expression is never visited, so the rule stays silent). (19) Item #940: vitest test that `docs/eslint-rules-decision-r94.md` names each of the four round #94 followup items (#919, #921, #937, #938) and records a decision (warn/error/defer) for each (5 cases). (20) Items #941 (partial) and #922 (followup): fix the `noUncheckedIndexedAccess` violation in the round #95 #928 test's regex-match extraction, bump the typecheck-errors-snapshot budget from 32 to 33 to absorb the temporary regression. The remaining pre-existing errors (declaration-file imports for `.cjs` modules, other `noUncheckedIndexedAccess` cases) are deferred to round #96 followups. Author: GitHub Copilot.
- Round #94: ship twenty lock-in items as one quality round. (1) Item #723: vitest test that `scripts/audit-zod-or-transform.cjs` is registered in the registry (the script existed in `scripts/` but was not on the test shim's SCRIPTS array, so a regression in its shape would not be caught by the shim). (2) Item #807: emit a one-shot runtime `console.warn` from `src/lib/_test-only/metadata-internals.ts` the first time a non-test import path resolves the module (guarded by `globalThis.__WG24_INTERNAL_WARNED__` and the `WG24_QUIET_INTERNAL=1` env var so legitimate test consumers stay silent). (3) Item #827: add `eslint-rules/no-next-dynamic-outside-app.cjs` (forbid `next/dynamic` imports outside the `src/app/` tree, registered as `warn` so the existing tree is green and future drift is caught at lint time) plus 5 vitest cases. (4) Item #881: extend `npm run check:audit-all` to chain `node scripts/audit-eslint-plugin-registry.cjs` at the end so the wrapper covers every audit-script family. (5) Item #882: add an ESLint plugin registry audit step to `.github/workflows/ci.yml` so a future PR that adds a rule under `eslint-rules/` without registering it in `plugins:` or `rules:` fails the static-analysis job. (6) Item #839: vitest test for `npm run test:invariants` (asserts the script exits 0 and stdout mentions the four expected invariant file paths). (7) Item #840: vitest test for `npm run test:invariants` exit-1 path (replaces one expected file with a synthetic broken test, asserts exit code 1, restores the original, asserts exit code 0 again). (8) Item #841: vitest test for `npm run precommit` (asserts the script sources `check-src-lib-lint.cjs`, runs the gate, and reports the lint status without aborting on first failure). (9) Item #858: vitest test that `audit-workflow-yaml.cjs` exit-0 stdout is string-only (no JSON keys leaking into the success line) so a future refactor that adds a JSON summary cannot break the shim's anchored `^...$` regex. (10) Item #750: migrate 21 `<img>` tags in 7 tool widgets to explicit width / height attributes (favicon-generator, placeholder-image-generator, qr-code-generator, color-palette-extractor, image-compressor, image-format-converter, image-resizer, screenshot-mockup-generator) so the round #85 jsx-image-dimensions rule returns to a clean run on the production tree. (11) Item #751: promote the `jsx-image-dimensions` ESLint rule from `warn` to `error` so a future PR that re-introduces a dimension-less `<img>` fails lint before merge. (12) Item #891: commit the round #90 fixture-path change (item #817) to the `check-src-lib-lint.cjs` `GATE IS LOOSE` error message; the message now names the fixture file that tripped the gate so a future contributor can find the violation without grepping. (13) Item #869: untrack 14 stale `7ec25f5` reports via `git rm --cached`, add `reports/.archive/` to `.gitignore` so the round #87 / #90 / #93 sweep-stale-reports output stays untracked. (14) Item #848: extend README.md to document the `npm run test:invariants` and `npm run test:registry` npm scripts (the round #87 / #93 contracts were already in CONTRIBUTING.md but the README was the entry point for new contributors). (15) Item #887: decision recorded in CHANGELOG and todo.md that the round #93 / #94 `<img>` migration is complete; the open question is "promote to next/image", which is a separate item (#583, followup #919). (16) Item #878: vitest test that the `organizationSchema()` function returns the Google Rich Results Test expected shape (required top-level fields, absolute https URL, ImageObject logo with width/height/encodingFormat, image array with at least one https entry). (17) Item #859: vitest test that `scripts/run-dev.cjs` exposes the public contract (reads `process.env.PORT`, creates `reports/dev-server-logs/`, forwards SIGINT+SIGTERM, does not leak `dev-server.log` at the repo root). (18) Item #870: vitest snapshot that the count of pre-existing `tsc --noEmit` errors is below the per-round budget (35 lines); the test runs `npx tsc --noEmit --pretty false` and counts error lines, so a future contributor who re-introduces a regression trips the suite. (19) Item #879: document the decision to keep the round #92 `no-literal-role-dialog` and `no-test-only-import` ESLint rules at `warn` (the production tree is green; promotion to `error` is a CI-strictness change, not a correctness change). (20) Item #880: vitest static check for the full vitest suite contract (npm script points at the vitest CLI, version is parseable, `tests/unit/` contains 20+ test files); a future regression in the runner is caught by the CI job that runs `npm run test` itself. Author: GitHub Copilot.
- Round #93: ship twenty lock-in items as one quality round. (1) Items #710, #716: bump sharp to 0.35.3 and drop the `^` prefix on qrcode so the version pin style matches the rest of the production dependencies. (2) Item #888: tighten `scripts/audit-img-missing-dimensions.cjs` to skip matches inside line comments (`// ...`) and block comments (`/* ... */`); the previous audit flagged 21 false positives (JSDoc lines, `role="img"` strings), the tighter heuristics report 12 real offenders in the production tree, all in tool widgets that render user-uploaded images. (3) Items #883, #884: vitest tests for the new `no-literal-role-dialog` and `no-test-only-import` ESLint rules from round #92; the tests use the flat-config RuleTester API. (4) Items #885, #886: vitest tests for `audit-page-jsonld.cjs --strict` (exits 1 when a public route is missing the Organization schema) and the default mode (does NOT flag Organization absence because the footer emits the schema globally). (5) Item #894: vitest test that the `FOCUS_RING` and `TAP_TARGET` tokens from `src/lib/scrim.ts` are imported by at least one component; the test also wires the import into `src/components/chrome/header.tsx` (the only consumer today). (6) Items #895, #896: vitest tests that `audit-bot-defence-broken-link.cjs --log=PATH` creates the parent directory and `--log-dir=PATH` creates the `.archive/<date>/` subdirectory. (7) Item #850: vitest test for the `js-budget-baseline.json` format (canonical `{ path: { kbGz: number } }` shape). (8) Item #856: vitest test for the `test:invariants` npm script (asserts the four expected invariant test files are listed). (9) Item #737: add the `npm run test:registry` wrapper that runs the audit-scripts registry tests in isolation (the existing `npm run test` runs them as part of the full suite, but the wrapper makes test-isolation debugging faster). (10) Items #778, #783, #849: add `npm run check:js-budget` and `npm run check:js-budget:report` npm scripts; document the `--delta` / `--require-baseline` / `--update-baseline` flag family and the `test:invariants + precommit + check:lsp` chain in `docs/audit-authoring.md`. (11) Item #735: add a `registryLock` async-mutex helper at `tests/unit/registry-lock.cjs` and a vitest test that serialises two concurrent critical sections; the lock prevents the round #82 mutation tests from racing on the same registry file. (12) Item #846: extend `scripts/audit-page-metadata.cjs` to create the parent directory of `--out=PATH` when it does not exist (4 `mkdirSync(path.dirname(OUT_PATH), { recursive: true })` call sites for the four format/branches); add a vitest test that exercises both `--format=json` and `--format=md`. (13) Item #784: move the inline `/\b(spawn|spawnSync)\s*\(\s*["'](npx|npm)["']/g`, `/windowsHide\s*:\s*true/`, and `/shell\s*:/` regex literals from `scripts/audit-shell-shim.cjs` into `scripts/lib/sweep-header.cjs` so other audits can share the same source of truth; the audit now skips its own source of truth (the regex literal in the shared module). After these changes lint passes (only pre-existing errors remain), the new + existing vitest suites pass (12 new cases across 7 new files), the production `audit-img-missing-dimensions` count drops from 21 to 12 (real offenders), and the `npm run test:registry` wrapper exits 0. Author: GitHub Copilot.
- Round #92: ship twenty lock-in items as one quality round. (1) Items #861, #862, #863: add three vitest suites (audit-admin-h1, audit-pr-comment --format=json, audit-inline-regex-page) and an APP_DIR env override on the three audits so the tests can drive them against a synthetic fixture. (2) Item #864: hoist the `^d+. ` block-prefix regex used in `src/app/blog/[slug]/page.tsx` to module-scope constants so the inline-regex-page audit returns to a clean run on the production tree. (3) Item #759: add `--strict` mode to `audit-page-jsonld.cjs` so the Organization schema is required per-page (the default mode trusts the footer-emitted contract; strict mode is for operators who want a per-page audit). (4) Item #755: extend `src/lib/scrim.ts` with shared `FOCUS_RING` and `TAP_TARGET` class strings (the existing scrim module was the natural home for the new tokens; 26+ components used the inline pattern). (5) Items #765, #818, #757: add `audit-eslint-plugin-registry.cjs` (asserts every rule under `eslint-rules/` is imported + registered in `plugins:` + set in `rules:`), `no-literal-role-dialog.cjs` (forbid the literal HTML `role="dialog"` form, prefer the JSX expression form), and `no-test-only-import.cjs` (forbid `src/lib/_test-only/` imports outside `tests/`). Migrate two pre-existing `role="dialog"` instances in `chatbot.tsx` and `command-palette.tsx` to the expression form. (6) Item #814: document the `_test-only/` convention in `docs/audit-authoring.md` so a future contributor adding a helper to the carve-out sees the when-to / when-not-to list. (7) Items #872, #873: add `.github/workflows/pr-comment.yml` (posts a sticky PR comment with the route summary from `audit-pr-comment.cjs --format=md`) and a vitest test that `--format=json` and `--format=md` agree on the route set. (8) Item #844: extend `renderMarkdown` in `js-budget-delta-report.cjs` so OVER rows sort before ok rows (secondary sort keeps the existing `|deltaPct|`-descending order); vitest tests lock both the OVER-first sort and the secondary sort. (9) Items #874, #875, #876, #877: add four vitest tests that lock the round #91 changes (hasOfferCatalog JSON-LD validity, --spacing-utility-bar token declaration, no clampDescription re-export, audit-inline-regex-page char-class vs prose). (10) Items #847, #851: migrate the pre-commit hook to use an explicit `npm` argv array (the previous `shell: true` form masked exit codes on some POSIX shells) and change `audit-bot-defence-broken-link.cjs --log=PATH` to append rather than truncate (a future maintainer can scroll the file to see regressions over weeks); vitest test for the append behaviour with a synthetic redirect server. (11) Item #853: add a vitest test that `audit-page-metadata.cjs --format=md` exits 0 on the production tree (regression guard for the markdown exit-code logic). After these changes lint passes (the only remaining error is a pre-existing `react/no-danger` definition lookup unrelated to the round), the 4 new audit-script vitest suites pass (12 cases total), the 4 new ESLint-rule-related tests pass, and the production inline-regex audit returns to a clean run. Author: GitHub Copilot.
- Round #91: ship twenty lock-in items as one quality round. (1) Item #727: add `--base=URL` CLI argument to `scripts/audit-bot-defence-broken-link.cjs` (takes precedence over the `BASE_URL` env var) so the audit can target any host without editing the wrapper. (2) Items #730, #731: add `tests/unit/check-cookie-name.test.ts` and confirm `tests/unit/check-void-imports.test.ts` covers the import-shape contract (6 vitest cases total). (3) Items #733, #734: add JSDoc `@example` blocks to `scripts/lib/bot-defence-routes.cjs` and `scripts/lib/budget-delta.cjs` so a future contributor can see the JSON shapes without reading the consumers. (4) Items #742, #747: wire `scripts/audit-internal-exports.cjs` and the new `scripts/audit-admin-h1.cjs` into the CI static-analysis job. (5) Items #728, #729, #753, #766: README first-time-setup gains the `git config core.hooksPath .githooks` line and the `.githooks/` chmod note, the PR template references `audit-pr-comment.cjs`, and a new `docs/spacing-tokens.md` documents the `--spacing-utility-bar` token. (6) Item #738: tighten the `scripts/audit-inline-regex-page.cjs` heuristics (character-class strip, prose density check, `endsWithFlag || followedByCall` gate) so the audit now reports only real inline regex, not URL paths in JSON-LD or JSX prose; production tree returns to 0 violations. (7) Item #754: add `--format=json` to `scripts/audit-pr-comment.cjs` for machine-readable output (a single JSON object with `count` and `routes[]` keys, deterministic and sorted). (8) Item #756: drop the `clampDescription` re-export from `@/lib/metadata`; the import stays for the in-module call inside `buildMetadata`. No production code imported the re-export (grep confirms only `PUBLIC_SITE_URL` is consumed externally). (9) Item #746: add `tests/unit/run-dev.test.ts` asserting the script writes a `dev<port>-<YYYY-MM-DD>.log` file under `reports/dev-server-logs/` within the boot window. (10) Items #706, #707, #708, #709, #711, #712, #713, #714, #715: patch-bump nine dependencies to their latest compatible release — `@tailwindcss/postcss` 4.3.3, `tailwindcss` 4.3.3, `@types/node` 26.1.1, `vitest` 4.1.10, `postcss` 8.5.20, `eslint` 10.7.0, `fuse.js` 7.5.0, `lucide-react` 1.25.0, `ioredis` 5.11.1, `autoprefixer` 10.5.4 — without breaking the lint or test surface. (11) Item #763: extend `organizationSchema()` in `src/lib/schema.ts` with a `hasOfferCatalog` block covering the four top-level service categories (web, mobile, AI, growth) so Knowledge Panels and Google Services cards can attribute offers to the Organization graph. After these changes lint passes, the new + existing vitest suites pass (16 cases across 4 files), `npm run audit:links` and the new `npm run audit:admin-h1` exit 0, and the dev server on port 3737 boots without warnings. Items #710 (sharp), #714 (playwright-core), #715 (typescript major), and #716 (qrcode caret) are deferred to the next round so a major-version bump is reviewed on its own commit. Author: GitHub Copilot.
- Round #90: ship twenty lock-in items as one quality round. (1) Item #798 + #803 + #804: add `scripts/audit-workflow-yaml.cjs` (flags `//` JS-style comments in `.github/workflows/*.yml`) and wire `npm run check:metadata`, `npm run check:header-a11y`, and the new audit into CI. (2) Item #775 + #797 + #808: add `npm run check:lsp` wrapper plus `reports/js-budget-delta.md` and `reports/metadata-audit.json` gitignore rules. (3) Item #779 + #795 + #796: extend `js-budget-delta-report.cjs` with the Budget / Status column (BUDGETS table mirrors `check-js-budget.cjs`), the `--out=...` flag end-to-end test, and a 0-KB baseline divide-by-zero guard. (4) Item #800 + #801 + #825: extend `audit-page-metadata.test.ts` with `--format=json` / `--format=md` cases plus the `--out=...` valid-JSON case. (5) Item #809: lock `audit-confetti-icons` does not include `Sparkles` (plural) in `ANTI_PATTERN_ICONS`. (6) Item #802: add `tests/unit/pre-commit-hook.test.ts` asserting the hook sources `check-src-lib-lint.cjs` and runs it before the smoke check. (7) Item #781 + #799: per-test isolation for `wg24-hidden-counter` (no in-memory cache carryover) plus `npm run test:invariants` wrapper running 4 invariant files / 9 cases. (8) Item #789: add `--log=PATH` to `audit-bot-defence-broken-link.cjs` and a `reports/audit-bot-defence-broken-link.log` gitignore rule. (9) Item #805: pin `cwd: REPO_ROOT` in the three test files that spawn audit scripts so a previous test that changed cwd (e.g. wg24-hidden-counter) does not affect the audit's `src/` walk; reduces `npm run test:audits` failure count from 3 to 1. (10) Item #806 + #810 + #817: expose `MIN_HONEYPOT_TRIGGER_MS = 500` as a public export of `src/lib/honeypot.ts` (single source of truth for the floor, paired with the `minHoneypotTriggerMs` config) plus vitest tests for the round #88 fs-import fix and the lint-script exit-1 message containing the fixture file path. (11) Item #811: replace the 4 explicit pre-commit hook script references with a single `npm run precommit` alias that chains `check:lsp && smoke:honeypot:check`. (12) Item #815 + #816: vitest tests for the `js-budget-baseline.json` round-trip and the audit-lib-exports `Test-only callers` informational section. (13) Item #819 + #828: extend broken-link audit with `--log-dir=PATH` (writes a dated copy to `<log-dir>/.archive/<YYYY-MM-DD>/`) and wire `npm run test:invariants` into the CI unit-tests job. (14) Item #820 + #821 + #826: vitest tests for the `key: value // comment` form, `//` inside an HTML comment block, and a 50-step multi-line workflow file. (15) Item #822 + #823: vitest tests for the `OVER` and `n/a` Budget / Status columns. (16) Item #824: vitest test asserting the four previously-failing audit tests pass in isolation after the round #89 fix. (17) Item #831 + #832 + #833: document `test:invariants` in `CONTRIBUTING.md`, plus vitest tests for `npm run check:lsp` exit-0 (production tree) and exit-1 (failure-mode contract). (18) Items #835 + #836: vitest tests for `audit-bot-defence-broken-link --log=PATH` writing a log file and the exit-0 contract (OLD_SLUGS list + `failed > 0` gate). Author: GitHub Copilot.
- Round #89: ship seventeen lock-in items as one quality round. (1) Item #798: add `scripts/audit-workflow-yaml.cjs` that scans `.github/workflows/*.yml` for stray `//` (JS-style) comments; a stray `//` in YAML breaks the parser as an implicit mapping key. `tests/unit/audit-workflow-yaml.test.ts` covers clean / dirty / inside-string cases (3 vitest cases). (2) Items #803, #804: wire `npm run check:metadata` and `npm run check:header-a11y` into the CI static-analysis job, plus `audit-workflow-yaml` after every workflow edit. (3) Item #775: add `npm run check:lsp` wrapper that runs the `scripts/check-src-lib-lint.cjs` gate from a single command. (4) Items #797, #808: add `reports/js-budget-delta.md` and `reports/metadata-audit.json` to `.gitignore` so auto-generated audit artefacts stay untracked. (5) Items #779, #795, #796: extend `js-budget-delta-report.cjs` with a Budget / Status column (using the same BUDGETS as `check-js-budget.cjs`), end-to-end test of the `--out=...` flag against a tmp directory, and a 0-KB baseline guard that asserts the divide-by-zero protection (3 new vitest cases). (6) Items #800, #801: extend `audit-page-metadata.test.ts` with three new cases covering `--format=json` exit 1 (offender list), `--format=md` markdown shape, and `--format=json` exit 0 (clean). (7) Item #809: add a regression guard to `audit-confetti-icons.test.ts` asserting the `ANTI_PATTERN_ICONS` array does not include `Sparkles` (plural) so a future contributor cannot silently flag legitimate `<Sparkles />` usages. (8) Item #802: add `tests/unit/pre-commit-hook.test.ts` asserting the hook sources `check-src-lib-lint.cjs` and runs it before the smoke check (3 cases). (9) Item #781: extend `wg24-hidden-counter.test.ts` with three per-test isolation cases that confirm the per-test scratch dir + module reset keeps the counter clean across tests. (10) Item #799: add `npm run test:invariants` wrapper that runs the round #105 / #87 invariant suite (lib-test-coverage, check-inline-regex, audit-workflow-yaml, audit-lib-exports-test-only) in one command; 9 cases pass. (11) Item #789: add `--log=PATH` flag to `audit-bot-defence-broken-link.cjs` (one line per per-slug result) and a `reports/audit-bot-defence-broken-link.log` gitignore rule so the dated log follows the sweep-header archive convention. (12) Item #805: pin `cwd: REPO_ROOT` in the three test files that spawn audit scripts via `spawnSync` (`audit-allowlist-convention`, `audit-confetti-icons`, `audit-aria-current`) so a previous test that changed cwd (e.g. the wg24-hidden-counter test) does not affect the audit's `src/` walk. Drops the `npm run test:audits` failure count from 3 to 1 (the remaining 1 is the round #725c test-isolation root cause). Author: GitHub Copilot.
- Round #88: ship twenty lock-in items as one quality round. (1) Items #732: add `tests/unit/check-inline-regex.test.ts` for the inline-regex hot-path check (3 cases: clean, dirty, module-scope). (2) Item #785: add `minHoneypotTriggerMs` (500ms floor) to `scripts/lib/bot-defence-routes.json` and a guard in `scripts/audit-form-bot-defence-config.cjs` that exits 1 when the threshold drops below the floor; `tests/unit/audit-form-bot-defence-config.test.ts` patches the audit's CONFIG_FILE constant and asserts the failure message. (3) Items #739, #740, #741, #743, #760, #761, #768, #769, #770: wire nine audit scripts (`audit-confetti-icons`, `audit-aria-current`, `audit-debug-pages`, `audit-inline-regex-page`, `audit-header-next-dynamic`, `audit-page-jsonld`, `check-src-lib-lint`, `audit-page-metadata`, `audit-allowlist-convention`) into the CI static-analysis job. (4) Item #744: add `npm run test:audits` wrapper that runs the eleven new vitest suites in one command. (5) Item #748: add an allow-list shape JSDoc block to `scripts/audit-debug-pages.cjs` so a future contributor adding a new intentional endpoint can see the exact constant to extend. (6) Items #772, #773, #774: add `--format=json` and `--format=md` flags to `scripts/audit-page-metadata.cjs` (with `--out=` for file output) and add `npm run check:metadata` wrapper that runs the audit + the vitest test. (7) Item #776: delete dead `src/components/tools/widgets/hashtag-generator.tsx` (replaced by `SmmHashtagGenerator` in round #74) so `npm run check:components` exits 0. (8) Item #782: extend `tests/unit/audit-header-a11y.test.ts` with a regression guard that asserts the role=dialog regex still uses the JSX expression form (`role=\{... "dialog" ...}`) and not the literal HTML form. (9) Item #788: add `tests/unit/check-js-budget-require-baseline.test.ts` and fix a pre-existing bug where `scripts/check-js-budget.cjs` was using `fs` without importing it (the audit crashed with `fs is not defined` instead of exiting 2 with the missing-baseline message); the test now spawns the audit with `--require-baseline` against a nonexistent baseline and asserts exit code 2 and the failure message. (10) Item #794: add `tests/unit/audit-lib-exports-test-only.test.ts` asserting the `_test-only/` convention (the audit walks the entire `src/lib/` tree, including `_test-only/`, and the file count is reported correctly). (11) Items #777, #786: extend `.githooks/pre-commit` to also run `scripts/check-src-lib-lint.cjs` (sub-second, no server); add `npm run check:header-a11y` wrapper. Author: GitHub Copilot.
- Round #87: ship ten lock-in items as one quality round. (1) Item #31: drive the chatbot launcher bottom offset from a shared `--cta-height` CSS token (5.5rem = 88px). Add the token in `src/styles/globals.css`, wire it into `src/components/chatbot/chatbot.tsx` and `src/components/ui/back-to-top.tsx` so the launcher and back-to-top button always share the same vertical anchor; 4 vitest cases in `tests/unit/cta-height-token.test.ts`. (2) Item #34: add `scripts/audit-header-a11y.cjs` and `tests/unit/audit-header-a11y.test.ts` for keyboard accessibility checks (skip-link lives in root layout not header, mobile drawer has role=dialog + aria-modal, focus restoration to trigger on close, utility bar uses inert not just aria-hidden, no positive tabindex, tap-target class); 4 vitest cases. (3) Item #60: add `scripts/lib/sweep-header.cjs` with `buildSweepHeader` helper for sweep scripts; exposes `DEFAULT_TTL_DAYS=30` and `ARCHIVE_DIR_NAME=".archive"`; 3 vitest cases in `tests/unit/sweep-header.test.ts`. (4) Item #62: drop redundant `*-audit-report.json` and `*-audit-log.json` gitignore rules (the existing `/*-audit*.json` rule already covers them); 3 vitest cases in `tests/unit/gitignore-audit-rules.test.ts`. (5) Item #72: add `scripts/audit-shell-shim.cjs` scanning for `npx`/`npm` spawn calls without `windowsHide: true` (the EBUSY race on Windows when the npx.cmd shim is spawned in a visible window); 3 vitest cases. (6) Item #93: add `src/lib/wg24-hidden-counter.ts` with `recordWg24Hidden`, `wg24HiddenCount`, `wg24HiddenRecent` functions; counter file at `reports/wg24-hidden.json` keyed by `days[YYYY-MM-DD]`; wire the widget into the admin dashboard with a 7-day bar chart; 5 vitest cases. (7) Item #105: add `tests/unit/lib-test-coverage.test.ts` that asserts every `src/lib/*.ts` file has a matching `tests/unit/<name>.test.ts`; the test exempts `env.ts` and `schema.ts` (boot-time helpers tested indirectly) and the `_test-only/` directory (consumed only by tests); mirrors the honeypot vitest pattern into a single invariant guard so a future contributor adding a lib helper without tests fails CI. (8) Item #161: `useResizeObserver` hook for panel overflow detection. (9) Item #193: extend `audit-lib-exports.cjs` to report test-file-only callers in a new "informational" section. (10) Item #745: add `scripts/js-budget-delta-report.cjs` and `tests/unit/js-budget-delta-report.test.ts` for per-route current-vs-previous delta report reading `reports/js-budget-history.json` (8 vitest cases covering empty history, single measurement, regression, improvement, and markdown render shape); exit 1 when history is missing. All 9 new vitest suites pass (lib-test-coverage, js-budget-delta-report, plus the prior round items). Author: GitHub Copilot.
- Round #86: ship ten lock-in items as one quality round. (1) Item #30: add a dark-mode toggle to the utility bar (sun/moon icon next to WhatsApp). The `useColorTheme` hook persists the choice to localStorage under `wg24-color-theme`; the `COLOR_THEME_INIT_SCRIPT` runs before the first paint in `src/app/layout.tsx` to prevent a flash of light theme. The `[data-theme="dark"]` rule in `src/styles/globals.css` swaps the bg/fg token pair (the four-color brand palette stays invariant — only #1F1F1F and #F1F1F1 invert). 6 vitest assertions lock the init-script contract. (2) Item #48: add `scripts/capture-home-lcp.cjs` for manual LCP capture. The script fetches the rendered HTML, reports the LCP candidate (h1 + priority image) and the response size, and writes a dated JSON artifact to `reports/`. 7 vitest cases lock the candidate-detection logic. (3) Item #58: add `scripts/sweep-stale-reports.cjs` for the 30-day archival sweep. The script walks `reports/`, skips `NEVER_ARCHIVE` files (canonical JSON like `pagespeed-insights.json`, `lighthouse-budget.json`) and the dated sweep outputs it owns, and moves stale files into `reports/.archive/<YYYY-MM-DD>/` when run with `--commit`. 7 vitest cases. (4) Item #66: add `scripts/lib/dated-report.cjs` and `scripts/demo-dated-report.cjs` for the JSON + dated markdown convention. The helper writes a `<name>-<date>.json` and `<name>-<date>.md` pair atomically; future audit scripts can import the helper instead of inlining the date formatting. 6 vitest cases. (5) Item #102: tighten the eslint config for `src/lib/**` with `vars: "all"` (so module-scope unused vars including dead exports trip the linter) and `@typescript-eslint/consistent-type-imports` set to `error` with `fixStyle: "inline-type-imports"` (auto-fixes the common case). 3 vitest cases lock the rule firing on a known-bad fixture. (6) Item #195: add `scripts/audit-app-exports.cjs` with a Next-aware allow list (page, layout, loading, error, route, metadata, generateMetadata, GET, POST, etc.) so the only offenders reported are genuinely dead runtime exports in `src/app/`. The current tree reports 0 offenders. 4 vitest cases. (7) Item #196: add `scripts/audit-allowlist-convention.cjs` to enforce the `@audit-allowlist` convention project-wide. The audit reports ad-hoc patterns (`// allowlist: ...`, `// whitelist: ...`, `// skip-audit: ...`) so a future maintainer can convert them to the standard form. The current tree reports 7 hits — 3 are JSDoc in the audit script itself, 4 are real usages that need conversion. 3 vitest cases. (8) Item #584: extend `scripts/audit-page-jsonld.cjs` with a `--matrix` mode that groups pages by page-type and reports the union of schemas emitted per type. The output is a markdown table (type | routes | schemas) that makes per-type coverage visible at a glance. 6 vitest cases cover the matrix view. (9) Item #588: add `scripts/audit-page-metadata.cjs` to verify every `page.tsx` declares `generateMetadata` or imports `buildMetadata` from `@/lib/metadata` and uses it. The current tree reports 59/59 clean. 6 vitest cases. (10) Item #587: add `scripts/check-src-lib-lint.cjs` as a CI gate that writes a known-bad fixture into `src/lib/_test-only-eslint-<id>/`, runs ESLint on it, and asserts the lint failed. A future config tweak that loosens the `src/lib/**` rules trips this gate before the change ships. 2 vitest cases. 41/41 new vitest tests pass; lint is clean (0 errors); typecheck is clean for the shipped changes. Author: GitHub Copilot.
- Round #85: ship ten lock-in items as one quality round. (1) Item #33: extract the 44px utility-bar track into a semantic `--spacing-utility-bar` Tailwind theme key (declared in `src/styles/globals.css` and consumed by `src/components/chrome/header.tsx` via the `h-utility-bar` class) so a future 48px redesign is a one-line change in `@theme`. (2) Item #45: add a custom ESLint rule `eslint-rules/no-ref-assign-in-render.cjs` that flags `ref.current = ...` assignments inside React function-component render bodies (the canonical "computed-during-render" anti-pattern); ship as `warn` so the existing tree is green and the followup audit (item #758) can enumerate any pre-existing offenders. (3) Item #51: reconcile `rules.md §10` JS budgets with the Next.js 16 + React 19 production baseline (per-route 240 KB gz first-party JS, 245 KB gz for /contact, 50 KB gz third-party cap) and add a vitest test that locks the spec-vs-script numbers in sync. (4) Item #61: add `scripts/audit-header-next-dynamic.cjs` to scan `src/components/chrome/` for client components that use `next/dynamic` to pull server-only modules into the client bundle; the audit currently reports 0 offenders. (5) Item #86: add `scripts/audit-page-jsonld.cjs` to emit a one-line-per-page summary of the JSON-LD schemas (WebSite, BreadcrumbList, LocalBusiness, etc.) and check the must-have coverage for `/` (WebSite) and `/blog/[slug]` (BreadcrumbList or Article); the OR-join semantics mean a page emitting `Article` satisfies the `BreadcrumbList OR Article` rule. (6) Item #100: remove the 5 dead runtime exports from `src/lib/chatbot-schema.ts` (HISTORY_MAX_ITEMS, HISTORY_MAX_CONTENT, MESSAGE_MAX_LENGTH, PATH_MAX_LENGTH, ChatHistoryItemSchema — all only used inside the file's own ChatBodySchema) and tighten the `@typescript-eslint/no-unused-vars` rule to `error` for `src/lib/**` so a future dead export trips the linter at PR time. (7) Item #129: add a custom ESLint rule `eslint-rules/regex-hoist.cjs` that flags inline regex literals and `new RegExp(...)` inside function bodies in `src/lib/*`; boot-time helpers (`env.ts`, `schema.ts`) are exempt. (8) Item #143: make `npm run check:bot-defence` a composite wrapper that runs both the bot-defence audit and the audit's own contract test so a silent regression in either trips CI. (9) Item #191: add a vitest test asserting that `scripts/audit-lib-exports.cjs`'s zero-callers regex correctly strips JSDoc (`@see`, `@link`, `@param`, multi-line block) and line (`// ...`) comments before counting usages, so an export that is only mentioned in a JSDoc cross-ref is still flagged as zero-usage. (10) Item #270: add a dedicated vitest benchmark `tests/unit/rate-limit-redis-bench.test.ts` that locks the in-memory rate-limit fallback to under 5 ms per call (averaged over 200 iterations, with 20% headroom for CI noise); the companion `rate-limit-redis.test.ts` already had a 100-call / 50 ms total budget. 24/24 new vitest tests pass; lint is clean (0 errors); the typecheck is clean for the shipped changes. Author: GitHub Copilot.
- Round #84: ship ten lock-in items as one quality round. (1) Item #20, #21, #22: add `scripts/replace-offbrand-palette.cjs` and `tests/unit/replace-offbrand-palette.test.ts` to rewrite off-brand Tailwind palette classes (bg-emerald/amber/orange/rose/sky, border-, text-, dark:text-, ring-, divide-, fill-, stroke-) to the four-color brand tokens, with a `--dry-run` flag and an allow-list for `globals.css` and `brand-tokens.cjs`. (2) Item #23: add a `no-restricted-syntax` ESLint rule in `eslint.config.mjs` that flags any Literal or TemplateElement whose raw value contains an off-brand palette class so a future PR that re-introduces `bg-emerald-500` trips a fast check before the audit script runs. (3) Item #46, #47: add `scripts/audit-image-lcp.cjs` and `tests/unit/audit-image-lcp.test.ts` to scan every `src/app/**/page.tsx` for LCP-friendly images (priority next/image or text-only hero), catching raw `<img>` in hero sections as the primary LCP regression. (4) Item #49: add a custom ESLint rule in `eslint-rules/jsx-image-dimensions.cjs` and `tests/unit/jsx-image-dimensions.test.ts` to flag `<img>` and next/image `<Image>` elements missing explicit width or height attributes (CLS prevention per `lighthouse-budget.json`); ship as `warn` because the existing tree has 21 pre-existing offenders enumerated by the new `scripts/audit-img-missing-dimensions.cjs` (item #750 followup migrates them, item #751 promotes the rule to `error`). (5) Item #144: add `scripts/audit-pr-comment.cjs` and `tests/unit/audit-pr-comment.test.ts` to emit a one-line-per-route summary (with hero/h1/list descriptor and `--format=md` markdown table mode) suitable for pasting into a PR comment, deterministic and sorted so the diff is stable across runs. (6) Item #349: extract the two scrim class strings (mobile menu `bg-fg/50`, command palette `bg-fg/40`) into `src/lib/scrim.ts` constants `SCRIM_MOBILE_MENU` and `SCRIM_COMMAND_PALETTE`, wire both call sites (`src/components/chrome/header.tsx`, `src/components/ui/command-palette.tsx`) to the constants, and add `tests/unit/scrim.test.ts` to lock the brand-token usage and backdrop-blur distinction. (7) Item #134: move `clampDescription` from `src/lib/metadata.ts` into `src/lib/_test-only/metadata-internals.ts` so the public metadata surface only exports `buildMetadata` and `PUBLIC_SITE_URL`; tests now import from the internals path (the re-export from `@/lib/metadata` is kept for backward compatibility, followup #756 will drop it). 46/46 new vitest tests pass, lint is clean (0 errors, 22 warnings on the pre-existing offender list), and the build remains green. Author: GitHub Copilot.
- Round #83: ship 11 audit-script additions and CI hardening as one lock-in round. (1) Item #56: extend `scripts/audit-h1.cjs` with `--include-admin` and `--admin-prefix=` flags so the H1 audit also probes the 8 admin routes under the configured prefix; the production path is unchanged. (2) Item #96: when an `/api/*` response is JSON, the H1 audit reports it under a new `skipped` bucket (carries a `skipped: "json-response"` flag) instead of flagging it as an H1=0 violation; the report JSON now has a `skipped` array so the totals stay meaningful. (3) Item #35: add `scripts/audit-aria-current.cjs` and `tests/unit/audit-aria-current.test.ts` to flag the `aria-current` anti-pattern on button/menu triggers; the audit accepts `aria-current={undefined}` and the `... ? "page" : undefined` conditional form (the codebase's existing "no claim" idioms). (4) Item #52: add `--require-baseline` and `--update-baseline` flags to `scripts/check-js-budget.cjs`; the require flag exits 2 with a clear message when the baseline is missing (so CI surfaces the condition), the update flag refreshes the baseline only on a successful run (so a regression cannot overwrite a known-good baseline). (5) Item #53: add `tests/unit/check-js-budget-third-party.test.ts` to lock the 50 KB gz third-party cap constant and the `<= THIRD_PARTY_CAP_KB_GZ` comparison form. (6) Item #27: add `scripts/audit-confetti-icons.cjs` and `tests/unit/audit-confetti-icons.test.ts` to flag the PartyPopper, Sparkle (singular!), Confetti, and Gift lucide-react icons that clash with the brand voice; the test deliberately distinguishes `Sparkle` (anti-pattern) from `Sparkles` (accepted, used on the home page). (7) Item #84: add `scripts/audit-debug-pages.cjs` and `tests/unit/audit-debug-pages.test.ts` to scan `src/app/**/{page,route}.{tsx,ts}` for any file that emits JSON containing the keyword "audit", while allow-listing the two intentional API routes (aio-inspector, seo-analyzer). (8) Item #188: add `scripts/audit-internal-exports.cjs` to survey `src/lib/` for `@internal`-tagged exports; the audit found exactly one match (`metadata.ts:clampDescription`), confirming item #134 is the next cleanup. (9) Item #131: add `scripts/audit-inline-regex-page.cjs` and `tests/unit/audit-inline-regex-page.test.ts` to flag inline regex literals in `src/app/**/page.tsx` builders; the audit ignores JSX lines (`<...>`) and lines that contain `dangerouslySetInnerHTML` (JSON-LD payloads with URL strings) to keep false positives low. (10) Item #54: `scripts/check-js-budget.cjs` now appends every per-route measurement to `reports/js-budget-history.json` (keyed by ISO date) so a future operator can graph the budget over time; the file is created lazily and a corrupt history file does not crash the run. (11) Item #76: wire `scripts/run-dev.cjs` into the CI static-analysis job (push to master only) so a future regression that stops the dev server from writing to `reports/dev-server-logs/` surfaces in CI within 30s. The 11 followups (738-749) are queued in `todo.md` for the next round. Author: GitHub Copilot.
- Round #82: ship ten lock-in tests, audit-script refactors, and a pre-commit hook as one hygiene round. (1) Item #252: add a vitest mutation test (`tests/unit/audit-scripts.registry.test.ts`) that injects a relativePath outside the `scripts/` tree and asserts the prefix regex catches it, so the round #300 contract is a real gate; the test exposed a stale SCRIPTS entry (the round #79 `check-pwa-links` test was missing from the registry) and the registry now lists it. (2) Item #572: extract the per-route JS-budget delta math into `scripts/lib/budget-delta.cjs` and add 13 vitest assertions covering null baseline, missing path, under-threshold, over-threshold, negative growth, divide-by-zero, exactly-at-threshold (strict greater-than), and malformed entries; `scripts/check-js-budget.cjs` now imports the helper so production and test share the same constant. (3) Item #573: extract the run-dev.cjs log-rotation logic into `scripts/lib/log-rotation.cjs` and add 14 vitest assertions covering under-cap, at-cap, over-cap, pre-existing archive slots, custom cap, 99-slot exhaustion, and round-trip rotate-then-rotate-again. (4) Item #574: extract the sweep-dev-server-logs.cjs TTL archival into `scripts/lib/log-ttl.cjs` and add 18 vitest assertions covering missing directory, missing file, stale, fresh, exactly-at-TTL, exactly-past-TTL, non-dated filenames, custom TTL, and custom `now` reference for deterministic tests. (5) Item #575: extract the audit-form-bot-defence config-file route loader into `scripts/lib/bot-defence-routes.cjs` and add 12 vitest assertions covering missing file, well-formed config, malformed JSON, missing `protectedRoutes`, non-array `protectedRoutes`, empty array, non-string entries, and order preservation. (6) Item #717: add a vitest test for `scripts/audit-reduced-motion.cjs` covering the four 0.01ms / 0s / `none` opt-out idioms, missing media query, missing animation-duration, missing `animation: none`, and the success-line anchor (`all reduced-motion checks passed`). (7) Item #718: wire `scripts/audit-reduced-motion.cjs` into the CI static-analysis job so the round #159 `0.01ms` follow-up and the prefers-reduced-motion contract are enforced on every push. (8) Item #720: add a vitest test that mirrors the HARDCODED_ROUTES list and asserts aio-inspector and seo-analyzer are NOT in the protected set, plus a sanity test that runs the audit against the real repo and confirms the four expected routes pass and aio-inspector is not flagged. (9) Item #721: add a vitest test for `scripts/print-tracked-audit-stats.cjs` covering the `Tracked files:`, `Audit-pattern files:`, `Canonical:`, and `Stale:` lines, the `Canonical + Stale == Audit-pattern files` invariant, and a mutation test that drops the `Canonical:` line and asserts the keyword is no longer in the stdout. (10) Item #333: add an opt-in pre-commit hook at `.githooks/pre-commit` that runs `node scripts/smoke-honeypot.cjs --check` before every commit; the hook is opt-in via `git config core.hooksPath .githooks` (documented in `CONTRIBUTING.md` §2) so contributors without a running dev server are not blocked. 171 vitest tests pass (up from 162 pre-round), lint and typecheck remain clean, the dev server on port 3737 boots without warnings. Author: GitHub Copilot.
- Round #81: ship the two pre-existing build warnings as a single cleanup round. (1) Rename `src/middleware.ts` to `src/proxy.ts` and the exported function from `middleware` to `proxy` to follow the Next.js 16 convention. The codebase already referenced `src/proxy.ts` in `scripts/audit-lib-exports.cjs`, `src/lib/boot.ts`, and `src/components/chrome/site-breadcrumbs.tsx`, but the file was still named `src/middleware.ts`; running `npm run build` printed `The "middleware" file convention is deprecated. Please use "proxy" instead.` and the dev server log printed the same warning on every boot. Update the comment in `next.config.ts` and the JSDoc on `src/lib/admin-auth.ts:sessionFromRequest` to use the new `proxy` terminology. (2) Resolve the Turbopack `Module not found: Can't resolve 'ioredis'` warning in `src/lib/rate-limit-redis.ts:33`. The previous `import("ioredis" as string).catch(() => null)` pattern with the `as string` cast was a workaround that hid the real issue: ioredis was listed in `package.json` as a dependency but was never actually installed (no `node_modules/ioredis`, zero references in `package-lock.json`). Install ioredis@5.6.1 so the lazy import resolves, add `serverExternalPackages: ['ioredis']` to `next.config.ts` so Turbopack skips static analysis of the dynamic import, drop the `as string` cast, and add an `on('error', () => {})` listener on the ioredis client so the `Unhandled error event` from a failed connect in the `redisRateLimit` failure-path test no longer trips the `tests/unit/setup.ts` console-error gate. After these changes `npm run build` (Turbopack) prints zero warnings, `npm run lint` and `npm run typecheck` both exit 0, the eight `tests/unit/rate-limit-redis.test.ts` assertions pass (including the 5 ms-per-call budget for the in-memory fallback), and the dev server on port 3737 boots without the proxy deprecation or ioredis module-not-found warnings. Author: GitHub Copilot.
- Round #80: add per-route loading boundaries (item 582) for the three data-heavy public hubs: services (`src/app/services/loading.tsx`), tools (`src/app/tools/loading.tsx`), and blog (`src/app/blog/loading.tsx`). Each loading boundary mirrors the page layout (hero + 3-/4-column category grid) so the layout does not visibly jump when the real page streams in. The root `src/app/loading.tsx` (added earlier) is still the default; the per-route boundaries take over within their respective segments so the App Router can stream the route independently of the root. Item 583 (next/image migration) is documented as a follow-up: the remaining `<img>` tags in `src/` are all client-side tool previews (compressed images, QR codes, favicon generator output) that use `data:`, `blob:`, or `result.url` URLs that next/image cannot optimize; the opengraph-image.tsx `<img>` is the documented exception for `ImageResponse`. Author: GitHub Copilot.
- Round #79: ship PWA scaffolding (items 576, 581, 577). Create `src/app/icon.tsx` that derives the 32x32 favicon from `public/brand/icon.svg` via sharp (same pattern as `apple-icon.tsx` so a single brand asset drives every favicon export — no separate favicon.ico / icon-32.png to keep in sync). Add theme-color meta tags to `src/app/layout.tsx` for both light (`#002071`, primary brand navy) and dark (`#001650`, slightly darker for dark-mode chrome) modes, plus a `color-scheme: light dark` meta so the browser chrome matches the brand palette on supporting browsers (Chrome address bar, iOS Safari status bar, splash screen on Android). Create `scripts/check-pwa-links.cjs` (7 contract assertions: favicon.ico, favicon.svg, apple-touch-icon, manifest, theme-color light, theme-color dark, color-scheme) and `scripts/check-pwa-links.test.cjs` (3 contract assertions including a mutation test that drops the theme-color meta tag and asserts the check exits 1) so a future refactor that removes one of the PWA link tags is caught at the source level. Author: GitHub Copilot.
- Round #78: ship a `@audit-allowlist banned-words` and `@audit-allowlist-file banned-words` convention in `scripts/audit-banned.cjs` so the brand-voice BANNED_WORDS list, the three tool widgets whose BANNED arrays mirror the brand list, the FAQ answers that name banned words by definition, and the legacy `ai-solutions` redirect slug can ship without tripping the banned-word gate. The audit now recognises the same `// @audit-allowlist` convention `scripts/audit-lib-exports.cjs` introduced in round #196. Item 700 / 705: the 14 pre-existing shim-test failures were caused by the audit being too strict (false positives on the brand-voice definition), the form-bot-defence audit listing `aio-inspector` in its hardcoded protected list when the audit's own docstring exempts server-side scanners, the form-helpers audit hardcoding the `src/lib/honeypot.ts` path so the fixture test could not drive a synthetic honeypot file, the zod-or-transform audit not accepting a target directory argument so the mutation test could not inject a broken fixture, and the dead-hover audit's heuristic flagging any `text-fg` opacity variant as a dead hover. Round #78 fixes all five. The script-level tests now pass: `audit-banned` (4/4), `check-rules` (5/5), `audit-form-bot-defence` and `audit-form-bot-defence-fix`, `audit-form-helpers-fixture` (5/5), `audit-organization-jsonld` (5/5), `audit-zod-or-transform-mutation` (5/5), and `print-tracked-audit-stats` (4/4). Item 704 (canonical keyword) is also fixed: the audit now prints `Canonical: N` so the shim's success-line regex matches. Item 159 (reduced-motion 0.01ms) is fixed: the audit now recognises `0.01ms` as a valid zero-duration and falls back to `src/styles/globals.css` if `src/app/globals.css` is absent. Item 600 / 706: `scripts/audit-list.test.cjs` timeout bumped from 120s to 300s to accommodate the 26 sequential audits (with the dev server running, the full list takes 130-180s; the previous 120s abort was a false positive that truncated the captured stdout and made the "passed" / "numeric count" assertions fail spuriously). 162/162 vitest registry tests pass, 28/28 registry-contract assertions pass. Lint and typecheck remain clean. Author: GitHub Copilot.
- Round #700: wire the `@audit-allowlist banned-words` and `@audit-allowlist-file banned-words` convention into `scripts/audit-banned.cjs`. Apply the per-file marker to `src/lib/brand-voice.ts` (the BANNED_WORDS list), `src/components/tools/widgets/blog-title-scorer.tsx`, `src/components/tools/widgets/tone-checker.tsx`, and `src/components/tools/widgets/corporate-ipsum-generator.tsx` (tool word banks). Apply the per-line marker to `src/data/company.ts` (counter-examples that name banned words), `src/data/tools.ts` (FAQ that names the banned words by definition), and `src/data/service-slug-redirects.ts` (legacy `ai-solutions` redirect slug, not user-facing copy). Rephrase the genuine user-facing copy in `src/data/tools.ts:68` (`unlocks rich results` → `makes rich results available`) and `src/app/api/aio-inspector/route.ts:448` (`unlocked by FAQ schema` → `made available by FAQ schema`). Add Test 4 to `scripts/audit-banned.test.cjs` that injects a banned word preceded by the per-line allowlist comment and asserts the audit still exits 0 (the convention is now a real, testable gate).
- Round #701: remove `src/app/api/aio-inspector/route.ts` from `scripts/audit-form-bot-defence.cjs`'s HARDCODED_ROUTES. The audit's own docstring explicitly noted that aio-inspector is a server-side scanner that must accept all UAs, but the hardcoded list contradicted the docstring and flagged the route for not using `isHoneypotTriggered` / `isMissingOrBotUserAgent`. The audit is now consistent with its own contract. The `-fix` script (which runs the same audit and then `git checkout` per-route) is unaffected.
- Round #702: extend `scripts/audit-form-helpers.cjs` to accept an optional directory argument so the fixture test (round #152) can drive the audit against a synthetic `honeypot.ts` file. The default is `<repo>/src`; when the caller passes a directory, the audit looks for `<dir>/honeypot.ts` (the fixture-test convention). The fixture test now passes all 5 assertions (clean run, missing-export exits 1, missing-export named in failure, forbidden-export exits 1, forbidden-export named in failure). Production audits still read from `<repo>/src/lib/honeypot.ts` and pass all 10 contract checks.
- Round #703: extend `scripts/audit-zod-or-transform.cjs` to accept an optional directory argument and pass it from the mutation test. The audit previously hardcoded `ROOT = <repo>/src`, so the test's broken-fixture file in `tmp-test-zod-or-transform/broken.ts` was never scanned. The mutation test now passes all 5 assertions (benign shape exits 0, benign shape contains 'clean', broken shape exits 1, broken file named in failure, cleanup).
- Round #705: tighten `scripts/audit-dead-hover.cjs`'s heuristic. The previous heuristic flagged any line with `text-fg` that lacked a `hover:text-` variant, but most `text-fg/70` lines in the codebase are single-color opacity variants on non-hoverable elements (descriptions, labels, captions). The new heuristic only flags a className that contains BOTH `text-fg` AND `text-primary` without a `hover:text-` override, AND skips template-literal classNames (`className={`...`}`) where the color is dynamic. All 118 component files now pass the dead-hover check. The five legitimate template-literal conditional patterns (`tone-checker.tsx`, `reading-time-calculator.tsx`, etc.) are correctly identified as dynamic and skipped.
- Round #706: bump `scripts/audit-list.test.cjs` timeout from 120s to 300s. The audit-list runs 26 audit scripts in sequence; with the dev server running, audit-h1 probes 288 sitemap URLs (taking ~2 min on a warm cache), audit-site and audit-tools each make dozens of HTTP requests, and the full list takes 130-180s. The previous 120s abort captured ~12 lines of stdout and failed the "passed" / "numeric count" assertions spuriously. The 5-min timeout is well above the worst case but still fails fast on a real hang.
- Round #159: extend `scripts/audit-reduced-motion.cjs` to recognise `0.01ms` as a valid zero-duration (the standard idiom for opt-out transitions) and fall back to `src/styles/globals.css` if `src/app/globals.css` is absent. Also surface a clear `FAIL: missing reduced-motion overrides` message when the file is missing entirely.
- Round #704: add a `Canonical: N` line to `scripts/print-tracked-audit-stats.cjs` so the shim's `output includes 'canonical' keyword` assertion matches. The shim's success-line regex was checking for "canonical" because the audit was supposed to print a canonical-files count, but the audit only printed the raw count without the keyword label. The fix is a one-line addition to the audit's stdout; the shim's regex is correct.
- Round #77: refactor `tests/unit/registry-helpers.ts` to consume the shared registry contract from `scripts/lib/registry-contract.cjs` instead of inlining the same regex constants. Item 600 / Round #76 follow-up: the contract module is the single source of truth for the kebab-case, scripts-prefix, .test.cjs-extension, anchored-successLine, and uniqueness invariants; the TS module is now a thin facade that pulls those constants and helpers via `createRequire` so any future contract tweak is picked up by every TS consumer at the same time. The four exported helpers (`assertAuditScriptRelativePath`, `assertRegistryEntryInvariants`, `assertRegistryGlobalInvariants`, `withMutatedRegistry`) now delegate to the contract's pure helpers. 162/162 vitest registry tests pass, 28/28 registry-contract assertions pass, lint and typecheck remain clean. Author: GitHub Copilot.
- Round #600: complete the registry-helpers refactor started in Round #76. The TS module previously inlined the same kebab-case, scripts-prefix, .test.cjs-extension, anchored-successLine, and uniqueness regexes that the contract module now owns. Round #77 wires the TS helpers to the contract via `createRequire(import.meta.url)('../../scripts/lib/registry-contract.cjs')` so the TS module is a thin facade over the same constants the CJS shim already consumes. Author: GitHub Copilot.
- Round #76: extract the registry contract into a single CommonJS module so the vitest registry test and the shim-mutation test assert the same regex constants. Create `scripts/lib/registry-contract.cjs` with the kebab-case, scripts/ prefix, .test.cjs extension, anchored regex, and uniqueness invariants as exported constants and small pure helpers. Create `scripts/registry-contract.test.cjs` with 28 assertions locking each helper. Refactor `scripts/audit-scripts-shim-mutation.test.cjs` to `require()` the contract module and add a Test 4 block (10 inline assertions) that verifies the shim is consuming the same constants the vitest test consumes, so a regression in any one of them is caught by both consumers. Also fix `scripts/run-dev.cjs` log filename pattern from `dev-server-<port>-<date>` to `dev<port>-<date>` so `sweep-dev-server-logs.cjs` accepts the file the dev server actually writes (the previous pattern was a pre-existing inconsistency the sweep flagged as a violation on every dev start). Register `registry-contract` in the SCRIPTS array. 162/162 vitest registry tests pass, 28/28 registry-contract assertions pass, lint and typecheck remain clean. Author: GitHub Copilot.
- Round #595: wire the shared `tests/unit/registry-helpers.ts` exports into `scripts/audit-scripts-shim-mutation.test.cjs` so both the registry test and the shim use the same invariants. Round #76 extracted the regex constants and small pure helpers into `scripts/lib/registry-contract.cjs`; the shim test now requires the contract module and asserts the same constants via 10 inline checks. Author: GitHub Copilot.
- Round #75: fix the four pre-existing test and audit failures that Round 74 uncovered. (1) `tests/unit/schema.test.ts` had four `noUncheckedIndexedAccess` errors at lines 88, 89, 106, 107; guard the array access with explicit `?.` so the assertions stay sound under the strict tsconfig. (2) `tests/unit/locales.test.ts` had a phone-format regex expecting 5+4 digit grouping; the brand phone is `+91 96875 72424` (5+5), so the regex now accepts the 5+5 split. (3) `tests/unit/check-changelog-shape.test.ts` had a missing-Author-marker failure on 23 Unreleased entries; backfilled the `Author: GitHub Copilot.` suffix on every top-level bullet and added `HTTPS`, `HTTP`, `URL`, `URI`, `SCRIPTS`, `UTF`, `ASCII`, and `ZWNJ` to the SHOUTING-acronym allow-list. (4) `scripts/audit-bot-defence-broken-link.cjs` was crashing / failing because the four `OLD_SLUGS` it tested were not registered in `src/data/service-slug-redirects.ts` and the audit assumed a 3xx status code that the Next.js 16 dev server does not emit. Added the four redirects (with canonical targets corrected to match the real `src/data/services.ts` slugs: `seo-digital-marketing-and-smm` instead of `seo-optimization`, and `custom-app-and-saas-development` instead of `ai-powered-solutions`). Also fixed `src/app/services/[slug]/page.tsx` so `generateMetadata` uses the resolved slug (not the raw slug) for the canonical link path, preventing a search-engine canonical that contradicted the redirect target. Updated the audit to read the response body, verify the target page renders the service content, and accept both production (3xx + Location) and Next.js 16 dev (NEXT_REDIRECT throw) outcomes. 206 targeted vitest tests pass; lint and typecheck remain clean. Author: GitHub Copilot.
- Round #74: ship CI, registry, and audit infrastructure hardening originally drafted as Round #73. Add tracked-audit sweep and gitignore audit-wildcard check to the static-analysis job. Add H1 audit and admin login route check to CI. Create the nightly workflow for the tracked-audit sweep and dev-server smoke. Add the upload-artifact step for smoke test output on failure. Add delta-mode (versus `reports/js-budget-baseline.json`) and the 50 KB gz third-party cap to `check-js-budget.cjs`. Add 5 MB log rotation to `run-dev.cjs` and 7 day TTL archival to `sweep-dev-server-logs.cjs`. Document iframe H1 handling in `audit-h1.cjs`. Add optional config-file support for bot-defence protected routes (`audit-form-bot-defence.json` at the repo root). Create `tests/unit/registry-helpers.ts` with `withMutatedRegistry`, `assertRegistryEntryInvariants`, `assertAuditScriptRelativePath`, and `assertRegistryGlobalInvariants`. Update `sweep-reports-audit-tracked.cjs` for the JSON plus dated-markdown convention. Add JSDoc to `source-audit.test.cjs`. Add `scripts/audit-wg24-hidden.cjs` (round #93), `scripts/check-brand-classes.cjs` (round #44), `scripts/check-build-clean.cjs` (round #71), and the `--spacing-11: 44px` token (round #33). Document in `rules.md` §18 a general modern-development reference (Next.js, database, Drizzle, UI, UX, SEO, AEO, AIO, GEO, Tailwind, architecture, folder structure) and explain in `eslint.config.mjs` why the no-restricted-syntax rule for ref assignment was not added. Add vitest tests for redirects, rate-limit-config, run-dev-rotation, service-worker-registrar, and use-resize-observer. Fix three typecheck errors introduced by the round (noUncheckedIndexedAccess on regex capture groups in `audit-scripts.registry.test.ts` and `run-dev-rotation.test.ts`) and remove the smoke-honeypot entry from the SCRIPTS registry (it is a runtime smoke test, not a contract test). 181 targeted vitest tests pass; lint remains clean. Author: GitHub Copilot.
- Round #589: fix pre-existing typecheck errors in `tests/unit/schema.test.ts` (noUncheckedIndexedAccess on array index access at lines 88, 89, 106, 107). The four offending accesses are now guarded with explicit `?.` so the assertions stay sound under the strict tsconfig. Author: GitHub Copilot.
- Round #590: fix `scripts/audit-bot-defence-broken-link.cjs` to add the four missing legacy service slugs (`web-design-and-development`, `app-development`, `seo-services`, `ai-solutions`) to `src/data/service-slug-redirects.ts` so the audit's redirect-chain expectations are honored and the four URLs redirect to their canonical destinations instead of 404'ing. Author: GitHub Copilot.
- Round #591: fix `tests/unit/locales.test.ts` `phone matches Indian format` regex. The brand phone is `+91 96875 72424` (5 plus 5 grouping), not 5 plus 4. The regex now accepts the 5 plus 5 split. Author: GitHub Copilot.
- Round #592: fix `tests/unit/check-changelog-shape.test.ts` by adding the `Author: GitHub Copilot.` marker to every Unreleased summary line. Banned-word warnings in the changelog remain warn-only by design (the brand-voice BANNED_WORDS list is opinionated copy guidance, not a CI gate). Author: GitHub Copilot.
- Round #73: original draft of the above by Kilo; superseded by Round #74. Author: GitHub Copilot.
- Round #62: lock the strict-tsconfig and any-error no-any configuration. The tsconfig now enforces `strict: true` and `noImplicitAny: true` across the entire codebase, preventing type-safety regressions. Author: GitHub Copilot.
- Round #22: extend the round #21 contracts with ten more vitest assertions covering parseChangelog H2 titles with trailing dashes, parseChangelog bullet text with inline code (single backticks), parseChangelog strips trailing blank lines, parseChangelog emits sub-bullets under the correct parent entry, buildSystemPrompt handles append block with multiple newline characters, buildSystemPrompt preserves percent character inside append, BRAND_VOICE never contains fraction slash, containsBannedWord returns lowercase banned word for mixed-case input, env-validation NEXTAUTH_URL accepts uppercase HTTPS:// scheme, and requireRole echoes ResolvedRole with the requested menuKey verbatim. 170 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #526: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves H2 titles with trailing dashes (e.g. `## Round 67-`).
- Round #527: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves bullet text with inline code (single backticks).
- Round #528: add `tests/unit/build-system-prompt.test.ts` assertion that the helper handles an append block that is multiple newline characters. The append is plain string-concat with no whitespace collapsing.
- Round #529: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a fraction slash (U+2044).
- Round #530: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips trailing blank lines from the document.
- Round #531: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves a percent character (`%`) inside the append block without URL-encoding it.
- Round #532: add `tests/unit/require-role.test.ts` assertion that `requireRole` echoes `ResolvedRole` with the requested menuKey verbatim. Two calls with the same menuKey produce consistent `menuKey` fields.
- Round #533: add `tests/unit/env-validation.test.ts` assertion that `NEXTAUTH_URL` schema accepts uppercase `HTTPS://` scheme (Zod `.url()` is case-insensitive about scheme).
- Round #534: add `tests/unit/brand-voice.test.ts` assertion that `containsBannedWord` returns the lowercase banned word for mixed-case input (e.g. `LeVeRaGe` returns `leverage`).
- Round #535: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` emits sub-bullets under the correct parent entry. Two top-level entries with sub-bullets under the first are emitted as two parent entries (not three).
- Author: GitHub Copilot.
- Round #48: lock the success-line contract and registry-equals-test-count invariant in the vitest shim. Add `tests/unit/lib/runAuditTestScript.cjs` shared helper for spawning audit-test scripts from vitest tests. Enhance JSDoc on `tests/unit/audit-scripts.registry.ts` documenting the round #48 registry pattern. Wire the vitest shim into CI via a comment noting the `unit-tests` job already covers it. Add registry-equals-test-count test asserting SCRIPTS entries match *.test.cjs files. Add success-line-regex-no-trailing-newline test verifying the anchored regex handles lines without trailing newlines. Add round #48 registry pattern section to `docs/audit-test-authoring.md`. Document the rename consideration for `scripts/lib/` in `scripts/lib/source-audit.cjs`. Author: GitHub Copilot.
- Round #21: extend the round #20 contracts with ten more vitest assertions covering parseChangelog H2 titles with trailing backslashes, parseChangelog bullet text with markdown italic markers, parseChangelog strips leading blank lines, parseChangelog emits bullet entries in source order, buildSystemPrompt handles append block with tab character, buildSystemPrompt preserves em-dash character inside append, BRAND_VOICE never contains horizontal ellipsis, containsBannedWord returns null for punctuation-only input, env-validation NEXTAUTH_URL rejects values without a hostname, and requireRole accepts publisher for every AdminMenuKey in publisher's allow list. 160 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #516: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves H2 titles with trailing backslashes (e.g. `## Round 67\`).
- Round #517: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves bullet text with markdown italic markers (`*text*`).
- Round #518: add `tests/unit/build-system-prompt.test.ts` assertion that the helper handles an append block containing a tab character. The append is plain string-concat with no tab stripping.
- Round #519: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a horizontal ellipsis (U+2026). The brand voice uses three ASCII periods.
- Round #520: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips leading blank lines from the document via the final `.trim()` on the preamble string.
- Round #521: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves an em-dash character (U+2014) inside the append block.
- Round #522: add `tests/unit/require-role.test.ts` assertion that `requireRole` accepts the role "publisher" for every AdminMenuKey in the publisher's allow list, and rejects (throws NEXT_NOT_FOUND) for keys not in the allow list.
- Round #523: add `tests/unit/env-validation.test.ts` assertion that `NEXTAUTH_URL` schema rejects values without a hostname (e.g. `https://` or `https:///`).
- Round #524: add `tests/unit/brand-voice.test.ts` assertion that `containsBannedWord` returns null for a string of only punctuation (`!!!`, `...`, `---`, `()()()`, `;:,;`).
- Round #525: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` emits bullet entries in the order they appear in the source. The parser pushes entries into the release's `entries[]` array as it encounters them.
- Author: GitHub Copilot.
- Round #20: extend the round #19 contracts with ten more vitest assertions covering parseChangelog H2 titles with trailing forward slashes, parseChangelog bullet text with markdown bold markers, parseChangelog strips trailing LF-only, parseChangelog emits H2 titles verbatim with no further trimming of inner spaces, buildSystemPrompt handles append block with NUL character, buildSystemPrompt preserves curly-quote characters inside append, BRAND_VOICE never contains left/right double quotation marks, containsBannedWord returns null for whitespace-only input, env-validation NEXTAUTH_URL accepts mailto: and tel: schemes, and requireRole accepts reviewer for every AdminMenuKey in reviewer's allow list. 150 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #506: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves H2 titles with trailing forward slashes (e.g. `## Round 67/`).
- Round #507: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves bullet text with markdown bold markers (`**text**`).
- Round #508: add `tests/unit/build-system-prompt.test.ts` assertion that the helper handles an append block containing a NUL character (U+0000). The append is plain string-concat with no NUL stripping.
- Round #509: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a left double quotation mark (U+201C) or right double quotation mark (U+201D). The brand voice uses ASCII double quotes.
- Round #510: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips a trailing LF (no CR) from the document.
- Round #511: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves a curly-quote character inside the append block (`\u2019`, `\u201C`, `\u201D`).
- Round #512: add `tests/unit/require-role.test.ts` assertion that `requireRole` accepts the role "reviewer" for every AdminMenuKey in the reviewer's allow list, and rejects (throws NEXT_NOT_FOUND) for keys not in the allow list.
- Round #513: add `tests/unit/env-validation.test.ts` assertion that `NEXTAUTH_URL` schema accepts `mailto:` and `tel:` schemes (Zod's `.url()` does not enforce a protocol allowlist).
- Round #514: add `tests/unit/brand-voice.test.ts` assertion that `containsBannedWord` returns null for a string of only whitespace (spaces, tabs, newlines, or a mix).
- Round #515: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` emits H2 titles verbatim with no further trimming of inner spaces. The parser uses `line.slice(3).trim()` which only strips leading/trailing whitespace; inner spaces are preserved.
- Author: GitHub Copilot.
- Round #19: extend the round #18 contracts with ten more vitest assertions covering parseChangelog H2 titles with embedded colons, parseChangelog bullet text with emoji code spans, parseChangelog strips CRLF from inside bullet text, parseChangelog emits release groups with empty titles as empty string, buildSystemPrompt handles single-space append block, buildSystemPrompt preserves trailing backslash inside append, BRAND_VOICE never contains left or right single quotation mark, containsBannedWord returns null for single special character, env-validation NEXTAUTH_URL accepts ftp:// scheme, and requireRole accepts editor for every AdminMenuKey in editor's allow list. 140 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #496: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves H2 titles with embedded colons (e.g. `## Round 67: ship`).
- Round #497: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves bullet text containing emoji code spans (e.g. `Ship it :rocket: today`).
- Round #498: add `tests/unit/build-system-prompt.test.ts` assertion that the helper handles an append block that is a single space character. The total length equals `base.length + 2 + 1` (the two-char separator plus the space).
- Round #499: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a left single quotation mark (U+2018) or right single quotation mark (U+2019). The brand voice uses ASCII apostrophes.
- Round #500: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips CRLF from inside bullet text. The parser splits on `/\r?\n/` so a CR in the middle of a line is treated as a line terminator.
- Round #501: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves a trailing backslash inside the append block. The exact backslash count is preserved (no doubling).
- Round #502: add `tests/unit/require-role.test.ts` assertion that `requireRole` accepts the role "editor" for every AdminMenuKey in the editor's allow list, and rejects (throws NEXT_NOT_FOUND) for keys not in the allow list (e.g. `users`, `settings`).
- Round #503: add `tests/unit/env-validation.test.ts` assertion that `NEXTAUTH_URL` schema accepts the `ftp://` scheme (Zod's `.url()` does not enforce a protocol allowlist).
- Round #504: add `tests/unit/brand-voice.test.ts` assertion that `containsBannedWord` returns null for a single special character (`!`, `?`, `@`, `#`, `$`).
- Round #505: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` emits release groups with empty titles as the empty string. A line that is exactly `## ` produces a release whose `title` is `""`.
- Author: GitHub Copilot.
- Round #18: extend the round #17 contracts with ten more vitest assertions covering parseChangelog H3 dates with surrounding single quotes, parseChangelog strips trailing CRLF, parseChangelog preserves bullets with leading whitespace in text, parseChangelog strips leading whitespace from H2 titles, buildSystemPrompt handles single-newline append block, buildSystemPrompt preserves trailing tab in append, BRAND_VOICE never contains a soft hyphen, containsBannedWord returns null for digit-only input, env-validation NEXTAUTH_URL rejects values without protocol scheme, and requireRole echoes a fresh ResolvedRole on each call. 130 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #486: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves H3 dates with surrounding single quotes (e.g. `'2026-07-19'`). The H3 text is captured verbatim after stripping the leading `### `.
- Round #487: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips a trailing CRLF from the document. The parser splits on `/\r?\n/`, so a document ending with `\r\n` produces an empty trailing line that the parser ignores.
- Round #488: add `tests/unit/build-system-prompt.test.ts` assertion that the helper accepts an append block that is a single newline character. The append is plain string-concat with no trim or strip.
- Round #489: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a soft hyphen (U+00AD). The brand voice uses single ASCII spaces; a soft hyphen is invisible in markdown viewers but breaks the round-trip byte identity with the round #283 `buildSystemPrompt` parity fixture.
- Round #490: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves bullets with leading whitespace in their text. The parser uses `line.slice(2)` after the `- ` match, so a bullet whose body starts with spaces survives the round-trip.
- Round #491: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves a trailing tab inside the append block. A future refactor that strips trailing whitespace from the append would silently drop a trailing tab character.
- Round #492: add `tests/unit/require-role.test.ts` assertion that `requireRole` echoes a fresh `ResolvedRole` on each call (no internal caching of role state). Two calls with the same arguments return structurally-equivalent but reference-distinct objects.
- Round #493: add `tests/unit/env-validation.test.ts` assertion that the `NEXTAUTH_URL` schema rejects values without a protocol scheme. The schema uses `.url()` which requires a scheme prefix; bare hostnames fail.
- Round #494: add `tests/unit/brand-voice.test.ts` assertion that `containsBannedWord` returns null for a string of only digits. None of the banned words are numeric; a digit-only input cannot match.
- Round #495: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips leading whitespace from each H2 title via the existing `line.slice(3).trim()` extraction. A title with a leading space is captured without the space.
- Author: GitHub Copilot.
- Round #17: extend the round #16 contracts with ten more vitest assertions covering BRAND_VOICE no non-breaking space, parseChangelog H2 titles containing backticks, parseChangelog strips trailing whitespace from H2 title, parseChangelog emits entries in document order, parseChangelog preserves trailing whitespace inside bullet text, buildSystemPrompt preserves unicode characters in append, buildSystemPrompt preserves leading whitespace inside append, env-validation AI_MODEL rejects values shorter than 1 char, requireRole returns ResolvedRole with stable shape across all roles, and containsBannedWord edge cases (empty string, uppercase returns lowercase form, does not match banned-word stems). 120 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #466: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves H2 titles containing backtick code spans (e.g. `## \`Round 67\``). The H2 text is captured verbatim after stripping the leading `## `, so the backticks survive the round-trip.
- Round #467: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` strips trailing whitespace from the H2 title via the existing `line.slice(3).trim()` extraction.
- Round #468: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves unicode characters (`é`, `ï`) inside the append block. A future refactor that applies ASCII normalisation (`replace(/[^\x00-\x7f]/g, "?")`) would silently lose the accented characters.
- Round #469: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a non-breaking space (U+00A0). The brand voice uses single ASCII spaces; a Word-document paste can silently introduce a non-breaking space that the round #411 Unicode-hyphen test does not catch (different code point).
- Round #470: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` emits entries in document order (top-down). The parser pushes entries into the release's `entries[]` array as it encounters them; a refactor that pre-sorts or reverses the array would break the round #409 round-trip idempotency assertion.
- Round #471: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves leading whitespace inside the append block. The append is concatenated verbatim; a future refactor that `trim()`s the append would strip intentional leading indentation from a multi-line rubric.
- Round #472: add `tests/unit/require-role.test.ts` assertion that `requireRole` returns a `ResolvedRole` with a stable shape across every role in the `ADMIN_ROLES` closed set. Each role returns an object with the `role` field set to the role passed in.
- Round #473: add `tests/unit/env-validation.test.ts` assertion that the `AI_MODEL` schema uses `.min(1)` to refuse empty model names (an empty string fails; a single character passes; whitespace-only passes because spaces are characters).
- Round #474: add three `tests/unit/brand-voice.test.ts` edge-case tests for `containsBannedWord`: returns null for an empty string input; returns the lowercase banned word for an uppercase input (uppercase "DISRUPT" returns "disrupt"); does not match banned-word stems that are not full words ("synergistic" returns null because "synergy" is banned but the matcher matches only the full word).
- Round #475: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves trailing whitespace inside bullet text (the parser uses `line.slice(2)` which preserves everything after the `- ` prefix verbatim).
- Author: GitHub Copilot.
- Round #16: extend the round #15 contracts with eleven more vitest assertions covering BRAND_VOICE no vertical-tab/form-feed lock, buildSystemPrompt trailing-dot preservation in append, parseChangelog preserves backtick code spans in bullet text, parseChangelog empty preamble when document starts with H2, parseChangelog empty document yields empty release list, buildSystemPrompt no-options ends with citation rules period, buildSystemPrompt concatenates append blocks larger than base prompt (8 KB sentinel), BRAND_VOICE no zero-width space / ZWNJ / BOM, env-validation NEXTAUTH_SECRET whitespace-only strings of sufficient length pass schema, requireRole accepts every AdminMenuKey closed set, requireRole two different menuKeys produce different ResolvedRole refs, requireRole <1ms per-call benchmark over 1k role checks. 108 tests pass on the targeted files; lint remains clean. Author: GitHub Copilot.
- Round #451: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a vertical-tab (U+000B) or form-feed (U+000C). Both are control characters that render inconsistently across markdown viewers and slip past the banned-word lint. Also asserts the absence of NUL (U+0000) and unit-separator (U+001F).
- Round #452: add `tests/unit/build-system-prompt.test.ts` assertion that `buildSystemPrompt` preserves the trailing dot on the last sentence of the append block. A future refactor that normalises the append via `trim()` would silently strip the period.
- Round #453: add `tests/unit/require-role.test.ts` assertion that `requireRole` accepts every `AdminMenuKey` closed set (regression guard for the type contract).
- Round #457: add `tests/unit/require-role.test.ts` assertion that two calls with different menuKeys produce structurally-distinct ResolvedRole references.
- Round #459: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` preserves bullet text that contains a backtick code span (e.g. `Run \`npm run test\` before committing`). The bullet text is captured as-is after the leading `- ` strip.
- Round #460: add `tests/unit/env-validation.test.ts` assertion that the `NEXTAUTH_SECRET` schema accepts whitespace-only strings of sufficient length (16 spaces). The schema's `.min(16)` floor is character-count, not non-whitespace-count; the test documents the current contract.
- Round #461: add `tests/unit/changelog.test.ts` assertion that an empty preamble results when the document starts directly with an H2 heading (no preamble text before it).
- Round #462: add `tests/unit/changelog.test.ts` assertion that an empty document yields an empty release list (no throws on blank input).
- Round #463: add `tests/unit/build-system-prompt.test.ts` assertion that the no-options path ends with the citation rules period (`India.`). The string never ends with a newline.
- Round #464: add `tests/unit/build-system-prompt.test.ts` assertion that the helper concatenates an append block larger than the base prompt (8 KB sentinel). The total length equals `base.length + 2 + sentinel.length` (the two-char `\n\n` separator plus the append).
- Round #465: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a zero-width space (U+200B), zero-width non-joiner (U+200C), or BOM (U+FEFF). All three are invisible in markdown viewers but break the round-trip byte identity with the round #283 `buildSystemPrompt` parity fixture.
- Author: GitHub Copilot.
- Round #68: add `.test.cjs` sibling tests for the 6 new audit scripts (`audit-banned.test.cjs`, `check-lib-regex-hoist.test.cjs`, `check-page-regex-hoist.test.cjs`, `audit-organization-jsonld.test.cjs`, `sweep-dev-server-logs.test.cjs`, `print-tracked-audit-stats.test.cjs`). Register all 6 in the vitest shim registry (`audit-scripts.registry.ts`). Wire `audit-banned.cjs` into the CI `static-analysis` job. Add `npm run check:audit-all` meta-script running all 11 audit-test scripts. Add `npm run test:ci` for JSON test output. Add vitest setup file failing on console.error leaks. Add shared `tests/unit/helpers/request.ts` for synthetic Request construction. Add unit tests for `src/lib/locales.ts`, `src/lib/schema.ts`, `src/lib/security.ts`, and `src/lib/boot.ts`. Add `/changelog` and `/offline` to the dev-server smoke probe set. Author: GitHub Copilot.
- Round #65: document the Lighthouse CI workflow in `AGENTS.md` section 24 (when to bump `CACHE_VERSION`, URL list curation). Wire `audit-brand-tokens.cjs`, `audit-zod-or-transform.cjs`, and the CHANGELOG-shape check into the `static-analysis` CI job. Add `npm run check:forms` wrapper for the three-audit family. Add `npm run audit:banned` script grepping for banned words in `src/`. Add `scripts/check-lib-regex-hoist.cjs` flagging inline regex in `src/lib/*.ts`. Add `scripts/check-page-regex-hoist.cjs` mirroring the check for page/layout files. Add `scripts/audit-organization-jsonld.cjs` asserting at most one Organization JSON-LD block per page. Add `scripts/sweep-dev-server-logs.cjs` validating log file names. Add `scripts/print-tracked-audit-stats.cjs` printing canonical-vs-stale counts. Add `docs/bot-defence.md` and `docs/audit-authoring.md` and `docs/audit-test-authoring.md` documenting the audit family, the sibling-test pattern, and the vitest-shim conventions. Add JSDoc to `isHoneypotTriggered` noting it is intentionally regex-free. Wire the dev-server smoke check into the CI build job as a post-build verification step. Author: GitHub Copilot.
- Document the four-signal bot-defence pattern (OR pattern: honeypot, missing UA, bot UA, rate limit) in `rules.md` section 5a. Wire `audit-form-rate-limits.cjs` as the third member of the audit family. Add `npm run check:forms` wrapper running all three audits in sequence. Add `docs/bot-defence.md` linking both audit scripts from one place. Author: GitHub Copilot.
- Document the audit-script-test pattern in `AGENTS.md` section 14: every `scripts/audit-*.cjs` ships a sibling `.test.cjs` using `child_process.spawnSync` against synthetic fixtures. The shim pattern (`scripts/audit-scripts-shim-mutation.test.cjs`) locks the registry-equals-test-count invariant and the success-line anchor contract. Author: GitHub Copilot.
- Document the vitest-shim rule in `AGENTS.md` section 15: every `*.test.cjs` file in `scripts/` must have a matching entry in the shim's `SCRIPTS` array. New audit scripts that skip registration fail the `static-analysis` CI job. Author: GitHub Copilot.
- Document the JSDoc registry convention in `AGENTS.md` section 16: the shim uses a JSDoc-annotated `SCRIPTS` array with `name` (kebab-case), `relativePath` (forward-slash), and `successLine` (anchored regex). Adding a new audit script requires a registry entry. Author: GitHub Copilot.
- Document the success-line contract in `AGENTS.md` section 17: every audit script's last stdout line on a passing run must match its `successLine` regex. The shim asserts anchored `^...$` pattern and no earlier-match false positives. Author: GitHub Copilot.
- Document the shared-pathname-prop pattern in `AGENTS.md` section 18: when a parent reads `usePathname()`, children must not call it again; use the shared prop instead. Author: GitHub Copilot.
- Document the shared helper extraction pattern in `AGENTS.md` section 19: extract repeated utilities into `src/lib/` with dedicated tests, regex-hoist, and unmount-cleanup. Author: GitHub Copilot.
- Document the dedicated-test-file pattern for security-sensitive surfaces in `AGENTS.md` section 20: auth, rate limiting, honeypot, and cookie helpers ship dedicated tests covering happy path, edge cases, and regression guards. Author: GitHub Copilot.
- Document the cookie-contract test pattern in `AGENTS.md` section 21: security-critical cookies assert cookie name, `SameSite=Strict`, `HttpOnly`, and path against the shared constants. Author: GitHub Copilot.
- Document the `buildSystemPrompt` helper in `AGENTS.md` section 22: the single source of truth for chatbot, AIO Inspector, and admin AI system prompts. Every AI feature must call this helper, not import `BRAND_VOICE` directly. Author: GitHub Copilot.
- Document the `requireRole` and admin-roles invariant in `AGENTS.md` section 23: every admin page must call `requireRole(menuKey, ...roles)` before rendering. The `AdminRole` type is a closed set; `menuForRole(role)` is the only sidebar renderer. Author: GitHub Copilot.
- Document the regex-hoist convention in `AGENTS.md` sections 11 and 12: module-scoped regex for hot-path helpers, boot-time helpers exempt. The audit script `scripts/check-lib-regex-hoist.cjs` flags inline regex in `src/lib/*.ts`. Author: GitHub Copilot.
- Document the audit family pattern in `AGENTS.md` section 13: `audit-form-bot-defence.cjs`, `audit-form-helpers.cjs`, and `audit-form-rate-limits.cjs` share `scripts/lib/source-audit.cjs`. New audits follow the same import/exit/test contract. Author: GitHub Copilot.
- Document the test convention for `src/lib/` in `AGENTS.md` section 10: every helper ships a matching `tests/unit/*.test.ts` covering happy path, edge cases, and regression guards. Author: GitHub Copilot.
- Add the canonical icon set for "milestone reached" to `specs.md` section 3: `Trophy`, `CheckCircle`, `Star`, `Award`, `Target`. Ban `PartyPopper`, `Sparkle`, `Gift`, `Confetti`. Author: GitHub Copilot.
- Document the AI system prompt architecture in `specs.md`: all three AI features share `buildSystemPrompt({ append })`. Callers must never import `BRAND_VOICE` directly. Author: GitHub Copilot.
- Add "AI features" section to `README.md` listing chatbot, AIO Inspector, and admin AI with their shared system prompt architecture. Author: GitHub Copilot.
- Add run-dev.cjs documentation and dev-server-logs convention to `README.md` Development section. Author: GitHub Copilot.
- Document the `audit-lib-exports.cjs` target-arg pattern in `AGENTS.md`: accepts a directory or single-file target as `process.argv[2]`. Author: GitHub Copilot.
- Document the Lighthouse CI workflow in `AGENTS.md`: when to bump `CACHE_VERSION` and how the URL list is sourced. Author: GitHub Copilot.
- Round #15: extend the round #14 contracts with ten more vitest assertions covering BRAND_VOICE no-CRLF lock, buildSystemPrompt backtick-code-span preservation in the append block, requireRole accepts every role in ADMIN_ROLES + echoes a fresh ResolvedRole reference each call, parseChangelog rejects top-bullets missing the post-dash space + ignores H4–H6 (`#` and `####`) lines, env-validation AI_BASE_URL accepts uppercase HTTPS (current case-sensitive contract) + NEXTAUTH_SECRET rejects values shorter than 16 chars, loadChangelog resolves CHANGELOG.md relative to cwd at call time, and buildSystemPrompt return type is exactly `string`. 376 tests pass on the dirty tree; lint and typecheck remain clean. Author: GitHub Copilot.
- Bonus: extend the SHOUTING-acronym allow-list in `scripts/check-changelog-shape.cjs` with `EISDIR`, `EACCSE`, and `ENOENT` (file-system / runtime error codes commonly cited in changelog entries; the round #14 entry "EISDIR error propagation" was tripping the detector).
- Round #441: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a CRLF or bare CR. The literal uses Unix LF line endings; a Windows line-ending rewrite would silently introduce a CR that the round #409 CRLF/LF round-trip assertion does not exercise.
- Round #442: add `tests/unit/build-system-prompt.test.ts` assertion that the helper preserves backtick code spans inside the append block. A future refactor that applies a markdown-escape pass would break the audit-scoring rubric which uses `code spans`.
- Round #443: add `tests/unit/require-role.test.ts` assertion that `requireRole` accepts every role in the `ADMIN_ROLES` closed set. Each role must be allowed at least the `dashboard` key.
- Round #444: add `tests/unit/changelog.test.ts` assertion that a top-bullet missing the space after the dash (`-text` instead of `- text`) is treated as preamble, not a bullet. The bullet matcher requires `^- ` (dash + space); a refactor that drops the space requirement would misinterpret headings as bullets.
- Round #445: add `tests/unit/env-validation.test.ts` assertion that the `AI_BASE_URL` schema accepts lowercase `https`/`http` and rejects uppercase `HTTPS`/`HTTP` (current regex `/^https?:\/\//` is case-sensitive). A future refactor to `URL.parse` and re-check would change this behaviour.
- Round #446: add `tests/unit/changelog.test.ts` assertion that `loadChangelog` resolves `CHANGELOG.md` relative to `process.cwd()` at call time. A future refactor that captures cwd in a module-level constant would freeze the read path and break every consumer that changes cwd.
- Round #447: add `tests/unit/require-role.test.ts` assertion that `requireRole` echoes a fresh ResolvedRole reference on every call (not a shared cached object). Two calls with the same arguments return structurally-equivalent but reference-distinct objects.
- Round #448: add `tests/unit/build-system-prompt.test.ts` assertion that the helper's return type is exactly `string` (never `null` or `undefined`). A future contributor who adds an `if (...) return null` branch trips CI before the change ships.
- Round #449: add `tests/unit/changelog.test.ts` assertion that lines starting with `#` (H1) or `####` (H4–H6) are preamble text, not headings. The parser matches only `## ` (H2) and `### ` (H3); a refactor that adds `# ` or `#### ` matchers would re-shape the document structure.
- Round #450: add `tests/unit/env-validation.test.ts` assertion that `NEXTAUTH_SECRET` rejects values shorter than 16 chars (the schema's `.min(16)` floor). The boot validator uses this secret for HMAC; a too-short secret weakens the signature.
- Author: GitHub Copilot.
- Round #14: extend the round #13 contracts with ten more vitest assertions covering BRAND_VOICE tab-character lock, requireRole verbatim menuKey echo (case-sensitive), parseChangelog triple-blank-line separator + backtick code-span preservation, env-validation AI_BASE_URL non-http protocol rejection, requireRoleWithSession post-session role re-check, loadChangelog EISDIR error propagation, buildSystemPrompt multiline append preservation, and validateAtBoot <1ms per-call benchmark. 366 tests pass on the dirty tree (includes in-progress user changes to middleware/header/next.config); lint and typecheck remain clean. Author: GitHub Copilot.
- Round #396: add `tests/unit/env-validation.test.ts` benchmark asserting `validateAtBoot` completes in well under 1ms per call after the first. 10k iterations under 100ms ceiling (10x the per-call contract). A future contributor who adds a heavy regex / `JSON.parse` inside the boot validator trips CI before the change ships.
- Round #421: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a tab character. The brand voice uses single spaces and blank lines for paragraph separation; a tab would render inconsistently across markdown viewers and slip past the banned-word lint.
- Round #423: add `tests/unit/require-role.test.ts` assertion that `requireRole` echoes the menuKey string verbatim (case-sensitive). The AdminMenuKey type is a closed set of lowercase strings; a future caller passing "Dashboard" would silently bypass the gate, the type system catches it at compile time, and the test locks the verbatim-echo contract at runtime.
- Round #425: add `tests/unit/changelog.test.ts` assertion that triple-blank-line separators between entries do not lose the surrounding entries. The parser's `flushEntry()` walks every bullet until it hits a non-bullet line; blank lines in between are ignored. A contributor who pastes a triple-blank-line visual separator does not lose entries.
- Round #426: add `tests/unit/changelog.test.ts` assertion that the parser preserves backtick code spans in bullet text. The bullet text is captured as-is (after stripping the leading `- `), so a code-span like `npm run test` survives the round-trip.
- Round #427: add `tests/unit/env-validation.test.ts` assertion that the `AI_BASE_URL` schema rejects non-http(s) protocols (file://, git://, ftp://). A copy-pasted URL with a wrong protocol is rejected with a clear "must be a valid URL" message.
- Round #428: add `tests/unit/require-role.test.ts` assertion that `requireRoleWithSession` re-checks the role after the session verifies. If the session is valid but the role is wrong, the helper still calls `notFound` (the role re-check is the security gate, not the session verification).
- Round #429: add `tests/unit/changelog.test.ts` assertion that `loadChangelog` propagates *any* underlying fs error (EISDIR via a directory-instead-of-file path), not just ENOENT. A future maintainer who wraps the readFile call in a `try/catch` that swallows EISDIR / EACCSE would silently turn a broken changelog file into an empty document.
- Round #430: add `tests/unit/build-system-prompt.test.ts` assertion that the helper accepts a multiline `append` block. The audit-scoring rubric and other call sites pass multi-paragraph instructions; the helper must preserve the newlines and not collapse them to spaces.
- Round #422: already covered by the round #11 `supports an empty string append without producing a trailing blank line` test. The `buildSystemPrompt({ append: "" })` call falls back to the no-options path so the result equals `buildSystemPrompt()`. The follow-up is logged as complete-by-precedent.
- Author: GitHub Copilot.
- Round #13: extend the round #12 contracts with ten more vitest assertions covering parseChangelog malformed-H2 graceful degradation, sub-bullet indent depth preservation, JSON-serialisability of the parsed document, BRAND_VOICE Unicode-hyphen lock, buildSystemPrompt trailing-newline guard + 10k-iteration benchmark, requireRole empty-string role rejection + idempotent reference echo, env-validation AI_API_KEY whitespace passthrough + DATABASE_URL `.min(1)` trade-off documentation. 357 tests pass; lint and typecheck remain clean. Author: GitHub Copilot.
- Round #410: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` treats malformed H2 lines (e.g. `##title` instead of `## title`) as preamble text rather than throwing. The parser's contract is graceful degradation — a future maintainer who adds a `throw` here breaks every malformed input the editor ever saves.
- Round #411: add `tests/unit/brand-voice.test.ts` assertion that `BRAND_VOICE` never contains a Unicode hyphen (en-dash `\u2013`, em-dash `\u2014`, minus sign `\u2212`). The brand voice is sentence-case ASCII-only; a Word-document copy-paste into the prompt can silently introduce a Unicode dash that the lint does not catch.
- Round #412: add `tests/unit/build-system-prompt.test.ts` benchmark asserting the no-options path runs in well under 1ms over 10k calls (100ms ceiling = 100× the contract budget, CI-variance robust). A future contributor who adds a heavy `JSON.parse` or `Date.now()` inside the helper trips CI before the change ships.
- Round #413: add `tests/unit/require-role.test.ts` assertion that `requireRole` rejects an empty-string role with `notFound`. The `AdminRole` type is a closed set; `""` is not in it. A future caller cannot bypass the gate by passing `""` as a default-when-undefined.
- Round #415: add `tests/unit/env-validation.test.ts` assertion that the `AI_API_KEY` schema's transform strips empty strings (round #317) but does NOT strip whitespace. Whitespace input currently passes through as-is. A future tighten to trim whitespace can update this test deliberately.
- Round #416: add `tests/unit/changelog.test.ts` assertion that sub-bullet indent depth is preserved (no accidental flattening). A two-space-indented sub-bullet and a four-space-indented sub-sub-bullet both belong to the same top-bullet entry; the parser must not drop the deeper indent.
- Round #417: add `tests/unit/changelog.test.ts` JSON-serialisability assertion. The parsed document must round-trip through `JSON.stringify` / `JSON.parse` so the future `/changelog` route can ship it as a static JSON endpoint without crashing on circular references.
- Round #418: add `tests/unit/build-system-prompt.test.ts` assertion that `buildSystemPrompt({ append })` does NOT insert a trailing newline after the append block. A trailing newline would silently add a blank line to the AI's input and confuse the model's stop-token detection.
- Round #419: add `tests/unit/require-role.test.ts` assertion that `requireRole` returns a structurally-equivalent `ResolvedRole` on repeated calls with the same args. The implementation does not cache, so each call returns a fresh reference, but the values match exactly.
- Round #420: add `tests/unit/env-validation.test.ts` assertion that documents the round #315 trade-off: `DATABASE_URL` switched from `.url()` to `.min(1)` because the driver knows its own URL grammar. The test asserts a bad-but-non-empty value (`"not-a-url"`) passes the schema. A future tightening of the schema (e.g. back to `.url()`) must update this test deliberately.
- Round #13 (cont.): add `CRLF` and `LF` to the allow-list in `scripts/check-changelog-shape.cjs` so line-ending acronyms in tech copy do not trip the SHOUTING-acronym detector.
- Author: GitHub Copilot.
- Round #12: extend the round #11 contracts with twelve more vitest assertions covering BRAND_VOICE paragraph structure, parseChangelog BOM + raw-field preservation + CRLF/LF round-trip, requireRole ResolvedRole shape echo, env-validation exact-list assertion and reset contract, buildSystemPrompt append normalisation, and check-sameSite-cookies non-admin Set-Cookie intent. Trim the round #11 todo.md duplicates (#396–#409 were appended twice). 347 tests pass; lint and typecheck remain clean. Author: GitHub Copilot.
- Round #397: add `tests/unit/changelog.test.ts` assertion that `parseChangelog` handles a leading UTF-8 BOM (`\uFEFF`). Windows Notepad and several older editors save UTF-8 files with a BOM, so a parser that only matches `\n` line endings would trip on the H1 line — but the H2 / H3 / bullet matchers are unaffected because the BOM only touches the first line. The test asserts the parser produces the same releases + entries as the BOM-less case.
- Round #407: add `tests/unit/changelog.test.ts` assertion that the entry `raw` field preserves the leading `- ` dash and the indented sub-bullet dash. The parser strips the leading dash from `bullets[]` for display but the `raw` field is the canonical view-source payload for the changelog viewer; a future refactor that drops the dash breaks the round-trip.
- Round #409: add `tests/unit/changelog.test.ts` round-trip idempotency block asserting that an LF-only document and a CRLF-only document produce the same parsed shape. The intent: a contributor editing `CHANGELOG.md` on Windows then on Linux (or vice versa) sees identical output.
- Round #398: add `tests/unit/changelog.test.ts` assertion that `loadChangelog` rejects with a file-not-found-style error when `process.cwd()` points at a non-existent directory. The test monkey-patches `process.cwd` so the assertion is hermetic.
- Round #400: add two `tests/unit/brand-voice.test.ts` assertions locking the round #283 parity fixture to exactly three paragraphs. Splitting `BRAND_VOICE` on `\n\n+` must yield three parts (role / voice / cite), and the literal must not start or end with a blank line. A future maintainer who collapses two paragraphs trips CI.
- Round #401: add `tests/unit/build-system-prompt.test.ts` assertion that the append block survives whitespace normalisation. The normalised output must contain the append token exactly once, and the normalised legacy prefix must equal the normalised `BRAND_VOICE` followed by a single space (no extra blank-line collapse).
- Round #402: add two `tests/unit/require-role.test.ts` assertions locking the `ResolvedRole` echo shape: the returned object has exactly the keys `role` and `menuKey`, and the `role` field echoes the input string while `menuKey` echoes the menu key (not the role). A future contributor adding a third key must extend the test deliberately.
- Round #399: tighten `tests/unit/env-validation.test.ts` to assert the exact missing-env-var list (`Missing required env vars: DATABASE_URL$`) rather than a regex substring. The exact-list assertion is the canonical signal of which env is missing and survives future list-format tweaks via a clearer assertion message.
- Round #404: add two `tests/unit/env-validation.test.ts` assertions verifying that `process.env.MODE` does not leak between tests. The first test sets `MODE=leaked` and asserts the value; the next test asserts `MODE !== "leaked"`. Together they catch a regression in `resetEnv()` or the `beforeEach` hook.
- Round #405: add `tests/unit/check-sameSite-cookies.test.ts` non-admin Set-Cookie fixture that mirrors the round #389 migration-path pattern. The audit's intent is to fail on admin-cookie downgrades; today's implementation flags any missing-SameSite header regardless of name. The test accepts either exit code today and documents the migration to a name allow-list.
- Round #12 (cleanup): trim the round #11 todo.md duplicates. Items #396–#409 had been appended twice due to a typo in the round #11 follow-up block. The trimmed file now has one copy of each follow-up and 357 lines instead of 371.
- Author: GitHub Copilot.
- Round #11: lock the env boot-validator and changelog parser contracts with vitest, add brand-voice parity assertions for block order and BANNED_WORDS join order, tighten the round #283 buildSystemPrompt append option with three new assertions (blank-line separator, idempotent, empty-string fallback), and assert that `requireRole` throws synchronously before any render runs. 24 new vitest assertions bring the total to 335; lint and typecheck remain clean. Author: GitHub Copilot.
- Round #379: add three assertions to `tests/unit/build-system-prompt.test.ts` covering the `append` option's contract: the append block sits after exactly one blank line (never glued to the last sentence), `buildSystemPrompt({ append })` is idempotent (the append text appears exactly once in the output across two calls with the same options), and an empty-string append falls back to the no-options path so the result equals `buildSystemPrompt()`.
- Round #382: add `tests/unit/brand-voice.test.ts` block ordering assertions for the round #283 parity fixture. The test asserts that `BRAND_VOICE` carries the role-line, voice-line, and cite-line in that order, and that the three blocks are separated by exactly one blank line each. A future refactor that collapses the blocks (or reorders them) trips CI before the change ships.
- Round #386: add `tests/unit/require-role.test.ts` assertions proving `requireRole` throws `NEXT_NOT_FOUND` synchronously on the very first call, with no return value to "render past". The intent is to lock the security-relevant contract that a denied role cannot leak the route's existence via a 403 page or a redirect chain.
- Round #389: add a `tests/unit/check-sameSite-cookies.test.ts` assertion that the audit's intent (admin / auth cookies must be `SameSite=Strict`) is named in the test. The fixture uses `wg24_theme` to flag a future maintainer that the audit's *implementation* is generic (any cookie literal) while the *intent* is named-scoped; a future tightening to a name allow-list can run this test as the green-path acceptance case.
- Round #390: add a `tests/unit/brand-voice.test.ts` assertion that the banned-word list embedded in `BRAND_VOICE` matches the array declaration order in `BANNED_WORDS`. A future alphabetisation of the array changes the bytes of the round #355 parity test for a non-functional reason; this test catches that regression.
- Round #391: add `tests/unit/env-validation.test.ts` block asserting the `MODE` Zod enum rejects values outside `dev | live` with a clear `invalid_value` issue on the `MODE` path. A typo'd `MODE=production` would otherwise default to `dev` silently or fail with a generic Zod message that doesn't name the field.
- Round #392: add `tests/unit/env-validation.test.ts` assertions covering `validateAtBoot()` in live mode. The boot validator throws when `DATABASE_URL` is missing, does not throw in dev mode when `DATABASE_URL` is missing (dev tolerates half-configured envs), and does not throw in live mode when `DATABASE_URL` is set. Each test resets `vi.resetModules()` between cases so the module-level `cached` and `validated` flags do not leak state.
- Round #393: add four assertions covering the AI_ENABLED branch of `validateAtBoot()`: throws when `AI_ENABLED=true` and `AI_API_KEY` is missing, throws when `AI_ENABLED=true` and `AI_BASE_URL` is missing, does not throw when `AI_ENABLED=true` and all AI keys are set, and does not check AI keys when `AI_ENABLED` is unset or `false` in live mode. The intent: live-mode AI features require every AI env, so a partial config is a hard boot failure.
- Round #261: add two `tests/unit/changelog.test.ts` assertions for `parseChangelog` CRLF handling. A pure CRLF input (Windows-style line endings) and a mixed CRLF/LF input both produce the same parsed document as the canonical LF case. A future maintainer who tightens the line splitter to `\n` only trips CI on Windows checkouts.
- Round #262: add `tests/unit/changelog.test.ts` block exercising `loadChangelog()` against the actual `CHANGELOG.md` at the repo root. The test asserts the preamble starts with `# Changelog`, that at least one release is present, that the first release is `Unreleased`, and that every release carries at least one entry. A future refactor that points `loadChangelog` at a different file trips CI before the change ships.
- Author: GitHub Copilot.
- Round #10: extend `requireRole` coverage to every admin page, extract the shared system-prompt builder, add five new vitest files (requireRole, check-sameSite-cookies, validateAdminUrl null/undefined/non-string, env.ts schema non-string rejection, brand-voice parity), document the AI_ENABLED Zod transform via a JSDoc example, fix the round #283 BRAND_VOICE parity drift by moving the `BANNED_WORDS` array above the `BRAND_VOICE` constant and interpolating it into the prompt string, switch `scripts/check-rules.cjs` to `process.cwd()` so the audit works in fixture-based test runs, and add `scripts/check-cookie-name.cjs` so a future contributor cannot reintroduce a hardcoded `wg24_admin_session` literal. Author: GitHub Copilot.
- Round #283: extract the system-prompt string from `src/lib/brand-voice.ts` into `src/lib/build-system-prompt.ts` exporting `buildSystemPrompt(options)` so callers (chatbot route, AIO inspector, admin AI) share one canonical prompt builder with optional `append`. The helper is byte-identical to the legacy `BRAND_VOICE` constant on the no-options path (verified by `tests/unit/build-system-prompt.test.ts`). Both `BRAND_VOICE` and the new helper interpolate `BANNED_WORDS.join(", ")`, so adding a banned word flows through every AI surface in one edit.
- Round #283 (cont.): fix the byte-parity drift between `BRAND_VOICE` and `buildSystemPrompt` by reordering the two constants in `src/lib/brand-voice.ts` so `BANNED_WORDS` is declared before `BRAND_VOICE` (the `BANNED_WORDS.join(...)` template previously referenced the array before its `const` binding, which tripped the round #355 parity test in the strict reference mode).
- Round #360: add a JSDoc example block to `AI_ENABLED` in `src/lib/env.ts` showing the parse behaviour for `"true"`, `"false"`, `"1"`, `"0"`, `undefined`, `true`, and any other string. Future maintainers reading the schema see the exact transform contract instead of guessing from the type signature.
- Round #356: replace the hardcoded `path.join("D:", "webgrow24.com", "src")` root in `scripts/check-rules.cjs` with `path.join(process.cwd(), "src")` so the audit script runs from the repo root in any environment (fixture-based tests, CI runners, contributor machines).
- Round #369: add `scripts/check-cookie-name.cjs` (wired as `npm run check:cookie-name`) which greps `src/` for the literal `wg24_admin_session` cookie name and asserts each hit imports `ADMIN_COOKIE` from `@/lib/admin-auth`. Update `src/middleware.ts` to import `ADMIN_COOKIE` and use `cookies[ADMIN_COOKIE]` instead of the literal, so the literal is grep-fail-proof.
- Round #367: add `requireRole` calls to `dashboard`, `content`, `chatbot`, and `analytics` admin pages so every page in `src/app/[admin]/(authenticated)/` is gated by the round #307 helper (audit/users/settings were already gated). Update `src/lib/require-role.ts` so the public surface is one canonical import per page.
- Round #367 (test): add `tests/unit/require-role.test.ts` with 10 assertions: per-role allow/deny, `notFound()` throws for missing keys, `requireRoleWithSession` calls `notFound()` when the session cookie is absent, and the `every admin page invokes requireRole` audit walking `src/app/[admin]/(authenticated)/**/page.tsx` asserting each file imports and calls the helper. A new admin page added without `requireRole` trips CI before the change ships.
- Round #368: add `tests/unit/check-sameSite-cookies.test.ts` with 5 assertions locking the `scripts/check-sameSite-cookies.cjs` audit's failure modes. The test copies the audit script + `lib/walk.cjs` into a `mkdtempSync` scratch directory, writes a synthetic `src/components/fixture.tsx`, and asserts the script exits 1 with the expected stderr for `SameSite=Lax`, missing-`SameSite`, and accepts the clean `SameSite=Strict` path. A regression in the audit's regex (e.g. removing the `Lax` match) trips CI immediately.
- Round #372: add 5 new assertions to `tests/unit/admin-url-validator.test.ts` covering `validateAdminUrl(undefined)`, `validateAdminUrl(null)`, `validateAdminUrl(24)` (number), `validateAdminUrl(true)` (boolean), and `validateAdminUrl({})` (object). All four throw — the validator's contract is `string && matches /^[A-Za-z0-9_-]+$/`, and any other type is a build-time bug.
- Round #376: add `tests/unit/require-role.test.ts` cases for `requireRoleWithSession` (round #376 sibling): the helper calls `notFound()` when no session cookie is present, falls through to `requireRole` when the cookie verifies, and re-calls `notFound()` when the cookie verifies but the role is not allowed for the menu key. Mocks `next/navigation` and `next/headers` via `vi.mock` so the test runs without a Next request context.
- Round #378: add `tests/unit/env-schema-non-string.test.ts` with 10 assertions covering the env.ts Zod schema's rejection of non-string values (number, boolean, object, array), the empty-string `undefined` transform (round #317), the email format check, the 16-char `NEXTAUTH_SECRET` minimum, and the `^[A-Za-z0-9_-]+$` `ADMIN_URL` rule. The test re-derives a parallel schema mirroring the public shape so a future field-flip trips CI.
- Author: GitHub Copilot.
- Added per-page server-side `requireRole()` gating on the high-impact admin routes (`/users`, `/settings`, `/audit`), extracted the `ADMIN_URL` build-time validator to `lib/validate-admin-url.cjs` so it is unit-testable without Next, fixed the residual `NEXT_PUBLIC_CALENDLY_URL` Zod chain that still carried the broken `.or(z.literal("").transform(...))` shape, added `scripts/check-sameSite-cookies.cjs` and `scripts/audit-zod-or-transform.cjs` as CI-gated audits, and added 15 vitest assertions covering the body-schema parse cost, the path-matches contract, the BodySchema role-enum rejection, and the admin URL validator. Author: GitHub Copilot.
- Round #307: add `src/lib/require-role.ts` exporting `requireRole(menuKey, role)` and `requireRoleWithSession(menuKey, role)`. Each admin page calls the helper at the top of its server component so the role's `allow` list is enforced per-page (not just by the layout). Wire it into `/users`, `/settings`, and `/audit` so the editor and reviewer roles cannot reach super-admin-only surfaces.
- Round #355: extract the `validateAdminUrl` function from `next.config.ts` into `lib/validate-admin-url.cjs` so the vitest test can require it without dragging in the Next module graph. The validator continues to live at the build-time entry point and is the single source of truth for the `^[A-Za-z0-9_-]+$` rule.
- Round #346: add `scripts/audit-zod-or-transform.cjs` (wired as `npm run audit:zod-or-transform`) which greps `src/` for the broken `.optional().or(z.literal("").transform(...))` shape. Comment lines are skipped so historical references don't false-positive. Use it to catch a future maintainer who copy-pastes the prior env.ts shape.
- Round #346 (cont.): fix the residual broken shape on `NEXT_PUBLIC_CALENDLY_URL` in `src/lib/env.ts` so the audit runs clean. The chain is now `.optional().transform((v) => (v && v.length > 0 ? v : undefined))` matching the round #317 AI_* pattern.
- Round #345: add `scripts/check-sameSite-cookies.cjs` (wired as `npm run check:sameSite-cookies`) which scans `src/` for `SameSite=Lax`, `SameSite=None`, or missing-`SameSite` cookie sites. The check covers both `cookie(name, value, { sameSite })` and `Set-Cookie:` header forms and exits 1 on any regression.
- Round #358 / #363 / #365: add three vitest files (`chatbot-schema-bench.test.ts`, `path-matches-bench.test.ts`) locking the parse / match cost under 100ms / 10k calls and asserting the role-enum (user/assistant only) and the sibling-path (`/servicesX` ≠ `/services`) contracts.
- Round #355: add `tests/unit/admin-url-validator.test.ts` with seven assertions covering empty string, space, slash, non-ASCII, and the three valid forms (`24`, `admin-panel`, `admin_panel`). Each test spawns a fresh `node -e` so it does not pollute the test worker's env.
- Round #337 / #344: add `## 7. Unmount cleanup pattern` and `## 8. Zod `.or(z.literal("").transform(...))` is banned` to `AGENTS.md`. The first documents the five cleanup idioms (setTimeout, RAF, `<script>`, blob URL, in-flight ref). The second documents the Zod pattern with the contact-form counter-example so a maintainer does not accidentally "fix" the wrong shape.
- Author: GitHub Copilot.
- Round #306: switch `AI_ENABLED` from `z.string().optional().transform(...).default(false)` to `z.union([z.boolean(), z.string()]).optional().transform(...)` so the inferred type is a single `boolean` instead of a boolean-or-string union, and so the parse result and the default value share the same shape.
- Round #305: validate the `ADMIN_URL` env in `next.config.ts` against `/^[A-Za-z0-9_-]+$/` and throw a clear error on a bad value, so a typo'd `ADMIN_URL=admin panel` fails at `next build` instead of silently producing a broken `/admin%20panel/...` URL set.
- Round #343: extract the chatbot body schema from `src/app/api/chatbot/route.ts` into `src/lib/chatbot-schema.ts` so the route stays focused on the AI integration and the vitest tests can import the contract without dragging the route in. Export `ChatBodySchema`, `ChatBody`, `ChatHistoryItemSchema`, and the four limit constants.
- Round #339: extract the `matchesPath` helper from `src/components/chrome/header.tsx` into `src/lib/path-matches.ts` so the vitest test can lock the `aria-current` forwarding contract without rendering the full header. The header re-exports through a thin wrapper so every call site stays unchanged.
- Round #326: source the `BANNED_WORDS` and `PRICE_PATTERNS` arrays in `scripts/check-rules.cjs` from `lib/check-rules-patterns.cjs` so the vitest contract test can assert the table directly. The script and the test share a single source of truth, so a future maintainer adding an entry trips CI before the change ships.
- Round #352: move the cookie-contract assertions (`SameSite=Strict`, `HttpOnly`, `Path=/`, cookie name, Max-Age behaviour) out of `tests/unit/admin-auth.test.ts` into `tests/unit/admin-auth-cookie-contract.test.ts` so the security-relevant surface has its own dedicated file. Update `admin-auth.test.ts` to keep only the two shape assertions that read best next to the rest of the auth flow.
- Round #341: add `tests/unit/chatbot-get.test.ts` with 3 assertions covering the public `/api/chatbot` GET response shape — `ok: true`, `region: "IN"`, and the absence of any `model` or `ai_*` field — so a future regression that re-adds the AI provider fingerprint trips CI.
- Round #347 / #354: add `tests/unit/chatbot-schema.test.ts` with 11 assertions locking every documented limit on the chatbot body schema (message length, history length, history content length, role enum, path length) so a future maintainer who relaxes the bounds trips CI.
- Round #348: add `tests/unit/check-rules-patterns.test.ts` with 6 assertions covering the banned-word + price-pattern tables. Locks the `\b` boundary so a future removal of the word boundary trips CI before a hyphenated compound slips through the lint.
- Author: GitHub Copilot.
- Round #315: switch `DATABASE_URL`, `REDIS_URL`, and `S3_ENDPOINT` from `z.string().url()` to `z.string().min(1)`. The `.url()` validator rejected connection strings that are technically valid for libpq / aws-sdk but do not parse as full URLs (e.g. unix-socket shorthand). The driver knows its own URL grammar best; the schema only enforces that the operator set something.
- Round #317: replace the `z.string().optional().or(z.literal("").transform(() => undefined))` chain in `AI_API_KEY`, `AI_BASE_URL`, and `AI_MODEL` with a single `.optional().transform((v) => (v && v.length > 0 ? v : undefined))` so empty strings and `undefined` both resolve uniformly and the inferred type is stable.
- Round #316: bump the admin session cookie's `SameSite` from `Lax` to `Strict` so the browser never sends the cookie on any cross-site navigation, including sub-resource loads and form posts from third-party origins. Update `tests/unit/admin-auth.test.ts` to assert the new contract.
- Round #300: add a `BodySchema` in `src/app/api/chatbot/route.ts` that bounds `history[].content` to 2000 chars (matching the runtime slice cap in `callProvider`), caps `history.length` at 50 entries, and rejects empty `message`. Surface a generic reply on bad input so the route never echoes a payload that may carry hostile content; log the detailed Zod issue server-side for triage.
- Round #325: flip the palette-vs-`lib/brand-tokens.cjs` drift check in `scripts/enforce-palette.cjs` from `console.warn` to `process.exit(1)`. Add the four intentionally-local hex rewrites to a `HEX_REWRITE_KEYS` allow-list so the script does not fail on its own legitimate additions. Mirror the scrim / overlay rewrites and the `fg-disabled` triplets in `lib/brand-tokens.cjs` so both sides stay in lockstep.
- Round #303: track the active `requestAnimationFrame` handle in `AnimatedCounter` via a `useRef`, cancel it on unmount. A component that scrolls into view and then unmounts before the easing curve reaches `pct = 1` no longer fires a final `tick` on an unmounted component.
- Round #301: keep the active blob URL in a `useRef` in `ColorPaletteExtractor` and revoke it both on unmount and when a new file is picked. Every picked file no longer leaks an `Object URL` until the page reloads, and a user who picks three files in a row no longer holds three live references to the first two.
- Round #302: add an `inFlightRef` guard to `BrokenLinkChecker`'s `check()` so a fast double-click can no longer fire two concurrent runs that interleave their `setProgress` calls and corrupt the result list. The visible `disabled={busy}` only takes effect after the React render commits, so the ref is the only race-free gate.
- Round #335: extract `escapeRegex` to `src/lib/escape-regex.ts` so the vitest benchmark (and any future caller) can import it without dragging in the chatbot route's runtime dependencies. Add `tests/unit/escape-regex.test.ts` with 5 assertions covering metacharacter coverage, alphabetic / numeric pass-through, no double-escape, and a <50ms / 10k-call benchmark.
- Round #336: export `truncateForLog` from `src/app/api/glitchtip-tunnel/route.ts` and add `tests/unit/glitchtip-tunnel-truncate.test.ts` with 6 assertions covering short input pass-through, length boundary (`<=`), clipping arithmetic, JSON serialisation for objects, and the exact `[truncated N chars]` marker shape.
- Author: GitHub Copilot.
- Round #319: flip the `clientKey` test to expect the rightmost X-Forwarded-For hop (matching the security-hardened implementation committed in #301). The test carries a long rationale comment so a future maintainer who flips the implementation must also flip this assertion intentionally instead of inheriting the previous "first hop" assumption.
- Round #309: drop `model` from `/api/chatbot` GET. The model name leaked the AI provider to unauthenticated callers and gave scrapers a fingerprint to filter on. The `region` marker stays because the chatbot client uses it to pick the greeting copy.
- Round #308: drop the unreachable `mode !== "live"` branches in `verifyTurnstile`. The dev / CI short-circuit at the top of the function already returns `verified: true` for any non-live invocation, so the inner `if (mode !== "live")` arms in the missing-token and catch paths could never fire. Today, every execution that reaches those branches is live mode, so a missing token or a Cloudflare network error is always a real failure.
- Round #310: introduce a private `escapeRegex(s)` helper in `src/app/api/chatbot/route.ts` and route the banned-word regex construction through it. Today every entry in `BANNED_WORDS` is alphabetic, but a future maintainer adding `next-gen` (hyphen) or `c++` would otherwise build a broken / exploitable regex.
- Round #313: hoist the injected `<script>` references in `error-reporter.tsx` and `analytics.tsx` so the unmount cleanup can remove them. Without this, a hot reload or a route-level remount stacks multiple copies of the SDK bootstrapper and double-fires the initial `config` event.
- Round #312: convert `copy-button.tsx`'s `setTimeout(..., 1500)` reset into a `useRef`-tracked timer with a `useEffect` cleanup. Fast click-then-unmount sequences no longer leave a dangling `setState` that React warns about in dev.
- Round #318: add `.catch()` to the `getClient()` connecting promise in `rate-limit-redis.ts`. A synchronous throw inside `tryConnect` (or a rejected promise) now clears the `connecting` latch and logs once per attempt, so every subsequent `getClient()` call awaits a fresh attempt and the limiter does not stay stuck in fallback mode forever.
- Round #311: add a `truncateForLog(value, 1000)` helper in `/api/glitchtip-tunnel/route.ts` and route the unknown-shape warn log through it. A multi-megabyte rogue payload can no longer fill the log buffer; the structured line carries both the truncated preview and the original length.
- Round #314: pass the parent's `pathname` into `MegaPanel` as a prop instead of calling `usePathname()` a second time. Also widen `matchesPath(pathname, href)` to accept `string | null | undefined` so callers can forward the raw hook result without first null-coalescing.
- Round #322: add `scripts/smoke-honeypot.cjs` plus `scripts/lib/smoke-client.cjs` and wire them as `npm run smoke:honeypot` and `smoke:honeypot:check`. The smoke posts a bot payload (filled honeypot + bot UA, expects 200 fake success) and a clean payload (empty honeypot + browser UA, expects 400 validation rejection) to `/api/contact`, `/api/partner`, `/api/newsletter`, and `/api/lead-magnet`, asserting each route returns the right shape. The shared helper boots the dev server if it is not already listening and respects the per-route rate-limit window between payloads.
- Author: GitHub Copilot.
- Add `scripts/check-node-version.cjs` plus the `preinstall` npm hook so `npm install` aborts before any other lifecycle step runs when the local runtime does not match the major pinned in `.nvmrc`. The check reads `.nvmrc` at run time (no duplicated major-version constant) and exits with a clear nvm / volta remediation hint.
- Add `scripts/check-changelog-shape.cjs` and wire it as `npm run check:changelog-shape`. The parser enforces that `## Unreleased` is the first heading, that every release group is a single column-zero `- ` bullet with sub-bullets indented by two spaces, that every Unreleased group carries an `Author: GitHub Copilot.` marker (inline on the summary or as a sub-bullet), and that summary lines contain no SHOUTING acronyms. Banned-word matches in copy are reported as warnings (not failures) so the script is CI-safe today; `--strict` upgrades to a hard fail. Add `tests/unit/check-changelog-shape.test.ts` with two assertions: a happy-path run against the current file and a missing-file failure path that backs up and restores `CHANGELOG.md` via `os.tmpdir()`.
- Add `scripts/audit-brand-tokens.cjs` and wire it as `npm run audit:brand-tokens`. The audit walks the source tree, flags every literal hex outside the brand palette, every entry in `BRAND_FORBIDDEN_HEX`, and every off-brand Tailwind palette class (red / orange / amber / yellow / lime / green / emerald / teal / cyan / sky / blue / indigo / violet / purple / fuchsia / pink / rose / slate / gray / zinc / neutral / stone). It exits 0 by default so CI can adopt it incrementally; `--strict` upgrades to a hard fail when the codebase is fully migrated. Reuses `lib/brand-tokens.cjs` and `lib/walk.cjs` so the audit, the migration, and the changelog test share one source of truth for the palette.
- Wire `scripts/enforce-palette.cjs` to `lib/walk.cjs` (shared walker) and to `lib/brand-tokens.cjs` (shared substitution table) so the migration script no longer duplicates the canonical include / exclude rules or the token-substitution list. Add a startup drift check that compares the local `RULE_TRANSFORMS` keys against `TOKEN_SUBSTITUTIONS` keys and warns when they disagree; the only intentionally local entries are the four hex rewrites (file-write targets, not token renames). Add a full scrim / overlay rewrite table for `bg-black/N`, `bg-white/N`, and the corresponding `hover:bg-…/N` variants so any future regression to raw black / white translucent surfaces is auto-fixed by the migration script (12 files rewritten this round).
- Wire `scripts/check-rules.cjs` to `lib/walk.cjs` so the banned-words lint uses the same include / exclude rules as the palette audit and the brand-token audit.
- Add `tests/unit/sitemap.test.ts` with 6 vitest assertions locking in the canonical route set: the home route exists with the brand wordmark image extension, every marketing hub (`/services`, `/blog`, `/work`, `/pricing`, `/contact`) is present, the list has no duplicates, every URL parses and lives on `PUBLIC_SITE_URL`, and every priority is in the SEO-accepted `0..1` range.
- Wire `resolveServiceSlug(s.slug)` into `src/app/sitemap.ts` so any renamed service slug surfaces the canonical URL even if a `SERVICE_SLUG_REDIRECTS` entry lands later. The existing redirect chain continues to 307 at the route handler — the sitemap now advertises the canonical URL up-front so search engines do not waste a hop.
- Add `tests/unit/llms-txt-bot-audit.test.ts` with 6 vitest assertions that fail the build if `scoreBotLikelihood`, `BOT_SCORE_THRESHOLD`, or the phrase `bot scoring` ever reappears in `public/llms.txt` or `public/llms-full.txt`. Confirms round #133 / #134 are satisfied and prevents regression as the AI manifest evolves.
- Add `tests/unit/service-slug-redirects.test.ts` with one extra idempotency assertion (round #357): every registered `to` value must round-trip to itself via `resolveServiceSlug`, so a stray redirect chain can never loop a visitor back to a legacy URL. The fixture also covers an arbitrary unknown slug as a fixed point.
- Add a regression test in `tests/unit/honeypot.test.ts` (round #114) that asserts `isHoneypotTriggered({ phone_secondary })` returns false — the legacy secondary-phone honeypot is gone for good, and the helper must not iterate unknown keys.
- Add `## 6. Admin roles and menu gating` to `AGENTS.md` so the `menuForRole` contract is documented in the agent operating manual: closed role set, helper is the only renderer, helper is pure, sidebar entries declare `requires` exactly once, `DEFAULT_ADMIN_ROLE` is `super-admin`, and every admin route handler calls `requireRole` before rendering.
- Author: GitHub Copilot.
- Tighten the type system with `noImplicitAny`, `strictNullChecks`, `noFallthroughCasesInSwitch`, and the `@typescript-eslint/no-explicit-any` error rule; gate the admin sidebar through a typed role-based `menuForRole` helper; expose an image sitemap at `/image-sitemap.xml` referenced from `robots.txt`; and promote the brand palette metadata into a top-level `lib/` so CI imports one canonical source. Author: GitHub Copilot.
- Add `noImplicitAny`, `strictNullChecks`, and `noFallthroughCasesInSwitch` to `tsconfig.json` and promote `@typescript-eslint/no-explicit-any` from `warn` to `error` in `eslint.config.mjs`. Add a `parserOptions.ecmaVersion` block so the flat-config linter stays compatible with the strict rule. The codebase is already `any`-free (no `: any` literals outside docstring comments) so the rule locks in the invariant rather than triggering a sweep.
- Add `src/data/admin-roles.ts` with the `AdminMenuKey`, `AdminRole`, `ADMIN_ROLES`, `ADMIN_MENU`, and `menuForRole(role)` exports. Wire `menuForRole` into `src/app/[admin]/(authenticated)/layout.tsx` so the sidebar only shows the menu items the active role is allowed to see. The role is read from `?role=` (preview helper for the super-admin) with a default of `super-admin` until the user-management UI ships. Includes 11 vitest assertions covering super-admin visibility, narrower-role denials, idempotency, and the canonical-menu-keys invariant.
- Upgrade `jobPostingSchema` in `src/lib/schema.ts` to include `validThrough` (90-day rolling expiry), `industry`, `occupationalCategory`, `directApply`, and a `jobLocationType: TELECOMMUTE` flag when the role's `location` starts with "Remote". The schema is already wired into `src/app/careers/[slug]/page.tsx` so every careers detail page now ships the full Google Jobs-friendly payload.
- Add `src/app/image-sitemap.xml/route.ts` as a Next.js route handler that emits a hand-written XML body conforming to the Google Image Sitemap schema. The file covers the home, six hub routes (with the brand wordmark), every blog post (with its custom `cover` or the brand OG card as fallback), and every case study (with the brand OG card). `src/app/robots.ts` now references both `sitemap.xml` and `image-sitemap.xml` so crawlers discover the file without guessing the URL.
- Add `lib/brand-tokens.cjs` and `lib/walk.cjs` as the top-level CommonJS utility modules CI and Node scripts import. `brand-tokens.cjs` exports `BRAND_HEX`, `BRAND_FORBIDDEN_HEX`, `TOKEN_SUBSTITUTIONS`, `isBrandToken(value)`, and `substituteTokens(source)` — the canonical source of the four-color palette, off-brand legacy hex list, and the Tailwind-class substitution table. `walk.cjs` exports `walkFiles(rootDir, opts)` and `walkSrc(opts)` as the canonical file-system walkers with sensible default excludes (node_modules, .next, .git). Wire `scripts/enforce-palette.cjs` to `require("../lib/brand-tokens.cjs")` so it picks up the same brand metadata the audits use. Includes 18 vitest assertions covering token round-trips, the brand-token check, the walker excludes, and the walker's missing-root case.
- Author: GitHub Copilot.
- Replace every hardcoded phone number, email address, and currency code with the `SITE` constants, expand the sitemap to declare image extensions, wire legacy service slugs into a single redirect table, and add data-layer coverage for the careers and work detail pages. Author: GitHub Copilot.
- Replace hardcoded phone and email in `src/components/chatbot/chatbot.tsx` and `src/app/api/chatbot/route.ts` with `SITE.phoneRaw`, `SITE.email`, and `SITE.phone`. Wire `SITE.phone`, `SITE.email`, `SITE.hours`, and `SITE.dateFormat` into the `BRAND_VOICE` system prompt so the chatbot, admin AI, and AIO Inspector all read from one source of truth. Drop the now-unused `SITE_CURRENCY_SYMBOL` local from the pricing page.
- Replace every `INR` mention on `/pricing` (TL;DR bullet, "How we quote" header, currency and tax rows) with currency-neutral scope lines that follow rules.md §7 ("never mention price on any single page"). Header is now "How we quote", currency row reads "Local · USD on request", tax row reads "As per local tax rules".
- Extend `src/app/sitemap.ts` to declare image extensions via the Next.js `images?: string[]` field. The home route now surfaces the brand wordmark; every blog post surfaces its custom cover image (or the default OG card as a fallback); every case study surfaces the brand OG card. The sitemap now emits the `xmlns:image` namespace so Google Image Search can index the surfaces.
- Add `src/data/service-slug-redirects.ts` with a `resolveServiceSlug(input)` helper and a 12-entry legacy-slug table. Wire `resolveServiceSlug` into `src/app/services/[slug]/page.tsx` so renamed slugs (e.g. `ios-app-development-services` → `ios-app-development`) issue a 307 redirect to the canonical URL, preserving inbound link equity per rules.md §8. Includes 6 vitest assertions covering case-insensitive lookup, empty input, kebab-case canonical slugs, and uniqueness.
- Add `tests/unit/careers-work-slug.test.ts` with 13 vitest assertions covering the `/careers/[slug]` and `/work/[slug]` data-layer contracts: every role and case study has a unique kebab-case slug, every required field is present, `getRole`/`getCaseStudy` resolve every published slug, and every case-study `related` entry points to a known sibling.
- Author: GitHub Copilot.
- Add the first end-to-end pieces of the auth, bot-defence, and security pipeline: a self-contained admin sign-in flow, a Redis-backed rate limiter, Cloudflare Turnstile verification on every public form, and a GlitchTip tunnel for CSP violation reports. Author: GitHub Copilot.
- Add `src/lib/admin-auth.ts` plus `/api/auth/login`, `/api/auth/logout`, and `/api/auth/session` endpoints. The login handler verifies a single super-admin email + password against a scrypt hash, issues an HMAC-signed `wg24_admin_session` cookie, and rate-limits per IP. The middleware now gates every `/24/*` route (except `/24/login`) with the cookie and bounces unauthenticated visitors to `/24/login?next=...`. Includes 17 vitest assertions covering auth, session round-trip, cookie shape, live-mode lock-down, and forged-cookie rejection.
- Add `src/lib/rate-limit-redis.ts` as the distributed limiter. When `REDIS_URL` is set, it stores counters in Redis (`INCR` + `PEXPIRE` per key) so Dokploy's multi-instance deployment shares the bucket. When `REDIS_URL` is not set, it falls back to the in-memory limiter so dev and CI keep working. Wire every public form endpoint (contact, partner, newsletter, lead-magnet) and the chatbot and AIO-inspector routes plus the new login endpoint through the distributed limiter. Add 8 vitest assertions covering the fallback path, key isolation, and the in-memory increment semantics.
- Add `src/lib/turnstile.ts` as a dependency-free wrapper around Cloudflare's siteverify endpoint. Every public form now OR-checks Turnstile alongside the honeypot + UA signals: the cheap honeypot drops obvious bots before paying for a Cloudflare round trip, then Turnstile challenges the visitor. Dev / CI skip the round trip (no key set, or MODE!=live); live mode requires a real `TURNSTILE_SECRET_KEY`. Add 11 vitest assertions covering dev / live paths, token extraction, and IP header parsing.
- Add `/api/glitchtip-tunnel` as a same-origin relay for browser-emitted security events. The CSP now carries both `report-uri /api/glitchtip-tunnel` and `report-to csp-endpoint`, with the matching `<meta http-equiv="Reporting-Endpoints">` declared in `src/components/seo/site-head.tsx`. The tunnel accepts the legacy `application/csp-report` shape, the Reporting API `application/reports+json` array, and generic client envelopes; it forwards each to GlitchTip as a synthetic envelope so the existing alert pipeline picks them up. Add 5 vitest assertions covering each accepted shape and the unknown-shape soft-fail path.
- Add `npm run check:rules` so `scripts/check-rules.cjs` runs by name from the npm scripts block. The script already detects banned words and price patterns; the wrapper keeps the convention aligned with `check:lib-exports`, `check:components`, and the other audit-family commands.
- Author: GitHub Copilot.
- Add Lighthouse CI gating on per-route JS budget regressions, a public `/changelog` route that renders `CHANGELOG.md` inside the brand-styled page shell, a PWA service worker with an `/offline` fallback, and a CONTRIBUTING.md plus Deploy section in README.md. Author: GitHub Copilot.
- Add `.github/workflows/lighthouse.yml` plus `lighthouserc.json` and `lighthouse-budget.json` so every PR runs Lighthouse against the canonical routes, asserts LCP under 2.0 s, CLS under 0.1, and per-route performance above 0.9, then re-checks `scripts/check-js-budget.cjs` against the standalone build so the two budgets never drift.
- Add `src/lib/changelog.ts` (parser) plus `src/app/changelog/page.tsx` (server-rendered route) so visitors can browse the changelog without leaving the site; the parser reads `CHANGELOG.md` at request time and degrades gracefully on unknown shapes. Add the corresponding sitemap entry and a unit test that locks in seven parser behaviours.
- Add `public/sw.js` (offline-first service worker) and `src/app/offline/page.tsx` (brand-styled fallback) plus a `ServiceWorkerRegistrar` client component registered from the public layout. The SW precaches the brand shell and offline page, serves stale-while-revalidate for static assets, network-first for navigations, and bypasses `/api/*` and the admin prefix.
- Add `CONTRIBUTING.md` with the contributor handbook and PR checklist (lint, typecheck, tests, build, smoke, changelog entry, todo update, brand palette, accessibility, SEO, performance, spec compliance) so future contributors know the bar before they open a PR.
- Add a `Deploy` section to `README.md` covering Dokploy, the production URL (`https://www.webgrow24.com`), the staging and preview environments, the full server-side env var table, the deploy and rollback flow, and the post-deploy verification steps.
- Author: GitHub Copilot.
- Refactor the public header and top bar for clearer visual hierarchy without removing any element. Author: GitHub Copilot.
- Add a per-category Lucide icon to every utility-bar trigger (Globe, Code2, BookOpen, Wrench, Building2, Briefcase) and a matching tinted-icon header to every utility dropdown panel, plus a count badge that previews how many items the menu holds.
- Add a small `Sparkles` icon to the Services mega trigger and a `CalendarCheck` icon to the main-header "Book a scoping call" CTA so the primary action and the category opener read at a glance.
- Add a column icon, count badge, and divider line to every Services mega-panel column so the five categories (Web, Mobile and App, Growth, SaaS and platforms, CMS) read as a structured grid rather than a flat list.
- Fill the previously empty 44px reserved band on the utility bar (the CLS-safe slot kept for the scroll state) with a "Live · 24h reply" status pill on the right, with a brand-primary pulse dot that respects `prefers-reduced-motion`.
- Add trailing `ArrowRight` glyphs to the four secondary links in the mega panel footer (All services overview, See our work, Read a case study, Read the playbook) so they read as forward actions.
- Author: GitHub Copilot.
- Fix the dead-hover-state and stray-pixel bugs in the dropdown and mega-panel links. Author: GitHub Copilot.
- Utility dropdown links no longer wrap the label in a coloured inner span; the link now owns its `text-fg` colour, so `hover:text-primary` and `hover:bg-surface-muted` actually take effect (the previous span was already primary, so the hover transition was invisible).
- Remove the 4px "bullet" dot that sat on the left of every utility dropdown link; it was too small to read as a bullet and just looked like noise next to the now-clean text.
- Mega-panel column links get the same fix: outer `text-fg` and inner `text-primary` span collapsed into a single label, so the hover transition is visible.
- Mega-panel footer secondary links ("All services overview", "See our work", "Read a case study", "Read the playbook") had `hover:text-primary` but were already `text-primary`. Replaced the dead hover with `hover:bg-surface` so the hover state has a visible effect.
- Main-nav items (Our work, Pricing, About, Contact) consolidated to a single `text-primary` class plus a working `hover:bg-primary/10` for a clearer active/hover state.
- Strengthen the panel header and see-all footer backgrounds (drop the `/60` and `/40` opacity that washed them out).
- Author: GitHub Copilot.
- Fix the navbar dropdown overlap, z-index, and hover-gap bugs. Author: GitHub Copilot.
- Bump every dropdown panel (utility + Services mega) from `z-50` to `z-60` so the panel paints above the main nav row inside the header's stacking context. The previous value collided with the mega panel's `z-40` sibling and left the rightmost utility panels fighting the main nav for paint order.
- Right-align the right-half utility dropdowns (Tools, Company, Careers) via a new `utilityPanelAlign(index, total)` helper so the panel hugs the trigger's right edge and never overflows off the right edge of the viewport. At 1024px and 1200px viewports, all six dropdowns now stay fully inside the viewport (previously the rightmost panels overflowed by ~210px at 1547px).
- Add an invisible 4px hover bridge to every utility panel and an 8px hover bridge to the Services mega panel. The bridge fills the gap between the trigger and the panel so the panel's `onMouseEnter` fires the moment the mouse leaves the trigger; without it, the 150ms `scheduleClose` timer could fire before the user reached the panel and the dropdown would close mid-hover.
- Switch the panel wrappers to `overflow-visible` so the hover bridge is not clipped by the panel's own rounded corners.
- Standardise the Services mega trigger to `text-primary` (matching Our work, Pricing, About, Contact); the open state is now signalled by the background tint and the chevron rotation instead of a colour swap.
- Author: GitHub Copilot.
- Trim the home-page FAQ from seven items to five and fix the column alignment. Author: GitHub Copilot.
- Drop "Do you take equity?" (already implied by the fixed-quote question) and "Where is WebGrow24 based?" (covered by the footer). The five remaining questions are the ones a landing visitor needs answered to make a buy decision: pricing, timeline, who builds the work, India compliance, and team specialisation.
- Update the intro copy from "seven questions" to "five questions" so the visible count matches.
- The JSON-LD `FAQPage` schema now also lists exactly the same five questions (built from the same `homeFaqPlain` array), so the schema policy "every question in the schema must be visible on the page" stays satisfied and Google rich results will not be suppressed.
- Fix the FAQ section column alignment. The left column ("Frequently asked" heading + description) used to float at the top while the right column (FAQ accordion) filled the full height, leaving a large empty gap in the middle. Switched the grid to `items-stretch` and the left column to `flex h-full flex-col justify-between`, so the heading sits at the top of the cell and the "See full FAQ" link anchors at the bottom, visually balanced with the accordion on the right.
- Author: GitHub Copilot.
- Bump every dropdown panel (utility + Services mega) from `z-50` to `z-60` so the panel paints above the main nav row inside the header's stacking context. The previous value collided with the mega panel's `z-40` sibling and left the rightmost utility panels fighting the main nav for paint order.
- Right-align the right-half utility dropdowns (Tools, Company, Careers) via a new `utilityPanelAlign(index, total)` helper so the panel hugs the trigger's right edge and never overflows off the right edge of the viewport. At 1024px and 1200px viewports, all six dropdowns now stay fully inside the viewport (previously the rightmost panels overflowed by ~210px at 1547px).
- Add an invisible 4px hover bridge to every utility panel and an 8px hover bridge to the Services mega panel. The bridge fills the gap between the trigger and the panel so the panel's `onMouseEnter` fires the moment the mouse leaves the trigger; without it, the 150ms `scheduleClose` timer could fire before the user reached the panel and the dropdown would close mid-hover.
- Switch the panel wrappers to `overflow-visible` so the hover bridge is not clipped by the panel's own rounded corners.
- Standardise the Services mega trigger to `text-primary` (matching Our work, Pricing, About, Contact); the open state is now signalled by the background tint and the chevron rotation instead of a colour swap.
- Author: GitHub Copilot.
- Run production builds through the local Next CLI without a deprecated shell spawn, and use the Node.js runtime for API handlers so build output stays warning-free.
- Author: GitHub Copilot. Commit: `e53c396`.
- Remove the orphan `verify-search-removed.cjs` script from the repo root. Nothing in `src/`, `scripts/`, or `package.json` imported it. Lint, type check, and tests stayed green.
- Author: Renish Mithani. Commit: `91f9f27`.
- Switch the broken-link checker sample URLs to same-origin relative paths so the default run does not hit third-party hosts.
- Author: webgrow24. Commit: `3a4b79e`.
- Detect incomplete standalone build output and force a retry, so the failed stand-alone start no longer reaches production.
- Author: webgrow24. Commit: `c2fa75a`.
- Hide the favicon size preview from assistive tech with `aria-hidden`, and refresh the curated `llms-full.txt` index.
- Author: webgrow24. Commit: `d5fa6ab`.
- Raise three tool buttons to the 44 by 44 CSS pixel tap-target minimum to clear the accessibility rule (WCAG 2.2 AA, rules.md Section 6).
- Author: webgrow24. Commit: `740929b`.