From 00c8fac080cbf0ed794bd7cc7cf55d20ae9ee3c4 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 14:44:16 +0100 Subject: [PATCH] docs(33-06): complete structured data, breadcrumbs, hreflang and pagination plan - Task 4 answers: chromedp v0.21.0 and gen2brain/webp v0.6.4 approved - the tracer resolve answer for the layer fixtures - deferred: a repeater cannot carry a trigger (local_hours) --- .../33-06-SUMMARY.md | 473 ++++++++++++++++++ .../deferred-items.md | 7 + 2 files changed, 480 insertions(+) create mode 100644 .planning/phases/33-seo-output-audit-and-content/33-06-SUMMARY.md diff --git a/.planning/phases/33-seo-output-audit-and-content/33-06-SUMMARY.md b/.planning/phases/33-seo-output-audit-and-content/33-06-SUMMARY.md new file mode 100644 index 00000000..ff982722 --- /dev/null +++ b/.planning/phases/33-seo-output-audit-and-content/33-06-SUMMARY.md @@ -0,0 +1,473 @@ +--- +phase: 33-seo-output-audit-and-content +plan: 06 +subsystem: seo +tags: [sm-seo-plugin, json-ld, schema-org, breadcrumbs, hreflang, pagination, publisher, local-business, custom-elements, mcp, package-legitimacy] + +requires: + - phase: 33-seo-output-audit-and-content + provides: "33-01 descriptor facts (Kind, Parents, Authors, PublishedAt, Images, Videos); 33-05 resolve answer with outbound, the /site answer, the page registry kinds" +provides: + - "One JSON-LD @graph per ok resolve answer (classes/schema Build), nodes linked by the P33-22 @ids" + - "Resolve keys html_lang, alternates, jsonld, breadcrumbs, pagination, seo.og.locale, seo.og.locale_alternates (additive)" + - "Publisher, Local business, Pagination tabs and the home crumb setting; P8 publisher preview" + - "Pagination forms (query or path) with one-hop 301s, invalid pages, suffixes with %pages% left for the layer" + - "GET /site pagination {form, param, segments} and home_crumb {name, url, names}" + - "SEO tab: Primary category (seoparent), Structured data (seoschema, E8), preview (E9, widget action seo_graph); P1 primary_gone, P2 schema type and trail rows" + - "GET /_seo/api/v1/graph and MCP seo_get_graph; primary_parent and schema on seo_set_url_seo and apply_batch" + - "Task 4 checkpoint answers: chromedp v0.21.0 and gen2brain/webp v0.6.4 approved" +affects: [33-07, 33-08, 33-09, 33-10, 33-11, 33-12, 33-13, 33-14] + +actuals: + tokens: 110000 + tasks: 4 + commits: 6 +plan_head_before: df93c860011ee33158657a27f1a82ea1eeef6595 +plan_head_after: d38049a + +tech-stack: + added: [] + patterns: + - "classes/schema is pure (no plugin imports): the resolver, the admin preview, the publisher preview and the MCP tool share Build and Validate" + - "Per-record structured data values live in the entry's data (primary_parent, schema) on every locale row, like the social image" + - "Pagination is folded into Resolve before the strict check, so normalisation, strict URLs and the page form reach the final address in one 301" + +key-files: + created: + - ../sm-seo-plugin/classes/schema/graph.go + - ../sm-seo-plugin/classes/schema/nodes.go + - ../sm-seo-plugin/classes/schema/validate.go + - ../sm-seo-plugin/classes/schema/custom.go + - ../sm-seo-plugin/classes/breadcrumbs.go + - ../sm-seo-plugin/classes/alternates.go + - ../sm-seo-plugin/classes/pagination.go + - ../sm-seo-plugin/classes/structured.go + - ../sm-seo-plugin/updates/202610280004_add_golem15_seo_publisher_pagination.go + - ../sm-seo-plugin/controllers/settings/_publisher_preview.htm + - ../sm-seo-plugin/controllers/api/graph.go + - ../sm-seo-plugin/assets/src/elements/schema.ts + - ../sm-seo-plugin/assets/src/elements/graph.ts + - ../sm-seo-plugin/assets/src/elements/parent.ts + - ../sm-seo-plugin/tests/elements/schema.test.ts + - ../sm-seo-plugin/structured_data_test.go + modified: + - ../sm-seo-plugin/classes/resolver.go + - ../sm-seo-plugin/classes/site.go + - ../sm-seo-plugin/classes/settings_write.go + - ../sm-seo-plugin/classes/sitemap_filter.go + - ../sm-seo-plugin/classes/entries.go + - ../sm-seo-plugin/classes/history.go + - ../sm-seo-plugin/classes/owner.go + - ../sm-seo-plugin/classes/partials.go + - ../sm-seo-plugin/classes/field_types.go + - ../sm-seo-plugin/models/settings.go + - ../sm-seo-plugin/models/settings/fields.yaml + - ../sm-seo-plugin/controllers/settings.go + - ../sm-seo-plugin/controllers/api/settings.go + - ../sm-seo-plugin/controllers/api/seo.go + - ../sm-seo-plugin/extensions.go + - ../sm-seo-plugin/routes.go + - ../sm-seo-plugin/mcp.go + - ../sm-seo-plugin/operations.go + - ../sm-seo-plugin/config/config.yaml + - ../sm-seo-plugin/assets/src/elements/index.ts + - ../sm-seo-plugin/assets/src/elements/labels.ts + - ../sm-seo-plugin/assets/src/elements/styles.css + - ../sm-seo-plugin/assets/dist/seo.js + - ../sm-seo-plugin/assets/dist/seo.css + - ../sm-seo-plugin/lang/en/lang.yaml + - ../sm-seo-plugin/lang/pl/lang.yaml + - ../sm-seo-plugin/README.md + - ../sm-seo-plugin/settings_test.go + - ../sm-seo-plugin/seo_tab_test.go + +key-decisions: + - "Task 4 (Jakub 2026-11-04): github.com/chromedp/chromedp approved, pin v0.21.0 (v0.21.1 and v0.22.0 exist but are under two weeks old)" + - "Task 4 (Jakub 2026-11-04): github.com/gen2brain/webp approved, pin v0.6.4 (its fallback is libwebp transpiled to Go with wasm2go, not wazero)" + - "Every graph carries the full WebSite and publisher nodes, not only the home page, so each page's graph is self-contained (the tracer expects both on a post)" + - "home_crumb in the site answer is {name, url, names}: name and url are what the committed 33-08 layer reads; names adds the per-locale names the plan asked for" + - "The graph is skipped in the sitemap filter's production resolver (it decides on robots and canonical only)" + - "Primary category is offered for article kinds only; the value is : of one of the descriptor's Parents" + - "Page 2 and later always uses its own canonical (D-21), and its hreflang alternates point at the same page of each translation" + +patterns-established: + - "A structured data refusal is a schema.Error code mapped to the schema.* copy, shared by the field type Decode, seo_set_url_seo and the graph tool" + +requirements-completed: [SC-2, SC-3, D-43, D-44, D-45, D-46, D-03, D-86, D-55, D-56, D-57, D-40, D-41, D-42, D-21, D-94, D-10, D-70, D-85] + +coverage: + - id: D1 + description: "One @graph with WebPage, BlogPosting (headline, author, dates, image, wordCount, articleSection), BreadcrumbList, VideoObject, WebSite and Organization linked by @id; breadcrumbs equal the BreadcrumbList items; alternates en, pl, x-default; none for a single-locale record" + requirement: "D-43" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTracer" + status: pass + human_judgment: false + - id: D2 + description: "Breadcrumbs through the primary parent (stored, else first; recursive; unpublished parents skipped), per-locale home crumb, home page trail" + requirement: "D-86" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTracer/TestBreadcrumbs" + status: pass + human_judgment: false + - id: D3 + description: "html_lang, og:locale and alternates from one source; paged alternates" + requirement: "D-55" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTracer/TestAlternates" + status: pass + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestPagedAlternates" + status: pass + human_judgment: false + - id: D4 + description: "Publisher and Local business settings with every C20 refusal (422) in the admin and the API; the local business node; a person publisher; P8 with the logo finding; home crumb name" + requirement: "D-45" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestPublisherSettings" + status: pass + human_judgment: false + - id: D5 + description: "Pagination query and path forms, one-hop 301s with normalisation, invalid pages, page 2+ canonical, robots and suffixes, /site pagination and home_crumb, segment and suffix refusals" + requirement: "D-40" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestPagination" + status: pass + human_judgment: false + - id: D6 + description: "Custom JSON-LD refusals (invalid JSON with line/column, not an object, 16 KB, depth 8, script tag, type, price, currency), merge with the schema.merged notice, appended nodes, re-serialised storage on every locale row" + requirement: "D-44" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestCustomJSONLD" + status: pass + human_judgment: false + - id: D7 + description: "Collection, profile, registry offer (Service with offer), contact page, home SearchAction; no e-commerce node" + requirement: "D-43" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestGraphPerKind" + status: pass + human_judgment: false + - id: D8 + description: "GET /graph with findings and rules, unsaved schema preview, 401/403, primary_parent via seo_set_url_seo and apply_batch (dry run), P1 primary_gone, P2 rows" + requirement: "D-46" + verification: + - kind: integration + ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTool" + status: pass + - kind: unit + ref: "../sm-seo-plugin/operations_test.go#TestEveryAdminOperationHasTool" + status: pass + human_judgment: false + - id: D9 + description: "E8, E9 and the parent element: type options, Service grid and price backstop, JSON status with line and column, 16 KB, nodes and findings, allGood, 800 ms re-check, locale switch, failure line" + requirement: "D-44" + verification: + - kind: unit + ref: "../sm-seo-plugin/tests/elements/schema.test.ts" + status: pass + human_judgment: false + - id: D10 + description: "The new settings tabs, E8, E9 and the primary category select in the real admin SPA: layout, spacing, the local business switch hiding its fields" + verification: [] + human_judgment: true + rationale: "Rendering in the embedded admin SPA; the tests cover schemas, saves and element DOM in happy-dom, not the visual layout" + +duration: 105min +completed: 2026-11-04 +status: complete +--- + +# Phase 33 Plan 06: Structured data, breadcrumbs, hreflang and pagination Summary + +**sm-seo-plugin now answers every page with one JSON-LD @graph (page, article, profile, service, breadcrumbs, videos, WebSite, publisher), its breadcrumb trail through the primary category, hreflang with x-default, and paginated addresses in the site's form, with publisher and local business settings, the Structured data fields and graph preview in the SEO tab, and seo_get_graph through MCP.** + +## Task 4 answers: Go module legitimacy (Jakub, 2026-11-04) + +33-10 and 33-11 pin exactly these versions, each after its decision note: + +| Module | Answer | Pin | Checked by the orchestrator | +|--------|--------|-----|-----------------------------| +| `github.com/chromedp/chromedp` | **approved** | **v0.21.0** | Owned by the chromedp organisation, MIT. Dependencies: `github.com/chromedp/cdproto` and `github.com/go-json-experiment/json`. v0.21.1 (2026-11-01) and v0.22.0 (2026-11-03) exist, but both are less than two weeks old, so v0.21.0 is pinned. | +| `github.com/gen2brain/webp` | **approved** | **v0.6.4** | MIT. The only dependency is `github.com/ebitengine/purego`. Correction to the plan: the fallback without a system libwebp is libwebp transpiled to Go with wasm2go, not a WASM build run through wazero. | + +Nothing was installed in this plan; no go.mod changed. + +## Read first: versions, breaking changes, consumers + +- **Versions.** The framework base is v0.6.0 and the target v0.6.1; this plan changed no framework code (`--marker` passed). `sm-seo-plugin` stays at tag v0.1.1 and now has 15 unpushed commits on main: 9 from 33-05 and 6 from this plan. The require on summercms and the go.work/replace setup are as 33-05 left them. Nothing was pushed or tagged. +- **Resolve answer.** Only additive keys: + - top level: `html_lang`, `alternates`, `jsonld`, `breadcrumbs`, `pagination`; + - inside `seo.og`: `locale`, `locale_alternates`. + + No existing key changed. The new top-level keys are left out of the JSON when they are empty, and the layer reads a missing key as empty. +- **What a consumer will notice when moving to the next sm-seo-plugin:** + - **Migration 202610280004** adds settings columns, all with defaults. + - **New settings tabs:** Pagination, Publisher and Local business, plus a home crumb field on the General tab. + - **New SEO tab fields:** Structured data and its preview on every content type, and Primary category on article types. + - **`?page=1` now redirects.** A request for `?page=1`, or for `/strona/N` while the site uses the query form, now answers 301 where it used to answer ok. Page 2 and later now get suffixes on the title and description. This is what D-41 asks for, but sites will see it. +- **Core plugins.** Untouched: user, blog, pages, payment and journal. The vue-* layers were not changed. + +## Tracer resolve answer (TestGraphTracer, `/blog/first-post`) + +For comparison with the layer fixtures of 33-07 to 33-09: + +```json +{ + "action": "ok", + "alternates": [ + {"hreflang": "en", "url": "https://www.example.test/blog/first-post"}, + {"hreflang": "pl", "url": "https://www.example.test/pl/blog/pierwszy-wpis"}, + {"hreflang": "x-default", "url": "https://www.example.test/blog/first-post"} + ], + "breadcrumbs": [ + {"name": "Home", "path": "/", "url": "https://www.example.test/"}, + {"name": "Recipes", "path": "/blog/category/recipes", "url": "https://www.example.test/blog/category/recipes"}, + {"name": "First post", "path": "/blog/first-post", "url": "https://www.example.test/blog/first-post"} + ], + "html_lang": "en", + "jsonld": { + "@context": "https://schema.org", + "@graph": [ + {"@id": "https://www.example.test/blog/first-post#webpage", "@type": "WebPage", + "breadcrumb": {"@id": "https://www.example.test/blog/first-post#breadcrumb"}, + "dateModified": "2026-10-05T12:30:00Z", "datePublished": "2026-10-01T08:00:00Z", + "description": "About the first post", "inLanguage": "en", + "isPartOf": {"@id": "https://www.example.test/#website"}, "name": "First post", + "primaryImageOfPage": {"@type": "ImageObject", "url": "https://www.example.test/media/first.jpg"}, + "url": "https://www.example.test/blog/first-post"}, + {"@id": "https://www.example.test/blog/first-post#article", "@type": "BlogPosting", + "articleSection": "Recipes", + "author": [{"@id": "https://www.example.test/blog/author/jan#profile", "@type": "Person", "name": "Jan Kowalski", "url": "https://www.example.test/blog/author/jan"}], + "dateModified": "2026-10-05T12:30:00Z", "datePublished": "2026-10-01T08:00:00Z", + "description": "About the first post", "headline": "First post", + "image": ["https://www.example.test/media/first.jpg"], "inLanguage": "en", + "isPartOf": {"@id": "https://www.example.test/blog/first-post#webpage"}, + "mainEntityOfPage": {"@id": "https://www.example.test/blog/first-post#webpage"}, + "publisher": {"@id": "https://www.example.test/#organization"}, "wordCount": 420}, + {"@id": "https://www.example.test/blog/first-post#breadcrumb", "@type": "BreadcrumbList", + "itemListElement": [ + {"@type": "ListItem", "item": "https://www.example.test/", "name": "Home", "position": 1}, + {"@type": "ListItem", "item": "https://www.example.test/blog/category/recipes", "name": "Recipes", "position": 2}, + {"@type": "ListItem", "item": "https://www.example.test/blog/first-post", "name": "First post", "position": 3}]}, + {"@id": "https://www.example.test/blog/first-post#video-1", "@type": "VideoObject", + "description": "About the first post", "embedUrl": "https://www.youtube.com/embed/dQw4w9WgXcQ", + "isPartOf": {"@id": "https://www.example.test/blog/first-post#webpage"}, "name": "First post", + "thumbnailUrl": ["https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"], + "uploadDate": "2026-10-01T08:00:00Z", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}, + {"@id": "https://www.example.test/#website", "@type": "WebSite", "inLanguage": "en", "name": "Acme Blog", + "publisher": {"@id": "https://www.example.test/#organization"}, "url": "https://www.example.test/"}, + {"@id": "https://www.example.test/#organization", "@type": "Organization", "name": "Acme", "url": "https://www.example.test/"} + ] + }, + "locale": "en", + "outbound": {"new_tab": true, "nofollow_domains": [], "sponsored_domains": []}, + "seo": { + "canonical": "https://www.example.test/blog/first-post", + "description": "About the first post", + "og": {"description": "About the first post", "image": "https://www.example.test/media/first.jpg", + "locale": "en_US", "locale_alternates": ["pl_PL"], "title": "First post | Acme Blog", + "url": "https://www.example.test/blog/first-post"}, + "robots": "index, follow", "title": "First post | Acme Blog", + "twitter": {"card": "summary_large_image", "site": ""}, "verification": [], "x_robots_tag": "index, follow" + }, + "staging": false, + "status": 200, + "url": "https://www.example.test/blog/first-post" +} +``` + +A page-3 answer in the query form adds `"pagination": {"form": "query", "invalid": false, "page": 3, "param": "page", "segment": ""}`. Its title is `Recipes – page 3 of %pages% | Acme Blog` and its description ends with `Page 3 of %pages%.` A garbage page value answers `not_found` with `"pagination": {..., "invalid": true}`. + +## Performance + +- **Duration:** about 105 min (11:56Z to 13:41Z) +- **Tasks:** 4: a tracer, two TDD tasks and the checkpoint, which Jakub answered in parallel. +- **Commits:** 6 in sm-seo-plugin (main, not pushed). The planning docs are committed in summercms.go. + +## Accomplishments + +- **classes/schema.** A pure package with: + - `Build`: one node builder per type; + - `Validate`: Google's required and recommended properties per type, with `rules` false for types Google has no rich-result rules for; + - `Normalize`, `DecodeCustom` and `Merge`: the C17 server rules for the structured data value and custom JSON-LD. +- **Resolve.** + - The breadcrumb trail goes through the primary parent: the stored choice, else the first parent, recursively up to 6 levels. + - Alternates list each translated locale, include x-default, and come with `html_lang` and the og locales. + - Pagination: the page form is parsed and folded into the one-hop decision, with invalid pages, suffixes, a self canonical and paged alternates. +- **Settings.** + - New Publisher, Local business and Pagination tabs and the home crumb field. + - The four-place pattern (model, writer, API, form) is applied to every new column, with all C20 refusals. + - P8 shows the generated publisher node with its findings. +- **SEO tab.** + - E8 (`seoschema`), E9 (widget `seo_graph`) and Primary category (`seoparent`). + - The `primary_parent` and `schema` values are stored in the entry data on every locale row. + - Saves answer the `schema.merged` notice. + - P1 gains `primary_gone`; P2 gains rows for the schema type and the breadcrumb trail. +- **MCP.** + - `seo_get_graph` (`GET /_seo/api/v1/graph?url=&schema=`). + - `seo_set_url_seo` and `apply_batch` accept `primary_parent` and `schema`. + - `seo_update_settings` documents every new key. + - The parity row `widget_action:seo_graph` maps to `get_graph`. + +## Task Commits + +sm-seo-plugin (branch main, not pushed): + +1. **Task 1 (tracer):** `ea2ce24` feat: JSON-LD graph, breadcrumbs and hreflang in the resolve answer (tracer) +2. **Task 2:** + - `fde5024` feat: publisher, local business and home crumb settings (group A) + - `032d3da` feat: pagination forms and suffixes in resolve and site (group B) +3. **Task 3:** + - `17da132` feat: graph validation and custom JSON-LD (groups A and C, plus the Go side of B) + - `10ff628` feat: structured data, graph preview and primary category in the SEO tab (group B: elements and dist) + - `d38049a` docs: structured data, breadcrumbs, hreflang and pagination in README (group D) +4. **Task 4:** no commit. The checkpoint answers are recorded at the top of this file. + +The tracer gate ran in interactive end-of-phase mode. `TestGraphTracer` was re-run and passed before the expansion tasks started. + +## Decisions Made + +See `key-decisions` in the frontmatter. + +## Deviations from Plan + +### Adapted to the actual repositories + +**1. [Plan shape] Fake content plugin `acme.magazine` instead of extending the fixture's pages plugin.** +- **Why:** `addresses_test.go` already registers an `acme.blog` plugin. +- **What I did:** the tracer and the per-kind tests use an in-memory plugin `acme.magazine` with three types: `acme-story` (article), `acme-section` (collection) and `acme-writer` (profile). It uses every 33-01 fact. fixture_test.go is unchanged. + +**2. [Plan shape] Commit groups.** +- **Validator moved earlier:** validate.go landed in Task 2 group A, because P8 needs the findings. +- **Groups 3A and 3C are one commit**, together with the Go side of 3B (the SEO tab fields, the action, P1 and P2). The operations parity test binds the new `seo_graph` widget action to its tool, and the Go tests drive the SEO tab. +- **Group 3B** is the element commit: elements, labels, styles, dist and vitest. + +**3. [Plan shape] `home_crumb` shape.** +- **The conflict:** the plan says "home_crumb per locale". The committed 33-08 layer reads `home_crumb.name` and `.url` (`SeoHtmlSitemapPage.vue`, `types.ts`). +- **What I did:** the site answer is `{"name": , "url": /, "names": {: }}`. This serves both. + +**4. [Rule 3 - Blocking] Repeater rows cannot have a trigger.** +- **The limit:** cabana refuses `trigger` on `type: repeater`. +- **Effect:** `local_hours` stays visible when the Local business switch is off. Every other local field hides as planned. +- **Logged** in deferred-items.md. + +**5. [Rule 2 - Correctness] Additions outside the plan's list.** +- `classes/structured.go`: input composition, graph answers, the E9 action and the merge notices. +- `classes/owner.go`: `seo_set_url_seo` checks the new keys. +- `classes/history.go`: data values in history. +- `classes/sitemap_filter.go`: signature only. The production resolver skips the graph. +- New copy keys: + - `settings.url_invalid` and `settings.email_invalid` (website and email checks, T-33-25); + - `api.record_only`; + - `schema.field_price`, `schema.field_currency`, `schema.price_invalid` and `schema.currency_invalid`; + - `graphFailed` in E9 (the host gives the element no server message). +- Hreflang of page 2 and later points at the same page of each translation. + +**6. [Plan shape] Files not touched.** These files from the plan's list needed no change: +- `updates/registry.go` (migrations register themselves); +- `controllers/settings/config_form.yaml`; +- `extension/_seo_status.htm` and `_seo_effective.htm`, which render the new callout and rows unchanged. + +**7. [Plan shape] `field_types.go` (root) has no new lines.** The new types are registered through `classes.FieldTypes`, which the root file returns. The acceptance grep counts 2 lines in `classes/field_types.go`. + +**8. [Rule 1 - Bug] The seoparent value round trip.** +- **Bug:** the admin sends the loaded `{selected, options}` object back when the editor leaves the field untouched. +- **Fix:** Decode reads `selected` from it. + +**9. [Plan shape] E8 default for registry offers.** The element cannot know a registry page's kind, so the default option of a page-kind record reads "Default (WebPage)" even when the server's default is Service (offer and service kinds). The server applies Service correctly. + +### TDD note + +The plan is `type: execute` with `tdd="true"` tasks. Each group's tests were written alongside its code and run before the commit. The failures fixed before committing were: +- the local type copy keys; +- a nil `Settings{}` in TestTemplates; +- the tab expectations; +- the site JSON field order; +- the `<` check. + +No separate RED commit was made: the plan needs green tests at every commit, as in 33-04 and 33-05. + +### Auth gates + +None. + +**Total deviations:** 9. Five are plan shape, one is a blocking framework limit, one is a correctness addition, one is a bug and one is a UI default note. +**Impact on plan:** every must-have truth is met. The only exception is the visual hiding of the opening-hours repeater. + +## Issues Encountered + +- **Unvalidated per-locale values.** For the translatable settings (`page_segment`, the suffixes, `breadcrumb_home`, `publisher_name`), the admin's other-locale values go through the translation writer without Decode, so only the default locale's value is validated on save. An invalid segment in another locale falls back to the default segment when it is read. +- **Undo does not restore the new data values.** Undo of an entry history row restores the Phase 32 columns, not `primary_parent` or `schema`. History does record both. +- **`go test ./... -run X`** prints "no tests to run" for subpackages while the named root tests pass, as in 33-02 to 33-05. + +## Known Stubs + +None. + +## Threat Flags + +None outside the plan's threat model: +- **T-33-24** (custom JSON-LD): parse, objects only, 16 KB and depth 8, `