commit 364b817116a80fa370785e64005377e27c7e0d21
parent 5fa4dd3bf8b750facdf32fddce173c0495082631
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 17:22:28 -0400
plans: slice CK, as shipped — a search reads what the visitor ticked; FACTS "Search in"; the export changelog
The record: the ruling's "today" was not the code (a plain query read no
posts on main), and the slice built the ruling, so a plain query now reads
posts where a site ships them — one flag takes it back. Mechanism (a), the
rewrite and the fold; notr and lc; nop threaded where it was not; the
refusal; the badge; the fixture's posts' own words; gates (tsc, common
2,372, mcp 271, export build, e2e: the full export suite 265/265, the hub
36/36, the editor's export-search 19/19); found and left; decisions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 248 insertions(+), 0 deletions(-)
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **A search reads what the visitor ticks under "Search in": Transcripts, Posts and Live chat.** The Filters panel has a new row, **Search in**, beside Type. **Transcripts** and **Posts** are ticked by default and **Live chat** is not; Posts is offered only on a site that has posts, and Live chat only on a site with live chat. The row decides what a plain query reads: with Posts ticked, a plain query now finds posts as well as videos (before, a post was found only by a layer whose scope was "Posts"); with Live chat ticked, it finds live-chat messages too, shown in the same video's card beside the transcript hits, each marked "live chat"; with Transcripts unticked it reads no transcripts. A layer whose scope is picked by name in the query builder ("Live chat", "Posts", "Title / channel", …) reads what it names, whatever the row says. An empty query still lists every video the Type row keeps. With nothing ticked, Search and Apply filters are disabled and the row says "Search in: pick at least one". The Posts box moved here from the Type row. The row is remembered, and saved with a profile; a shared link does not carry it, so it opens with the reader's own row. A live-chat hit now wears its "live chat" badge wherever it is shown, and the hint under the search bar says to tick Live chat under Search in. Posts unticked is now also remembered after a reload and restored with a profile, which it was not. Needs a rebuild and deploy of each site and the hub.
- **The charts count every transcript, once the site is rebuilt.** A transcript that arrived after its video was first indexed was missing from the charts' transcript and cue counts and from "Transcribed over time", and a video with YouTube captions alone had no transcription date. Both are counted now, and a captioned video is dated by when its captions arrived.
- **A social icon that fails the check is shown as its label, and every icon paints inside its box.** The footer inlines a social link's SVG only if it passes the same check a save runs (what an icon may contain is in `SITE.md`); otherwise the link shows its label as text, at most 10rem with an ellipsis. Each icon is clipped to its own box. Needs a rebuild and deploy of each site.
- **A chart's stacked bars are separated by a 2 px gap in the chart card's colour.** A stacked bar's segments were drawn touching; they now have a 2 px gap in the card's colour between them, and in high-contrast mode the system's background colour. Stacked areas keep their line in each series' colour along the top, charts of one series, line charts and side-by-side bars are unchanged. Needs a rebuild and deploy of each site.
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -7876,3 +7876,47 @@ source mirror (homepage)". Anchors are at the branch.
without it the manifest comes back without the block). Its e2e reads a fixture publish from
`E2E_SOURCE_PUBLIC_DIR` while it holds a manifest (`sourcePublicDir`, `:69`; never in a production
build).
+
+## Search in (verified 2026-09-30, branch `r16/search-in`)
+
+- **The row governs "transcripts"-scope leaves only.** The Filters panel's "Search in" row
+ (`common/components/FiltersPanel.tsx:419`, `data-testid="search-in-row"`) says what a leaf of
+ scope `"transcripts"` reads — the default leaf, `newLeaf({scope: "transcripts"})` in
+ `emptyRoot()` (`common/lib/searchQuery.ts:86`), and every plain query. A leaf asked for by
+ name ("Live chat", "Posts", "Title / channel", "Description", "Keywords") is not the row's
+ business. The browse listing (an empty query) is the Type row's.
+- **The mechanism is a rewrite of the committed tree just before it runs**, never of the tree
+ itself: `applySearchIn(root, {transcripts, posts, chat})` (`searchQuery.ts:534`) turns an active
+ transcripts leaf into itself (transcripts only), itself with another scope and the same id (one
+ other kind), or an OR group `<id>~in` over copies `<id>~transcripts` / `~posts` / `~chat` (two or
+ three kinds; a negated leaf is a negated one-child AND `<id>~not` around the OR). The session
+ (`SearchSessionContext.tsx:569`) runs the rewrite and folds the progress back with
+ `foldSearchIn` (`common/lib/search/searchIn.ts:18`; called at `SearchSessionContext.tsx:931`):
+ copies' hits are re-filed under the visitor's leaf id, their states folded into one (the count
+ is the OR group's union once every copy has started). `committedRoot`, its canonical hash and
+ `qt=` stay the visitor's tree. `lib/search/` has a seventh module for this, `searchIn.ts`.
+- **Posts, as read by a plain query, is new in release 16.** On `main` before it a transcripts
+ leaf never read posts: posts joined the global scope only when a posts leaf was in the tree
+ (`needsPostsManifests`), and a non-posts leaf subtracts the posts slugs
+ (`searchEval.ts:337`). `nop` still also gates the global scope
+ (`SearchSessionContext.tsx:873`), so an unticked Posts also empties a leaf of scope "Posts" —
+ as it always did.
+- **What counts as ticked is what the site can honour** (`SearchSessionContext.tsx:546`): Posts
+ only where `postsManifest.channels` is non-empty, Live chat only where
+ `subsManifest.liveChatTotalCount > 0` — the same conditions the panel offers the boxes on.
+ Nothing ticked (`draftSearchInEmpty`, `:564`) disables Search and `Apply filters`, and
+ `commitSearch` and the two profile saves refuse it.
+- **The keys:** `notr` (Transcripts unticked) and `lc` (Live chat ticked) beside `nop` in
+ `FilterSnapshot` (`common/components/exportFilterStorage.ts:37-39`), in the working snapshot and
+ the profiles of `ytdlp-tb:export-filters`, each written only off its default. **Share links do
+ not carry them** (nor `nop`): no share-v1 key, no `ShareSelection` field. The row is read from
+ the stored snapshot on hydration whatever the URL carries, so a `qt=` or share link reads with
+ the visitor's own row.
+- **The display:** every track hit wears its `TrackBadge`, live chat included
+ (`SearchResults.tsx:933`); a transcripts leaf's section is named for what it holds
+ (`sectionScope`, `:949`): "Posts" on a post's card, "Live chat" when every hit is a chat hit.
+- **Not governed by the row:** the charts' own search series (`components/charts/useSearchSeries.ts`)
+ and `/ask`'s retrieval (`export/app/lib/askRetrieval.ts`) build and run their own trees; the MCP
+ has no row.
+- **The export e2e fixture's posts have words of their own** (`kappa`, `sigma`, `omega` —
+ `export/e2e/fixtures/data.ts`, `postsPage`), so a plain "alpha" or "gamma" in a spec reads no post.
diff --git a/plans/release-16.md b/plans/release-16.md
@@ -40,3 +40,206 @@ slice's prompt carries its ruling, and this record carries what was built. Rules
- The hub's search gets the row through the shared components; nothing hub-specific.
## Record
+
+### Slice CK, as shipped — a search reads what the visitor ticked (2026-09-30)
+
+Branch `r16/search-in` off `main` `6c6dcd04`, worktree `~/Projects/plans-export-header-first-search`
+(editor 3401, test 3411, export 3410; the export suite's server on 3420), one Opus implementer.
+Scratch files `ck-*` in the job's `tmp`. The ruling is above ("Slice CK — the ruling").
+
+**The ruling's "today" was not the code; the slice built the ruling.** On `main` a plain query — a
+leaf of scope "transcripts" — read the transcript cues and nothing else. The Posts box in the Type
+row put the posts corpus into the global scope only when a leaf of scope "Posts" was in the tree
+(`needsPostsManifests`), and every other leaf subtracts the posts slugs (`searchEval.ts:337`), so a
+plain query never read a post. The row as ruled (Posts ticked by default, the row governs the
+transcripts leaf, the toggles say what a query reads) therefore makes a plain query read posts on a
+site that ships them: what a plain search returns there changes. Taking it back is one flag — `posts`
+false in the session's `committedSearchIn` and `draftSearchIn` — and the Posts box would then govern
+only a "Posts" leaf, as before.
+
+**What was built.**
+
+- **The row.** "Search in" beside Type in the Filters panel (`FiltersPanel.tsx`,
+ `data-testid="search-in-row"`): **Transcripts** (ticked), **Posts** (ticked; the box moved out of
+ the Type row with its key `nop`, its accessible name and its gate, `postsManifest.channels`
+ non-empty), **Live chat** (unticked; offered only when `subsManifest.liveChatTotalCount > 0`). The
+ Type row keeps Videos and Livestreams. The hub gets it through the shared components.
+- **Mechanism (a), the smaller: a rewrite of the committed tree just before it runs.**
+ `applySearchIn(root, {transcripts, posts, chat})` (`common/lib/searchQuery.ts`) turns each active
+ "transcripts" leaf into:
+ - the leaf itself, the same object, with Transcripts alone ticked (so the default row on a site
+ with neither posts nor live chat runs the very tree it was given);
+ - the leaf with the one other kind's scope, same id, same `negate`, with one other kind alone;
+ - an OR group `<id>~in` over copies `<id>~transcripts`, `~posts`, `~chat`, each with the leaf's
+ `contributeHits`, with two or three kinds. A negated leaf is NOT of the union: the OR sits inside
+ a negated one-child AND `<id>~not`, so the OR's own group state is the union whatever `negate`;
+ - with nothing ticked, the leaf unchanged (see the refusal below).
+
+ Posts are not left on the global-scope path, as the prompt proposed: that path reads posts only
+ for a "Posts" leaf, so the rewrite makes one (the posts copy) and the path then feeds it (`nop`
+ still gates the global scope). A leaf of any other scope, and an empty leaf, are untouched.
+ (b) would have run up to three pipelines per leaf inside `runLeaf`, each with its own cache key,
+ controller and streaming merge; (a) touches the evaluator not at all.
+- **The fold.** `foldSearchIn(progress, tree)` (`common/lib/search/searchIn.ts`, new) files the
+ copies' hits back under the visitor's leaf id (a chat hit keeps `scope: "chat"` and
+ `track: "live_chat"`, so it lands in the same section of the same video row, in time order), and
+ folds the copies' states into one: the count is the OR group's union once every copy has started
+ (before that, the evaluator reads a copy with no result as the whole scope, so the largest copy
+ stands in and the leaf shows as active), hits, processed and to-process summed, capped if any,
+ cached if all. Nothing else reads the copies' ids.
+- **The session** (`SearchSessionContext.tsx`). What counts as ticked is what the site can honour:
+ Posts only with a posts corpus, Live chat only with live chat — the panel's own conditions — so a
+ stored `lc` on a site without chat asks for no subs manifest (see "Found and left" for what a
+ "Live chat" leaf does there). The committed tree runs as `applySearchIn(committedRoot, …)` and its progress
+ is folded before `setTreeProgress`; `needsChatManifests` and `needsPostsManifests` read the
+ rewritten trees (draft and committed), so Live chat ticked loads the subs manifests and passes
+ `chatScopeSlugs` exactly as a "Live chat" leaf does (the prompt's `|| committedLiveChat`, but only
+ when there is a leaf to read it). The run and the hit-cap reset also depend on the rewritten tree's
+ hash, so a change of the row alone re-runs. `committedRoot`, its hash, `qt=`, the builder's leaves
+ and the result cards' sections stay the visitor's tree.
+- **State.** `notr` (Transcripts unticked) and `lc` (Live chat ticked) in `FilterSnapshot`, threaded
+ like `nop`: `parseSnapshot`, `snapshotsEqual`, the draft/committed pairs, `buildDraftSnapshot`,
+ `committedSnapshot`, `filtersDirty`, `promoteDraftsToCommitted`, `filterKey`, hydration,
+ `applyDraftSnapshot`, `applySnapshot`, each written only off its default. Not in `UrlParams`, not in
+ `ShareSelection`: **share links do not carry the row this release** (they do not carry `nop`
+ either). Hydration reads the row from the stored snapshot (active profile, else working) whatever
+ the URL carries, so a `qt=` link — and a share-v1 link, whose filters otherwise replace the
+ stored ones — reads with the visitor's own row.
+- **`nop` was not threaded, three ways, and is now.** Hydration set the draft `nop` to false and
+ never set the committed one, so Posts unticked came back ticked on every reload;
+ `committedSnapshot` did not write it, so a profile with Posts unticked always showed as changed
+ against what it had just loaded; `applySnapshot` did not commit it, so a loaded profile with Posts
+ unticked searched posts until the next Search.
+- **Nothing ticked.** When nothing the site offers is ticked (`draftSearchInEmpty`), Search is
+ disabled (with a title naming the row), `Apply filters` in the sheet is disabled
+ (`FiltersContainer.tsx`, a new `applyDisabled` prop), and `commitSearch` (where Enter and Apply
+ arrive) and both profile saves (which commit the draft) refuse. The row shows one line, "Search in:
+ pick at least one" (`data-testid="search-in-empty"`, `role="status"`), and the bar's "Press Enter
+ or click Search to apply" is withheld while Search is refused. A tree committed with nothing ticked
+ some other way (a hand-edited profile) reads its transcripts rather than matching nothing.
+- **Display.** The `hit.track !== "live_chat"` suppression in `SearchResults.tsx` is gone: every track
+ hit wears its `TrackBadge`, so a "Live chat" leaf's hits now carry the badge too (no spec asserted
+ its absence; `live-chat.spec.ts` searches nothing and was not changed). A transcripts leaf's
+ section bar is named for what it holds (`sectionScope`): "Posts" on a post's card, "Live chat" when
+ every hit in it is a chat hit, else "Transcripts".
+- **The bar.** The hint reads "Live chat available on N videos — tick Live chat under Search in." and
+ goes once Live chat is ticked. The Filters chip's count gains one for the row off its default (Live
+ chat ticked widens rather than narrows, but the panel that says so may be behind the chip); Videos
+ and Livestreams keep their own one.
+
+**The fixture.** The export e2e fixture's posts said "alpha" and "gamma", like every video's cues. With
+posts read by default, every spec searching those words for its own reasons got two post cards: a first
+full run at `2d68d9d9` (stopped at 174 of 265) had 17 failures — 7 in `ask-chat`, 5 in `ask-workspace`,
+2 in `posts-search`, 2 in `query-tree`, all post cards, and 1 in `charts` (a browse-mode stacked-bar
+case the row cannot reach; it passed in both later runs). The posts now say "kappa" and "sigma"
+(`omega` was already theirs); `posts-search.spec.ts` and `tag-chips.spec.ts` name them in their posts
+leaves, and `search-in.spec.ts` reads both corpora with a regex `alpha|kappa` in one plain leaf.
+
+**Commits**
+
+| Commit | What |
+|---|---|
+| `0edc628f` | `common:` `applySearchIn`, `SearchIn`, `SEARCH_IN_DEFAULT`, `searchInReadsNothing` in `lib/searchQuery.ts`; `foldSearchIn` in `lib/search/searchIn.ts`; tests. |
+| `41e75543` | `common:` `notr` and `lc` in the filter snapshot; `parseSnapshot` exported; the round-trip test. |
+| `2d68d9d9` | `common:` the row; the session (rewrite, fold, manifests, state, refusal, the three `nop` fixes); the bar; the badge and the section names; `applyDisabled` on the sheet's Apply. |
+| `5699a1e7` | `export(e2e):` `search-in.spec.ts`; the fixture's posts get their own words; `posts-search` and `tag-chips` follow them. |
+| this commit | `plans:` this section; FACTS "Search in"; the export changelog. |
+
+**Tests** (unit)
+
+| File | What it pins |
+|---|---|
+| `lib/searchQuery.test.ts` (11, new) | Transcripts only returns the very same root; the default is Transcripts and Posts; Transcripts + Live chat is `OR(a~transcripts, a~chat)` with both mapped to `a` and `a` → `a~in`; Live chat only and Posts only swap the scope and keep the id; all three in a fixed order; nothing ticked is left alone, and `searchInReadsNothing`; leaves of every named scope and an empty leaf are untouched (same root); a negated leaf becomes a negated AND over an un-negated OR whose copies keep `contributeHits: false`; nested: an untouched subtree keeps its identity, group ids are kept, the input's `stringifyRoot` and `canonicalHash` do not change; two leaves get two sets of copies. |
+| `lib/search/searchIn.test.ts` (8, new) | Through the real `runQueryTree` with a fake leaf runner: Transcripts alone reads the cues only; Transcripts + Posts returns the video and the post, the post hit filed under the visitor's leaf with scope `posts`; Live chat ticked adds the chat-only video, its hit under the leaf with `track: "live_chat"`, the leaf's state folded (only `a` left, count 2 = the union, not active); Transcripts off + Live chat on: a cue-only word finds nothing; a "Live chat" leaf ignores the row; NOT reads NOT of the union and still counts what it matched. The fold alone: before every copy has started, the largest copy and active (not the 30,000 the evaluator reports); nothing rewritten returns the same progress. |
+| `components/exportFilterStorage.test.ts` (5, new) | `notr`, `nop`, `lc` survive a JSON round trip; absent reads as the default and a pre-row profile equals one spelling the defaults; a non-boolean is dropped; `snapshotsEqual` tells each apart; the working snapshot and a profile keep the row through `saveStoredState`/`loadStoredState`. |
+
+**e2e** (`export/e2e/search-in.spec.ts`, new, 9 cases): (1) the defaults — Transcripts and Posts
+ticked, Live chat not, exactly one checkbox named "Posts" on the page, Videos and Livestreams still
+in Type, and the bar's hint, gone once Live chat is ticked; (2) a plain "kappa" returns the two posts
+under a section named "Posts", and none with Posts unticked; (3) one plain regex leaf `alpha|kappa`
+returns 3 videos and 2 posts, "5 videos, 5 hits"; (4) "message" (only in one video's chat) finds
+nothing by default, and with Live chat ticked finds that video, "1 video, 30 hits", every shown hit
+badged "live chat", the section named "Live chat", and `qt=` still the visitor's one transcripts
+leaf; (5) Transcripts off + Live chat on: "line" (only in cues) finds nothing, "message" finds the
+chat video, Transcripts back on finds all three; (6) nothing ticked: Search disabled, the line
+shown, the "Press Enter" line withheld, Enter commits nothing (no `qt=`, no results), Live chat or
+Posts re-enables it; (7) the row survives a reload, is saved with "Save as…", a diverging Search
+leaves the profile, and loading the profile brings the row back with no unsaved-changes dot and its
+results; storage holds `notr` and `lc` and no `nop`; (8) an empty query with Transcripts off lists
+"All videos (3)"; (9) a "Live chat" leaf from `qt=` reads the chat with the row at its default.
+
+#### Gates (logs `$T/ck-*.log`)
+
+- **tsc** (all workspaces): clean before each code commit — 71 s at `2d68d9d9` (after removing a
+ truncated, stale `editor/.next/dev/types/validator.ts` left in this worktree by an earlier dev run,
+ which failed the first attempt with TS1002); export's alone after the e2e edits.
+- **Unit:**
+
+ | Suite | Result |
+ |---|---|
+ | common | **2,372/2,372**, 84 s (24 new: 11 + 8 + 5) |
+ | mcp | **271/271**, 27 s (it imports `lib/searchQuery.ts`, which only gained exports) |
+ | editor unit, `test:scripts` | not run: no editor file, script or anything they import changed |
+
+- **Build:** `pnpm --filter export exec next build` — exit 0, 35 s, `export/public` links refreshed from
+ the primary and none dangling (three that were — `hub-sites.json`, `hub-summary.json`,
+ `duplicates.json`, gone from the primary — removed first). The editor and umtool builds: not run,
+ neither imports a changed module.
+- **e2e** (detached and queued; none waited in the queue):
+
+ | Run | At | Specs | Result |
+ |---|---|---|---|
+ | 1 | `2d68d9d9` | the prompt's eight (`search-in`, `posts-search`, `live-chat`, `filter-profile-persistence`, `first-search`, `browse-all`, `query-tree`, `share-current-search`) | 50 passed, **6 failed**, 4.4 min — `search-in` 8/8; the six were post cards (above) |
+ | 2 | `2d68d9d9`, old fixture | the full export suite | stopped at 174 of 265: 157 passed, **17 failed** (above) |
+ | 3 | `2d68d9d9` + the fixture and spec edits | the full export suite | 264 passed, **1 failed**, 14.0 min — `tag-chips` named "alpha" in a posts leaf |
+ | 4 | `5699a1e7` | the eight + `tag-chips` | **72 passed**, 0 failed, 3.5 min |
+ | 5 | `5699a1e7` | the hub suite (`e2e:hub`) | **36 passed**, 0 failed, 1.6 min — its fixtures ship no posts and no live chat, so the row is a lone Transcripts box and no tree is rewritten |
+ | 6 | `5699a1e7` | the editor suite's `export-search.spec.ts` (it drives the export's filter rows) | **19 passed**, 0 failed, 1.0 min |
+ | 7 | `5699a1e7` | the full export suite | **265 passed**, 0 failed, 15.1 min |
+
+- **Numbers tool:** none.
+
+#### Found and left
+
+- **Not governed by the row:** a chart's own search series (`components/charts/useSearchSeries.ts`
+ runs its own tree), `/ask`'s retrieval (`export/app/lib/askRetrieval.ts`, its own OR of keywords —
+ though the grounding it is handed from the search page is the session's results, which the row
+ governs), and the MCP (no row).
+- **A restart when a manifest answers late.** The row counts Posts and Live chat only once the posts
+ and subs manifests say the site has them. A query that runs before they answer (a `qt=` link, when
+ the summaries come first) runs once without posts or chat and again when they arrive; the first
+ run's transcript fetches are memoized, so the second is cheap. The same race was already there for
+ a "Posts" leaf.
+- **A lone Transcripts box.** On a site with neither posts nor live chat (the hub's e2e fixture) the
+ row is one box, whose only effect unticked is to refuse Search. Shown as ruled (Transcripts is not
+ conditional).
+- **Each post twice under an explicit `(transcripts OR posts)` tree.** With Posts ticked the
+ transcripts leaf reads posts too, so a post matched by both leaves shows one section per leaf. The
+ tree says to read posts twice; the row does not merge leaves.
+- **"Matching videos (N videos, …)" counts posts as videos** — as before, for a "Posts" leaf.
+- **The builder's scope select still says "Transcripts"** for the default leaf, which now reads what
+ the row ticks; its placeholder still says "Search transcripts...". Unchanged (the labels are
+ contracts; `filter-profile-persistence` finds the input by that placeholder).
+- **A "Live chat" leaf on a site with no subs manifest at all waits for ever** (as on `main`): the
+ run waits for `subsManifestReady`, which needs a manifest that 404s. The row does not reach it,
+ since it counts Live chat only where `liveChatTotalCount > 0`.
+- **A stored `lc` on a site without live chat** is kept (the panel does not show the box, and the
+ session counts it as unticked); it applies again on a site that has chat, since profiles are per
+ browser, not per site.
+
+#### Decisions the operator could overturn
+
+| What I assumed | The alternative |
+|---|---|
+| **A plain query reads posts by default** (the ruling as written; the ruling's "today" said it already did, and it did not) | Posts in the row governs only a "Posts" leaf, and a plain query reads no posts, as on `main` (one flag) |
+| Mechanism (a), a rewrite before the run plus a fold of the progress | (b), branching inside `runLeaf` |
+| Posts are read through a posts copy of the leaf, not left on the global-scope path, which reads posts only for a "Posts" leaf | — |
+| Posts and Live chat count as ticked only where the site has them, for the rewrite and for "nothing ticked" | The stored booleans as they are, whatever the site ships |
+| The row always renders, even as a lone Transcripts box | Hide it where neither Posts nor Live chat is offered |
+| With nothing ticked, `Apply filters` and the two profile saves refuse too, and the bar's "Press Enter" line is withheld | Only the Search button |
+| A committed tree with nothing ticked (a hand-edited profile) reads its transcripts | Matches nothing |
+| The row is read from the stored snapshot whatever the URL carries, share-v1 links included | A share-v1 link resets the row to its defaults, as it resets the other filters to the link's |
+| A transcripts leaf's section is named for what it holds ("Posts", "Live chat") | Always "Transcripts" |
+| The Filters chip counts the row off its default as one, Live chat ticked included | Only unticks count, as narrowing |
+| The fixture's posts get words of their own, rather than every spec that searches "alpha" or "gamma" growing two post cards (16 failures in four specs by 174 of 265 tests) | Keep the posts' "alpha" and add the posts to each expectation |
+| The hint goes once Live chat is ticked | Always shown where live chat exists |