diff --git a/.planning/phases/15-journal-plugin/15-VERIFICATION.md b/.planning/phases/15-journal-plugin/15-VERIFICATION.md new file mode 100644 index 0000000..e2a95c1 --- /dev/null +++ b/.planning/phases/15-journal-plugin/15-VERIFICATION.md @@ -0,0 +1,272 @@ +--- +phase: 15-journal-plugin +verified: 2026-10-06T17:29:31Z +status: human_needed +score: 22/22 must-haves verified +covered_files: + - .planning/phases/15-journal-plugin/15-01-PLAN.md + - .planning/phases/15-journal-plugin/15-01-SUMMARY.md + - .planning/phases/15-journal-plugin/15-02-PLAN.md + - .planning/phases/15-journal-plugin/15-02-SUMMARY.md + - .planning/phases/15-journal-plugin/15-03-PLAN.md + - .planning/phases/15-journal-plugin/15-03-SUMMARY.md + - .planning/phases/15-journal-plugin/15-04-PLAN.md + - .planning/phases/15-journal-plugin/15-04-SUMMARY.md + - scripts/check-phase15.sh +covered_digest: "v2:sha256:dffb5e2bb692df38cfc2be6a36f6e3305b4d7d81304c9c6fa15bf86362f2aa80" +covered_files_note: "verification.fingerprint covers only paths under the summercms.go root. Sibling implementation read this pass: sm-journal-plugin d09edde (plugin.go, models, seven gormigrate steps, YAML admin, FormatHTML, /_journal/api/v1, commands, tests); sm-grzybyfunkcjonalne-app ff20e6c (summer.yaml, plugins.gen.go, go.work, gitlinks, boot_test.go, config/http.yaml). Host journal gitlink is d09edde. PHP pin /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal HEAD 02110eb1c0c3861370b0b9b47b209a0702ac5d88, clean tree, not edited." +behavior_unverified: 0 +overrides_applied: 0 +decision_coverage: + honored: 23 + total: 23 + not_honored: [] +deferred: + - truth: "REQUIREMENTS.md API-09 (all 154 Płytarium routes) is mapped to Phase 15 in the stale traceability table" + addressed_in: "Phase 20" + evidence: "Phase 20 goal: the parity harness is green on every manifest route. Plans 15-01..15-04 explicitly do not claim API-09; D-16 forbids adding Journal to the tide harness." + - truth: "REQUIREMENTS.md QA-05 (cutover: 154-route harness green against vue-fonoteka-app and fonoteka-mcp) is mapped to Phase 15 in the stale traceability table" + addressed_in: "Phase 20" + evidence: "Phase 20 goal is the Płytarium definition of done. Journal is not a Płytarium route set." +human_verification: + - test: "Boot sm-grzybyfunkcjonalne-app, sign in as an administrator holding golem15.journal.*, and open Journal in the admin SPA" + expected: "Journal nav appears; Posts/Categories/Tags list and create work; post content is an mlmarkdown field; a user without golem15.journal.access_posts cannot open Posts" + why_human: "Harvested from 15-VALIDATION.md manual row (D-19, SC-1). Named Go tests prove cabana HTTP 403/schema/create, not the running SPA chrome against the proof host." + - test: "Confirm the Gitea remotes git@git.golem15.com:golem15/sm-journal-plugin.git and git@git.golem15.com:golem15/sm-grzybyfunkcjonalne-app.git still exist" + expected: "git ls-remote against both remotes returns a HEAD SHA" + why_human: "Harvested from 15-VALIDATION.md. Local origin URLs match D-18/D-22; this pass could not complete git ls-remote within 8s (likely SSH/network)." +--- + +# Phase 15: Journal plugin Verification Report + +**Phase Goal:** The Golem15 Journal plugin is ported to Go as `sm-journal-plugin` and mounts in a host application the same way `sm-user-plugin` does, so a blog can run on SummerCMS without the PHP plugin. +**Verified:** 2026-10-06T17:29:31Z +**Status:** human_needed +**Re-verification:** No — initial verification + +**MVP note:** ROADMAP marks this phase `mode: mvp` but the ROADMAP goal is not in user-story form (`user-story.validate` → `valid: false`). PLAN.md goals are valid user stories. This report uses the PLAN story for User Flow Coverage and does not refuse verification. + +**User story (from plans):** As an application developer, I want to mount `sm-journal-plugin` in a host the same way `sm-user-plugin` mounts, so that a blog can run on SummerCMS without the PHP plugin. + +## User Flow Coverage + +| Step | Expected | Evidence | Status | +|------|----------|----------|--------| +| Mount | Host lists journal beside user and translate as git submodules | `sm-grzybyfunkcjonalne-app/.gitmodules`, `git ls-files -s` mode `160000` for `plugins/golem15/{user,translate,journal}`; journal gitlink `d09edde`; `summer.yaml` + `plugins.gen.go` blank-import `sm-journal-plugin` | ✓ | +| Boot | Host Activate returns the three plugin IDs and assembles Journal routes | `boot_test.go:TestBootUserTranslateJournal` required by `check-phase15.sh --host` (PASS this pass) | ✓ | +| Public content | Anonymous GET lists published posts; categories/tags/rss exist | `TestJournalPublicList`, `TestJournalPublicCategories`, `TestJournalPublicTags`, `TestJournalRSS` in plugin suite (PASS this pass) | ✓ | +| Outcome | A blog can run on SummerCMS without the PHP plugin | Plugin module `git.golem15.com/golem15/sm-journal-plugin`, PHP pin clean at `02110eb1`, `--php` PASS; PHP components `journalPost` documented as Phase 16 successor in plugin README | ✓ | + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +| --- | ------- | ---------- | -------------- | +| 1 | SC-1: Journal plugin repo exists with models, migrations and YAML admin screens, permission-gated | ✓ VERIFIED | `sm-journal-plugin` HEAD `d09edde`; models Post/Category/Tag/Settings; seven gormigrate IDs; controllers `golem15.journal.posts\|categories\|tags`; `TestPostsFormCompiles` (mlmarkdown/mltext); `TestPostsAdminForbidden` 403 without `access_posts` | +| 2 | SC-2: A host can mount the plugin as a submodule and serve public posts/categories/PHP-equivalent routes | ✓ VERIFIED | Host gitlink + `TestBootUserTranslateJournal` asserts GET/POST `/_journal/api/v1/posts`; `routes.go` public group; `TestJournalPublicList` PASS | +| 3 | SC-3: Plugin README stays application-neutral | ✓ VERIFIED | README uses `the application`, `acme`, `blog`; `--forbidden` greps README for fonoteka/płytarium/grzybyfunkcjonalne (PASS) | +| 4 | SC-4: Unit tests in the phase's last plan | ✓ VERIFIED | 15-04 added fillable/translatable/FormatHTML/migrations/API/search/integration tests; `check-phase15.sh --plugin` ran `go test ./...` and `-race` (PASS this pass) | +| 5 | D-01/D-02/D-03/D-07: Schema/models from PHP SHA `02110eb1`; PHP tree unmodified | ✓ VERIFIED | `check-phase15.sh --php`: HEAD `02110eb1c0c3861370b0b9b47b209a0702ac5d88`, porcelain empty | +| 6 | D-04: gormigrate creates `golem15_journal_*` tables, pivots, settings singleton, `backend_users.golem15_bloghub_author_slug`; rollback remigrates | ✓ VERIFIED | `updates/202610060001`–`007`; `TestJournalTables`; `TestJournalMigrationsRollbackAndRemigrate` in PLUGIN_REQUIRE (PASS) | +| 7 | D-05: RESEARCH §8 PHPUnit rows have named Go tests | ✓ VERIFIED | JOURNAL-001/002 `TestFillable`; JOURNAL-005 `TestJournal005DraftShow`; JOURNAL-006 `TestJournal006MediaUpload`; JOURNAL-003/004 substitute `TestFormatHTMLRejectsUnsafeHTML`; redactor_id `TestFillable` + `TestJournalAPIMassAssignRedactor` | +| 8 | D-06: HasLang catalogs exist for en and pl only | ✓ VERIFIED | `lang/en/lang.yaml`, `lang/pl/lang.yaml` only; `plugin.go` `HasLang` + `//go:embed lang` | +| 9 | D-08: Admin nav SVG is embedded | ✓ VERIFIED | `assets/images/journal-icon.svg` byte-identical to PHP; `TestJournalSVGEmbed` opens it from `AdminFS`; `NavigationItem` has no `IconSvg` (framework), so nav uses lucide `pencil` per RESEARCH | +| 10 | D-09/D-10: ID `golem15.journal`, Requires exactly `golem15.translate`, Post/Category Translatable/MorphName PHP strings, mltext/mlmarkdown | ✓ VERIFIED | `plugin.go` `Requires() []string{"golem15.translate"}`; `TestPostTranslatableSmoke`; `TestTranslatable`; `models/post/fields.yaml` `mltext`/`mlmarkdown` | +| 11 | D-11: post content YAML type is `mlmarkdown`; cabana markdown types not re-added this phase | ✓ VERIFIED | `TestPostsFormCompiles`; `TestCabanaMarkdownFieldTypes`; `git diff -- modules/cabana/field_markdown.go` empty; `--forbidden` refuses a dirty `field_markdown.go` | +| 12 | D-12: `search_use_typesense` defaults false; unpublished/gate-off not searchable; fresh save makes zero Typesense HTTP | ✓ VERIFIED | DDL `DEFAULT FALSE`; `TestSearchGateOff` must not skip (gate detector refuses skip); zero httptest hits this pass | +| 13 | D-13: `journal:export-posts` / `journal:import-posts` registered; Posts toolbar requires `golem15.journal.access_import_export` | ✓ VERIFIED | `TestJournalCommands` PASS; commands write JSON-per-slug files matching PHP ExportPosts/ImportPosts (not a Winter CSV framework — RESEARCH) | +| 14 | FormatHTML regenerates `content_html` on admin/API save with goldmark footnote/table/attribute + rejectUnsafe; cabana.RenderMarkdown not edited | ✓ VERIFIED | `controllers/posts.go` `preparePostWrite`; API `posts_helpers.go`; import path; `TestFormatHTMLRejectsUnsafeHTML`; `TestPostsAdminCreateSmoke` stores HTML | +| 15 | Without `access_other_posts`, list/form queries restrict to `user_id`; publish without `access_publish` is ForbiddenError | ✓ VERIFIED | `restrictToOwnPosts` in `controllers/posts.go`; `TestPostsAdminCreateSmoke/publish_without_access_publish` 403; JOURNAL-005 owner vs stranger vs `access_other_posts` | +| 16 | D-14: anonymous GET `/_journal/api/v1/posts`, `posts/{slug}`, `categories`, `tags`, `rss`; show is slug; per_page default 9 max 30 | ✓ VERIFIED | `routes.go`; `TestJournalPublicList` (per_page 9, drafts omitted); `TestJournalPerPageClamp`; `TestClampPerPage`; categories/tags/RSS named tests PASS | +| 17 | D-15: writes require cabana backend JWT (`aud=backend`); missing Bearer is JSON `{error:Authentication required}`; frontend audience rejected | ✓ VERIFIED | `requireBackendPrincipal`; `TestJournalWriteUnauthenticated`; `TestJournalWriteFrontendAudience`; `TestJournalFeaturedImageUnauthenticated` | +| 18 | D-16: Journal is not added to the Płytarium tide 154-route harness | ✓ VERIFIED | `TestNoTideJournalFixtures`; `--forbidden` greps fonoteka.go parity/tide and `modules/tide` for `/_journal/api/v1` (no hits) | +| 19 | D-17: buckets `journal-public-api` and `journal-api` Max 120; host CORS `_journal/api/*`; `supports_credentials` false | ✓ VERIFIED | `TestJournalBuckets`; `TestCORS`; `config/http.yaml`; 429 body remains surf `"Too Many Attempts."` (no Journal 429 writer) | +| 20 | D-18–D-23: proof host is `sm-grzybyfunkcjonalne-app` at the sibling path; three submodules; plugin module/ID/path | ✓ VERIFIED | `--layout` PASS; host not fonoteka.go / sm-summercmsio-app / in-module testhost; `go.mod` module `git.golem15.com/golem15/sm-journal-plugin`; origin URLs match D-18/D-22 | +| 21 | JOURNAL-005: unpublished or future `published_at` show is 404 with no `data` unless owner or `access_other_posts` | ✓ VERIFIED | `Show` in `controllers/api/posts.go`; `TestJournal005DraftShow`; `TestJournalEndToEnd` anonymous/stranger 404, owner/priv 200 | +| 22 | `scripts/check-phase15.sh --all` is fail-closed; 15-SECURITY-REVIEW.md closes every high T-15-* threat | ✓ VERIFIED | `--self-test` detector (fail/skip/zero/no-tests/race/missing-named); this pass executed every `--all` stage (self-test, php, layout, plugin, host, forbidden, security) — all PASS; review marks T-15-01..15 and T-15-SC mitigate | + +**Score:** 22/22 truths verified (0 present, behavior-unverified) + +### Deferred Items + +Items not yet met but explicitly addressed in later milestone phases. + +| # | Item | Addressed In | Evidence | +|---|------|-------------|----------| +| 1 | API-09 (154 Płytarium routes) stale map onto Phase 15 | Phase 20 | Phase 20 cutover goal; plans forbid claiming API-09; D-16 | +| 2 | QA-05 (cutover harness + Nuxt/MCP unchanged) stale map onto Phase 15 | Phase 20 | Phase 20 definition of done | +| 3 | Winter theme components `journalPost` / `journalPosts` / live site HTML | Phase 16 | CONTEXT deferred; plugin README names them as a later successor | + +### Required Artifacts + +| Artifact | Expected | Status | Details | +| -------- | ----------- | ------ | ------- | +| `../sm-journal-plugin/plugin.go` | compiled plugin, Requires translate, migrations/models | ✓ VERIFIED | ID `golem15.journal`; HasMigrations/Models/Lang/Commands/Routes; BucketProvider | +| `../sm-journal-plugin/updates/202610060001_create_golem15_journal_posts.go` | squashed posts DDL | ✓ VERIFIED | `golem15_journal_posts`; Rollback DROP | +| `../sm-journal-plugin/models/post.go` | TableName, MorphName, Translatable | ✓ VERIFIED | PHP class string `Golem15\Journal\Models\Post` | +| `../sm-grzybyfunkcjonalne-app/summer.yaml` | three compiled plugins including journal | ✓ VERIFIED | user, translate, journal | +| `../sm-grzybyfunkcjonalne-app/boot_test.go` | three-plugin boot | ✓ VERIFIED | `TestBootUserTranslateJournal` | +| `../sm-grzybyfunkcjonalne-app/config/http.yaml` | CORS `_journal/api/*` | ✓ VERIFIED | path present; `supports_credentials: false` | +| `../sm-journal-plugin/controllers/posts.go` | YAML controller, ListExtendQuery, FormatHTML | ✓ VERIFIED | `access_posts`; `preparePostWrite` | +| `../sm-journal-plugin/models/post/fields.yaml` | mlmarkdown content | ✓ VERIFIED | `type: mlmarkdown` | +| `../sm-journal-plugin/assets/images/journal-icon.svg` | embedded nav SVG | ✓ VERIFIED | bytes match PHP; go:embed in AdminFS | +| `../sm-journal-plugin/classes/format_html.go` | `func FormatHTML` | ✓ VERIFIED | goldmark + rejectUnsafe | +| `../sm-journal-plugin/console/export_posts.go` | `journal:export-posts` | ✓ VERIFIED | command name registered | +| `../sm-journal-plugin/models/settings/fields.yaml` | settings code journal | ✓ VERIFIED | `search_use_typesense` | +| `../sm-journal-plugin/routes.go` | `/_journal/api/v1` | ✓ VERIFIED | public GET + write groups | +| `../sm-journal-plugin/controllers/api/posts.go` | PHP `{error}` string JSON, slug show, draft 404 | ✓ VERIFIED | `Authentication required`; no `data` on draft 404 | +| `../sm-journal-plugin/controllers/api/media.go` | JOURNAL-006 media upload | ✓ VERIFIED | `media/upload`; `journal/` prefix | +| `../sm-journal-plugin/search.go` | beachcomber Gate | ✓ VERIFIED | reads ID=1 `search_use_typesense`; errors count as off | +| `../sm-journal-plugin/integration_test.go` | end-to-end | ✓ VERIFIED | `TestJournalEndToEnd` in package `journal_test` | +| `../sm-journal-plugin/models/fillable_test.go` | JOURNAL-001/002 | ✓ VERIFIED | `TestFillable` | +| `../sm-journal-plugin/controllers/api/posts_test.go` | clampPerPage unit | ✓ VERIFIED | HTTP JOURNAL-005 lives at plugin-root `journal_api_writes_test.go` (dispatch-authorized) | +| `scripts/check-phase15.sh` | fail-closed phase gate | ✓ VERIFIED | all stages PASS this pass | +| `.planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md` | ASVS L1 | ✓ VERIFIED | T-15-01 present; `--security` PASS | + +### Key Link Verification + +| From | To | Via | Status | Details | +| ---- | --- | --- | ------ | ------- | +| `plugin.go` | `golem15.translate` | `Requires` | ✓ WIRED | `Requires() []string{"golem15.translate"}` | +| `models/post.go` | posts migration | `TableName` | ✓ WIRED | both `golem15_journal_posts` | +| `summer.yaml` | `plugins.gen.go` | `summer build` blank-import | ✓ WIRED | `_ "git.golem15.com/golem15/sm-journal-plugin"` | +| `controllers/posts.go` | `classes/format_html.go` | FormBeforeCreate/Update | ✓ WIRED | `classes.FormatHTML(post.Content)` | +| `admin_permissions.go` | `controllers/posts.go` | `RequiredPermissions` | ✓ WIRED | `golem15.journal.access_posts` | +| `console/export_posts.go` | `plugin.go` | `HasCommands` | ✓ WIRED | `console.ExportPosts` in `Commands()` | +| `routes.go` | `controllers/api/auth.go` | backend JWT on writes | ✓ WIRED | `requireBackendPrincipal` / `optionalBackendPrincipal` | +| `controllers/api/posts.go` | `classes/format_html.go` | store/update | ✓ WIRED | `posts_helpers.go` calls `FormatHTML` | +| `search.go` | `models/settings.go` | Gate ID=1 | ✓ WIRED | `SetGate` + `settingsSearchEnabled` | +| `integration_test.go` | `routes.go` | assembled handlers | ✓ WIRED | `TestJournalEndToEnd` hits public GET and Bearer POST | +| `check-phase15.sh` | plugin suite | `go -C` full, not `-short` | ✓ WIRED | `--plugin` PASS this pass | +| `15-SECURITY-REVIEW.md` | named tests | T-15-* | ✓ WIRED | `--security` requires mitigate on high IDs | + +### Data-Flow Trace (Level 4) + +| Artifact | Data Variable | Source | Produces Real Data | Status | +| -------- | ------------- | ------ | ------------------ | ------ | +| GET `/_journal/api/v1/posts` | `data` / `meta` | GORM `Model(&models.Post{})` + `lagoon.Paginate` | Yes — `TestJournalPublicList` seeds rows and asserts listed slug | ✓ FLOWING | +| Admin POST posts | stored `Post` + translate attributes | cabana form → GORM + `golem15_translate_attributes` | Yes — `TestPostsAdminCreateSmoke` / `TestJournalEndToEnd` | ✓ FLOWING | +| FormatHTML `content_html` | `post.ContentHTML` | goldmark convert of `content` | Yes — create smoke asserts markup, rejects script | ✓ FLOWING | +| Settings gate | `search_use_typesense` | `golem15_journal_settings` id=1 | Yes — default false in DDL; `TestSearchGateOff` | ✓ FLOWING | +| Nav SVG | AdminFS file | `//go:embed assets/images/journal-icon.svg` | Bytes present; SPA nav uses lucide `pencil` (no IconSvg field) | ✓ FLOWING (embed); SPA icon is lucide | + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +| -------- | ------- | ------ | ------ | +| PHP pin frozen and clean | `bash scripts/check-phase15.sh --php` | `phase15 php passed (02110eb1…)` | ✓ PASS | +| Host layout + gitlinks + CORS | `bash scripts/check-phase15.sh --layout` | `phase15 layout passed` | ✓ PASS | +| Plugin vet + full suite + race | `bash scripts/check-phase15.sh --plugin` | `phase15 plugin passed` (~85s) | ✓ PASS | +| Host vet + `TestBootUserTranslateJournal`/`TestCORS` + build | `bash scripts/check-phase15.sh --host` | `phase15 host passed` | ✓ PASS | +| Forbidden surfaces | `bash scripts/check-phase15.sh --forbidden` | `phase15 forbidden passed` | ✓ PASS | +| Security review + VALIDATION sign-off | `bash scripts/check-phase15.sh --security` | `phase15 security passed` | ✓ PASS | +| Detector fail-closed | `bash scripts/check-phase15.sh --self-test` | `phase15 self-test passed` | ✓ PASS | + +### Probe Execution + +No `scripts/*/tests/probe-*.sh`. Phase-declared gate is `scripts/check-phase15.sh`. + +| Probe | Command | Result | Status | +| ----- | ------- | ------ | ------ | +| `scripts/check-phase15.sh --self-test` | `bash scripts/check-phase15.sh --self-test` | exit 0 | PASS | +| `scripts/check-phase15.sh --php` | `bash scripts/check-phase15.sh --php` | exit 0 | PASS | +| `scripts/check-phase15.sh --layout` | `bash scripts/check-phase15.sh --layout` | exit 0 | PASS | +| `scripts/check-phase15.sh --plugin` | `bash scripts/check-phase15.sh --plugin` | exit 0 | PASS | +| `scripts/check-phase15.sh --host` | `bash scripts/check-phase15.sh --host` | exit 0 | PASS | +| `scripts/check-phase15.sh --forbidden` | `bash scripts/check-phase15.sh --forbidden` | exit 0 | PASS | +| `scripts/check-phase15.sh --security` | `bash scripts/check-phase15.sh --security` | exit 0 | PASS | + +`--all` is those seven stages in order. Each stage was executed in this verification process. + +### Requirements Coverage + +ROADMAP lists **Requirements: TBD**. REQUIREMENTS.md has **no Journal IDs**. Plan frontmatter IDs are CONTEXT decisions D-01–D-23. Every ID from plans is accounted for below. + +| Requirement | Source Plan | Description | Status | Evidence | +| ----------- | ---------- | ----------- | ------ | -------- | +| D-01 | 01, 04 | Port contract PHP SHA `02110eb` | ✓ SATISFIED | `--php` HEAD match | +| D-02 | 01, 04 | Freeze at that SHA | ✓ SATISFIED | gate pins full SHA | +| D-03 | 01, 04 | Read frozen tree at fonoteka journal path | ✓ SATISFIED | `PHASE15_PHP` default path; tree present | +| D-04 | 01, 02, 04 | Models/tables/YAML/perms/commands/API | ✓ SATISFIED | migrations + YAML + routes + commands | +| D-05 | 04 | PHPUnit map | ✓ SATISFIED | named Go tests for §8 rows | +| D-06 | 01, 02, 04 | en+pl only | ✓ SATISFIED | two lang dirs; 02 prohibition | +| D-07 | 01, 04 | Do not edit PHP journal | ✓ SATISFIED | porcelain empty; test-tier prohibition wired in `--php` | +| D-08 | 02, 04 | Embed journal-icon.svg | ✓ SATISFIED | AdminFS + byte cmp | +| D-09 | 01, 04 | Do not port Translate inside Journal; Require translate not apparatus | ✓ SATISFIED | `Requires` exactly translate; no apparatus ID | +| D-10 | 01–04 | Translatable attributes | ✓ SATISFIED | Post/Category Translatable + MorphName | +| D-11 | 02, 04 | Use existing cabana markdown; no second field type | ✓ SATISFIED | mlmarkdown; field_markdown.go untouched | +| D-12 | 03, 04 | Typesense off by default | ✓ SATISFIED | `TestSearchGateOff` | +| D-13 | 02, 04 | Import/export CLI + toolbar (PHP JSON files, not Winter CSV) | ✓ SATISFIED | `TestJournalCommands` | +| D-14 | 03, 04 | Full `/_journal/api/v1` | ✓ SATISFIED | routes + public tests | +| D-15 | 03, 04 | Backend Bearer writes only | ✓ SATISFIED | 401 PHP shape; frontend aud rejected | +| D-16 | 03, 04 | Not tide | ✓ SATISFIED | `--forbidden` + `TestNoTideJournalFixtures` | +| D-17 | 01, 03, 04 | Limiters + CORS | ✓ SATISFIED | buckets + http.yaml | +| D-18 | 01, 04 | Proof host sm-grzybyfunkcjonalne-app | ✓ SATISFIED | sibling checkout + origin | +| D-19 | 01, 04 | Mount, migrate, admin, API (SPA chrome → human) | ✓ SATISFIED | boot + plugin HTTP; SPA listed under Human Verification | +| D-20 | 01, 04 | Local host path | ✓ SATISFIED | `/media/nvme/dev/golem15/summercms.io/summercms/sm-grzybyfunkcjonalne-app` | +| D-21 | 01, 04 | Submodules journal+translate+user | ✓ SATISFIED | gitlinks 160000 | +| D-22 | 01, 04 | Plugin remote/module/package/ID | ✓ SATISFIED | go.mod + plugin.go + origin | +| D-23 | 01, 04 | Local plugin checkout + host submodule replace | ✓ SATISFIED | sibling `sm-journal-plugin`; host `replace => ./plugins/golem15/journal` | +| API-09 | REQUIREMENTS.md map only | 154 Płytarium routes | deferred | Phase 20; not in any Phase 15 plan `requirements:` | +| QA-05 | REQUIREMENTS.md map only | Cutover harness | deferred | Phase 20; not in any Phase 15 plan `requirements:` | + +**Orphaned REQUIREMENTS.md IDs mapped to Phase 15:** API-09, QA-05 — recorded under Deferred, not gaps. Plans treat them as Phase 20. + +**Prohibitions (test-tier):** D-07, D-09, D-18, D-11, D-13 (no Pages/dashboard), D-06, D-15, D-14 (PHP error envelope), D-12, D-16 — each has wired `--php`/`--layout`/`--forbidden` or a named test. Not unverified. + +### Decision Coverage + +All trackable CONTEXT.md decisions are honored by shipped artifacts. **23/23 honored.** + +### Test Quality Audit + +| Test File | Linked Req | Active | Skipped | Circular | Assertion Level | Verdict | +|-----------|-----------|--------|---------|----------|-----------------|---------| +| `models/fillable_test.go` | D-05 JOURNAL-001/002 | yes | no | no | Value | OK | +| `journal_api_writes_test.go` | D-14/D-15/JOURNAL-005 | yes | no | no | Behavioral | OK | +| `journal_api_task3_test.go` | JOURNAL-006, RSS | yes | no | no | Behavioral | OK | +| `search_test.go` | D-12 | yes | no | no | Behavioral | OK | +| `classes/format_html_test.go` | JOURNAL-003/004 subst. | yes | no | no | Value | OK | +| `updates/migrations_test.go` | D-04 | yes | only `-short` | no | Behavioral | OK | +| `integration_test.go` | D-05/D-14/D-15 | yes | only `-short` | no | Behavioral | OK | +| empty `TestJournalEndToEnd` shims in other packages | gate `-run` hygiene | yes (no-op) | no | no | Existence | ⚠️ WARNING — real proof is `journal_test.TestJournalEndToEnd` | + +**Disabled tests on requirements:** 0 blockers. `t.Skip("requires testcontainers postgres")` only when `-test.short`; full `--plugin` run is not `-short` and TestMain exits 1 if Docker fails. +**Circular patterns detected:** 0 +**Insufficient assertions:** empty EndToEnd shims only (other tests cover the requirement) + +### Anti-Patterns Found + +| File | Line | Pattern | Severity | Impact | +| ---- | ---- | ------- | -------- | ------ | +| `plugin_test.go` (and sibling packages) | `TestJournalEndToEnd` empty | no-op test shim | ℹ️ Info | Keeps `go test ./... -run TestJournalEndToEnd` from printing `no tests to run`; real test is `integration_test.go` package `journal_test` | +| `updates/postgres_test.go` | rainlab table name in test | historical name check | ℹ️ Info | Test-only; `--forbidden` excludes `_test.go` | +| `go.mod` | require `sm-user-plugin` | test harness module | ℹ️ Info | Production `.go` files do not import it (`--forbidden` + `TestJournalEndToEndFixtureAbsentFromProduction`) | +| `15-SECURITY-REVIEW.md` | reviewer | executor self-review | ℹ️ Info | No dedicated auditor in this Cursor session; cited tests were re-run by `--plugin` this pass | +| production plugin sources | — | TBD/FIXME/XXX | none | No unreferenced debt markers | + +### Human Verification Required + +### 1. Journal admin SPA on the proof host + +**Test:** Boot `sm-grzybyfunkcjonalne-app`. Sign in as an administrator holding `golem15.journal.*`. Open Journal nav. List and create a post whose content is `mlmarkdown`. Repeat without `golem15.journal.access_posts`. +**Expected:** Posts/Categories/Tags screens work; mlmarkdown editor is present; missing permission cannot open Posts. +**Why human:** Needs a running host, admin login, and visual confirmation. Automated tests cover HTTP 403/schema/create, not SPA chrome. + +### 2. Gitea remotes still exist + +**Test:** `git ls-remote git@git.golem15.com:golem15/sm-journal-plugin.git` and the proof-host remote. +**Expected:** Each remote returns a HEAD SHA. +**Why human:** Local `origin` URLs match D-18/D-22; this verifier could not complete `git ls-remote` within 8s. + +### Gaps Summary + +No blocking gaps. All 22 must-have truths are present, wired, and exercised by named tests or the fail-closed gate. Status is `human_needed` because harvested end-of-phase UAT (admin SPA and remote reachability) remains. ROADMAP/STATE were not marked complete. + +PHPUnit XSS template tests (`\|raw_safe` on `.htm`) stay Phase 16 by RESEARCH; FormatHTML unsafe-tag tests substitute this phase and passed. + +--- + +_Verified: 2026-10-06T17:29:31Z_ +_Verifier: the agent (gsd-verifier)_