feat(marketing): phase 4 — the marketing pages #7

Merged
whitlocktech merged 1 commits from feat/phase-4-marketing into main 2026-08-24 07:53:04 +00:00
Member

PLAN.md §13 phase 4 — /features/, /architecture/, /modules/, /integrations/, and /community/, plus the two scope items the phase table never assigned to anyone.

What you approved before I wrote anything

Six decisions, now recorded in PLAN.md §10 as D20–D25 so they are not re-litigated:

# Decision
D20 /features/ is the homepage's list with a detail line, not a second list. One data file, two renderings.
D21 /architecture/ draws reasons, not reference — three new inline SVGs, one per boundary. No endpoint tables or config keys; those are phase 8's and stay canonical in docs/.
D22 §2's deliberate absences become one tagged data file, rendered on the three pages that promise them.
D23 Phase 4 absorbs /community/ and checkLinks.mjs.
D24 needsModule — the Teams correction below.
D25 The per-capability demo affordance is a deep link.

The accuracy bug this found

Writing the /features/ detail line for Teams exposed a claim phase 3 shipped: the Community group said "everything here works on a deployment with no game module installed at all". That is false, and website main says so plainly — teams.module_id is NOT NULL, there is no create route anywhere under /api/v1/admin/teams, and sync is gated on teamProvider.providerModuleId().

Core owns the entire Team machinery — tables, roster resolver, forums, notification streams, Discord bridge, voice, activity feed, /admin/teams — and deliberately cannot originate a Team, because core does not own the word for one. On a bare core the feature is present, correct and permanently empty. Teams and Team forums are now marked; the group summary is requalified; the homepage changed by one sentence and kept D18's five named groups.

The two absorbed scope items

checkLinks.mjs (§12, previously unowned). It reads dist/client rather than src/, and that is the design: half the links these pages carry are assembled from data files and template literals, and a source scan sees an expression rather than a URL. It fetches nothing — the outbound rule is about the shape of a URL, and a check that fails when someone else's host is slow is a check people learn to ignore.

Its one exemption list, PLANNED_ROUTES, exists because §10's convention is that the header, footer and homepage link the final routes rather than growing links phase by phase. That is safe only because it is checked in both directions: a link to a route that is neither built nor listed fails, and an entry whose route has since been built also fails, so it cannot rot into a permanent exemption once the page arrives. It currently holds one live entry (/app/, phase 5).

/community/ — specified in §10 and §14 N3, linked from the header and footer since phase 1, built by no phase. It describes the honest split N3 asks for: Discord is the front door in fact, because Gitea registration is disabled and nobody outside the org can file anything. The sentence that changes if registration is reopened is marked in the source.

brand.json has promised /features/ a "per-capability affordance" since phase 2 without one ever being defined. D25 defines it, and it needed the branding pipeline extended: the phase-3 slot is a literal swap of a whole URL, so it can only ever put the demo's root in an href, and reversing it would not even find a deep link — whose href is the root plus a path, matching no literal the script knows.

applyBrand.mjs gained a second pass that recomputes all three attributes from the immutable data-demo-path, making it idempotent and exactly reversible. checkBrand.mjs gained a guard that lifts the pattern out of applyBrand.mjs and runs it against the stock markup, so a template and a script sharing no code cannot drift. Proved against a real mount in both directions: 12 links filled and 12 reversed with zero traces left.

A cascade bug no check could see

[data-demo-url=''] { display: none } is specificity 0,1,0 — and so is the scoped class Astro puts on the same element. Component styles are emitted after global.css, so /features/'s .demo-link { display: inline-flex } won on source order, and twelve links to a demo that does not exist rendered on the page, each pointing at href="" — which a browser resolves to the page it is already on.

checkBrand.mjs was green throughout: the attributes were perfect and the defect was three files away, in the cascade. It was found by looking at the rendered page at 390px. The rule is now !important and the stylesheet says why.

Also in here

  • The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css — it was duplicated the moment a second diagram existed. DataPath.astro keeps its own layout (its right column is a numbered walk, not a notes column).
  • Two new build-time assertions: assertDetailCoverage() (a capability with no detail is a heading with nothing under it) and assertScopeNonEmpty() (an empty absences section is the one defect that looks deliberate). Both negative-tested.
  • astro check caught a real type hole — the inferred per-group item union made demoPath unreadable on /features/. Fixed with explicit JSDoc typedefs and a capabilityGroup(id) lookup that throws rather than returning undefined.

Verification

From a clean checkout (git archivenpm ci), not the working tree:

  • checkTokens — 34 files, every colour from the token file
  • checkBrand — 19 /brand/ URLs resolve, demo slot + 1 deep-link site match their contracts
  • astro check0 errors
  • production build — 7 routes
  • checkLinks — 141 internal links resolve, 24 repository links point at a branch
  • checkFacts — 15 facts agree with their authorities

Live browser pass on the built server: all five new routes 200, one <h1> each, no <img> without alt, no <svg> without aria-hidden, no horizontal overflow at 390px on any of the five, and the demo links verified hidden with no mount and correct with one.

Every new check was negative-tested before being trusted: a broken internal link, a commit permalink, a 40-hex ref on a branch path, the planned-route reverse check, reordered demo attributes, a missing detail and an empty scope all fail as intended.


  • This pull request was written with AI assistance (Claude Code).

🤖 Generated with Claude Code

PLAN.md §13 phase 4 — `/features/`, `/architecture/`, `/modules/`, `/integrations/`, and **`/community/`**, plus the two scope items the phase table never assigned to anyone. ## What you approved before I wrote anything Six decisions, now recorded in PLAN.md §10 as **D20–D25** so they are not re-litigated: | # | Decision | |---|---| | **D20** | `/features/` is the homepage's list with a `detail` line, not a second list. One data file, two renderings. | | **D21** | `/architecture/` draws **reasons, not reference** — three new inline SVGs, one per boundary. No endpoint tables or config keys; those are phase 8's and stay canonical in `docs/`. | | **D22** | §2's deliberate absences become one tagged data file, rendered on the three pages that promise them. | | **D23** | Phase 4 absorbs `/community/` and `checkLinks.mjs`. | | **D24** | `needsModule` — the Teams correction below. | | **D25** | The per-capability demo affordance is a deep link. | ## The accuracy bug this found Writing the `/features/` detail line for Teams exposed a claim **phase 3 shipped**: the Community group said "everything here works on a deployment with no game module installed at all". That is false, and `website` `main` says so plainly — `teams.module_id` is `NOT NULL`, there is no create route anywhere under `/api/v1/admin/teams`, and sync is gated on `teamProvider.providerModuleId()`. Core owns the *entire* Team machinery — tables, roster resolver, forums, notification streams, Discord bridge, voice, activity feed, `/admin/teams` — and deliberately cannot **originate** a Team, because core does not own the word for one. On a bare core the feature is present, correct and permanently empty. Teams and Team forums are now marked; the group summary is requalified; the homepage changed by one sentence and kept D18's five named groups. ## The two absorbed scope items **`checkLinks.mjs`** (§12, previously unowned). It reads `dist/client` rather than `src/`, and that is the design: half the links these pages carry are assembled from data files and template literals, and a source scan sees an expression rather than a URL. It fetches nothing — the outbound rule is about the *shape* of a URL, and a check that fails when someone else's host is slow is a check people learn to ignore. Its one exemption list, `PLANNED_ROUTES`, exists because §10's convention is that the header, footer and homepage link the **final** routes rather than growing links phase by phase. That is safe only because it is checked in both directions: a link to a route that is neither built nor listed fails, **and an entry whose route has since been built also fails**, so it cannot rot into a permanent exemption once the page arrives. It currently holds one live entry (`/app/`, phase 5). **`/community/`** — specified in §10 and §14 N3, linked from the header and footer since phase 1, built by no phase. It describes the honest split N3 asks for: Discord is the front door *in fact*, because Gitea registration is disabled and nobody outside the org can file anything. The sentence that changes if registration is reopened is marked in the source. ## The demo deep links `brand.json` has promised `/features/` a "per-capability affordance" since phase 2 without one ever being defined. D25 defines it, and it needed the branding pipeline extended: the phase-3 slot is a literal swap of a whole URL, so it can only ever put the demo's *root* in an `href`, and reversing it would not even find a deep link — whose `href` is the root plus a path, matching no literal the script knows. `applyBrand.mjs` gained a second pass that **recomputes** all three attributes from the immutable `data-demo-path`, making it idempotent and exactly reversible. `checkBrand.mjs` gained a guard that lifts the pattern out of `applyBrand.mjs` and runs it against the stock markup, so a template and a script sharing no code cannot drift. Proved against a real mount in both directions: 12 links filled and 12 reversed with zero traces left. ## A cascade bug no check could see `[data-demo-url=''] { display: none }` is specificity 0,1,0 — and so is the scoped class Astro puts on the same element. Component styles are emitted *after* `global.css`, so `/features/`'s `.demo-link { display: inline-flex }` won on source order, and **twelve links to a demo that does not exist rendered on the page**, each pointing at `href=""` — which a browser resolves to the page it is already on. `checkBrand.mjs` was green throughout: the attributes were perfect and the defect was three files away, in the cascade. It was found by looking at the rendered page at 390px. The rule is now `!important` and the stylesheet says why. ## Also in here - The four diagrams' shared SVG vocabulary moved to `src/styles/diagram.css` — it was duplicated the moment a second diagram existed. `DataPath.astro` keeps its own layout (its right column is a numbered walk, not a notes column). - Two new build-time assertions: `assertDetailCoverage()` (a capability with no detail is a heading with nothing under it) and `assertScopeNonEmpty()` (an empty absences section is the one defect that looks deliberate). Both negative-tested. - `astro check` caught a real type hole — the inferred per-group item union made `demoPath` unreadable on `/features/`. Fixed with explicit JSDoc typedefs and a `capabilityGroup(id)` lookup that throws rather than returning `undefined`. ## Verification From a **clean checkout** (`git archive` → `npm ci`), not the working tree: - `checkTokens` — 34 files, every colour from the token file - `checkBrand` — 19 `/brand/` URLs resolve, demo slot + 1 deep-link site match their contracts - `astro check` — **0 errors** - production build — 7 routes - `checkLinks` — 141 internal links resolve, 24 repository links point at a branch - `checkFacts` — 15 facts agree with their authorities Live browser pass on the built server: all five new routes 200, one `<h1>` each, no `<img>` without `alt`, no `<svg>` without `aria-hidden`, **no horizontal overflow at 390px on any of the five**, and the demo links verified hidden with no mount and correct with one. Every new check was negative-tested before being trusted: a broken internal link, a commit permalink, a 40-hex ref on a branch path, the planned-route reverse check, reordered demo attributes, a missing detail and an empty scope all fail as intended. --- - [x] This pull request was written with AI assistance (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-08-24 06:54:04 +00:00
feat(marketing): phase 4 — the marketing pages
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
2d19ee4220
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and
/community/ — plus the two scope items the phase table never assigned to anyone.

Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as
D20-D25:

- D20 /features/ is the homepage's list with a `detail` line, not a second list.
  One data file, two renderings, so they cannot disagree about what exists.
- D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per
  boundary. No endpoint tables, no config keys — those are phase 8's and stay
  canonical in docs/.
- D22 The deliberate absences of §2 become one tagged data file, rendered on the
  three pages that promise them.
- D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the
  header since phase 1, built by no phase) and checkLinks.mjs.
- D24 `needsModule`: writing the Teams detail exposed a false claim phase 3
  shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is
  no create route, sync is gated on providerModuleId() — so the Community group
  no longer says a bare core does all of it.
- D25 The per-capability demo affordance brand.json had promised since phase 2 is
  a deep link, filled at boot from data-demo-path.

checkLinks.mjs reads the built HTML rather than src/, because half these links
are assembled from data files and template literals. Its PLANNED_ROUTES list is
checked in both directions, so it cannot rot into a permanent exemption.

applyBrand.mjs gained a pass that recomputes deep links from their immutable
path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out
and runs it against the stock markup so the two cannot drift. Both proved
against a real mount, in both directions.

Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped
component class are both specificity 0,1,0, so .demo-link's `display` beat the
hide rule and twelve links to a nonexistent demo rendered, each resolving to the
current page. The rule is now !important.

The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css.

Verified from a clean checkout: npm ci, all five checks, astro check (0 errors),
production build, and a live browser pass at desktop and 390px.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit fbd7bbe6fd into main 2026-08-24 07:53:04 +00:00
whitlocktech deleted branch feat/phase-4-marketing 2026-08-24 07:53:05 +00:00
Sign in to join this conversation.
No description provided.