# Changelog

All notable changes to BW Guides are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.3.5] - 2026-08-13

### Added
- **Reading and changing guides are now separate, filterable capabilities.**
  Both default to `edit_posts`, so nothing changes on an existing site until it
  deliberately filters one.
  - `bw_guides_read_capability` — browse, search and open a guide.
  - `bw_guides_manage_capability` — tags, private notes, triggering a sync, and
    the authoring screens.

  ```php
  // Let every logged-in user read the documentation, changing nothing else:
  add_filter( 'bw_guides_read_capability', function () { return 'read'; } );
  ```

### Fixed
- **A site widening access to let staff *read* guides was also handing them the
  ability to change guides for everybody.** One capability answered two very
  different questions. The AJAX endpoints all write — tags are shared taxonomy
  terms, notes annotate the guide itself rather than a per-user copy, and the
  update check runs a full sync that creates, updates and trashes posts — so
  they are now gated by the manage capability, never the read one.
- A read-only viewer no longer sees the tag editor or notes field. They rendered
  and then failed on save, which read as a bug rather than as a boundary.
- The background update check is no longer fired for readers, whose request
  would only 403. Their view still reflects whatever the daily cron, or any
  colleague with manage rights, last pulled in.

### Notes
- The **settings screen deliberately stays on `manage_options`** rather than
  moving to the manage capability: it exposes the site key, a bearer credential.
  Routing it through a capability that defaults to `edit_posts` would have
  widened access to that key on every existing site.

## [0.3.4] - 2026-07-27

### Fixed
- Section search no longer returns an empty state for a naturally-phrased
  question. Matching required **every** word of the query to appear, so one word
  that exists nowhere discarded every other signal — and the longer and more
  natural the question, the more likely it failed. Asking *"how do I write a news
  story for the school blog"* found nothing, while "news story" found plenty.
  When the all-words pass finds nothing at all, a second pass now ranks sections
  by the **proportion** of query words they match, requiring a clear majority so
  the result stays "the closest answers" rather than "anything related". Precise
  short queries are unaffected — they never reach the fallback.
- The fallback needed a stricter word matcher than the exact pass. Substring
  matching is safe when every word must match, but when counting proportionally
  it lets `at` match "th**at**" and "cre**at**e", which floated unrelated
  sections over the threshold. Words shorter than five characters must now match
  whole, and tokens under three characters are ignored — and, importantly, are no
  longer counted in the total either. Counting a token that can never match made
  some queries require an impossible score: *"where did the editor go"* reduces to
  `did`/`editor`/`go`, of which two can never match, so it needed 2 of 3.

## [0.3.3] - 2026-07-24

### Fixed
- Search and keyword matching now normalise both sides before comparing, fixing
  two silent failures where a section became unfindable with no error:
  - **HTML entities.** A keyword row written `width & height` never matched
    content rendering `width &amp;amp; height` (`wp_strip_all_tags` strips tags but
    doesn't decode entities), so that section's keywords did nothing.
  - **Curly and straight apostrophes.** A query of `cant get in` now reaches a
    keyword written `can't get in`, and content using a curly `don't` matches a
    keyword using a straight one.
  Non-breaking spaces and runs of whitespace are collapsed too. The normaliser
  is deliberately conservative — it only removes differences a reader can't see.

## [0.3.2] - 2026-07-24

### Changed
- "Getting started" now always leads the browse page instead of whichever
  category sorts first alphabetically.
- When section results match but no whole guide does, the list no longer says
  "No guides found." under the answers — it says the sections above are the
  closest matches.

## [0.3.1] - 2026-07-24

### Fixed
- Search now ignores filler words ("how do I", "my") in the all-words match, so
  "forgot my password" finds a section whose keywords say "forgot password".
  The exact-phrase match is untouched.
- Plural-tolerant matching: "reorder blocks" now matches keywords that say
  "block". (A singular query already matched plural text as a substring.)

## [0.3.0] - 2026-07-24

### Added
- **Ranked section search with hidden keywords.** Search results now score
  every matching heading instead of just returning the first 8 encountered:
  a phrase match in the heading text scores highest, then an all-words
  heading match, then a phrase or all-words match against a guide's hidden
  `section_keywords` (client vocabulary a heading doesn't literally contain —
  "permalink" now finds "Changing a page's web address (URL / slug)"). A
  guide-level keyword match adds a small boost across that guide's sections
  so a query that only identifies the guide still surfaces it. New class
  `BW_Guides_Keywords` reads this from the payload `meta` object
  (`_bw_guides_remote_meta`) for hub guides and from `_bw_guides_local_category`
  / `_bw_guides_local_keywords` for local guides — both optional, both
  tolerated absent (every guide synced before this had neither). No protocol
  change: this is the first reader of fields the hub has been carrying since
  0.1.0 (invariant 6 — forward-compat fields read at render/query time,
  never used to rewrite stored content).
- "Jump straight to" results now show a subtle `(matches: <search term>)`
  hint on a result found only through a hidden keyword, not the visible
  heading — no scores are ever displayed.
- **Category-grouped browse grid.** With a guide's `category` (same source as
  the keywords above), the guides list now groups cards under category
  headings, alphabetically; guides with no category fall under "More
  guides"; every client-created guide is grouped last under "Your guides"
  regardless of any category it carries, so a client's own guides stay a
  single, easy-to-find block. New `BW_Guides_Keywords::group_guides()`.
- Each card now shows up to 3 quick links to its first h2 sections, so a
  client can jump straight to a task from the browse grid without opening
  the guide first.
- The search box is now visually prominent (its own row, larger), with the
  placeholder "What do you need help with?" in place of "Search guides…".

### Changed
- Browse grid page size raised from 20 to 60 guides per page. Grouping by
  category applies to the current page's result set; a 20-per-page slice
  tended to cut a category's cards across two pages. A library that outgrows
  60 will still see a group split across pages — an accepted tradeoff for
  now, not solved here (see docs/PLATFORM-ROADMAP.md).

### Notes
- Heading text still needs no keyword metadata to be found: a plain heading
  match alone already outscores a keyword-only match, so search behaves
  exactly as in 0.2.0 wherever a guide carries no `meta.category` /
  `meta.section_keywords` — which is every guide as of this release; the hub
  side of this contract (0.2.0 on `bw-guides-server`) ships in parallel.
- Heading matching for `section_keywords` is by case-insensitive, trimmed,
  **exact** heading text — never by slug — so this plugin never has to agree
  with the hub (or itself) on slug generation to find the right section.

## [0.2.0] - 2026-07-24

### Added
- **Find the answer, not just the guide.** Searching the guides list now also
  matches the headings inside guides and offers a "Jump straight to" list of
  matching sections, each linking directly to that point in the guide. This is
  what makes small questions — "how do I change the page url" — answerable
  without giving every question its own guide.
- Every heading in a guide gets a stable anchor, so any section can be linked to
  directly (`…&guide=172#bw-common-questions`).
- An "In this guide" contents list, generated automatically from a guide's
  headings. No authoring effort, and it appears only when a guide has enough
  headings to be worth navigating.

### Notes
- All of it is derived at render/query time. Nothing is stored and
  `post_content` is never rewritten, so `content_hash` stays stable
  (invariant 6) and no protocol change was needed — the hub is untouched.
- Anchors are generated from heading text, so editing a heading on the hub
  changes its anchor. A link saved against the old one lands at the top of the
  guide rather than the section: degraded, not broken.
- Authoring implication: write one task per heading, and put the words clients
  actually use into the heading itself ("Changing a page's web address (URL)")
  so section search finds it.

## [0.1.2] - 2026-07-24

### Added
- Guide content now renders with full styling for the core blocks used to build
  guides: groups with backgrounds, columns, buttons, collapsible details, tables,
  quotes, separators, media &amp; text and images. WordPress only loads theme.json
  global styles on the front end, never in wp-admin where guides are read, so
  without this a Buttons block rendered as a bare link and columns had no gap.
- Styles for the BW guide design kit (`bw-callout` and its warning/success/danger
  variants, `bw-callout-label`, `bw-steps`), the components used by the authoring
  patterns on the hub. Appearance is carried by class names rather than inline
  styles, so the house style can be restyled here without editing any published
  guide.

### Notes
- All rules are scoped to `.bw-guides-content` and cannot affect the rest of
  wp-admin. Because guides render in wp-admin rather than through the theme, the
  rendering environment is identical on every client site — a guide that looks
  right on one site looks right on all of them.

## [0.1.1] - 2026-07-24

### Changed
- Guides now update the moment someone opens the Guides section: each Guides
  screen fires a background update check after load (throttled to once per
  10 minutes per site, filterable via `bw_guides_check_interval`). If anything
  changed it syncs immediately and shows a "refresh to see the latest" banner —
  including on a guide that was updated or withdrawn while being read.
- Scheduled sync reduced from twice daily to a single daily backstop run (it
  also keeps the hub's last-sync bookkeeping alive for sites nobody opens).

## [0.1.0] - 2026-07-23

### Added
- Guides library as a `bw_guide` post type with a `bw_guide_tag` taxonomy.
- Sync engine: pulls guides from the Bowden Works hub (`bw-guides/v1` REST API,
  authenticated by a per-site key) twice daily via WP-cron, plus a manual
  Sync Now button. Failed syncs never modify local content; untargeted guides
  move to Trash (recoverable) and are restored without duplicates if re-targeted.
- Hub-delivered guides are read-only (no edit/delete for anyone) but can be
  tagged and annotated; tags and notes are local and survive every sync.
- Client-created guides use the normal block editor.
- wp-admin reading UI: Guides menu with searchable, tag- and source-filterable
  list plus a clean single-guide view (core block rendering, no shortcode
  execution of hub content).
- Per-guide private notes panel, saved via admin-ajax.
- Settings screen: hub URL (HTTPS enforced, overridable via the
  BW_GUIDES_HUB_URL constant), masked site key with immediate first sync,
  sync status, Sync Now and Test Connection.
- Uninstall removes settings, cron, and hub-delivered guides, but keeps
  client-created guides and tags.
