previews · Changelog

Changelog

Platform and tooling changes to the html-previews app itself — hosting and auth, the comment / reaction / approval system, search, the design system, and the cross-preview conventions.

This is not a log of content — the artifacts the platform hosts, *including generated ones*. Publishing a preview (an RCA, an architecture doc, a sprint report, the service catalog, a code-analysis dashboard) does not get an entry here. The test: would every preview benefit (platform), or is it one more thing published on the site (content)? See AGENTS.md for when to add an entry.

24 Aug 2026

  • Comment/reaction markers stop covering neighbouring blocks in horizontal layouts. Markers stay in the right gutter and always visible, as before — but a block laid out horizontally (a .stat in a row, a .twocol / .stage-split panel, a grid tile) used to hang its marker off its own right edge, which sits mid-page, so the marker landed on top of the neighbour to its right. Such a block now hangs its marker off the right edge of its whole row instead (found by climbing to the horizontal-layout ancestor), so it clears every sibling and reaches the true gutter; when several blocks in one row have threads, their markers stack vertically there rather than piling on one spot. Full-width blocks (paragraphs, headings) are unchanged. The hover affordance for starting a thread follows the same rule (rowRightFor / stackTop in assets/comments.js).

19 Aug 2026

  • Multi-PR stacks post to #pr-feed again. Since 2026-08-09 the feed replaced each stack's per-line boost link with a boost button per target, but gave every button the same action_id. Slack requires action_id to be unique within an actions block, so any stack with more than one open target was rejected with invalid_blocks and posted nothing — while single-target stacks kept working, so the breakage looked intermittent. The failed post also rolled back its own feed record each tick, leaving no trace until the error was logged. Each boost button now keys its action_id by target (boost:<key>); the button's routing data was always in its value, so the handler matches on the boost prefix and nothing else changes.
  • A tech plan leaves Draft automatically when someone else comments on it. Until now a plan moved out of Draft only when its author clicked "Ready for review". Now the first comment by anyone other than the author does the same thing: it writes the same ready.json the button writes, so the widget's state pill moves out of Draft and the plan joins the #pr-feed review channel — a draft people are already reviewing is, in effect, ready for review. Scoped to architecture pages (the tech-plan category), triggered only by a commenter who is not the page's declared author, and write-once so later comments never re-stamp the ready moment (which would reset the feed's freshness window). Runs as a background post-effect on the comment, so it never delays the write.

13 Aug 2026

  • The generated indexes stopped being a merge conflict, and the root page's counts stopped being hand-maintained. search-index.json and each <category>/index.json are rewritten whole, so any two branches that each added a preview collided on them, and the root page's card counts (91 docs, 5 squads) collided a third time. Every count on / now renders at load from a counts map that build-index.mjs --root writes alongside the titles — counting listing entries, not the recursive page walk, so a preview's nested companion pages don't inflate its category — which means adding a preview no longer edits /index.html at all. The two JSON indexes route through a merge driver that regenerates instead of reconciling lines, but a driver alone is not enough and quietly gets it wrong: git updates the working tree in path order, so mid-merge it cannot see the previews arriving from the other side. So .githooks/post-merge and post-rewrite rebuild against the finished tree and stage the result, and CI now runs build-index.mjs --all (not just --root) and bot-commits any drift, so main self-heals even from a clone with nothing wired. Git will not read repo-supplied config or hooks on its own, so build-index.mjs calls scripts/ensure-git-config.mjs to opt the clone in on first run and say that it did.
  • The site has a tab icon, and it animates. An indigo page with an amber render pass sweeping down it, in the house palette. /favicon.ico is generated from assets/favicon.js by node scripts/build-favicon.mjs, so the drawing code is the only definition of the mark; because browsers request the origin root icon for any page that doesn't name one, that single file covers all 169 previews — the generated dashboards included — with no per-page tag and no generator edits. The sweeping version comes from the script, which assets/comments.js loads lazily, so every preview carrying the comment widget animates without a tag of its own. It paints one still frame under prefers-reduced-motion and pauses while the tab is hidden.

11 Aug 2026

  • MDX pages can render their Contents as a sticky sidebar. toc: sidebar in frontmatter moves the h2 Contents list from an inline block after the TL;DR to a sticky rail beside <main>, in the style of the hand-authored review pages. The rail collapses to a static block above the content under 900px, and a page with fewer than three sections falls back to the inline list. toc: off and the default inline behaviour are unchanged.

10 Aug 2026

  • Narrated explainers have shared tooling, and their sources survive. explainers/<topic>/ is a published directory, so it holds exactly one file: the built index.html, the same shape as every other explainer. Everything needed to rebuild it lives outside the served tree in scripts/explainer/ — the engine template (versioned like plan-1.css), the builder, the mascot and background track, and per video a videos/<topic>/script.py (its META + SCENES + BEATS) beside its stills/. Sources in the served tree would be publicly fetchable and the stills delivered twice, once as files and once embedded. The first two explainers committed only that built page — which serves perfectly, since narration, music and stills are all data: URIs and the page needs no other file, but it left no way to fix a single word without rewriting the video from scratch. Build with python3 scripts/explainer/build.py <topic>: it synthesises narration (cached by a hash of the spoken text, gitignored, so only changed lines cost anything), inlines every asset, and derives the chapter count and runtime from the beats rather than trusting hand-written strings, which had already gone stale twice while a cut was still changing. Four engine fixes came out of building the second video: move can set h and height animates, so a still can zoom rather than stretch; a zoomed still takes z-index: 3 because every node was z-index: 2 and siblings later in the DOM painted over it; hide now beats dim instead of losing to it and leaving ghosts at 0.2 opacity; and a beat may carry cap, so a name spelled phonetically for the voice is not what readers see in the captions.

6 Aug 2026

  • Site-wide title search on the root page. / now has an instant, keyboard-accessible search bar for finding any published preview by its HTML title while preserving the existing collection navigation when the query is empty. node scripts/build-index.mjs --root deterministically regenerates the compact search-index.json from every visible preview under the collections linked by the root page; root collection pages, the changelog, and pages marked preview:hidden stay out of results. On pushes to main, the page-version capture workflow regenerates a stale index, bot-commits it, and captures versions against the resulting commit.

5 Aug 2026

  • Concurrent sessions in one directory no longer steal each other's project. ~/.claude/intent/active.json held one row per working directory and intent-doc activate evicted by directory, so two Claude Code sessions in the same repo could not hold two projects: the last activation won, and *every* session in that directory then resolved to it — the wrong capture document, and requests and steer gates attributed to another project. Rows are now keyed on (cwd, session) using CLAUDE_CODE_SESSION_ID (the same id hooks receive as session_id), and resolution ranks ownership before path depth: this session's own row, then a row with no session, then another session's — the last still eligible, because a fresh session in a directory an earlier one registered must still find the project. nearestProject() takes an optional session (default behaviour unchanged), capture.mjs mirrors the ladder, and session-track.mjs passes its session id, which is the part that fixes request ownership. deactivate now removes only this session's row unless given --all-sessions, and status shows the session per row so rows left behind by ended sessions are visible — nothing detects a session ending, so they are surfaced rather than auto-pruned. INTENT_ACTIVE_JSON also overrides the registry for cli.mjs and capture.mjs, which is what makes the write side and the hook testable at all. Tests 123 → 128.
  • Intent docs authored from a second clone now register in the hub. intent-doc new / activate derived the hub key from the doc's path *relative to the checkout the CLI lives in*, so a doc in a second clone (e.g. html-previews-main, the tracked main-branch copy where intent docs are actually written) produced a ..-prefixed path, skipped registration, and left the project missing from the operator's Projects hub — with sessions in that cwd attributed to whichever project was registered before. It now falls back to locating the category segment inside the absolute path, which is what the hub key is derived from anyway. A path with no recognisable category still refuses, but now says what it could not derive rather than implying the doc was merely misplaced.
  • Intent documents: levels now open with their obligations, and per-level word targets are gone. Two changes from reviewing a document end to end. (1) template.html ships an obligations slot at the top of L1–L4 — product requirements at L1, system properties at L2, components-and-responsibilities at L3, a requirement named per section at L4 — as a bolded lead-in list rather than a table, because a level that states a design with nothing to check it against reads as a series of assertions. This mostly replaces the per-level scope line, now optional. (2) Per-level word-count targets are removed from the skill: a count cannot tell padding from load-bearing content, so it licensed bloat under the limit and forced bad cuts over it — length is now diagnosed by naming the habit causing it, with the only length bar being a 3-minute read on L0. The skill also gained rules that each fix an observed failure: headings name the question a section answers rather than its topic; a named technology is explained (with what it buys over the obvious alternative) before being relied on; proposal voice before execution starts; state the capability boundary and the design's weakest point; flag unilaterally-excluded scope; and draw failure paths and lifecycle states, not just flows. Plus framing answers now accept bold, *italic* and ` code `.
  • Intent documents: the three opening questions now open L0 instead of preceding it. The separate "The short version" section is gone; template.html renders the intent:framing region *inside* the L0 · Outcome card, under one heading. Two sections both answering "why does this exist" — one at framing altitude, one at L0 altitude — reliably drifted into restating each other, so a reader paid twice before reaching any design. The division of labour is now strict and stated in the skill: the three questions own problem / value / approach, the L0 body owns only measures, budget and risk, and the two together must read in under 3 minutes. The framing command and region are unchanged, so existing documents keep working and read --section framing still resolves; only the template and the authoring guidance changed. Supersedes the 2026-07-28 rule that put the block before L0.

3 Aug 2026

  • Root navigation split by activity — the single 12-card grid on / became two tiers: full cards under "In active use" (architecture, sprint reports, product newsletter, dashboards, research, audits, service catalog) and a compact name-plus-count row under "Smaller collections" for categories with a couple of entries or none yet (RCAs, visual recaps, explainers, PRDs, code analysis, post-mortems). A category graduates to a full card when it's genuinely in use, not pre-emptively.
  • New category: product-newsletter/ — a one-page digest of what shipped across the product squads (BOS, FOX, LEX), written for product stakeholders rather than engineers and human-edited before it goes out. Issues live at product-newsletter/issue-N-YYYY-MM-DD/ and start from the new copy-me starter assets/templates/newsletter-starter.html (plan-1 editorial system plus three page-local components: highlight cards with an amber "In practice" editor's note, grouped "also shipped" rows, and a per-squad "coming up" grid — promotion candidates for a shared newsletter-1.css once a second issue ships).

30 Jul 2026

  • intent/ ready for other people to run — a second full review, then the fixes, then a pre-merge audit from a clean clone. Setup: setup.mjs now installs the host dependencies the agent-side tools need (grading runs on your machine, not in the container — a fresh clone passed every check and then failed on the first grade), doctor *exercises* that path instead of only describing what Docker owns, the seeded decision frontier is neutral rather than one person's real altitudes, and a missing .env reports one failure instead of six. Removed: Slack notifications (the Requests UI shows the queue, the projects and the live sessions, so a DM said nothing new), the LISTEN/NOTIFY channel and its MCP registration, and the unattended worker — resuming a session that has already closed is now explicitly unsupported, documented in the README, SETUP.md and the steer skill rather than left to be discovered. ts-fsrs is the backend's only runtime dependency. Pacing is measured in minutes of the agent's *observed working time* rather than turn boundaries (a long turn with interruptions is one boundary, so dense work accrued nothing); the top bar shows per-kind Minutes buttons that each restart their own count; a switched-off kind is now *refused at the write* rather than discouraged in a skill; and concurrent sessions claim numbered slots so two cannot deliver the same owed request. Learning: spaced repetition schedules TOPICS, not requests (47 due dates had been written and never read), a hint rung sits between attempting and surrendering (answering after one caps at 3/4; revealing after one records 2, not 1), every request now carries a rubric written *with* the question and every practice steer the agent's actual decision — both withheld by the query until you answer — an optional confidence turns a grade into a calibration signal, and an exercise gets a code editor *plus* a text box per question (prose asks used to have to be typed into the code box). Session presence was rebuilt to store observations and derive state at read time, so a working claim decays instead of lying, /compact no longer strands a session as ended, and a mid-turn heartbeat covers work started by a background wake. Request briefs render real markdown (tables, headings, lists) via react-markdown. Tests 74 → 109.

27 Jul 2026

  • intent/ hardened for other people to run it — a full review (correctness, dead code, UI, setup, security) followed by fixes. Security: all three compose ports now bind 127.0.0.1 (they were on every interface, with a *committed* Postgres password for a superuser role — reachable from any machine on the same wifi), the password is generated into intent/.env by setup.mjs, scripts/serve-api.mjs no longer serves the working tree to the network, and api/projects.js / api/pages.js stopped authorizing on the client-supplied Host header (one shared api/_intent-auth.js, constant-time token compare). Correctness: every request write path now guards its source state and asserts a row changed (deciding a nonexistent gate used to report success; dismissing an open gate hid it while the agent stayed blocked; re-answering a graded request made it permanently ungradeable), and the Stop hook no longer marks a steer resumed *before* delivering the decision. Data loss: saving your Learning-setup profile no longer wipes codebase_level / last_reviewed, and typing your email no longer blanks the queue or discards unsaved edits. Fresh installs: the seed is per-operator (it hardcoded one person), Projects links resolve locally instead of 302-ing to Google sign-in, and doctor now checks all five hooks, the migration level, and that the served page is the React build. Dead code: the pre-React front end, the never-read pedagogy-rating feature, three unused catch-all queries and two unused components deleted; 15 API handlers now share one route() wrapper. Tests went 41 → 74, covering the request state machine, session presence and the SQL escaper — the three modules where all of the above lived.
  • Requests UI rebuilt on React + shadcn/ui (intent/ui/) — the hand-rolled single-file queue page is replaced by a Vite + React + Tailwind + shadcn/ui app (operator decision, steer #56), ported with feature parity: queue with pacing sliders/toggles, unseen-feedback tracking, resizable rail, multi-question steer forms, skip/reveal, thread with mermaid rendering, plus the Projects/Pages/History/Learning-setup views. The learner picker is now a real searchable combobox (keyboard + a11y handling the hand-rolled one lacked). The warm-beige palette and the five-style type system carry over as design tokens. The build runs inside the Docker image (multi-stage; server.ts serves ui/dist and refuses to start without a build — the legacy page is gone); hot-reload dev loop via npm run dev in intent/ui/ — see "Working on the UI" in intent/SETUP.md.
  • The intent-layer runtime moved in-repo at intent/ — the requests queue + learner-model backend, queue UI, requests-check Stop hook, agent tools/skills, and its Docker stack (formerly the standalone local requests-ui repo) now live at intent/, so one clone carries the whole intent-layer system. Local-Docker-only: a new .vercelignore excludes it from every deploy. One-command cold start (node intent/setup.mjs: preflight with named fixes, .env generation, compose up on ports 3210/3211/5433, migrations + seed, both Stop hooks installed with stale-path repair, MCP channel registration, health checks; doctor subcommand for read-only status) with the teammate guide at intent/SETUP.md. Kept one-git mv-separable: own package/tests/docs, html-previews coupling is HTTP-only via api/projects.js + the shared INTENT_API_TOKEN.

24 Jul 2026

  • MDX compiler: spacing before the Contents list — the auto-injected TOC now carries a top margin, so it no longer sits flush against the TL;DR box; the compiler also injects the TOC before feature-detecting page CSS, so TOC-conditional styles actually ship.
  • MDX compiler: cache-busted comments widget — compiled pages reference /assets/comments.js?v=<content-hash>, so browsers pick up widget changes on a plain reload instead of heuristically caching the old script for days.
  • Component cards are commentable — the comments widget now anchors threads to component cards, not just prose tags: any element with a data-commentable attribute, plus the shared card-shaped house classes (.tldr, .stat, .step, .panel, .card, .plane, .decision, .narrative). Cards anchor as one block and only as leaves (a card containing <p>s defers to them), so existing pages keep their granularity and nothing double-anchors. Bespoke page components (merge maps, phase cards, stat tiles) opt in per row/tile with the attribute — see AGENTS.md "Making component cards commentable".

21 Jul 2026

  • Slide-deck engine (deck-1) — previews can be presentation decks — a new versioned shared asset (assets/players/deck-1.css / deck-1.js, same conventions as walkthrough-1) turns a page of <section class="deck-slide"> blocks into a full-screen presentation: a fixed 1600×900 stage scaled to fit any viewport (letterboxed on black), Arrow/Space/PageUp/PageDown/Home/End + click-thirds navigation, per-keypress data-frag bullet builds, #/N deep links, an injected progress bar and slide counter, and a media lifecycle — <video>/<audio> pause when their slide is left, <video data-autoplay> restarts muted on slide entry, and <iframe data-deck-embed> has its src stashed on exit and restored on return so embedded players (e.g. Loom) go silent when the presenter moves on. Clicks on players never navigate; with JS off the slides stack and scroll. First consumer: the LEX Sprint 9 review deck (sprint-reports/lawrence-experience/sprint-9-2026-07-22/), which replaces the HTML mirror-report format with a presentable deck on the Lawhive master-deck brand (Rubik/Inter, black canvas, peach ramp).

17 Jul 2026

  • House hero — crumbs moved inside, glow reaches the top of the page — the breadcrumbs now render inside the hero's content column, left-aligned with the eyebrow and title just above them (normal weight), instead of as a separate centered bar above the hero. The hero's tint and drifting glow now fill the whole top region — its overflow: hidden clip lands on the invisible page edge rather than slicing a hard line across the top blob. Fixed in assets/plan-1.css + the MDX compiler (scripts/compile-mdx.mjs) and the plan-starter / walkthrough-demo templates, then backfilled across the existing house-style pages so every plan-1 preview matches.
  • MDX authoring pipeline — write previews as prose, compile to house HTML.mdx pages compile to the same static plan-1 HTML as hand-written ones (scripts/compile-mdx.mjs). Source and compiled HTML are committed together, so the deploy story is unchanged: Vercel still serves static files. Live capability demo + adoption guide: architecture/html-previews-from-mdx-2026-07-17/.
  • MDX: the writing surface — GFM (tables → .tablewrap, task lists, footnotes); compile-time KaTeX (new vendored assets/vendor/katex-0.17.0/, CSS+fonts only); Shiki-highlighted code; Mermaid fences; Vocs-style directives (:::note family → accented panels, ::::steps[vertical] → step strip, ## Title [subtext]); house components (<TLDR>, <PullQuote>, <Cols>, <StepStrip>, <Figure>, <Chart>); auto reading time and Contents.
  • MDX: charts are plain Reactcomponents/<name>/index.jsx next to the page imports @mui/x-charts / @visx/* directly; esbuild bundles at build time and everything server-renders to static HTML. Pages ship no new runtime JS.
  • MDX: comments auto-wired — the discussion widget mounts with a path-derived data-doc (comments: off opts out; plugins: walkthrough pulls the step-through player).
  • MDX: workflowpnpm dev runs the static server plus a repo-wide watcher with self-reloading pages (Ctrl-C rewrites every page clean before exit); pnpm mdx builds all pages and exits non-zero on any compile error.

15 Jul 2026

  • Notification digests + mutes — a burst is one ping — Slack notifications no longer ping per event: the first event DMs you normally, and further activity on the same page within a 15-minute moving window *edits that DM in place* into a growing "*N updates on <page>*" digest (Slack doesn't re-notify on edits), so a lively review session lands as a single ping with full detail. You can also mute a page (🔔 bell in the widget's top bar) or mute one discussion (bell in the thread popover) — both toggles, per reviewer, stored as presence blobs like reactions. @-mentions cut through everything: always a fresh, immediate ping, muted or not. Prompted by review feedback asking for aggregation over per-comment pings. New mutePage/muteThread actions and a notify/<doc>/<who> digest-state blob in api/comments.js; chat.update support in api/_slack.js; vitest coverage in test/notifications.test.mjs (97 total).
  • Slack DMs for replies, page authors, approvals, and comment reactions — the discussion widget's Slack notifications now cover the whole review loop, Notion-style, instead of only explicit @-mentions: a new comment DMs everyone who commented earlier in the same thread ("replied in a discussion you're in") and the page's author ("commented on"), an approval DMs the page author, and an added emoji reaction on a comment DMs that comment's author. Each event DMs a person at most once (mention > thread reply > page author), never the actor, and block reactions stay silent. The page author is declared with a new <meta name="preview:author-email"> tag — required on new pages (see AGENTS.md) and stamped onto 53 existing discussion-enabled pages from their git creation history by scripts/backfill-author-emails.mjs. Same delivery path as mention DMs (background post-effect, best-effort, time-boxed; only verified sign-ins are reachable). Vitest coverage in test/notifications.test.mjs.

14 Jul 2026

  • Dashboards category + live-data pattern — a new top-level dashboards/ category for hand-built dashboards that recompute from live data on view, distinct from the generated code-analysis/ charts. Establishes a reusable shape: a Vercel function (api/pr-coins) computes and caches a snapshot in Blob (15-min TTL), and the page ships a baked snapshot so it renders fully with JS off, then fetches for fresh numbers. A second sanctioned exception to "no runtime fetch", alongside comments. Needs a GITHUB_TOKEN env var (read-only) for the GraphQL pulls.

13 Jul 2026

  • Review watermark — entry-point pill for first-time viewers — the watermark diffs the live page against *your last visit on this device*, so a reviewer with no prior visit (no stored baseline) previously saw nothing at all — not even a way to opt in, because the widget bailed before drawing any chrome when there was no baseline. Now, when you're signed in and the page has at least one captured version, a first-time visit shows the collapsed "Changes" pill; clicking it opens the "changes since ▾" picker so you can compare against a specific captured version without first accruing a visit. Signed-out visitors and pages with no captured versions still show nothing (no noise). Headless coverage in scripts/verify-version-watermark.mjs (24 checks). See AGENTS.md.

10 Jul 2026

  • Watermark picker persists at zero changes; data-hpc-id authoring guidance — the "changes since ▾" picker no longer vanishes when you pick a baseline with no changes: the panel stays with a "No changes" state and a ✕ to reset, so you can switch baselines freely. Documented in AGENTS.md how to tag preview blocks with a stable data-hpc-id for precise typed diffs (and backfilled them on the intent-layer system doc).
  • Watermark baseline: 8-hour sessions + default-diff fix — the "last visit" baseline now freezes for an 8-hour session of inactivity (a refresh or re-open keeps the same diff; after an 8h gap the next visit becomes a new session and the baseline advances to the version you last saw), replacing the tab-session freeze. Also fixed an ordering bug where "seen" advanced *before* the baseline was frozen, which could make the default "changes since your last visit" view compare a version against itself and show nothing. Headless coverage in scripts/verify-version-watermark.mjs (12 checks).
  • Review watermark is now version-based (git-backed), not a per-user snapshot — "changed since you last saw this" is computed against a document version (a git commit) instead of a snapshot each reviewer had to capture by clicking "Mark as reviewed". On load the widget resolves a baseline commit — your last visit by default (per-device, frozen for the tab-session so a refresh keeps the same diff), overridable with a "changes since ▾" picker (last visit / last review / a specific version, persisted in the URL) — fetches the page as it was at that commit from a new api/version endpoint, and diffs it against the live page with the existing typed add/changed/removed + word-diff UI. Every reviewer gets this automatically, with no snapshot to seed. Per-block **✓ now *dismisses* a change (one blob per reviewer per block — race-free, presence-pattern like reactions — hidden until that block changes again); "Mark as reviewed" / approving records a tiny review pointer ({reviewedCommit}) and clears your dismissals; the button reads "✓ Reviewed" until the page changes. Old versions are captured at deploy time by a GitHub Action (.github/workflows/capture-versions.ymlscripts/capture-versions.mjs) that snapshots each changed preview's HTML into the private blob store keyed by commit — so the runtime never calls GitHub** (no token, no rate limit; the Action uses its own repo-scoped token + BLOB_READ_WRITE_TOKEN). This replaces the per-reviewer content snapshot entirely (no more duplicate content keyed by user, no last-write-wins on acks). Server rework in api/comments.js (markReviewed/dismiss/undismiss actions, per-user reviewed+dismissals folded into the thread), new api/version.js, and a headless check in scripts/verify-version-watermark.mjs. Known limitation: pages whose content is generated by JavaScript on load diff approximately (the captured HTML is pre-JS); static previews are exact. See AGENTS.md.
  • Comments survive edits, not just moves — via a real anchoring library — a comment was anchored to a content hash of its block, so editing the block's text changed the hash and orphaned the thread (it dropped to the "on a previous version" panel). The widget now re-locates an affected thread onto the live, edited block using a vendored fuzzy text-anchoring library — Hypothesis dom-anchor-text-quote (W3C TextQuoteSelector matching via diff-match-patch), the same anchoring Hypothes.is uses — matching the text we already snapshot per comment (a highlight's quoted text, or a block comment's paragraph), so it re-finds the passage even after the surrounding words changed. It works retroactively on every comment ever made (no storage change, no migration) and replaces the bespoke exact-substring findQuoteRange; only a near-total rewrite defeats the match, which still falls back to the previous-version panel. The library can't come from package.json (the org CodeArtifact registry doesn't proxy it), so scripts/vendor-text-anchor.mjs fetches the pinned version from public npm and bundles it (deps inlined) to assets/vendor/dom-anchor-text-quote-4.0.2.js, which assets/comments.js loads lazily — no per-page tag. Headless check in scripts/verify-comment-anchor.mjs proves an edited-block comment re-anchors with the library and orphans without it. See AGENTS.md.
  • Review watermark — "changed since you last reviewed" — the discussion widget now remembers, per signed-in reviewer, what you had seen when you last reviewed a page, and on your next visit gives changed/added blocks an amber left edge and a floating panel (top-right, collapsible to a pill) listing every change with jump-to links. You set the watermark with a "Mark as reviewed" button in the approvals strip (reviewing is enough — no signature), or when you sign a formal approval. The snapshot is one more private blob per reviewer in the existing comment backend (comments/<doc>/w_<who>.json); your own watermark is also cached in localStorage so it takes effect immediately despite Blob's read-lag. Two detection modes: when the page tags blocks with a stable data-hpc-id, changes are typed precisely — added / changed / removed (deletions detected on their own, no comment needed) — the word-level diff is rendered in the document text in one of two toggleable, persisted styles — inline (struck deletions + highlighted insertions) or before/after (the block shown twice: a "was" line with removals in red, a "now" line with additions in green) — while the panel stays a compact index (type + short label) where each change can be commented on (💬, including removed blocks) or acknowledged individually with ✓, or all at once; a Hide toggle turns the highlighting off (persisted per doc). Pages without ids fall back to flat content-hash detection (changed/added lumped as "changed"; deletions surface via orphaned comments). Works on any preview loading assets/comments.js. Vitest (test/watermark.test.mjs) pins the store: per-reviewer single-blob overwrite, the block-id map + snippets, id/hash shape-checks, approval capture, and the session gate. See AGENTS.md.

9 Jul 2026

  • Intent documents — the documentation layer of the intent-layer system (M1) — a new document type plus the tooling to keep it alive during execution. scripts/intent-doc/template.html is a self-contained scaffold whose editable regions are delimited by <!-- intent:<region>:start/end --> comments; scripts/intent-doc/cli.mjs (dependency-free Node) only ever edits between those markers, so untouched blocks keep the content hashes assets/comments.js anchors comments to. The CLI (new · read · set-level · note · frontier · milestone · log-decision · gate · notify · activate) creates a doc, records intent decisions into an append-only log with OpenSpec-style delta badges, and sends a milestone Slack DM (local-print fallback with no token). A Stop hook (~/.claude/intent/hooks/capture.mjs) prompts the agent to classify+record or gate decisions, but only inside a registered intent project (intent-doc activate), and anti-loops via stop_hook_active. Authoring is driven by the intent-doc skill (successor to the removed html-tech-plan skill). See scripts/intent-doc/README.md and the intent-layer system plan. The change-since-review watermark (M2) and the learning/steer queue (M3, in the separate learning-ui repo) are not built yet.
  • Walkthrough player module — the animated left-pane/right-pane step player (Play/Prev/Next, progress dots, auto-advance) that three plans each hand-rolled is now a shared engine: assets/players/walkthrough-1.js + walkthrough-1.css. window.Walkthrough({ root, steps, renderRight }) owns timing, controls, dots, wrap/clamp, cumulative reveal, opt-in keyboard, and reduced-motion (starts paused); the page writes a steps array and one render callback. Derived from the three shipped players (faithful to their ~2.6–3s cadence), with a11y added. Live copy-me reference at assets/templates/walkthrough-demo.html. New-pages-only — existing plans keep their inline copies.
  • Shared plan scaffold — new plan/architecture pages now link versioned platform assets instead of copy-pasting the design system: assets/plan-1.css (tokens, hero, and every component rule already shared by 2+ terracotta pages — derived from the shipped pages, with provenance comments), assets/plan-1.js (lazy Mermaid in <details>, hash reveal, the planCopy export helper) and assets/templates/plan-starter.html (copy-me skeleton). Style changes ship as plan-2.css; published pages keep the version they were reviewed with; page-local components stay inline until a second page needs them. Existing plans are deliberately not retrofitted — the scaffold applies to new pages (validated against the shipped pages' extracted CSS and a non-shipped retrofit exercise). Also fixed the hero-grain spec in AGENTS.md (0.28 soft-light, as the canonical page actually ships — the doc had mis-transcribed it).
  • Mermaid consolidated on one npm-sourced version, with a render gate for upgradespackage.json is now the source of truth (mermaid devDependency); scripts/vendor-mermaid.mjs copies the installed dist bundle to assets/vendor/mermaid-<version>.min.js, exactly one version lives in the repo, and all nine Mermaid pages point at 11.16.0. scripts/verify-mermaid-render.mjs renders every diagram headlessly with the old and new bundle and pixel-compares them: across the consolidation, 44 of 46 diagrams were identical; the two that re-laid-out (both flowcharts in forms-service-unified-ingestion) were reviewed by eye. The old extracted mermaid-11.15.0.min.js is deleted.
  • Vendored, versioned libraries — the self-contained rule is now "content is self-contained; third-party libraries are vendored once under /assets/vendor/ with the version in the filename" (a shipped page keeps the exact library version it was reviewed with; assets/comments.js stays the one deliberately mutable shared widget). Applied in the same stack: mermaid-11.15.0.min.js and mermaid-11.16.0.min.js now serve the nine plans that each inlined a ~3 MB Mermaid bundle (each page pins the byte-identical bundle it shipped with), and skills-feature's nine base64 screenshots moved to files in its folder — every plan is now 46–134 KB and agent-readable in one pass (~15K tokens instead of ~900K), with visible body text unchanged so existing comment anchors are unaffected. Also codified when to diagram with Mermaid vs hand-authored SVG (Mermaid for structure — ~15× cheaper to write, read, and keep true on drift passes; SVG for product mockups, meaningful spatial layouts, and JS-driven figures). See AGENTS.md.

8 Jul 2026

  • Security tests for the identity layer — a Vitest suite (npm test) pins the guarantees behind Google sign-in and the session cookie: HMAC sign/verify, rejection of tampered/expired/foreign-secret (forged) sessions, the Google token gates (audience, issuer, verified email, Lawhive domain), the HttpOnly/SameSite/Secure cookie flags, and that a comment's author is always taken from the verified session — never the request body — so nobody can post as someone else. Run with npm test.

5 Jul 2026

  • Slack DM notifications for @-mentions — tagging someone in a comment now DMs them in Slack (skipping the commenter): their verified email is resolved to a Slack user, the DM is opened, and a message with the comment excerpt and a link back is posted. The comment is written and the response returned first; the DMs are sent afterwards as a background post-effect (waitUntil), so notifications never delay a comment. Best-effort and time-boxed; edits notify only newly-added mentions. The DM's link-back always uses the requesting deployment's own host (from the request, never the client-sent URL), so a signed-in user can't aim the bot at a phishing link and it works on every preview with no host allowlist to maintain. Requires the bot scopes im:write and chat:write (on top of users:read/users:read.email from the directory). Vitest tests pin the notify path: a DM per mentioned colleague (never the commenter), the response returning before delivery, failures swallowed, the link-back taking its host from the request (a phishing URL can't redirect it), and edits notifying only the newly-added.

2026-07-04 (2)

  • @-mention tagging in comments — typing @ in any composer opens an autocomplete of taggable colleagues and inserts a rich, atomic mention chip; the comment stores the mentioned {email, name} and renders the mention highlighted. The taggable directory is sourced from the Slack workspace (users.list via a shared api/_slack.js, reusing the link-unfurl bot token), blob-cached (directory/slack.json, 30-min TTL) with stale-while-revalidate; the sign-in roster is the fallback. Everyone taggable is by definition reachable by DM. Needs the bot scopes users:read and users:read.email. Covered by Vitest tests for the Slack client (dmByEmail sequence, users.list pagination and filtering), the directory endpoint (session gate, blob cache with stale-while-revalidate, roster fallback) and mention sanitisation/storage. Builds on the verified-identity change below.

4 Jul 2026

  • Verified identity via Google sign-in — the discussion widget no longer asks reviewers to type their name. The top-right chip is now "Sign in with Google"; signing in with a Lawhive Workspace account attaches a *verified* name, email, and avatar to every comment, reaction, and approval. A new api/auth function verifies the Google ID token (domain-restricted to lawhive.co.uk, email verified) and issues an HMAC-signed session cookie (api/_session.js); api/comments now takes the author from that cookie instead of trusting the request body, so nobody can post or approve as someone else. Items are keyed by verified email. Secrets (GOOGLE_OAUTH_CLIENT_ID, SESSION_SECRET) come from Doppler; run locally with npm run dev (doppler run -- vercel dev) — see AGENTS.md. This lays the groundwork for @-mentions and notifications. (Comments written before this change keep their typed name and will be migrated.)

3 Jul 2026

  • Fixed duplicate comments from the inline editor — with an edit open, the popover composer was wired to the edit textarea (the popover's first <textarea>), so Cmd/Ctrl+Enter or the send button posted the draft as a brand-new comment instead of (or as well as) saving the edit. Also guarded the panel reply and video composer against double-submit (assets/comments.js).
  • Deterministic visual recap helpers — agents can now collect PR facts, fetch full source by PR head SHA, render Shiki-highlighted code excerpts, and validate recap pages with scripts/visual-recaps/*.
  • Visual recaps category and content contract — PR, Graphite stack, and stack-subset review artifacts now have a first-class visual-recaps/ home plus scope-specific generation guidance and product-facing copy rules in docs/content-types/visual-recaps.md.

29 Jun 2026

  • Slack link previews — pasting any preview link in Slack now unfurls a branded card — title, summary, and a per-doc image on the warm gradient — instead of a blank Vercel placeholder. A Slack app calls a webhook (api/slack-events) that reads the SSO-gated page server-side and renders the card with @vercel/og; card images are served from a separate public Blob store, so the documents themselves stay private.

23 Jun 2026

  • Discussions panel — a top-right button opens a Notion-style index of every thread on the page; click a current thread to jump to it and open its popover.
  • Comments survive edits — a comment whose text has been edited away no longer disappears. It moves to the panel under "On a previous version", showing the paragraph it was left on and a link to the commit it was made against. New comments snapshot their containing paragraph and are stamped with the deployed commit (assets/comments.js, api/comments.js).
  • Comment on code, tables and more — the commentable surface now covers code blocks (<pre>), table cells, all heading levels and captions, not just paragraphs and h2/h3. Selecting text inside a code block also makes line- and token-precise comments work there. Mermaid diagrams are deliberately left out, pending a pin-based model.

18 Jun 2026

  • Comments widget: no more horizontal scroll on mobile — gutter comment markers (and the floating chrome) could be placed past a phone's right edge; they're now clamped to the viewport.
  • Changelog pageCHANGELOG.md now renders as a styled page at /changelog/, linked from the site footer. Generated by scripts/build-changelog.mjs.
  • Architecture page: full-text search + grouped grid. The category page is now data-driven — instant client-side search over each doc's title, summary and body, with results ranked and matched terms highlighted, plus a card grid grouped by week (toggle to month). Generated by scripts/build-index.mjsarchitecture/index.json; counts no longer drift from a hand-maintained list.

12 Jun 2026

  • "Copy for Claude Code" button on plan pages — one click to copy a page reference (with its pulled discussion) back into a Claude Code session.
  • Research category added for competitive landscapes and external grounding.

11 Jun 2026

  • Discussion presence and editing — viewer facepile with idle dimming, per-comment emoji reactions, who-reacted-on-hover, edit-your-own-comment, text-selection highlight comments, and Cmd/Ctrl+Enter to send.
  • scripts/pull-comments.mjs — pull a preview's discussion (comments, reactions, approvals, viewers) straight from the Blob store, bypassing the Vercel auth gate, for agents and scripts.
  • House style and enforced design system codified in AGENTS.md — the blog-post standard, design tokens, fonts, gradient, and motion.

10 Jun 2026

  • Persistent discussions — Notion-style margin comments anchored to blocks, per-block emoji reactions, and DocuSign-style signature approvals, stored one-blob-per-item in Vercel Blob behind the auth gate (api/comments.js, assets/comments.js).

27 May 2026

  • Multi-layer navigation rule documented, with a fix for stale category counts across nav levels.

12 May 2026

  • Typed categories — previews reorganised into rcas/, architecture/, sprint-reports/, etc.; folder path becomes the URL.
  • AGENTS.md authoring guide — use cases, FAQ, and the contract for generating previews that match the house style.

11 May 2026

  • Initial platform — self-contained HTML previews, one folder per preview, path = URL, with the "why HTML over markdown" framing.
  • Hosting on Vercel (migrated from GitHub Pages), gated by Lawhive team authentication.