test(32): verification report and human verification items as UAT

This commit is contained in:
2026-10-27 16:12:00 +01:00
parent 6472f2861e
commit adf5cccc4a
2 changed files with 320 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
---
status: testing
phase: 32-seo-foundation
source: [32-VERIFICATION.md]
started: 2026-10-27T15:12:00.387Z
updated: 2026-10-27T15:12:00.387Z
---
## Current Test
number: 1
name: MVP user-flow walk-through in the demo admin, built from the CURRENT (unreleased) heads: framework 6472f28, sm-seo-plugin ad419e9, sm-journal-plugin a6fc7cd. Open a Journal post, then a category, then a tag; open the SEO tab; watch the SERP preview load; fill title, description and keyphrase; save; change the post's slug and save; open SEO > Redirects; open SEO > robots.txt, edit, and use the tester; open Settings > SEO and check the per-type tabs. Then GET /_seo/api/v1/resolve?url=<old and new post URL>.
expected: |
The SEO tab shows on posts, categories and tags. The SERP preview leaves its skeleton and shows the card, counters and duplicate warnings. Saving shows values back. The slug change shows the redirects.auto_created notice and a 301 old->new row appears. The robots tester answers allowed/blocked with the matching line. Settings shows General, one tab per content type, Indexing, Addresses, Verification. Resolve answers redirect 301 for the old URL and the effective SEO with sources for the new one. Also check the E1 320px and E2 360px layout items above.
awaiting: user response
## Tests
### 1. MVP user-flow walk-through in the demo admin, built from the CURRENT (unreleased) heads: framework 6472f28, sm-seo-plugin ad419e9, sm-journal-plugin a6fc7cd. Open a Journal post, then a category, then a tag; open the SEO tab; watch the SERP preview load; fill title, description and keyphrase; save; change the post's slug and save; open SEO > Redirects; open SEO > robots.txt, edit, and use the tester; open Settings > SEO and check the per-type tabs. Then GET /_seo/api/v1/resolve?url=<old and new post URL>.
expected: The SEO tab shows on posts, categories and tags. The SERP preview leaves its skeleton and shows the card, counters and duplicate warnings. Saving shows values back. The slug change shows the redirects.auto_created notice and a 301 old->new row appears. The robots tester answers allowed/blocked with the matching line. Settings shows General, one tab per content type, Indexing, Addresses, Verification. Resolve answers redirect 301 for the old URL and the effective SEO with sources for the new one. Also check the E1 320px and E2 360px layout items above.
result: [pending]
### 2. MCP session from a real Claude client linked to an admin with golem15.seo.manage: get_url_seo for a post URL; set_url_seo noindex by URL; apply_batch with dry_run true (include a redirect that would create a loop), then without the loop; undo_batch from history.
expected: The dry run returns the diff and conflicts and writes nothing. The loop is refused with redirect_loop. The applied batch is undone as a whole. A read-only grant lists only GET tools.
result: [pending]
### 3. Release decision for the post-review fixes: framework v0.5.2 (7 code commits after v0.5.1, unpushed), sm-seo-plugin v0.1.1 (20 fix commits after v0.1.0 plus a new migration, unpushed), sm-journal-plugin v0.2.6 (3 commits after v0.2.5, unpushed), then the demo bump.
expected: Jakub decides to release or hold. Until then, the released and mounted versions carry the 4 critical findings listed under 'Goal items that depend on unreleased fixes'.
result: [pending]
### 4. Confirm the meaning chosen for pact.RecordSaved.Locale in WR-02: the primary tag of the request locale, not the default content locale.
expected: Accept, or ask for the content-locale meaning before v0.5.2 freezes it
result: [pending]
### 5. Review the logic of the fixes the fixer flagged: WR-08 (exact path?query wins over path-only), WR-09 (address-change redirect validation and in-memory loop simulation), WR-19 (generation job routes an unseen URL change through AddressChanged), WR-22 (coversSQL as the SQL form of exactMatches).
expected: The matching and redirect-graph semantics are confirmed correct. DB tests pass: TestExactRedirectKeys, TestAddressChangeGuards, TestGenerateEntries, TestCoverAndChainFilters.
result: [pending]
### 6. Decide WR-03 (skipped): a failing extension Load after a successful save rolls back the whole save, and show fails the whole record.
expected: Either accept it as a known limitation for v0.5.x, or schedule an 'extension unavailable' state (a todo or a Phase 33 or later item).
result: [pending]
### 7. Judgment-tier prohibitions of 32-06: (a) nothing was pushed, tagged, deployed or posted without Jakub's go-ahead, and no version was hard-coded before the checkpoint; (b) the changelog token was never printed, copied or minted.
expected: Confirm, based on 32-06-release-log.md. The log records 'Task 4 answer (Jakub, 2026-10-27)' before step 1 and says the token was read by the script and never printed.
result: [pending]
## Summary
total: 7
passed: 0
issues: 0
pending: 7
skipped: 0
blocked: 0
## Gaps

View File

@@ -0,0 +1,264 @@
---
phase: 32-seo-foundation
verified: 2026-10-27T15:09:40Z
status: human_needed
score: 5/5 roadmap success criteria verified (plan must-haves: all automatable truths verified; 2 layout backstops and the UI-SPEC visual states routed to human)
covered_files:
- .planning/phases/32-seo-foundation/32-01-PLAN.md
- .planning/phases/32-seo-foundation/32-01-SUMMARY.md
- .planning/phases/32-seo-foundation/32-02-PLAN.md
- .planning/phases/32-seo-foundation/32-02-SUMMARY.md
- .planning/phases/32-seo-foundation/32-03-PLAN.md
- .planning/phases/32-seo-foundation/32-03-SUMMARY.md
- .planning/phases/32-seo-foundation/32-04-PLAN.md
- .planning/phases/32-seo-foundation/32-04-SUMMARY.md
- .planning/phases/32-seo-foundation/32-05-PLAN.md
- .planning/phases/32-seo-foundation/32-05-SUMMARY.md
- .planning/phases/32-seo-foundation/32-06-PLAN.md
- .planning/phases/32-seo-foundation/32-06-SUMMARY.md
- admin/src/components/form/fields/WidgetField.vue
- modules/cabana/actions.go
- modules/cabana/contracts.go
- modules/cabana/crud.go
- modules/cabana/deferred.go
- modules/cabana/field_ml.go
- modules/cabana/field_type.go
- modules/cabana/field_type_upload.go
- modules/cabana/form_extensions.go
- modules/cabana/http.go
- modules/cabana/partial_render.go
- modules/cabana/record_events.go
- modules/cabana/relation.go
- modules/cabana/relation_child.go
- modules/cabana/reorder.go
- modules/pact/capabilities.go
- modules/pact/content.go
- modules/pact/record_events.go
- modules/signpost/content.go
- modules/signpost/signpost.go
- scripts/apisurface/main.go
- scripts/check-phase32.sh
covered_digest: "v2:sha256:3d160533886964f919d0b009ef28fe87e9b314c89b991e50bdff020197a3e7b3"
behavior_unverified: 2
overrides_applied: 0
behavior_unverified_items:
- truth: "E1 counter line at 320px container width: status text and count wrap onto two lines without overlap (32-06 backstop)"
test: "Open a Journal post's SEO tab in the admin, narrow the form column to about 320px, type a long SEO title"
expected: "The status text and the character/pixel count wrap onto two lines and do not overlap"
why_human: "tests/elements/text.test.ts asserts the CSS contract (flex, wrap, gap, no fixed width) under happy-dom, which has no layout engine; actual wrapping is not observable in the test"
- truth: "E2 at 360px: the two segmented groups stack and no segment label is clipped in either language (32-06 backstop)"
test: "Open the SEO tab's robots control at about 360px width in en and in pl"
expected: "Index and Follow groups stack on separate lines; no segment label is clipped"
why_human: "tests/elements/robots.test.ts checks the wrap rules, not rendered geometry"
human_verification:
- test: "MVP user-flow walk-through in the demo admin, built from the CURRENT (unreleased) heads: framework 6472f28, sm-seo-plugin ad419e9, sm-journal-plugin a6fc7cd. Open a Journal post, then a category, then a tag; open the SEO tab; watch the SERP preview load; fill title, description and keyphrase; save; change the post's slug and save; open SEO > Redirects; open SEO > robots.txt, edit, and use the tester; open Settings > SEO and check the per-type tabs. Then GET /_seo/api/v1/resolve?url=<old and new post URL>."
expected: "The SEO tab shows on posts, categories and tags. The SERP preview leaves its skeleton and shows the card, counters and duplicate warnings. Saving shows values back. The slug change shows the redirects.auto_created notice and a 301 old->new row appears. The robots tester answers allowed/blocked with the matching line. Settings shows General, one tab per content type, Indexing, Addresses, Verification. Resolve answers redirect 301 for the old URL and the effective SEO with sources for the new one. Also check the E1 320px and E2 360px layout items above."
why_human: "Visual rendering and the real WidgetField host. CR-04 showed that the fake-host element tests missed a real-admin deadlock in the released v0.5.1 + v0.1.0, so the fix (framework d660cd0 and plugin 3ecbd1f) has never been observed in a real browser."
- test: "MCP session from a real Claude client linked to an admin with golem15.seo.manage: get_url_seo for a post URL; set_url_seo noindex by URL; apply_batch with dry_run true (include a redirect that would create a loop), then without the loop; undo_batch from history."
expected: "The dry run returns the diff and conflicts and writes nothing. The loop is refused with redirect_loop. The applied batch is undone as a whole. A read-only grant lists only GET tools."
why_human: "External client (32-VALIDATION.md manual-only row). TestSEOToolsCatalog and TestMCPSetURLSeoTracer prove the catalog and the routes, not a live client session."
- test: "Release decision for the post-review fixes: framework v0.5.2 (7 code commits after v0.5.1, unpushed), sm-seo-plugin v0.1.1 (20 fix commits after v0.1.0 plus a new migration, unpushed), sm-journal-plugin v0.2.6 (3 commits after v0.2.5, unpushed), then the demo bump."
expected: "Jakub decides to release or hold. Until then, the released and mounted versions carry the 4 critical findings listed under 'Goal items that depend on unreleased fixes'."
why_human: "Pushing and tagging need Jakub's go-ahead (CLAUDE.md)."
- test: "Confirm the meaning chosen for pact.RecordSaved.Locale in WR-02: the primary tag of the request locale, not the default content locale."
expected: "Accept, or ask for the content-locale meaning before v0.5.2 freezes it"
why_human: "Public API semantics inside a patch release; the fixer flagged it for human confirmation."
- test: "Review the logic of the fixes the fixer flagged: WR-08 (exact path?query wins over path-only), WR-09 (address-change redirect validation and in-memory loop simulation), WR-19 (generation job routes an unseen URL change through AddressChanged), WR-22 (coversSQL as the SQL form of exactMatches)."
expected: "The matching and redirect-graph semantics are confirmed correct. DB tests pass: TestExactRedirectKeys, TestAddressChangeGuards, TestGenerateEntries, TestCoverAndChainFilters."
why_human: "Semantic correctness of matching and graph rules beyond what the tests encode (32-REVIEW-FIX.md asks for a human review)."
- test: "Decide WR-03 (skipped): a failing extension Load after a successful save rolls back the whole save, and show fails the whole record."
expected: "Either accept it as a known limitation for v0.5.x, or schedule an 'extension unavailable' state (a todo or a Phase 33 or later item)."
why_human: "Needs a design decision (32-REVIEW-FIX.md, Skipped Issues)."
- test: "Judgment-tier prohibitions of 32-06: (a) nothing was pushed, tagged, deployed or posted without Jakub's go-ahead, and no version was hard-coded before the checkpoint; (b) the changelog token was never printed, copied or minted."
expected: "Confirm, based on 32-06-release-log.md. The log records 'Task 4 answer (Jakub, 2026-10-27)' before step 1 and says the token was read by the script and never printed."
why_human: "unverified-prohibition, human review recommended. The verifier's verdict (consistent with the release log) is a non-authoritative LLM judgment."
---
# Phase 32: SEO foundation Verification Report
**Phase Goal:** As a site editor on any SummerCMS application, I want per-record and global SEO in the admin, with redirects and robots.txt, all manageable through MCP, so that every site ships correct, editable SEO without per-site work. The framework gains only generic seams; the SEO code lives in a new shared plugin.
**Verified:** 2026-10-27T15:09:40Z
**Status:** human_needed
**Re-verification:** No (initial verification)
**Mode:** mvp
**MVP format note:** `user-story.validate` answers `valid: false` ("Story must include ', I want to [capability],'"). The goal reads "I want per-record and global SEO …" with no "to". The narrowing rule was still applied, using the `[outcome]` clause ("every site ships correct, editable SEO without per-site work"), because the roadmap SCs give a precise contract. To meet the canonical regex, reword the goal through `/msd mvp-phase 32`, for example "I want to manage per-record and global SEO …".
## User Flow Coverage
| # | Step of the story | Expected | Evidence in codebase | Status |
|---|---|---|---|---|
| 1 | The editor opens a record of any registered content type | The SEO tab is there, and the content plugin does not import the SEO plugin | `extensions.go:27` builds one form extension per `reg.ContentTypes()`; Journal declares 3 `Content: &pact.ContentDescriptor` (`classes/menu_item_types.go:124,148,182`); marker stage: "journal: imports neither sm-seo-plugin nor sm-sitemap-plugin"; demo `TestSEOTabMounted` passes for posts (tabs Edit, Manage, Categories, SEO), categories and tags | ✓ |
| 2 | The editor edits per-record and global SEO | Values stored per (model type, record id, locale); settings with per-type tabs | `TestSEOTabTracer`, `TestSEOTab`, `TestSettingsTypeTabs`, `TestSEOTabPermission` pass (Docker) | ✓ (visual: human) |
| 3 | The frontend asks for the effective SEO of a URL | URL rule > record > global > derived, with sources | `GET /_seo/api/v1/resolve` → `Resolver.Resolve` (`controllers/api/resolve.go:31`); `TestResolverPrecedence` (unit, all layer masks, plus DB) passes | ✓ |
| 4 | The editor changes a slug | 301 old→new, chains flattened, loops refused | `TestSlugChange301`, `TestRedirectGraph` (back-and-forth), `TestAddressChangeFromEvent`, `TestLoopRefused` (admin, MCP, batch, import) pass | ✓ |
| 5 | The editor manages robots.txt and the AI bots | Served file, tester, staging lock, 11 bots incl. Claude-User | `TestRobotsServed`, `TestRobotsTester`, `TestRobotsMatcherGoogleExamples`, `classes/bots.go:27-37` | ✓ (tester in a real admin: human, CR-04) |
| 6 | An agent does all of it through MCP | One tool per admin operation, dry run, undo | 29 tools in `mcp.go`; `TestEveryAdminOperationHasTool` derives the operations from the admin controllers; `TestBatch`, `TestUndo`, `TestConcurrentUndo`; demo `TestSEOToolsCatalog` passes | ✓ (live client: human) |
| Outcome | "every site ships correct, editable SEO without per-site work" | The plugin mounts by `summer.yaml` alone; the framework stays generic | The demo mounts `golem15.seo` with no app code beyond the mount (`9451b31`, generated `plugins.gen.go`); marker: no SEO names under `modules/` or `docs/`. Frontend emission of the tags (Nuxt layer) is Phase 33 (D-97) by design | ✓ (backend side) |
## Goal Achievement
### Observable Truths (Roadmap Success Criteria)
| # | Truth | Status | Evidence |
|---|---|---|---|
| SC-1 | A plugin can add a field between two fields of another plugin's admin form (A, B plus C after A renders A, C, B), and add a tab between two existing tabs; unknown anchors, duplicate names and anchor cycles stop the boot with a clear error | ✓ VERIFIED | `compileFormExtensions(reg, plugins)` is wired at `modules/cabana/http.go:120`. I ran the following in this session: `TestFormExtensionPlacement` (subtests `C_after_A_gives_A_C_B`, `a_new_tab_between_two_tabs`, `a_tab-less_form_gains_the_default_tab_then_the_new_tab`), `TestFormExtensionBootErrors` (`unknown_field_anchor`, `unknown_tab_anchor`, `duplicate_field_name_*`, `duplicate_tab_key_*`, `an_anchor_cycle`, `an_anchor_outside_Requires()`, `a_fileupload_field`, zone refusals) and both matrices: all pass. `TestFormExtensionTracer` (Postgres) asserts the schema `[a c b]` and that the owner row is unchanged. The SPA backstop (FormView.test.ts, "form extensions in compiled order (UI-SPEC C1, D-91)") passes. |
| SC-2 | Journal posts, categories and tags show the SEO tab without importing `sm-seo-plugin`, and the frontend gets the effective SEO of any URL with the precedence URL rule > record > global > derived | ✓ VERIFIED | Demo `TestSEOTabMounted` (tagged) passes for posts, categories and tags. The marker stage passes the Journal import check. `TestResolverPrecedence` enumerates every layer mask per value. Note: per P-14 (32-03-PLAN), the per-type and general templates form the "global" layer above content, while the site-wide default description and default image are last-resort fallbacks after content. The approved UI-SPEC copy `settings.default_description_comment` states this. It is a documented reading of D-02, not a deviation. |
| SC-3 | Changing a record's slug creates a 301 and flattens existing chains; redirect loops are refused from the admin, MCP and imports | ✓ VERIFIED | Behavioral: `TestSlugChange301` (auto_created notice, 301 old→new, /pages/x flattened, pl locale), `TestAddressChangeFromEvent` (writers outside the admin), the back-and-forth case in `classes/redirect_graph_test.go:87` and `redirects_test.go:293`, and `TestLoopRefused` (admin 422 redirects.loop with nothing saved; MCP `redirect_loop`; MCP batch; CSV import preview `row_loop` and apply `import_refused`). Post-review hardening: `TestAddressChangeGuards` and `TestConcurrentRedirectWrites`. All pass. |
| SC-4 | Every SEO operation in the admin is available as an MCP tool, and a batch change can be previewed with dry-run and undone from history | ✓ VERIFIED | `TestEveryAdminOperationHasTool` derives the required operations from the admin controllers' config, actions and extensions (at least 20 operations) and checks that each one maps to a declared tool bound to a registered `/_seo/api/v1/` route. `TestMCPReadToolsAreGET` passes. All 29 management handlers go through `manage()` (401 without an admin, 403 without `golem15.seo.manage`). `TestBatch` checks that a dry run leaves a snapshot of every table unchanged and that a loop refuses the whole batch. `TestUndo` covers stale, loop, gone and batch cases, and `TestConcurrentUndo` passes. |
| SC-5 | The framework release with the seams is tagged, and the new code has unit tests, delivered in the phase's last plan | ✓ VERIFIED | `v0.5.1` (tag object aa6e0a5, commit 15b0d0d) is on origin (`git ls-remote` confirmed); `v0.1.0` (9cf0447) and `v0.2.5` are on origin; the demo go.mod requires all three and its submodules are at those tags. The 32-06 unit tests exist and pass: cabana matrices, `TestRecordEventDiff`, signpost `TestContent`, plugin `./classes` (resolver, graph, normalise, robots, templates, CSV, owner, issues) and 77 vitest element tests. |
**Score:** 5/5 roadmap success criteria verified (0 present-but-behavior-unverified among the SCs).
### Plan must-haves (summary)
| Plan | Truths | Result |
|---|---|---|
| 32-01 (D-91 seam) | 15 incl. 1 backstop | All verified by `TestFormExtension*` (unit and Postgres) and the FormView C1 backstop. "admin/src unchanged by this plan" holds at v0.5.1 (`git diff v0.5.0 v0.5.1 -- admin/src` is empty). WidgetField.vue changed later in the CR-04 fix, outside this plan. |
| 32-02 (D-92, D-93, Journal) | 11 | Verified: `TestRecordEvents` (create, update, translated slug, rollback, failing listener, reorder, child write, hasMany link and unlink, deferred children, delete, bulk delete), `TestRecordSavedDescribesNewURL`, `TestAPIRecordEvents`, `TestContentDescriptors(Matrix)`, `TestJournalTagType(Pages)`, `TestTranslatedSlug`. |
| 32-03 (plugin core) | 32 | Server-side truths verified (`TestSEOTabTracer`, `TestSEOTab`, `TestSEOTabPermission`, `TestSettingsTypeTabs`, `TestResolverPrecedence`, `TestHistoryRecordsAdminWrite`, `TestEntryDeletedWithRecord`, `TestGenerateEntries`, `TestRuleFormPreview`). The UI-SPEC visual states (skeletons, callouts, badges, truncation) go to the human walk-through. |
| 32-04 (redirects, 404, robots) | 21 | Verified: `TestRedirectEngine`, `TestExactRedirectKeys`, `TestNormalize(Matrix)`, `TestStrictURL`, `TestOneHop`, `TestNotFoundLog`, `TestRobotsServed`, `TestRobotsTester`, `TestRobotsScreen`, `TestVerificationFile`, `TestCoverAndChainFilters`. No root route for verification files (`routes.go:36-41`). The staging lock reads only `cfg.Environment()` (`classes/resolver.go:143`). The UI list and screen visuals go to human. |
| 32-05 (MCP, batch, CSV, gate) | 14 | Verified: `TestMCPSetURLSeoTracer`, `TestManagementAPI`, `TestIssues` (8 kinds in `classes/issues.go:15-23`), `TestCSVTransfer`, `TestBatch`, `TestUndo`. The demo mount was later done for real (`9451b31`). `check-phase32.sh` has every stage. |
| 32-06 (tests, release) | 8 plus 8 backstops | 6 backstops verified by passing vitest (E1 thresholds, E2 grammar, E3 title and truncation, E4 lint per rule, E6 gating, E3 stem matching). 2 layout backstops (E1 320px, E2 360px) are ⚠️ PRESENT_BEHAVIOR_UNVERIFIED. Release truths verified against git and the release log. |
### Prohibitions
| Prohibition | Tier | Disposition |
|---|---|---|
| No breaking framework API change (32-01, 32-02, 32-06) | test | ✓ `check-phase32.sh --additive` passes (HEAD vs v0.5.1: +0 lines in cabana, pact, signpost). The release log records v0.5.1 vs v0.5.0: +2, +87, +8 lines, nothing removed. The self-test proves the stage refuses removals, signature changes and new interface methods. `--compat`: 23 siblings OK. |
| Extensions never write owner columns, translation rows or attachments | test | ✓ `TestFormExtensionTracer` (owner row asserted), `a_fileupload_field` boot error |
| Framework and docs name no SEO concept or consuming app (D-90) | test | ✓ marker stage |
| No record event for an uncommitted write | test | ✓ `TestRecordEvents/rollback` |
| Content plugins never import sm-seo-plugin; SEO data not in content tables | test | ✓ marker stage; `golem15_seo_entries` table |
| Resolver never answers unpublished SEO and never uses the Host header | test | ✓ `a_draft_is_not_found`; no `r.Host` use in the plugin |
| Non-production lock not switchable from admin or MCP | test | ✓ `Staging()` reads only the environment; `the_staging_lock` |
| Loops never saved (admin, MCP, import) | test | ✓ `TestLoopRefused` |
| /robots.txt never 5xx | test | ✓ `TestRobotsServed` (see IN-13 in Anti-Patterns) |
| Regex capture or query never makes an internal target external | test | ✓ `redirects_test.go:314-328`, `:423` |
| Batch or import never partial | test | ✓ `TestBatch`, `TestLoopRefused/import` |
| Read grant can't write; every management route needs golem15.seo.manage | test | ✓ `TestMCPReadToolsAreGET`; all 29 handlers wrap `manage()` |
| Undo never silently overwrites a newer change | test | ✓ `TestUndo` (undo_stale), `TestConcurrentUndo` |
| Nothing pushed, tagged or posted without Jakub's go-ahead | judgment | ⚠️ flagged (unverified-prohibition, human review recommended). The LLM verdict is that the release log is consistent with it. |
| Changelog token never printed, copied or minted | judgment | ⚠️ flagged (same) |
### Required Artifacts
| Artifact | Status | Details |
|---|---|---|
| `modules/pact/capabilities.go` (`Form *AdminFormExtension`) | ✓ VERIFIED | line 436; types at line 518 and onward |
| `modules/cabana/form_extensions.go` | ✓ VERIFIED | 1591 lines; compile, placement, save and hydrate |
| `modules/pact/record_events.go`, `content.go` | ✓ VERIFIED | RecordSaved, RecordDeleted, ContentDescriptor |
| `modules/cabana/record_events.go` | ✓ VERIFIED | `EmitRecordSaved`; emitted at `crud.go:971` inside the save transaction (after commit) |
| `modules/signpost/content.go` | ✓ VERIFIED | `MatchPath` and the rest; `TestContent` |
| `../sm-journal-plugin/classes/content_descriptors.go` | ✓ VERIFIED | 3 descriptors wired in `menu_item_types.go` |
| `../sm-seo-plugin/*` (plugin.go, extensions.go, classes/resolver.go, migrations, assets/dist/seo.js, mcp.go, operations.go, classes/batch.go, classes/undo.go, classes/redirect_graph.go, classes/addresses.go, classes/robots.go) | ✓ VERIFIED | All present and substantive; `check-dist.sh`: "assets/dist matches a fresh build" |
| `.planning/notes/2026-10-27-robots-txt-grobotstxt.md` and the STACK.md row | ✓ VERIFIED | D-99 |
| `scripts/check-phase32.sh`, `scripts/apisurface/main.go` | ✓ VERIFIED | `--all` exit 0 in this session |
| `32-05-demo-mount.patch`, `../sm-demo-app/seo_mcp_test.go` | ✓ VERIFIED | applied in `9451b31`; `TestSEOToolsCatalog` passes |
### Key Link Verification
| From | To | Via | Status |
|---|---|---|---|
| cabana `http.go` | `form_extensions.go` | `compileFormExtensions(reg, plugins)` (line 120) | ✓ WIRED |
| cabana `crud.go` | the extension's Save | `saveFormExtensions` in the save transaction (line 965) → `ext.def.Save(ctx, pact.AdminFormSave{…})` (form_extensions.go:1394) | ✓ WIRED |
| cabana `crud.go` | `record_events.go` | `emitRecordSaved(ctx, s.app, tx, …)` (line 971) | ✓ WIRED |
| docs/backend/admin-controllers.md | example_form_extension_test.go | `src=` snippet (line 561); docs stage passes | ✓ WIRED |
| Journal `controllers/api/posts.go` | `cabana.EmitRecord*` | lines 237, 239, 305, 307, 349 | ✓ WIRED |
| Journal `menu_item_types.go` | `pact.ContentDescriptor` | `Content: &pact.ContentDescriptor` ×3 | ✓ WIRED |
| seo `extensions.go` | signpost `ContentTypes()` | line 27 | ✓ WIRED |
| seo `controllers/api/resolve.go` | `Resolver.Resolve` | line 31 | ✓ WIRED |
| seo `listeners.go` | `*pact.RecordDeleted` / `*pact.RecordSaved` | lines 23, 30; `AddressChanged` at line 65 | ✓ WIRED |
| seo `extensions.go` | `AddressChanged` | line 337 (in the save transaction) | ✓ WIRED |
| seo `routes.go` | `/robots.txt` | line 33, throttled, with no backend middleware | ✓ WIRED |
| seo `mcp.go` | `routes.go` | every tool's Method+Path is a registered route (parity test) | ✓ WIRED |
| `check-phase32.sh` | apisurface / Journal worktree compat | additive and compat stages pass | ✓ WIRED |
| `sm-demo-app/go.mod` | sm-seo-plugin | `sm-seo-plugin v0.1.0`; submodule at b19cd06 (v0.1.0) | ✓ WIRED |
### Data-Flow Trace (Level 4)
| Artifact | Data | Source | Real data | Status |
|---|---|---|---|---|
| SEO tab fields | entry values | extension Load/Save on `golem15_seo_entries` in the owner transaction | yes (`TestSEOTabTracer`) | ✓ FLOWING |
| `/resolve` answer | effective SEO plus sources | rules, entries, type settings, settings and signpost Describe (DB) | yes (`TestResolverPrecedence` DB part) | ✓ FLOWING |
| E3 SERP preview | context and duplicates | widget actions → plugin `Run` → DB | yes in tests through a fake host; real host per the CR-04 fix (human) | ✓ FLOWING (real-host: human) |
| MCP tools | all SEO data | `/_seo/api/v1` handlers → classes.Service → DB | yes (`TestManagementAPI`) | ✓ FLOWING |
### Behavioral Spot-Checks and Probe Execution (run in this session)
| Check | Command | Result | Status |
|---|---|---|---|
| Seam unit tests | `go test -short ./modules/cabana ./modules/signpost ./modules/pact -run 'TestFormExtension\|TestRecordEventDiff\|TestRecordEventLocale\|TestContent'` | ok | ✓ PASS |
| Seam Postgres tests | `go test ./modules/cabana -run 'TestFormExtensionTracer\|…Save\|…Required\|…Upload\|TestRecordEvents'` | 5 top-level, 19 subtests PASS | ✓ PASS |
| Journal | `go test ./... -run 'TestContentDescriptors\|TestJournalTagType\|TestTranslatedSlug\|TestAPIRecordEvents\|TestRecordSavedDescribesNewURL'` | 8 PASS | ✓ PASS |
| sm-seo-plugin full suite (Docker) | `go test -count=1 ./...` | 55 top-level PASS, 0 FAIL | ✓ PASS |
| Plugin elements | `npm run test` + typecheck + `scripts/check-dist.sh` | 77/77; dist matches | ✓ PASS |
| Admin SPA | `npm --prefix admin run test -- FormView WidgetField` | 84/84 | ✓ PASS |
| Demo mount | `go test -tags summercms_mcp -run 'TestGoModRequiresTags\|TestSEOTabMounted\|TestSEOToolsCatalog' .` | 3 PASS | ✓ PASS |
| Phase gate (probe) | `bash scripts/check-phase32.sh --all` | exit 0, "Phase 32 gate passed" (self-test, go, plugins, consumers, admin, docs, marker, additive vs v0.5.1, compat across 23 siblings, docker) | ✓ PASS |
### Requirements Coverage
REQUIREMENTS.md has no IDs for this phase. The plans declare 51 IDs, and each is accounted for against 32-DECISIONS-MAP.md:
| IDs | Source plans | Status | Evidence |
|---|---|---|---|
| SC-1..SC-5 | 32-01..32-06 | ✓ SATISFIED | table above |
| D-90, D-91, D-92, D-93, D-98, D-100, D-101, D-104, D-106 | 32-01, 32-02, 32-06 | ✓ SATISFIED | seams, marker, additive and compat, boot errors (zone, fileupload), settings `AdminSingleton` (`controllers/settings.go:134`) |
| D-94, D-95, D-96, D-102, D-58 | 32-02, 32-03 | ✓ SATISFIED | D-95's Phase 32 part: `Requires()` returns nil (`plugin.go:51`); Journal descriptors, journal-tag and API events; translated slug |
| D-01..D-09, D-13, D-15, D-21, D-23 | 32-03 | ✓ SATISFIED | entries table, rules, settings, resolver, robots grammar, the one permission, SERP and counters (real-host rendering: human) |
| D-05, D-06, D-08, D-16..D-20, D-20a/b/c, D-22, D-24, D-25, D-99, D-103, D-105 | 32-04 | ✓ SATISFIED | redirect engine, 404 log, normalise, strict, robots and tester, bots incl. Claude-User, verification API only |
| D-10..D-14, D-19 | 32-05 | ✓ SATISFIED | 29 tools, owner resolution, batch, undo, CSV, issues |
| Q-01 | 32-06 | ✓ SATISFIED | patch tag v0.5.1 computed at release |
Orphaned: none. Every map ID with a Phase 32 column entry appears in a plan's `requirements`. Phase-33-only IDs (D-30..D-34, D-40..D-46, D-50..D-57, D-70..D-72, D-80..D-86, D-97) are out of scope by design.
### Goal items that depend on unreleased fixes
The released and mounted set is framework v0.5.1, sm-seo-plugin v0.1.0, Journal v0.2.5 and sm-demo-app 9451b31. Everything above was verified at the local heads, which carry the review fixes (unpushed: framework 16 commits after v0.5.1, of which 7 are code; sm-seo-plugin 21 after v0.1.0; Journal 3 after v0.2.5). In the released versions:
| Goal item | Finding | Released behavior | Fixed in (unreleased) |
|---|---|---|---|
| SERP preview (D-09, E3) and robots.txt tester (D-22, E5) in the real admin | CR-04 | Deadlock: the skeleton shows forever and the tester never answers | framework d660cd0 (WidgetField.vue and the dist), plugin 3ecbd1f (ActionQueue with retry). Either release alone fixes it. |
| Exact redirects (D-16, D-17, SC-3 runtime) | CR-03 | Sources with a trailing slash or a mixed-case query never fire | plugin 2d81c26 plus the new migration `202610280001_add_golem15_seo_redirects_source_key` |
| Loops refused on every path (SC-3, D-19) | WR-09, WR-10 | Address-change 301s skip validation and the loop simulation; the loop check races | plugin cb614cb, c2f136a |
| Slug change through Journal API or MCP fires the event (SC-3, D-102) | WR-04, WR-06 | A slug can commit with no RecordSaved on a failing association; auto-created tags fire nothing | Journal 62c75b1, a6fc7cd (WR-05: 7a03640) |
| Generation job keeps the 301 (SC-3) | WR-19 | The job can overwrite entry.URL and swallow an address change | plugin d8aa989 |
| Undo (SC-4) | WR-11 | Concurrent undo is not idempotent | plugin d20f14f |
| MCP/import validation (SC-4) | WR-16, WR-17, WR-18, WR-23 | Invalid values are silently coerced or answer 500 | plugin c8e0961, 24e6f38, cff2d29, 4ca26e9 |
| D-91 seam generality (SC-1) | CR-01, CR-02, WR-07 | `required` extension fields break host saves; uploads ignore extension visibility; hidden fields' relation and partial routes stay reachable | framework 676ca53, 5471b66, 0e187cf. The SEO plugin itself is not exposed: it declares no `required` fields, and its `seoimage` Upload checks `golem15.seo.manage` itself (32-03 deviation 6). |
| D-92 coverage | WR-01, WR-02 | hasMany link/unlink and deferred children fire no events; Locale is inconsistent | framework 468af06, 1583651 |
The additive stage shows +0 exported lines since v0.5.1, so plugin v0.1.1 needs no new framework API. However, the CR-02, WR-01 and WR-07 fixes reach consumers only through v0.5.2.
### Anti-Patterns Found
| File | Pattern | Severity | Impact |
|---|---|---|---|
| (all phase files in 4 repos) | TBD/FIXME/XXX | none found | — |
| sm-seo-plugin, cabana | 23 open info findings (IN-01..IN-23, 32-REVIEW-DISPOSITION.md) | ℹ️ Info | Not blocking. Notable: IN-13 (`/robots.txt` serves allow-all without the noindex header when the service is nil, even outside production; this is an edge of the D-22 lock in a boot-failure state); IN-05 (Journal `requestLocale` drops the region, so translated slugs in region-coded locales never match, which narrows D-58); IN-09 (transfer token has no expiry or user binding) |
| modules/cabana/crud.go:982 | WR-03 skipped | ⚠️ Warning | A failing extension Load turns a committed save into a rollback and a 500 (design decision pending) |
| 32-VALIDATION.md | stale status | ℹ️ Info | Still `status: draft`, with 32-06-T4 and T5 rows "pending" although both completed and `--all` is green. Update it to validated. |
| sm-seo-plugin README | WR-15 documentation-only fix | ℹ️ Info | The public throttle is site-wide unless the SSR host is in `http.trusted_proxies`; a deploy concern for Phase 33 layer integration |
### Human Verification Required
1. **MVP user-flow walk-through on the current heads.** Covers the SEO tab, SERP preview, slug-change 301, robots tester and settings tabs, plus the E1 320px and E2 360px layout. CR-04 proved that the fake-host tests can miss a real-admin failure.
2. **Live MCP client session.** get/set by URL, dry-run batch with a loop, apply, undo the batch, check the read-only grant.
3. **Release decision for the fixes** (v0.5.2, v0.1.1, v0.2.6, then the demo bump). The released set carries the 4 critical findings.
4. **WR-02**: confirm the meaning of `RecordSaved.Locale`.
5. **Logic review** of WR-08, WR-09, WR-19 and WR-22.
6. **WR-03**: design decision.
7. **Judgment-tier prohibitions** (release go-ahead, token handling): confirm against the release log.
### Gaps Summary
No blocking gaps. Each roadmap success criterion is backed by passing behavioral tests run in this session against the local heads of the four repositories, and the phase gate `check-phase32.sh --all` passes again (exit 0). The framework carries only generic seams (marker and additive stages), and the SEO code lives in sm-seo-plugin. Status is `human_needed` for three reasons:
- The visual and real-host behavior of the elements has never been observed in a browser, which matters because CR-04 slipped past the element tests.
- A live MCP client session is still to be run.
- The released versions (v0.5.1, v0.1.0, v0.2.5, mounted in the demo) predate 28 review fixes, including 4 critical ones. The goal is met in the codebase, but sites on the released tags get a broken SERP preview and robots tester and dead trailing-slash exact redirects until the fixes are released.
---
_Verified: 2026-10-27T15:09:40Z_
_Verifier: Claude (msd-verifier)_