Knowledge: Dissolve the Guidelines singleton into per-scope rows#79263
Conversation
1f20bdf to
9a4a5c7
Compare
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>
63d105f to
18e953e
Compare
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| 'description' => __( 'Set your writing standards for tone, voice, style, and formatting.', 'gutenberg' ), | ||
| 'order' => 20, | ||
| ), | ||
| 'images' => array( |
There was a problem hiding this comment.
That's outside of this PR, but would it be helpful to also have generic guidelines for blocks used as a fallback if something specific isn't defined?
| public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable | ||
| if ( ! current_user_can( 'read_knowledge_items' ) ) { | ||
| return new WP_Error( | ||
| 'rest_forbidden', |
There was a problem hiding this comment.
Nit (consistency, non-blocking): this returns rest_forbidden, but after #79149 the sibling /wp/v2/knowledge collection read now returns rest_cannot_read. Since this endpoint is explicitly modeled on /wp/v2/statuses (which itself uses rest_cannot_view) and sits right beside the knowledge collection, it'd be nice to align the error code so REST clients get a consistent code across the knowledge routes — either rest_cannot_read to match the collection, or rest_cannot_view to match the /wp/v2/statuses precedent.
| $existing_ids = get_posts( | ||
| array( | ||
| 'post_type' => Gutenberg_Knowledge_Post_Type::POST_TYPE, | ||
| 'name' => $slug, | ||
| 'post_status' => 'any', | ||
| 'posts_per_page' => 1, | ||
| 'fields' => 'ids', | ||
| 'no_found_rows' => true, | ||
| 'post__not_in' => empty( $prepared_post->ID ) ? array() : array( (int) $prepared_post->ID ), | ||
| ) | ||
| ); |
There was a problem hiding this comment.
Nit (edge case, non-blocking): this uniqueness query uses 'post_status' => 'any', which excludes trash (and auto-draft). So if a guideline- row is trashed rather than force-deleted, its slug frees up — a new row can claim it, and restoring the trashed row then leaves two rows sharing the same guideline- slug (which is identity here). The Settings UI always force-deletes, so this doesn't surface today; it'd only bite if a row is trashed via another path (WP-CLI, future UI, another plugin). Consider including trash in the status list (e.g. array( 'publish', 'draft', 'pending', 'private', 'future', 'trash' )) or documenting the force-delete assumption.
|
Suggestion: reclaim the row instead of blocking the save — and enforce it for every insert path, not only REST. Creating a guideline with an existing Instead of blocking the save, we can reclaim the existing row:
This is safe to do automatically because the row has both the One more point: I would put this guard outside the REST path — a shared helper, or a hook that runs on every insert — not only in |
|
After more research, I think the rule can be smaller. We may only need one public row per slug, instead of preventing duplicate slugs everywhere. If the Settings page trusts only public
Two nice results:
|
|
Thanks for the review @gziolo, this is a great suggestion - the "one public row per slug, reclaim on save" model is cleaner than the 409, and it removes both the trash edge case and the private-row-blocking concern. I'm on board with the direction. A few things to pin down before I implement:
Also confirming an assumption: scope rows are only ever published (no draft state), so "the public row" = the published row. Let me know if that's off. I'll also fix the |
What?
Follow up to #79149. Part of the Guidelines/Knowledge effort (#77230).
Dissolves the Settings → Guidelines singleton-plus-meta model into one
guideline-typedwp_knowledgerow per scope (content stored inpost_content), driven entirely through the standard/wp/v2/knowledgeentity. Both specialized REST controllers (~1,150 lines) and the hand-rolled client store/api are removed. The visible Settings → Guidelines UI is unchanged.Why?
The old screen rested on special-purpose machinery: a single
wp_knowledgepost holding every category as_guideline_*meta, a dedicated/wp/v2/content-guidelinescontroller, a custom revisions controller, and a bespoke client API/store, with the categories hardcoded in the UI. Re-using thewp_knowledgeprimitive'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?
guideline-typed row addressed by aguideline-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.wp_guideline_scopesfilter is the source of truth for the Settings sections (plugins can add sections); a small read-only/wp/v2/knowledge/guideline-scopescontroller exposes it (gated onread_knowledge, the/wp/v2/statusespattern), preloaded on the page.guidelineterm onto prefixed slugs, keeps them unique (409 on a duplicate, on create and update) and verbatim (no-2suffix), sanitizespost_contentto plain text capped at 5000 chars, and re-stamps registry scope titles in the site locale.wp_knowledge_typebuilt-in terminstructionbecomesguideline; the upgrade migration convergescontentand the interiminstructionontoguideline.routes/guidelinesnow reads/writes through@wordpress/core-data(useEntityRecords+ a runtimeguidelineScopeentity); sections render from the registry; revision history is hidden; import/export keeps the same JSON shape so existing files round-trip.class-gutenberg-content-guidelines-rest-controller.php,class-gutenberg-content-guidelines-revisions-controller.php, the guideline meta registration/helpers, and the clientstore.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
gutenberg-experimentsoption to{"gutenberg-guidelines":"1"})./wp/v2/knowledgeand/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