--- phase: 15-journal-plugin plan: 04 type: execute wave: 4 depends_on: ["15-03"] files_modified: - ../sm-journal-plugin/updates/postgres_test.go - ../sm-journal-plugin/updates/migrations_test.go - ../sm-journal-plugin/models/fillable_test.go - ../sm-journal-plugin/models/translatable_test.go - ../sm-journal-plugin/classes/format_html_test.go - ../sm-journal-plugin/admin_harness_test.go - ../sm-journal-plugin/controllers/api/posts_test.go - ../sm-journal-plugin/controllers/api/media_test.go - ../sm-journal-plugin/search_test.go - ../sm-journal-plugin/integration_test.go - ../sm-grzybyfunkcjonalne-app/boot_test.go - scripts/check-phase15.sh - .planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md - .planning/phases/15-journal-plugin/15-VALIDATION.md autonomous: true requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11, D-12, D-13, D-14, D-15, D-16, D-17, D-18, D-19, D-20, D-21, D-22, D-23] estimate: tokens: 90000 raw_tokens: 90000 tasks: 3 confidence: low must_haves: truths: - "D-05: every RESEARCH §8 PHPUnit row has a named Go test (JOURNAL-001/002 fillable, JOURNAL-005 draft 404, JOURNAL-006 media, FormatHTML substitutes JOURNAL-003/004 templates, redactor_id not fillable)." - "Real Postgres migrates all golem15_journal_* tables plus author_slug, rolls back, and remigrates." - "Anonymous list hides drafts; GET categories and tags return 200 PHP {data} lists; GET rss is well-formed RSS 2.0 honoring rss_*; write without Bearer is 401 Authentication required including featured-images POST/DELETE; Typesense gate off records zero HTTP." - "Host Activate still returns user+translate+journal; CORS includes _journal/api/*; plugin README stays application-neutral." - "scripts/check-phase15.sh --all is fail-closed; 15-SECURITY-REVIEW.md closes every high T-15-* threat." artifacts: - path: "../sm-journal-plugin/integration_test.go" provides: "end-to-end migrate, admin create, public GET, Bearer write, draft 404" contains: "TestJournalEndToEnd" - path: "../sm-journal-plugin/models/fillable_test.go" provides: "JOURNAL-001/002" contains: "TestFillable" - path: "../sm-journal-plugin/controllers/api/posts_test.go" provides: "JOURNAL-005, 401 PHP shape, categories/tags lists, RSS XML, featured-image auth" contains: "TestJournal005DraftShow" - path: "scripts/check-phase15.sh" provides: "fail-closed phase gate" contains: "sm-journal-plugin" - path: ".planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md" provides: "ASVS L1 threat evidence" contains: "T-15-01" key_links: - from: "../sm-journal-plugin/integration_test.go" to: "../sm-journal-plugin/routes.go" via: "assembled public GET and Bearer POST through production handlers" pattern: "TestJournalEndToEnd" - from: "scripts/check-phase15.sh" to: "../sm-journal-plugin/updates/migrations_test.go" via: "full plugin suite with Docker/Postgres, not -short as final evidence" pattern: "go -C" - from: ".planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md" to: "../sm-journal-plugin/controllers/api/posts_test.go" via: "each high threat cites an executed TestName" pattern: "T-15-" prohibitions: - requirement_id: D-07 category: safety statement: "Gate fails if the PHP journal tree at SHA 02110eb has a git diff" status: resolved verification: test - requirement_id: D-16 category: architecture statement: "Gate fails if fonoteka.go parity/tide files newly mention /_journal/api/v1" status: resolved verification: test - requirement_id: D-12 category: safety statement: "TestSearchGateOff must not skip and must observe zero search HTTP" status: resolved verification: test --- ## Phase Goal **As a** 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. This plan's slice: the last plan of the phase — full unit/integration coverage, PHPUnit map, phase gate, and security review. Finish Phase 15 with the dedicated test plan: PHPUnit behavioral map, migration rollback, HTTP/security cases, fail-closed gate, and ASVS L1 review. Purpose: every locked D-ID and high threat fails closed when broken. Output: test matrix, scripts/check-phase15.sh, 15-SECURITY-REVIEW.md, validated 15-VALIDATION.md. @~/.codex/gsd-core/workflows/execute-plan.md @~/.codex/gsd-core/templates/summary.md @.planning/phases/15-journal-plugin/15-VALIDATION.md @.planning/phases/15-journal-plugin/15-RESEARCH.md @.planning/phases/15-journal-plugin/15-PATTERNS.md @../sm-translate-plugin/updates/postgres_test.go @../sm-translate-plugin/admin_harness_test.go @scripts/check-phase14.2.1.sh ## Spec-less probe fallback Visible skip: no REQUIREMENTS.md IDs and no phase SPEC Edge Coverage/Prohibitions to lift. Tests map to D-01..D-23, RESEARCH §8, and VALIDATION rows. Do not generate probe predicates. Do not claim API-09 or QA-05. ## API coverage No external API integration: this phase ports a compiled plugin's own `/_journal/api/v1` surface and an optional beachcomber Gate that stays off; it does not integrate a third-party SaaS SDK. Do not fabricate a capability matrix. ## Artifacts this phase produces - `TestJournalEndToEnd` spanning migrate, Activate, admin mlmarkdown save, anonymous list, Bearer write, JOURNAL-005, media 403. - Real-Postgres up/down for all seven journal migrations. - `scripts/check-phase15.sh` with plugin/host/PHP-pin/docs-neutral/security stages using `go -C ../sm-journal-plugin` and `go -C ../sm-grzybyfunkcjonalne-app`. - `15-SECURITY-REVIEW.md` and completed `15-VALIDATION.md`. ## Multi-source coverage audit | SOURCE | ID | Feature/Requirement | Plan | Status | Notes | |---|---|---|---|---|---| | GOAL | — | Port Golem15.Journal to sm-journal-plugin and mount in a host | 01-04 | COVERED | Schema, admin, API, tests | | REQ | — | No mapped requirement IDs (TBD) | — | COVERED | Visible spec-less fallback; D-IDs used; API-09/QA-05 are Phase 20 | | CONTEXT | D-01..D-04 | Frozen PHP pin, tables, YAML/API binding | 01-04 | COVERED | SHA asserted; PHP unchanged | | CONTEXT | D-05 | PHPUnit map | 04 | COVERED | RESEARCH §8 | | CONTEXT | D-06 | en+pl only | 01,04 | COVERED | Lang files + gate | | CONTEXT | D-07 | No PHP edits | 01,04 | COVERED | git diff empty | | CONTEXT | D-08 | Nav SVG | 02,04 | COVERED | Embedded bytes | | CONTEXT | D-09 | Translate is a prior phase; Journal Requires it | 01,04 | COVERED | No Translate port inside Journal | | CONTEXT | D-10 | Translatable attributes | 01-04 | COVERED | MorphName PHP strings | | CONTEXT | D-11 | cabana markdown | 02,04 | COVERED | No-op; mlmarkdown only | | CONTEXT | D-12 | Typesense off by default | 03,04 | COVERED | Gate + TestSearchGateOff | | CONTEXT | D-13 | CSV CLI + toolbar | 02,04 | COVERED | TestJournalCommands | | CONTEXT | D-14 | Full /_journal/api/v1 | 03,04 | COVERED | routes.php wins | | CONTEXT | D-15 | Backend Bearer writes | 03,04 | COVERED | Not frontend tokens | | CONTEXT | D-16 | Not tide | 03,04 | COVERED | Gate forbids harness add | | CONTEXT | D-17 | Limiters + CORS | 01,03,04 | COVERED | Buckets + http.yaml | | CONTEXT | D-18..D-23 | Proof host and remotes | 01,04 | COVERED | Three-plugin boot | | RESEARCH | — | Squash golem15_journal_*; no rainlab-era names | 01,04 | COVERED | Migration tests | | RESEARCH | — | YAML rewrite; FilterScopes | 02,04 | COVERED | Form compile tests | | RESEARCH | — | FormatHTML plugin-local | 02,04 | COVERED | XSS substitute tests | | RESEARCH | — | PHP {error} JSON; per_page 9/30; slug show | 03,04 | COVERED | API tests | | RESEARCH | — | Sibling replace ../summercms.go | 01,04 | COVERED | Isolated go test | Excluded (deferred / other phases): Phase 16 HTML/views/components; Winter.Pages menu types; dashboard widget; Apparatus personal tokens; 19 extra locales; WYSIWYG/redactor; editing wn-journal-plugin; tide 154-route harness; sitemap. Task 1: Prove migrate → admin save → anonymous list → Bearer write → draft 404 end to end ../sm-journal-plugin/updates/postgres_test.go, ../sm-journal-plugin/admin_harness_test.go, ../sm-journal-plugin/integration_test.go, ../sm-grzybyfunkcjonalne-app/boot_test.go ../sm-translate-plugin/updates/postgres_test.go, ../sm-translate-plugin/admin_harness_test.go, ../sm-translate-plugin/integration_test.go, ../sm-journal-plugin/plugin.go, ../sm-journal-plugin/routes.go, ../sm-journal-plugin/controllers/posts.go, ../sm-journal-plugin/controllers/api/posts.go, .planning/phases/15-journal-plugin/15-VALIDATION.md, .planning/phases/15-journal-plugin/15-RESEARCH.md (§8 PHPUnit map) Copy the translate/user fail-closed TestMain: testcontainers Postgres, dedicated database, Docker unavailability fails full runs; only explicit -short may skip. TestJournalEndToEnd (D-05, D-14, D-15, D-19): migrate user, translate, and journal in Requires order; party.Activate those three plus a process-local test fixture only if needed (no production fixture plugin); cabana.Activate; surf.Assemble. Mint a backend principal with golem15.journal.access_posts plus access_publish, and a second principal without access_other_posts. Create a published post and a draft via admin or model helpers using mlmarkdown maps for en/pl. Assert English on host columns and Polish in golem15_translate_attributes under MorphName Golem15\Journal\Models\Post. GET /_journal/api/v1/posts with no Authorization returns 200 and only the published post (D-14). POST /posts without Bearer is 401 Authentication required (D-15, T-15-01). POST with backend Bearer creates a row. GET draft slug as anonymous is 404 with no data key (JOURNAL-005, T-15-02). Owner or access_other_posts sees 200. Host TestBootUserTranslateJournal still activates three plugins, sees Journal controller IDs, CORS path, and /_journal/api/v1 GET+POST. It must not duplicate the full fixture matrix. Do not import sm-user-plugin from journal production code; host tests may join users if present. redactor_id column exists and is not in Fillable; set only via explicit assign (PostRedactor analog, D-05). go -C ../sm-journal-plugin test ./... -count=1 -v -run '^(TestJournalEndToEnd)$' && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslateJournal)$' Non-zero exit; either run prints "--- FAIL", "--- SKIP", "no tests to run", or container startup treated as skip; lacks its named PASS line. - Integration uses real Postgres and production gormigrate/party/surf/cabana paths. - Anonymous list hides the draft; draft show 404 has no data key. - Bearer write succeeds for a permitted backend principal and 401s without Authorization using the PHP string error. - Polish title is in translate attributes keyed by the PHP Post morph string. - Host boot still lists exactly the three production plugins. The Phase 15 user-visible path is proven on real Postgres before the horizontal test matrix. Task 2: Complete PHPUnit map, migrations, YAML, FormatHTML, and search-gate tests ../sm-journal-plugin/updates/migrations_test.go, ../sm-journal-plugin/models/fillable_test.go, ../sm-journal-plugin/models/translatable_test.go, ../sm-journal-plugin/classes/format_html_test.go, ../sm-journal-plugin/controllers/api/posts_test.go, ../sm-journal-plugin/controllers/api/media_test.go, ../sm-journal-plugin/search_test.go, ../sm-journal-plugin/admin_harness_test.go .planning/phases/15-journal-plugin/15-RESEARCH.md (§8 PHPUnit map, JOURNAL-001..006), /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/tests/security/AccessControlTest.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/tests/security/MassAssignmentTest.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/tests/security/XssTest.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/tests/unit/models/PostRedactorTest.php, ../sm-journal-plugin/models/post.go, ../sm-journal-plugin/classes/format_html.go, ../sm-journal-plugin/controllers/api/media.go, modules/cabana/markdown_test.go Complete D-05 without porting Phase 16 Twig templates. Migrations (D-01, D-04): TestJournalTables and rollback/remigrate assert every final table/column/index, unique slugs, JSONB metadata/sources, settings defaults including search_use_typesense false, author_slug on backend_users, and absence of rainlab-era journal table names. Fillable (JOURNAL-001/002, T-15-04): Tag allow-list name/slug/description only; Category excludes nest_*; extra JSON keys dropped; Post API assigns never persist redactor_id or user_id from the body. Translatable (D-10): Post/Category MorphName PHP strings; slug is indexed; Tag has no Translatable. FormatHTML (JOURNAL-003/004 substitute): reject script, iframe, event handlers, javascript/vbscript/data schemes; footnotes/tables from goldmark extensions still pass the reject gate. Do not port .htm files. API: per_page 9/30; slug show; numeric fallback; previous_post/next_post/related_posts present on show; unpublished lock prefix absent from JSON; editor Bearer on GET sees drafts; invalid frontend-audience token on POST is 401; publish without access_publish 403. Named TestJournalPublicCategories and TestJournalPublicTags: anonymous GET /_journal/api/v1/categories and /tags return 200 PHP {data} list shapes (categories honor include_empty; tags ordered by name). Named TestJournalRSS: GET /rss is well-formed RSS 2.0 XML honoring rss_title, rss_posts_per_feed, rss_include_content, and rss_enabled. Named TestJournalFeaturedImageUnauthenticated: featured-image POST/DELETE without Bearer is 401 Authentication required; without canEdit is 403 You do not have permission to edit this post. Media (JOURNAL-006, T-15-03): 403 without access_posts; 201 with permission; folder .. rejected; stored path under journal/. Search (D-12, T-15-11): TestSearchGateOff zero HTTP; unpublished ShouldBeSearchable false even if someone flipped the setting in-memory. Admin: 403 without access_posts; YAML compile TestPostsFormCompiles; FilterScopes without illegal filter keys; commands registered; SVG embed present. Limiter names and CORS: TestJournalBuckets; host or plugin test reading ../sm-grzybyfunkcjonalne-app/config/http.yaml requires _journal/api/* (D-17). Isolated plugin tests that cannot see the host file skip only that assertion, not the bucket test. D-11: assert content field type mlmarkdown in compiled schema; assert modules/cabana/form_schema.go already lists markdown/mltext/mlmarkdown (no-op this phase). D-16: no new tide fixtures. go -C ../sm-journal-plugin test ./... -count=1 -v -race && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -race Non-zero exit; Go race detector reports a race; any package reports FAIL; integration tests unexpectedly SKIP in the plugin run; output lacks PASS lines for TestJournalPublicCategories, TestJournalPublicTags, TestJournalRSS, and TestJournalFeaturedImageUnauthenticated. - Named tests exist for JOURNAL-001, JOURNAL-002, JOURNAL-005, JOURNAL-006, FormatHTML unsafe tags, redactor_id not fillable, TestSearchGateOff, TestJournalCommands, TestPostsFormCompiles, TestJournalBuckets, TestJournalPublicCategories, TestJournalPublicTags, TestJournalRSS, TestJournalFeaturedImageUnauthenticated. - Migration rollback and remigrate pass on real Postgres. - High threats T-15-01, T-15-02, T-15-03, T-15-04, T-15-07, T-15-08, T-15-09, T-15-10, T-15-11, T-15-13 have fail-when-broken tests. - No test requires Pages menu types, dashboard widgets, extra locales, or PHP tree writes. Every D-05 PHPUnit row and every in-scope high threat has named Go evidence. Task 3: Phase gate, security review, and validation sign-off scripts/check-phase15.sh, .planning/phases/15-journal-plugin/15-SECURITY-REVIEW.md, .planning/phases/15-journal-plugin/15-VALIDATION.md scripts/check-phase14.2.1.sh, .planning/phases/15-journal-plugin/15-VALIDATION.md, .planning/phases/15-journal-plugin/15-RESEARCH.md (Security Domain), .planning/phases/15-journal-plugin/15-01-PLAN.md, .planning/phases/15-journal-plugin/15-02-PLAN.md, .planning/phases/15-journal-plugin/15-03-PLAN.md Create scripts/check-phase15.sh modeled on check-phase14.2.1.sh. Stages: PHP SHA 02110eb1c0c3861370b0b9b47b209a0702ac5d88 and empty git diff on the PHP journal tree (D-01, D-02, D-03, D-07); plugin module/layout/replace ../summercms.go (D-22, D-23, pitfall 9); plugin go vet, full tests, race; host go vet/test/build including TestBootUserTranslateJournal (D-18..D-21); CORS _journal/api/*; plugin README forbidden-name scan (the application / blog only); no rainlab-era table names; no cabana field_markdown.go diff from this phase (D-11 no-op); no tide harness add (D-16); security-review file present. Use go -C ../sm-journal-plugin and go -C ../sm-grzybyfunkcjonalne-app. Full mode requires Postgres; Docker missing is failure; -short is not final evidence. Require named PASS lines for TestJournalEndToEnd, TestJournal005DraftShow, TestJournal006MediaUpload, TestFillable, TestSearchGateOff, TestJournalWriteUnauthenticated, TestBootUserTranslateJournal, TestJournalPublicCategories, TestJournalPublicTags, TestJournalRSS, TestJournalFeaturedImageUnauthenticated. End with Phase 15 gate passed. Run the security-review lane over local Phase 15 changes (CONTEXT discretion: after implementation, not a code-writing plan). If a typed security-review subagent is unavailable, self-perform as 14.2.1-04 did and disclose that in 15-SECURITY-REVIEW.md frontmatter. Produce 15-SECURITY-REVIEW.md at ASVS L1, block_on high. Preserve unique threat IDs T-15-01 through T-15-15 plus T-15-SC (reserved, never colliding). For every high threat cite source control and executed TestName. Review draft enumeration, media traversal, fillable, stored XSS in content_html, cross-user edit, publish permission, frontend token on writes, Typesense leak, rate-limit XFF, envelope mixup, submodule provenance. Record the API-coverage declaration in the review: no external SaaS SDK this phase. Update 15-VALIDATION.md frontmatter to validated / nyquist_compliant / wave_0_complete only after mapped commands pass. Replace pending rows with exact test names and threat refs. Keep the manual SPA UAT row (admin login, Journal nav, mlmarkdown post) as human-check, not a silent pass. Do not commit unless the user asks; this planner run also does not commit. bash scripts/check-phase15.sh --all Non-zero exit; any stage absent or skipped; output contains FAIL, a Go race, no-tests-to-run, unexpected SKIP, unmitigated high threat, PHP tree dirty; lacks PASS evidence for TestJournalPublicCategories, TestJournalPublicTags, TestJournalRSS, or TestJournalFeaturedImageUnauthenticated; or lacks the final Phase 15 gate passed line. - Gate uses go -C ../sm-journal-plugin and go -C ../sm-grzybyfunkcjonalne-app. - PHP pin SHA matches 02110eb1c0c3861370b0b9b47b209a0702ac5d88 and the PHP tree has no diff. - 15-SECURITY-REVIEW.md lists each T-15-NN once, keeps T-15-SC, and blocks on high findings. - VALIDATION rows name existing tests; frontmatter is validated only after green execution. - Gate confirms no PHP edits, no extra locales, no Pages/dashboard, no tide add, no Typesense contact in TestSearchGateOff, no consuming-application name in the plugin README. - Gate requires named PASS for TestJournalPublicCategories, TestJournalPublicTags, TestJournalRSS, and TestJournalFeaturedImageUnauthenticated. The two-repository phase gate is green, high threats are mitigated with executed evidence, and validation is signed off. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Test/gate → production claims | A no-op filter, skipped container, or dirty PHP tree must not pass | | Security review → phase completion | High findings block completion | | Public HTTP → draft rows | JOURNAL-005 regressions must fail the gate | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-15-14 | Repudiation | phase gate | high | mitigate | Require named PASS lines and final marker; reject no-tests and unexpected skips | | T-15-15 | Information Disclosure | unpublished title prefix on API | medium | mitigate | Assert JSON titles omit the unpublished lock prefix | | T-15-SC | Tampering | package installs | high | mitigate | Gate confirms no new undecided require in plugin go.mod | ASVS L1: block_on high. T-15-01..T-15-13 originate in plans 01–03 and must appear in 15-SECURITY-REVIEW.md with executed tests, not as duplicate rows here. Run TestJournalEndToEnd, the race suites, then bash scripts/check-phase15.sh --all. - PHPUnit map D-05 is covered by named Go tests. - Plugin and host vet/test/race are green. - Phase 15 gate passed. - High T-15 threats are mitigated with evidence. - Deferred Phase 16/Pages/dashboard/Apparatus-token/extra-locale/PHP-edit items remain absent. Create `.planning/phases/15-journal-plugin/15-04-SUMMARY.md` when done