Skip to content

Knowledge: Dissolve the Guidelines singleton into per-scope rows#79263

Draft
aagam-shah wants to merge 8 commits into
WordPress:update/rename-guideline-cpt-to-knowledgefrom
aagam-shah:update/rename-guideline-cpt-to-knowledge
Draft

Knowledge: Dissolve the Guidelines singleton into per-scope rows#79263
aagam-shah wants to merge 8 commits into
WordPress:update/rename-guideline-cpt-to-knowledgefrom
aagam-shah:update/rename-guideline-cpt-to-knowledge

Conversation

@aagam-shah

@aagam-shah aagam-shah commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What?

Follow up to #79149. Part of the Guidelines/Knowledge effort (#77230).

Dissolves the Settings → Guidelines singleton-plus-meta model into one guideline-typed wp_knowledge row per scope (content stored in post_content), driven entirely through the standard /wp/v2/knowledge entity. Both specialized REST controllers (~1,150 lines) and the hand-rolled client store/api are removed. The visible Settings → Guidelines UI is unchanged.

Stacked on #79149 — this PR's diff is the three commits on top of that branch and should be reviewed after it. It will retarget to trunk automatically when #79149 merges.

Why?

The old screen rested on special-purpose machinery: a single wp_knowledge post holding every category as _guideline_* meta, a dedicated /wp/v2/content-guidelines controller, a custom revisions controller, and a bespoke client API/store, with the categories hardcoded in the UI. Re-using the wp_knowledge primitive's own building blocks removes that machinery, gives each scope a real row (so per-row revision history later comes free from the default endpoint), and makes the data model addressable and extensible via a filter instead of hardcoded lists.

How?

  • Per-scope rows + identity. Each scope is backed by at most one guideline-typed row addressed by a guideline- slug prefix (guideline-copy, guideline-block-core_paragraph). The canonical block name is stored in the row title; the namespace separator is encoded as _ so distinct block names can't collide on one slug.
  • Scope registry. A new wp_guideline_scopes filter is the source of truth for the Settings sections (plugins can add sections); a small read-only /wp/v2/knowledge/guideline-scopes controller exposes it (gated on read_knowledge, the /wp/v2/statuses pattern), preloaded on the page.
  • Reservation guard. A guard on the REST insert path forces the guideline term onto prefixed slugs, keeps them unique (409 on a duplicate, on create and update) and verbatim (no -2 suffix), sanitizes post_content to plain text capped at 5000 chars, and re-stamps registry scope titles in the site locale.
  • Type term rename. The wp_knowledge_type built-in term instruction becomes guideline; the upgrade migration converges content and the interim instruction onto guideline.
  • Client. routes/guidelines now reads/writes through @wordpress/core-data (useEntityRecords + a runtime guidelineScope entity); sections render from the registry; revision history is hidden; import/export keeps the same JSON shape so existing files round-trip.
  • Removals. Deletes class-gutenberg-content-guidelines-rest-controller.php, class-gutenberg-content-guidelines-revisions-controller.php, the guideline meta registration/helpers, and the client store.ts / api.ts / revision-history.tsx.

Note: existing singleton-meta data is not migrated (the feature is experimental and flag-gated); see discussion for whether a one-time migration is warranted.

Testing Instructions

  1. Enable the Guidelines experiment (Gutenberg → Experiments, or set the gutenberg-experiments option to {"gutenberg-guidelines":"1"}).
  2. Go to Settings → Guidelines. Confirm the Site, Copy, Images, Blocks, and Additional sections render and the Actions card shows Import/Export (no Revert/revision-history UI).
  3. Edit and Save a scope (e.g. Copy); reload and confirm it persists. Clear it and confirm it's removed after reload.
  4. In Blocks, Add a guideline for a block, Edit it, then Remove it.
  5. Export to JSON, then Import the file back and confirm the guidelines are restored.
  6. In DevTools → Network, confirm requests hit /wp/v2/knowledge and /wp/v2/knowledge/guideline-scopes (and never /wp/v2/content-guidelines).

Automated:

  • npm run test:unit:php -- --group knowledge (PHPUnit, incl. the scopes controller and reservation-guard tests).
  • npm run test:e2e -- test/e2e/specs/admin/guidelines.spec.js.

Testing Instructions for Keyboard

Tab to Settings → Guidelines, expand a section with Enter/Space, Tab into its textarea, type, and Tab to Save guidelines / Clear guidelines. For Blocks, Tab to Add guidelines, operate the block combobox and guideline textarea, and reach the modal's Save/Remove with Tab; confirm focus returns to the page on close.

Screenshots or screencast

No visual change — the Settings → Guidelines UI is intentionally identical; only the backend storage and client data layer changed.

Use of AI Tools

This PR was authored with the assistance of Claude Code (Anthropic, Claude Opus 4.8). AI was used for the implementation, the PHPUnit/e2e tests, and this description; all changes were reviewed and verified by a human (PHPUnit --group knowledge, the e2e spec, PHPCS, and a manual browser pass), and the author takes responsibility for the contents per the WordPress AI Guidelines.

🤖 Generated with Claude Code

gziolo and others added 8 commits June 12, 2026 12:08
Renames the storage-primitive identifiers per the consolidation proposal in
WordPress#77230: the CPT slug (wp_guideline -> wp_knowledge), the type taxonomy
(wp_guideline_type -> wp_knowledge_type), the types registry and filter
(wp_guideline_types -> wp_knowledge_types), the capability namespace
(*_guidelines -> *_knowledge), and the generic REST route
(/wp/v2/guidelines -> /wp/v2/knowledge).

The capability registration uses
capability_type => array( 'knowledge_item', 'knowledge' ) because
"knowledge" is a mass noun: with identical singular/plural bases the
generated per-post meta caps would collide with the primitives. The
*_knowledge_item forms are never granted; map_meta_cap() resolves them.

User-facing surfaces are intentionally unchanged: the Settings > Guidelines
page, the /wp/v2/content-guidelines singleton route, all Guidelines labels,
the gutenberg-guidelines experiment id, and the _guideline_* meta keys.
The built-in type slugs (content, artifact, memory) are renamed separately.

Ships a one-time data migration (lib/upgrade.php) that moves existing
wp_guideline rows and wp_guideline_type terms to the new identifiers so
sites already building on the experiment are not orphaned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renames two of the three built-in knowledge types per the WordPress#77230 proposal
so every type is defined by behavior rather than by relation:

- `content` -> `instruction`: loaded by default when applicable. The
  site-wide guidelines singleton managed by Settings > Guidelines now
  carries the `instruction` term; the /wp/v2/content-guidelines route is
  otherwise unchanged.
- `artifact` -> `note`: private freeform working text, and the fallback
  term assigned on save when no type is given.
- `memory` stays as is.

The one-time migration in lib/upgrade.php now also re-slugs existing
terms (content -> instruction, artifact -> note), replacing term names
only when they still match the previous default labels so customized
labels survive. The re-slug runs independently of the legacy taxonomy
flip so partially migrated rows are covered too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gutenberg-guidelines flag gates more than the Settings page since the
storage primitive rename: it also enables the wp_knowledge CPT, the
/wp/v2/knowledge API, and the capability namespace integrators build on.
The experiment id and label stay unchanged — the id is persisted in the
gutenberg-experiments option on sites that enabled it, and the label
follows the proposal's rule that the user-facing name stays Guidelines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every other test file in the directory carries a group annotation;
this one was missed when it was introduced, so group-filtered runs
silently skipped it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the content-guidelines singleton + meta model with one
`guideline`-typed `wp_knowledge` row per scope (content in post_content),
addressed by a `guideline-` slug prefix.

- Rename the `wp_knowledge_type` term `instruction` -> `guideline`
  (wp_knowledge_types(), TERM_GUIDELINE, and the upgrade migration, which
  now converges both `content` and the interim `instruction` onto
  `guideline`).
- Add the `wp_guideline_scopes` registry filter and a read-only
  `/wp/v2/knowledge/guideline-scopes` controller; preload it on the
  Settings page.
- Enforce identity with a reservation guard (force the guideline term,
  keep slugs unique with no suffix, reject duplicate creates) and a save
  filter (sanitize content + cap length, re-stamp scope titles in the
  site locale).
- Delete both specialized controllers and the meta machinery; data flows
  through the standard /wp/v2/knowledge collection.
- Drive the Settings UI through @wordpress/core-data (useEntityRecords +
  a runtime guidelineScope entity); delete the hand-rolled store/api.
- Render sections from the registry, hide revision history, and keep the
  import/export JSON shape unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Importing onto (or editing) a guideline row that was loaded via the
collection request after a reload threw "Cannot read properties of
undefined (reading 'content')".

The `wp_knowledge` (postType) entity already fetches with `context: 'edit'`
via its `baseURLParams`, so the collection response includes raw field
values regardless of the query. Passing `context: 'edit'` in the query as
well only changed the *storage bucket* to `edit`, where
`editEntityRecord`/`getRawEntityRecord` (which read the `default` bucket)
could not find the row — so `editEntityRecord` dereferenced `undefined`.

- Drop `context: 'edit'` from the collection query; raw content still
  arrives via the entity baseURLParams, and rows now land in the `default`
  bucket where edits resolve.
- Pass options to `saveEditedEntityRecord` in the correct argument
  position so `throwOnError` is honored on updates.
- Gate the loading spinner on `hasResolved` rather than `isResolving` so
  the form doesn't mount with empty content and clobber freshly-typed
  text when the rows arrive.
- Add an e2e regression test that edits a scope guideline after a reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…update

Two correctness fixes for the guideline-row identity model.

- Block slugs were lossy: deriving `guideline-block-<ns>-<name>` by
  replacing `/` with `-` collapsed distinct block names such as
  `foo/bar-baz` and `foo-bar/baz` onto the same slug, so one block's
  guideline could overwrite another's. Encode the namespace separator as
  `_` instead — block names match `[a-z0-9-]+/[a-z0-9-]+` and never
  contain `_`, so the mapping is injective. The canonical block name still
  lives in the row title.

- The reservation guard only checked slug uniqueness on create, so a REST
  update could repoint an existing row's slug onto an already-used
  `guideline-` slug, producing duplicate-identity rows. Run the check on
  update too, excluding the row itself so content-only saves still pass.

Adds reservation tests for the update-collision and content-only-update
cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The guidelines route's package.json dropped @wordpress/api-fetch and
@wordpress/date and added @wordpress/core-data; regenerate the lockfile so
the check-local-changes CI gate passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants