Feature plan: Step 1 · Step 2 · Step 3 · Step 4

Debug mode: enabled

Follow-up cycle: predecessor docs_discovery; reused branch feature/docs-discovery (unmerged). Planning commit 93e98f89.

Requests and decisions

  • Request: site-wide skip link. Disposition: separate work, recorded in todo.txt at the user's request (the todo.txt edit is left uncommitted for the user). Approval: deferred before Step 3 approval. Outcome: not part of this cycle.

Stage 1 - Data foundation and contrast audit

  • Changes:

    • PlatformDocsCatalog::categorySlug(): inventory category is now a lowercase slug, with a new categoryLabel (the curated label when one exists, otherwise the capitalized folder name). architecture and Architecture now share one key.

    • PlatformDocsCatalog::topics() (curated guides grouped in listed order) and counts(inventory) (collection, category, and folder counts, where folder counts include subfolders) are computed on the server.

    • Search matches categories by slug, so legacy category=Architecture and category=Extending the Platform URLs keep working. platform_docs.js mirrors the slug matching.

    • The controller uses topics() and counts(); the folder option list now comes from the counts. The temporary category select shows labels with slug values (Stage 4 replaces it).

    • Added tests/theme_contrast_audit.py (token-pair contrast for every registered theme, alpha composited over the page background).

  • Verification:

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest: passed 25/25, including the new testCategoriesMergeCaseVariantsAndCountsMatchInventory (no case duplicates, totals equal the inventory, legacy URLs, topic order).

    • node --check public/assets/platform_docs.js, php -l on changed PHP, and git diff --check: passed.

    • Contrast audit: report. Only vapor passes every pair. Summary of failures on tokens this page relies on:

      • --line below 3:1 on --panel or --input-bg in 14 themes (all except vapor and lcd).

      • --ink-soft below 4.5:1 on --page-bg in lcd, chicago, sticker, arena, word97 and midnight, and on --panel in arena (3.66).

      • --ink on --page-bg in chicago (3.96). --active-line on --panel in chicago and qdb. --active-ink on --active-bg in mitrapclub (3.85).

  • Notes:

    • Design consequences for Stages 2–7:

      • --line is used only for decorative dividers, which WCAG 1.4.11 exempts. Chips, badges and filter state are identified by text and shape, not by borders alone.

      • Paths in --ink-soft sit on --panel, never directly on --page-bg.

      • Theme token fixes are evaluated in Stage 7.

    • Audit limits: it reads CSS custom properties only. Image or gradient backgrounds (for example chicago's) and theme-scoped selector overrides are not modeled, and chicago inherits the default --active-* tokens. Browser measurement in Stage 8 confirms the actual results.

    • The existing paned_users_filter_list partial is a JS listbox, not a link list, so it can't serve the no-JS filters. Stage 4 needs a new link-list component.

    • The inventory count includes this cycle's new planning files (1,317+), so the count is not hard-coded anywhere.

Stage 2 - Landing view

  • Changes:

    • /docs/ with no parameters renders the landing view. Any of q, folder, category, group or page (even empty) renders the results view. Both views are in the same document, with server-side hidden, so the static index.html can switch views with JS (showView() in platform_docs.js). The search index is fetched only when results are shown.

    • The intro card holds the H1 and the restored intro paragraphs from main (including the source-path note), a compact labeled search (placeholder uses the live count, prefilled with q) and "Browse all docs →" (?page=1).

    • "How it works" is an open section again (Stage 3 re-lays it). The "Starting guides" <details> is removed.

    • Curated topic sections use the .docs-entry blocks: a title link in --body-font with a quiet --line underline that becomes solid on hover or focus, the description, and the path in --code-font/--ink-soft on --panel. Topics with several guides span the row, and adjacent single-guide topics pair up (auto-fit, minmax(min(100%, 20rem), 1fr)). The curated list order now puts CLI Reference after Trust and Identity so pairs read left to right in source order, without dense reordering.

    • "All documents" panel: live count sentence, Browse all, and collection quick links with counts (?group=…) as a list with non-announced separators.

    • Links and controls have at least 2.75rem targets. .docs-page [hidden] overrides author display rules.

  • Verification:

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest: passed 25/25. The landing test now checks section order, every curated description and path, quick-link and browse URLs, the live-count placeholder, the hidden library on landing, the hidden landing on results, and the prefilled query.

    • node --check, php -l and git diff --check: passed.

    • Chromium on an isolated dev server (port 8191): landing at 1280 and 320px in default, 1280 in Word97, and 320 CSS px at 200% in Midnight, all with no horizontal page overflow. No-JS ?group=Workflow renders the server results.

  • Notes:

    • Restoring the path note and descriptions reverses the earlier UI-copy cleanup at the user's approved request. A new UI-copy checklist entry records this, with verification pending Stage 8.

    • The local server on port 8004 runs from this checkout and no longer shows main. Port 8007 serves the v3-codex-3 checkout. Both were left running and untouched.

    • Known interim state: the results view still contains the Stage 1 select form, so it shows two search inputs. Stage 4 replaces it. The diagram still scrolls horizontally inside its frame on narrow screens (Stage 3).

Stage 3 - How it works diagram and concepts

  • Changes:

    • Replaced the fixed-viewBox SVG with an ordered list (.docs-flow) labeled by the profile's architecture title. The four boxes and three labeled edges (signed write, builds, serves) are real text, so the list is the text equivalent and no hidden duplicate is needed. Arrows are decorative CSS (aria-hidden). The key icon stays as a decorative inline SVG drawn in currentColor.

    • Vertical by default, with labels set beside the arrows so they never touch. It switches to a row only when the panel is at least 52rem wide (container query), which the shared 760px shell never reaches. Boxes are capped at 22rem and centered.

    • All colors come from --ink, --ink-soft, --line, --panel and --input-bg. Box backgrounds use --input-bg because chicago's --page-bg is the teal desktop. Diagram text is at least 0.875rem.

    • The concept blurbs stack, and become three columns only when each column gets at least 18rem (a 58rem container).

    • Removed the now-unused architectureDescription profile string.

  • Verification:

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest ProfilePresentationContentTest: passed 28/28. Diagram assertions now check the labeled list for every site profile, 4 nodes and 3 decorative arrows, and the absence of command text.

    • php -l, git diff --check: passed.

    • Chromium screenshots: default at 1280 and 320px, chicago at 1280, and midnight at 200% zoom (320 CSS px). No horizontal overflow, no label overlapping an arrow, and no diagram text below 0.875rem (the only text under 0.85rem on the page is the site's existing inline code at 13px in the blurbs).

  • Notes:

    • Departure: the brief allowed either an SVG with <title>/<desc> or HTML/CSS. HTML/CSS was chosen because a fixed SVG can only shrink or scroll, not stack.

    • Inside the 760px shell the diagram is always vertical. A horizontal layout would have to break code paths mid-word to fit.

Stage 4 - Results view (server-rendered)

  • Changes:

    • Removed the select-based library form. The intro card's search now serves both views: prefilled with q, carrying active non-search filters as hidden inputs, with "← Docs home" shown only in the results view.

    • Results section ("Documents"):

      • Removable chips for each active filter (accessible names like "Remove Category: Architecture"), plus Clear all.

      • A polite status count and the existing index-error/Retry.

      • A closed "Filters" <details> holding Collection and Category link lists with server counts, and a nested <details> folder tree. Branches open along the active folder, and each branch starts with an "All in …" link because a summary can't contain a link. The active link carries aria-current="page", bold text and a bar (--active-* tokens), so state doesn't rely on color.

    • Choosing a filter replaces the other filters and keeps the search, so every filter link is a single-filter URL that Stage 6 can prebuild. Legacy combined URLs still work.

    • Result rows are an ordered list with 1px --line dividers. Each row shows the title, a one-line summary (curated description, otherwise the first prose sentence via PlatformDocsCatalog::summary(); 409 mostly list-only plans have none), the path, and a category badge. The collection badge appears only when no collection filter is active and the row isn't Current guidance.

    • Pagination reads "← Previous · Page N of M · Next →", with aria-current on the page indicator and the active filters kept. The empty state offers Clear filters and Docs home.

    • The landing view sends an empty results shell (no rows), and the controller no longer builds the unused folder option list.

    • platform_docs.js got the minimum needed to keep working against the new markup. Rendering parity is Stage 5.

  • Verification:

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest ProfilePresentationContentTest: passed 29/29. The new testResultsViewFiltersWithLinksChipsAndPages covers pagination links and the page indicator, filter aria-current, chip names, Clear all, no repeated collection badge, the legacy mixed-case category URL, hidden filter inputs, summaries, folder-tree open state and nested "All in" links, and the empty state.

    • node --check, git diff --check: passed.

    • Chromium with JS disabled: results at 1280px, 320px, and 200% zoom (320 CSS px), with filters opened by clicking the summary. No horizontal overflow; the active filter and tree markers are visible.

  • Notes:

    • Measured the search index at 8,692,028 bytes uncompressed with summaries (+~0.2 MB versus Stage 4 of the predecessor cycle).

    • Filter counts are library-wide, not narrowed by the current search. This keeps prebuilt pages identical to their dynamic versions.

    • Departure: the brief's sidebar on wide screens isn't used, because the shared 760px shell (kept per Step 3) leaves no room. Filters stay in the <details> at every width.

Stage 5 - JS enhancement parity

  • Changes:

    • platform_docs.js renders the same chips, hidden filter inputs, filter aria-current and links (keeping the search), folder-tree open state, rows (summary, path, category badge, collection badge rule), page indicator and Previous/Next, empty state and count as the server does. The count stays in the server's role="status" element, so changes are announced politely.

    • Same-page /docs/?… links (landing quick links, Browse all, filters, chips, pages) and the search form update in place with history entries. After a link navigation, focus moves to the "Documents" heading (tabindex="-1"). Back and forward switch between the landing and results views.

    • Return-link parameters keep the URL's own order, matching the server.

    • Template data hooks: data-docs-chips (always rendered, hidden when empty), data-docs-filter/-value/-label on filter links, data-docs-folder-branch on tree branches.

  • Verification:

    • New tests/browser/docs_landing.mjs (Chromium, isolated dev server): for 6 queries (paged collection, legacy mixed-case category with search, nested folder, empty search, browse all, search) it navigates in place from the landing page, as the static host does, and compares the result with the JS-off server render (chips, count, rows HTML, page text, empty state, aria-current targets, open branches, hidden inputs, search value): all identical. Also passed: a landing quick link moves to results with focus on the heading, Back restores the landing view, and an injected index failure shows Retry and recovers.

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest ProfilePresentationContentTest: passed 29/29. node --check, git diff --check: passed.

  • Notes:

    • Rendering logic is duplicated between PHP and JS. The parity test is the guard, and Stage 8 runs it on the static host too.

    • The predecessor harness tests/browser/docs_discovery.mjs still drives the removed select form and data-docs-clear. Stage 8 updates it.

Stage 6 - Static prebuilt browse pages

  • Changes:

    • PlatformDocsCatalog::browsePath(query) maps a query with no search text and at most one filter (empty parameters ignored, category matched by slug, page 1–99999) to /docs/_browse/{all|group/…|category/…|folder/…}/{page}.html. browseQueries(inventory, pageSize) lists every such page. PAGE_SIZE replaces the hard-coded 40.

    • StaticArtifactBuilder writes all browse pages (390 for the current inventory: all, 3 collections, 14 categories, 204 folders, by page), plus /docs/_search.html, a results shell rendered with no rows. Its <noscript> notice says search needs JavaScript in this copy and points to the filters.

    • FrontController (static release path): /docs/ with any results parameter serves the matching browse page, otherwise the search shell (search text, combined filters, out-of-range pages, unknown folders). /docs/ without parameters still serves the landing page. Application serves /docs/_search.html like /docs/_missing.html.

    • Size work: server-rendered results pages no longer include the hidden landing sections (the browser only switches views in place from the landing page). Filter links carry their filter only in the href (no per-link classes or data attributes), and the JS reads it from there.

  • Verification:

    • php tests/run.php PlatformDocsStaticTest: passed 2/2 (183.5 s). It covers every browse page existing and matching a live Application render of the same query, real FrontController routing for collection, legacy-case category, nested folder and page 2, and the shell with its notice for search, combined filters, page 999 and an unknown folder. The landing page has no notice.

    • Real HTTP against a built release (isolated host on port 8192, fixture repository so the image flag matches): routes above confirmed over HTTP.

    • tests/browser/docs_landing.mjs with DOCS_CLIENT_BASE_URL on the static host: all six JS-vs-server parity cases, landing → results focus and Back, and index failure/Retry passed.

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest ProfilePresentationContentTest: passed 29/29. node --check, git diff --check: passed.

  • Measurements (fixture build, scratch directory):

    • Full build 80.7 s (the predecessor recorded 77.93 s for 1,314 files).

    • Release 87.9 MB against the predecessor's ~58 MB. Browse pages account for 27.4 MB (390 pages at ~70 KB; ~15 KB gzipped). Before trimming they were 43.5 MB at ~115 KB each.

    • Search index 8.7 MB.

  • Notes:

    • Decision for the user: folder pages are 241 of 390 and about 17 MB, mostly the plans tree repeated on every page. Dropping folder prebuilds would send static no-JS folder links to the search shell instead.

    • The static test now takes about 3 minutes (it was about 2.6 minutes in the predecessor) because of the per-page live comparison.

Stage 7 - Accessibility and theme hardening

  • Changes:

    • New tests/browser/docs_landing_contrast.mjs measures rendered text contrast for each docs component in every registered theme. It swaps the theme in its own <link> so the real cascade order is kept, and composites backgrounds up the ancestor chain. It caught failures the token audit can't see (theme link-color rules).

    • Theme link rules (:root[data-theme] a) were overriding the chip and active-filter colors in word97 (1.86:1), midnight (3.74) and mitrapclub (1.59). The component selectors are now scoped above them.

    • Token fixes (contrast improves, same hue):

      • arena --ink-soft #666666 → #808080 (3.66 → 5.32 on black).

      • mitrapclub --active-bg #d94f2b → #c4441f (3.85 → 4.69 with --active-ink). The swatch keeps its own literal color.

    • A 3px --ink focus ring (2px offset) on every docs link, button, input and summary, plus the programmatically focused results heading.

    • prefers-contrast: more: solid title underlines, and paths, captions, dividers, diagram boxes and badges switch to --ink.

    • Standalone links (Browse all docs, Docs home, empty-state links) get 2.75rem targets. Remaining smaller targets are links inside sentences, which the 24px inline-link exception covers.

  • Verification:

    • Rendered contrast: report. All docs components pass AA in 15 of 16 themes. qdb still fails (below).

    • Token audit rerun: report. arena and mitrapclub text pairs now pass.

    • Target-size check (Chromium, 1280 and 320px, landing, filtered, empty and paged results, all filters and tree branches open): no non-inline target under 44px tall, and no inline link under 24px.

    • php tests/run.php PlatformDocsCatalogTest PlatformDocsPageTest ProfilePresentationContentTest: passed 29/29. git diff --check: passed.

  • Notes:

    • Reported, not fixed: qdb. The theme colors every link var(--line) (2.91:1 on its panel), which fails AA on every page. Changing it alters the theme's site-wide link identity, so it is left for the user to decide.

    • Reported, not fixed: --line below 3:1 in 14 themes. This page uses --line only for decorative dividers and the quiet underline (state and identity come from text, weight and the active pair). Site-wide input borders still rely on it, which is outside this cycle.

    • prefers-reduced-motion: the docs page has no animation or smooth scrolling, and focus moves without animated scrolling, so no rule was needed.

    • The skip link remains deferred to todo.txt.

Stage 8 - Known-user acceptance and docs

  • Changes:

    • tests/browser/docs_landing.mjs now runs the maintainer's matrix: dynamic and static hosts × JS on/off × (1280px, 1280px at 200% zoom, 320px) × default/Word97/Chicago/Midnight. Each run covers the landing, a browse page (?group=Workflow) and a search (?q=recovery). It asserts:

      • no page-wide horizontal scroll

      • diagram text of at least 13.6px

      • expected rows

      • the static no-JS search notice appearing only there

      • a ring of at least 2px on every docs control reached by tabbing

      • no page errors

    • The matrix caught Word97's and Midnight's 1px dotted focus styles overriding the docs ring. The docs focus ring is now !important, on this page only.

    • Updated the predecessor harnesses for the new UI. docs_discovery.mjs starts on the landing view, then searches, filters with links and clears with the chip or empty-state link. platform_docs.mjs no longer opens the removed "Starting guides" disclosure.

    • Updated docs/runbooks/markdown_renderer.md (curation, landing/results URLs, slugs, static browse pages and search shell, new checks) and marked the UI-copy checklist entry done.

  • Verification:

    • docs_landing.mjs (dynamic 8191, static release 8192): 6/6 JS-vs-server parity cases, landing navigation and focus, index failure/Retry, and 48/48 acceptance scenarios passed. 145 screenshots plus evidence.json are kept locally in the session scratchpad (not committed).

    • docs_discovery.mjs: 16/16 dynamic/static × 1280/390 × four themes passed, including the Tools launcher and subnavigation Docs icons in Word97, Chicago and Midnight (destination unchanged, no icon mapping change needed).

    • platform_docs.mjs: 8/8 no-JS reader scenarios passed.

    • python3 tests/platform_docs_release_test.py: passed, including complete-package rollback and regeneration and 8 static browser scenarios.

    • Focused PHP suites (catalog, page, profile content, static): passed as recorded in Stages 1–7.

    • Full repository PHP suite: partial. It was stopped after ~38 minutes, near the end of LocalAppSmokeTest, at the user's request because concurrent runs were loading the machine. Tests after that point did not run. Seven failures among the tests that ran:

      • Five also fail on the pre-cycle baseline (76761678, separate worktree), so they are pre-existing. These are ApprovalSeedCommandTest::testApprovalIsEffectiveForExistingAuthenticatedMemberOnNextRequest, AgentIdentityServiceTest::testReplyAgentBootstrapCreatesApprovedIdentityAndPrivateKeyOutsidePublicArtifacts and three in LocalAppSmokeTest: testBuildStaticCommandReportsProgressAndArtifactSummary and testSharedStaticRefreshCommandKeepsDetailArtifactsWithoutRebuildingTheReadModel (both expect a stale hard-coded "/20" shared-page total, which the browse pages change further), and testSqliteViewerIncludesPresetReadOnlyQueryContract.

      • Two did not reproduce when run alone: LegacyTimestampImportTest::testLegacyBootstrapPublishesAndDatesSurviveRebuildAndAnotherArchive (the build had detected a planning doc being edited mid-run) and LocalAppSmokeTest::testStartAcceptsPortShorthand (timing under load). Both passed.

  • Departures from the reviewer brief:

    1. Static host, no JavaScript, search text: shows a notice and the browsable filters instead of results. Every browse, filter and page URL is prebuilt. Search text can't be prebuilt, and serving it live would bypass the release snapshot (Step 1, Option B).

    2. No results sidebar or wider page: the shared 760px .shell was kept so Board and the rest of the site are unaffected. Filters stay in a "Filters" <details> at every width. The concept blurbs stack, because three 18rem columns never fit. Single-guide topics still pair up.

    3. Diagram: HTML/CSS ordered list instead of an SVG with <title>/<desc>. The visible text is the text equivalent, it stacks instead of shrinking, and it stays vertical inside the 760px shell.

    4. Curation: kept in the existing single list in PlatformDocsCatalog::entries() rather than front matter or a new file. CLI Reference moved after Trust and Identity so the paired topics read in source order.

    5. Folder filter: a nested <details> tree with an "All in …" link per branch (a summary can't contain a link). No combobox.

    6. Choosing a filter replaces other filters (keeping the search). Combined-filter URLs still work, but filter links don't build them, so each is a prebuildable page.

    7. Counts are library-wide, not narrowed by the current search, so prebuilt and live pages match.

    8. Skip link: not added. It changes shared layout on every page, so it's deferred to todo.txt at the user's request.

    9. Theme fixes limited to tokens that only gained contrast (arena --ink-soft, mitrapclub --active-bg). qdb's site-wide link color (2.91:1) and --line below 3:1 in 14 themes are reported, not changed.

    10. The docs focus ring overrides themes' own focus styles on this page (!important), at the cost of theme fidelity.

    11. Results heading is "Documents" (an h2 under the shared H1). The live count reuses the server's role="status" element.

    12. The live count is 1,321 (it grows with this cycle's own planning files), not the brief's 1,315. It is never hard-coded.

  • Notes:

    • Not run: production deployment (out of scope) and screen-reader software. Accessible names, landmarks, heading order and the live region are checked structurally only.

    • Static no-JS search can't prefill the reader's query in the shell, because the static page doesn't see it. The notice explains this.