Files
summercms/.planning/phases/15-journal-plugin/15-RESEARCH.md
2026-10-06 18:02:17 +02:00

60 KiB
Raw Blame History

Phase 15: Journal plugin - Research

Researched: 2026-10-06 Domain: PHP-to-Go plugin port (WinterCMS Golem15.Journal → compiled sm-journal-plugin) plus proof-host wiring Confidence: HIGH

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

PHP source of truth

  • D-01: The port contract is github.com/golem15com/wn-journal-plugin at git SHA 02110eb (2026-08-27), the pin currently checked out in Płytarium. That tree is newer than grzybyfunkcjonalne.pl (2ad25e0) and the Winter starter (7237ed0); it includes tags, /_journal/api/v1, Typesense, and the security fixes. — Reversibility: costly — every model, table, YAML key and route in the Go plugin is copied from this tree.
  • D-02: Freeze at 02110eb. Planner and researcher must not silently pick a newer master. — Reversibility: reversible — a later discussion can re-pin.
  • D-03: Downstream agents read the frozen source from /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal (already at that SHA). Do not clone a second copy unless that path is missing or the SHA does not match.
  • D-04: Binding from that tree: models, tables, YAML forms/lists/filters, permission codes, console commands, and /_journal/api/v1 request/response shapes. Same bar as the user plugin port.
  • D-05: PHPUnit under tests/ (security access/XSS/mass-assignment plus PostRedactor) is a behavioral spec. Researcher maps each test to a Go case; the phase's last plan covers them.
  • D-06: The Go plugin ships English and Polish phrasebook files only. The other 19 PHP locales stay in PHP until a later request.
  • D-07: This phase does not edit or PR wn-journal-plugin. An urgent PHP upgrade on a live Winter site before that site migrates is allowed as operations. If it lands before plan or execute, re-pin the SHA rather than mixing trees.
  • D-08: Embed the same admin nav SVG (assets/images/journal-icon.svg) in the Go plugin.

What ships this phase

  • D-09: Do not port Golem15.Translate inside Phase 15. Insert sm-translate-plugin as its own roadmap phase before Phase 15 ($gsd-phase after this discussion). — Reversibility: one-way — Journal's translatable fields and Phase 16 blogs assume that plugin exists first.
  • D-10: Once that translate phase exists, Journal depends on it. Title, slug, content and the other PHP $translatable attributes stay translatable, so Phase 16 blogs work. — Reversibility: costly — the storage shape and admin fields couple to the translate plugin's contract.
  • D-11: Add a cabana markdown field (and preview) in summercms.go this phase, the same "framework first" pattern as Phase 12.1. Journal's post content uses it instead of a textarea stub. — Reversibility: costly — a new field type in the typed admin schema and generated TS types.
  • D-12: Port search_use_typesense, keep it off by default, and wire it to the Phase 11 search stack when enabled. A fresh install never contacts Typesense. — Reversibility: reversible — the setting and the hook can sit unused.
  • D-13: CSV import/export and the journal:export-posts / journal:import-posts commands ship now (admin + CLI). Winter.Pages menu item types and the dashboard report widget are deferred (no pages plugin, no dashboard widgets in the admin SPA).

Public HTTP

  • D-14: Ship the full /_journal/api/v1 surface this phase: anonymous GET posts, posts/{slug}, categories, tags, rss; backend-authenticated POST/PUT/DELETE posts, featured-image and media upload. Shapes follow API.md and routes.php in the frozen tree. — Reversibility: one-way — Phase 16 views and any importer will call this contract.
  • D-15: Write routes authenticate with the Phase 9 backend Bearer guard (admin session). They do not use frontend sm-user-plugin user.api_token (wrong identity) and they do not fold Apparatus PersonalApiToken / g15_ tokens (still .planning/todos/pending/backend-admin-api-tokens.md). — Reversibility: costly — clients that later expect g15_ tokens would need a second guard.
  • D-16: Proof is API.md plus the mapped PHPUnit tests. Journal is not added to the Płytarium tide 154-route harness.
  • D-17: Keep PHP's two limiter buckets (journal-public-api and journal-api, 120/min) and the CORS path _journal/api/*.

Where it boots

  • D-18: The proof host is the empty repo git@git.golem15.com:golem15/sm-grzybyfunkcjonalne-app.git (already created). Not an in-module testhost and not fonoteka.go / sm-summercmsio-app. — Reversibility: one-way — the Go module path and submodule layout land in that app.
  • D-19: Phase 15 proof on that host: Journal mounts, migrations run, admin screens work, /_journal/api/v1 answers. Live site HTML and reusable blog views stay Phase 16.
  • D-20: Local checkout: /media/nvme/dev/golem15/summercms.io/summercms/sm-grzybyfunkcjonalne-app/ (sibling of summercms.go, same as fonoteka.go and sm-summercmsio-app).
  • D-21: The app mounts sm-journal-plugin, sm-translate-plugin and sm-user-plugin (submodules at plugins/golem15/{journal,translate,user}), following .planning/notes/core-plugins-own-repos.md.
  • D-22: The plugin remote git@git.golem15.com:golem15/sm-journal-plugin.git already exists and is public. Module path git.golem15.com/golem15/sm-journal-plugin, package journal, plugin ID golem15.journal.
  • D-23: Local plugin checkout: /media/nvme/dev/golem15/summercms.io/summercms/sm-journal-plugin/ (sibling of summercms.go). The host mounts it as a submodule at plugins/golem15/journal with a go.work replace, same as sm-user-plugin in fonoteka.go.

Claude's Discretion

  • Markdown field YAML type name, Go identifiers and SPA registry keys, provided they follow existing cabana fail-loud rules and update the module README, docs/, admin OpenAPI, generated TS types and committed dist/ in the same change.
  • Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Framework markdown work may land in an early plan and be tagged if a framework version bump is needed.
  • Exact app wiring (summer.yaml, go.work, embed vs not) inside sm-grzybyfunkcjonalne-app, as long as D-18 to D-21 hold.
  • How Winter component pages (journalPost, journalPosts, …) are documented as a Phase 16 successor — they are not implemented here.
  • Run the security-review agent: the phase touches the plugin API (pact), authorization (golem15.journal.*, draft visibility) and public HTTP.

Deferred Ideas (OUT OF SCOPE)

  • Insert sm-translate-plugin as a new phase before Phase 15 ($gsd-phase). Journal will depend on it (D-09, D-10).
  • Phase 16: grzybyfunkcjonalne.pl HTML, reusable blog views, Winter component successors (journalPost, journalPosts, RSS page, author/tags pages).
  • Winter.Pages menu item types (journal-category, journal-post, …) until a pages plugin exists.
  • Admin dashboard report widget until the SPA has dashboard widgets.
  • Apparatus backend personal API tokens (g15_) — todo backend-admin-api-tokens.md.
  • Remaining 19 PHP locales beyond en and pl.
  • WYSIWYG / redactor as a second editor mode, unless research shows the markdown field must cover PHP's switch this phase.

Reviewed Todos (not folded)

  • sitemap-plugin-port.md: next blog / Phase 16; user chose leave on backlog.
  • 2026-10-01-benchmark-the-application-on-wintercms-vs-summercms.md, nest-framework-packages-under-modules.md, bonfire-duplicate-command-names.md, lagoon-readme-after-commit-callback-order.md, mailblocker-with-mailing.md, per-module-readmes-after-nest.md, readme-go-fences-src.md, refresh-fonoteka-readme.md, rewrite-summercms-readme.md, scaffold-admin-controller-incomplete.md, scaffold-same-second-migration-order.md, wristband-neutral-resource-default.md, scaffold-generated-header-and-command-deps.md, backend-admin-api-tokens.md: keyword matches only, not this phase. </user_constraints>

Phase Requirements

ROADMAP lists Requirements: TBD. REQUIREMENTS.md has no Journal IDs. Stale traceability still maps API-09 and QA-05 to Phase 15; those IDs belong to Phase 20 cutover (154 Płytarium routes). Do not treat them as Phase 15 acceptance.

Map locked decisions and success criteria instead:

ID Behavior this phase must prove
D-01–D-08 Frozen PHP pin; models/tables/YAML/perms/commands/API; PHPUnit mapped; en+pl only; no PHP edits; embed SVG
D-09–D-13 Require golem15.translate; translatable attributes; markdown/mlmarkdown; Typesense off by default; CSV + CLI
D-14–D-17 Full /_journal/api/v1; backend Bearer writes; not tide; limiter names + CORS
D-18–D-23 Proof host sm-grzybyfunkcjonalne-app; plugin module/path/ID
SC-1 Plugin repo: models, migrations, YAML admin, permission-gated SPA
SC-2 Host mounts plugin and serves public content / PHP-equivalent routes
SC-3 Neutral README (the application, blog)
SC-4 Unit tests in the last plan

Project Constraints (from CLAUDE.md)

  • Lean planning: few larger plans; unit tests are always the last plan of a phase; earlier plans may smoke-test but must not be blocked on coverage.
  • Plan-count checkpoint before writing PLAN.md files.
  • Stdlib first (net/http ServeMux, html/template, encoding/json). Add a dependency only when this research or a phase decision names it. go vet and go test ./... green at every commit.
  • Compiled plugins only. No runtime plugin loading.
  • API parity is the acceptance test for ported contracts; do not "improve" shapes.
  • GORM on Postgres only; gormigrate (not goose/atlas); AutoMigrate is never the schema source (DATA-02).
  • Two-repo rule: summercms.go is framework only and knows nothing about Płytarium. This phase writes the plugin in sm-journal-plugin, the proof host in sm-grzybyfunkcjonalne-app, and summercms.go only if a framework feature is missing. Planning docs stay in summercms.go/.planning.
  • Per-request state travels in context.Context; no package-level globals hold request state (KERN-07).
  • Framework READMEs never name a consuming application. Neutral examples (blog, acme). Plugin README same rule.
  • A change to a module's exported API, config keys or CLI commands updates that module's README.md and the affected docs/ pages in the same change. Cabana field types also update admin OpenAPI, generated TS types, and committed boardwalk dist/.
  • PHP originals are not changed. Core plugin contracts stay non-breaking.
  • Commits: no co-author tags; one logical change per commit; planning docs and code in separate commits.
  • This phase is plugin API (pact) plus security (public HTTP, draft visibility, golem15.journal.*). Do not skip the security-review agent later.

No AGENTS.md in the working directory.

Summary

Phase 15 ports frozen PHP Golem15.Journal at SHA 02110eb1c0c3861370b0b9b47b209a0702ac5d88 into a new compiled plugin sm-journal-plugin (package journal, ID golem15.journal) and mounts it in sm-grzybyfunkcjonalne-app beside sm-user-plugin and sm-translate-plugin. The PHP tree is present at /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal and matches D-01. Do not clone a second copy. The plugin checkout path D-23 does not exist yet; clone the empty remote in plan 01.

Primary recommendation: Copy the user/translate mount pattern. Squash PHP rainlab→golem15 history into final golem15_journal_* tables. Require golem15.translate and implement Translatable() / TranslatableIndexes() / MorphName() with PHP class strings. Treat D-11 as already shipped in Phase 14.2.1 (type: markdown / mlmarkdown); do not add a second YAML type. Adapt PHP YAML to cabana's fail-loud key set (do not byte-copy). Ship /_journal/api/v1 with PHP {error} JSON, optional backend principal on public GET, required backend Bearer on writes (not cabana's admin envelope, not g15_, not user.api_token). Keep limiter names and add host CORS _journal/api/*. Unit tests last.

Architectural Responsibility Map

Capability Primary Tier Secondary Tier Rationale
Posts/categories/tags/pivots/settings DDL Database / Storage API / Backend gormigrate squash; plugin models own rows
Translatable title/slug/content/excerpt API / Backend Database / Storage sm-translate-plugin tables; Journal implements Translatable
Admin Posts/Categories/Tags/Settings API / Backend Browser / Client cabana YAML schema + SPA
Public /_journal/api/v1 JSON + RSS API / Backend — pact.HasRoutes; PHP shapes
Write API auth API / Backend — cabana backend JWT verification; plugin-owned 401 JSON
Rate limits journal-public-api / journal-api API / Backend — surf.BucketProvider
CORS _journal/api/* API / Backend — host config/http.yaml
Typesense sync API / Backend External beachcomber Gate off by default
Markdown → content_html API / Backend — plugin FormatHTML; goldmark already in tree
CSV import/export CLI CLI API / Backend pact.HasCommands + toolbar gated by permission
Proof host boot API / Backend — sm-grzybyfunkcjonalne-app
HTML/blog views — — Phase 16; document successor only
Pages menu types, dashboard widget, g15_ tokens — — Deferred

Standard Stack

No new libraries. Use the already-decided stack in summercms.go go.mod and sm-translate-plugin.

Core

Library Version Purpose Why Standard
Go 1.27.0 Language Project pin; go version go1.27.0-X:nodwarf5 this session
GORM v1.31.2 Models Decided; translate/user plugins require it
gorm.io/driver/postgres v1.6.3 Postgres Decided
go-gormigrate/gormigrate/v2 v2.1.7 Plugin migrations Decided
goldmark v1.8.6 Markdown → HTML Already a direct framework dep; cabana.RenderMarkdown
goccy/go-yaml v1.19.2 fields.yaml / columns.yaml Cabana already parses with this
gocloud.dev v0.46.0 Media blob for /media/upload Already in host/plugin graphs
Vue 3 + TypeScript admin SPA Existing markdown/mlmarkdown controls No new SPA field type this phase

Supporting

Library Version Purpose When to Use
testcontainers-go + modules/postgres v0.44.0 Real Postgres for migration/API tests Last plan; -short skip
stretchr/testify v1.12.1 Assertions Last plan only; keep func Test...(*testing.T)
go-i18n/v2 v2.6.1 Phrasebook Plugin HasLang catalogs; not model translations
golang-jwt/jwt/v5 v5.3.1 Backend Bearer on writes Already cabana backend guard

Alternatives Considered

Do not use these. CONTEXT locked the pin, auth, host, and limiter names.

Instead of Could Use Why rejected
SHA 02110eb Newer master D-02 forbids it
Backend Bearer user.api_token or g15_ D-15 forbids both
sm-grzybyfunkcjonalne-app testhost / fonoteka.go D-18 forbids them
Cabana mlmarkdown New YAML type / plugin widget D-11 already shipped; fail-loud registry
golem15_journal_* squash Replay rainlab history Same as translate/user ports
Cabana admin WriteError envelope on /_journal/api/v1 PHP {error} D-14 shapes
New markdown library goldmark Already in go.mod
goose / atlas gormigrate Project stack

Installation: none. Plugin go.mod copies the translate-plugin require set (GORM, gormigrate, summercms replace). Do not add goldmark to the plugin unless FormatHTML lives there and needs the import (then require the same v1.8.6 already in the framework graph).

Version verification: read summercms.go/go.mod, sm-translate-plugin/go.mod, and host go.mod this session. Goldmark v1.8.6 is already a host indirect require.

Package Legitimacy Audit

This phase installs no new external packages.

Package Registry Verdict Disposition
(none new) — — —

Packages removed due to [SLOP] verdict: none Packages flagged as suspicious [SUS]: none

Verified PHP pin

git rev-parse HEAD in /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal is 02110eb1c0c3861370b0b9b47b209a0702ac5d88. Matches D-01/D-02/D-03. Do not read a newer tree. Do not edit it (D-07).

Local plugin path /media/nvme/dev/golem15/summercms.io/summercms/sm-journal-plugin/ is missing. Plan 01 clones git@git.golem15.com:golem15/sm-journal-plugin.git there.

Proof host exists at /media/nvme/dev/golem15/summercms.io/summercms/sm-grzybyfunkcjonalne-app/ with user+translate only.

1. Tables, columns, indexes (squash to final names)

PHP created rainlab_journal_* then renamed to golem15_journal_* in v2.0.0. Go squashes to the final names. Do not replay the rainlab era. Do not emit rainlab_journal_*.

Final tables (use these)

Table PHP $table / create Role
golem15_journal_posts "public $table = 'golem15_journal_posts';" [VERIFIED: /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/Post.php:37] Posts
golem15_journal_categories "public $table = 'golem15_journal_categories';" [VERIFIED: .../models/Category.php:21] Categories
golem15_journal_tags "public $table = 'golem15_journal_tags';" [VERIFIED: .../models/Tag.php:14] Tags
golem15_journal_posts_categories pivot 'table' => 'golem15_journal_posts_categories' [VERIFIED: Post.php:110] Post↔category
golem15_journal_posts_tags pivot 'table' => 'golem15_journal_posts_tags' [VERIFIED: Post.php:116] Post↔tag
golem15_journal_settings PHP settingsCode = 'golem15_journal_settings' in system_settings [VERIFIED: .../models/Settings.php:13] Go: dedicated singleton table ID=1, same as cabana settings docs — not system_settings JSON

Posts columns (final, after all PHP ALTERS)

Squash create + content_html (already in v1.0.1 create) + metadata + is_pinned + sources + redactor_id. Unique slug. Nullable timestamps.

Column Type (Postgres) Notes
id SERIAL PK
user_id INTEGER NULL indexed Backend author (backend_users.id)
redactor_id INTEGER NULL indexed Frontend user; not fillable
title TEXT NULL Translatable host column
slug TEXT NOT NULL UNIQUE Translatable + indexed in translate plugin
excerpt TEXT NULL Translatable
content TEXT NULL Translatable markdown source
content_html TEXT NULL Translatable; regenerated on save
published_at TIMESTAMPTZ NULL
published BOOLEAN NOT NULL DEFAULT FALSE
is_pinned BOOLEAN NOT NULL DEFAULT FALSE v2.4.0
metadata JSONB NULL v1.3.0 jsonable
sources JSONB NULL v2.4.0 jsonable
created_at, updated_at TIMESTAMPTZ NULL PHP v1.2.4 nullable

Attachments stay in framework system_files with attachment_type = PHP class string Golem15\Journal\Models\Post (v2.0.0 rename already used that string).

Categories columns

From create + nested fields (already in the create_categories file at this pin): id, name, slug indexed, code nullable, description, parent_id indexed nullable, nest_left, nest_right, nest_depth, timestamps. Unique slug. Keep nest_* columns. No NestedTree reorder UI this phase; parent_id is a relation.

Tags columns

id, name, slug indexed, description, timestamps. Unique slug.

Companion: backend_users.golem15_bloghub_author_slug

PHP v2.2.2 adds golem15_bloghub_author_slug VARCHAR(128) NULL UNIQUE on backend_users [VERIFIED: updates/v2.2.2/add_author_slug_to_backend_users.php:13-16]. Framework backend_users table exists [VERIFIED: modules/cabana/contracts.go:56 return "backend_users"]. Ship a Journal plugin migration that ALTER TABLE backend_users ADD COLUMN IF NOT EXISTS golem15_bloghub_author_slug TEXT UNIQUE. Do not put the column in lagoon's framework migration. Phase 16 journalAuthor consumes it; include the column now (D-04 tables).

Settings singleton

Cabana stores settings as ID=1 of the plugin's own table, not PHP system_settings [VERIFIED: docs/backend/settings.md:7-9]. Create golem15_journal_settings with typed columns matching PHP $rules / fields.yaml: show_all_posts bool default true, use_rich_editor bool default false, search_use_typesense bool default false, search_title_weight int default 5, search_excerpt_weight int default 3, search_content_weight int default 1, RSS fields. use_rich_editor is stored and ignored at compile time (deferred WYSIWYG; always mlmarkdown).

Migration IDs

Use dated squash IDs in plugin history (pattern 202610060001_create_golem15_journal_posts, …). One migration per table plus settings plus author_slug ALTER. Seed optional Uncategorized category only if PHP seeder at this pin still inserts it — verify in execute; do not invent seed posts.

2. Models, fillable, translatable, permissions

Translatable (D-10)

Post [VERIFIED: Post.php:58-65]:

DATA_k7m2p9qx_START

public $translatable = [
    'title',
    'content',
    'content_html',
    'excerpt',
    'metadata',
    ['slug', 'index' => true],
];

DATA_k7m2p9qx_END

Category [VERIFIED: Category.php:36-40]: 'name', 'description', ['slug', 'index' => true].

Tag: not translatable.

Go: Translatable() []string, TranslatableIndexes() []string (slug only on Post/Category), MorphName() returns PHP class strings:

  • Golem15\Journal\Models\Post
  • Golem15\Journal\Models\Category

Do not invent a Go type string. Translate indexes and attributes morph on those names.

Fillable / Rules

Model Fillable Rules
Post PHP has no $fillable; API writes field-by-field. Admin: cabana Fillable on GORM struct for form columns only. Never fill redactor_id, content_html, user_id from public API body (user_id stamped from backend principal on create). title required; slug required+regex+unique; content required; published+published_at together (PHP afterValidate)
Category name, slug, code, description, parent_id [VERIFIED: Category.php:45-51] name required; slug required|between:3,64|unique; code nullable|unique
Tag name, slug, description [VERIFIED: Tag.php:27-31] name required; slug required|between:3,64|unique
Settings all settings columns PHP $rules [VERIFIED: Settings.php:17-31]

protected $guarded = ['*'] with fillable on Tag/Category is JOURNAL-001/002. Go: Fillable() lists only those names.

Post relations: user belongsTo cabana.BackendUser (user_id); redactor_id stored without importing sm-user-plugin (PHP $require is Translate+Apparatus, not User). categories/tags belongsToMany. featured_images/content_images attachMany via system_files.

canEdit: owner or golem15.journal.access_other_posts [VERIFIED: Post.php:194-197].

filterFields: hide published/published_at without golem15.journal.access_publish [VERIFIED: Post.php:164-178]. Go: controller hook returns cabana.ForbiddenError on publish writes without permission, and omit those fields from the compiled form when the principal lacks the grant (or keep fields but refuse save — refuse is mandatory; hiding is UX).

Permissions (copy codes exactly)

[VERIFIED: Plugin.php:55-90]

  • golem15.journal.manage_settings
  • golem15.journal.access_posts
  • golem15.journal.access_categories
  • golem15.journal.access_other_posts
  • golem15.journal.access_import_export
  • golem15.journal.access_publish
  • golem15.journal.access_tags

Navigation golem15.journal.* on the main item; sideMenu: new_post / posts / categories / tags with the PHP permission lists [VERIFIED: Plugin.php:96-134]. Embed assets/images/journal-icon.svg (D-08). Map icon-pencil to the existing lucide mapping used by other plugins; do not invent a new icon pack.

cabana.Allows is Winter hasAnyAccess (OR) [VERIFIED: modules/cabana/contracts.go:143-161].

Requires

PHP: Golem15.Translate + Golem15.Apparatus [VERIFIED: Plugin.php:15-18]. Apparatus is dissolved — do not require an apparatus plugin. Go: Requires() []string{ "golem15.translate" }. User plugin is optional (KERN-05) for redactor FK tests on the host.

3. YAML admin — adapt, do not byte-copy

Cabana formFieldTypes [VERIFIED: modules/cabana/form_schema.go:24-30]:

DATA_w3n8r4jt_START

"text": {}, "textarea": {}, "number": {}, "checkbox": {},
"switch": {}, "dropdown": {}, "relation": {}, "relation-manager": {},
"widget": {}, "partial": {}, "fileupload": {}, "datepicker": {},
"password": {}, "permissioneditor": {},
"markdown": {}, "mltext": {}, "mlmarkdown": {},

DATA_w3n8r4jt_END

Allowed field keys [VERIFIED: form_schema.go:37-48]: label, comment, span, type, required, tab, context, attributes, size, default, nameFrom, emptyOption, options, relation, widget keys, fileupload keys, datepicker keys, preset.

PHP models/post/fields.yaml uses keys that boot-fail in cabana: placeholder, cssClass, stretch, commentAbove, trigger, type: taglist, type: repeater, widget class Golem15\Journal\FormWidgets\JournalMarkdown, toolbar partial, metadata[preview_page] CMS dropdown, tabs.stretch / paneCssClass / icons.

Rewrite the YAML:

PHP field Go YAML
title type: mltext
slug type: mltext + preset field title type slug (preset is a legal key)
content type: mlmarkdown (D-11 + D-10). Not JournalMarkdown widget.
excerpt type: mltext or textarea for default locale only — use mltext
categories type: relation nameFrom: name
tags type: relation nameFrom: name — not taglist. On-the-fly customTags is a UX gap; create tags on the Tags admin.
published type: switch or checkbox
is_pinned type: checkbox
user type: relation nameFrom: login emptyOption current user
published_at type: datepicker mode: datetime. Drop trigger.
featured_images type: fileupload mode: image imageWidth/imageHeight 200
sources repeater Omit from admin form. Keep JSONB column; write API still accepts sources.
metadata[preview_page] Omit (CMS pages are Phase 16). Keep metadata JSONB.
toolbar partial Omit

Settings YAML: drop every trigger block on search weights; always show the weight fields. placeholder → drop or use comment.

Filters

PHP config_filter.yaml uses conditions: [VERIFIED: controllers/posts/config_filter.yaml:30-43]. Cabana filterScopeKeys has no conditions [VERIFIED: modules/cabana/filter_schema.go:17-20]. A conditions key is a boot error (list_schema_test.go "conditions list").

Re-express as pact.FilterScope:

  • published switch → FilterScopes name matching YAML scope: (e.g. FilterPublished) applying published <> true / published = true.
  • published_date daterange → scope on created_at between :after/:before.
  • category already has scope: FilterCategories — implement FilterCategories (include child categories as PHP does).

Lists

type: date is a legal list column [VERIFIED: modules/cabana/list_schema.go:23]. Keep. Row class unpublished → list row state disabled if cabana supports it; else skip (SPA-only).

Import/export (D-13)

No cabana ImportExport behavior. Ship:

  1. CLI journal:export-posts / journal:import-posts via pact.HasCommands (PHP Plugin.php:169-170).
  2. Admin toolbar actions on Posts, RequiredPermissions: []string{"golem15.journal.access_import_export"}.

Port PHP PostImport/PostExport column sets. Do not invent a generic CSV framework.

Controllers

PHP Go pact.AdminController ID Perm
controllers/Posts.php golem15.journal.posts access_posts
controllers/Categories.php golem15.journal.categories access_categories
controllers/Tags.php golem15.journal.tags access_tags
Settings registerSettings HasSettings code journal manage_settings

ListExtendQuery / FormExtendQuery: without access_other_posts, restrict to user_id = principal.ID.

FormBeforeCreate: stamp user_id from backend principal if empty [VERIFIED: Post.php:144-150].

Hooks regenerate content_html via plugin FormatHTML on save (not only API).

4. D-11 markdown field — already shipped

Phase 14.2.1 landed markdown / mltext / mlmarkdown in cabana and the SPA (MarkdownField.vue, MLMarkdownField.vue). cabana.RenderMarkdown uses goldmark without html.WithUnsafe and rejects script/iframe/event handlers/dangerous URLs [VERIFIED: modules/cabana/field_markdown.go:19-32].

Discretion recommendation: Do not add a second YAML type this phase. Journal post content is mlmarkdown. If a plan still lists "add markdown field", treat it as a no-op unless docs/README are missing a Journal example — they already document the types [VERIFIED: docs/backend/forms.md:324-346].

PHP formatHtml enables footnotes, attributes, tables then Html::clean unless backend.allow_unsafe_markdown [VERIFIED: Post.php:199-225]. Cabana's default goldmark does not enable those extensions. Do not change cabana.RenderMarkdown globally (mail-aligned gate). Put journal.FormatHTML in the plugin: goldmark with footnote/table/attribute extensions (same github.com/yuin/goldmark module, no new package) + the same rejectUnsafe gate. Stored content_html and public API use FormatHTML. SPA preview may use cabana.RenderMarkdown (simpler). Ignore use_rich_editor when compiling the form (deferred WYSIWYG).

5. /_journal/api/v1 contract

routes.php wins over API.md where they disagree

Topic API.md routes.php / controller Use
Auth All endpoints need g15_ Public GETs anonymous; writes token.auth D-15 backend Bearer on writes only
Get post GET /posts/{id} GET /posts/{slug} with [a-z0-9][a-z0-9\-\/]*; numeric slug falls back to id routes.php
List per_page default 15 max 50 default 9 max 30 [VERIFIED: PostApiController.php:34] controller
RSS not in endpoint table GET rss ship (D-14)
401 body {"error":"Invalid token"} writes {"error":"Authentication required"} writes: Authentication required; do not emit g15_ copy

Route table (implement this)

Public group prefix /_journal/api/v1, middleware throttle:journal-public-api only (no required auth):

Method Path Handler
GET /posts index
GET /posts/{slug} show; Where("slug", [a-z0-9][a-z0-9\-/]*) — note Go mux: constrain so it does not steal /posts/{id} writes
GET /categories categories
GET /tags tags
GET /rss rss XML

Write group same prefix, required backend principal + throttle:journal-api:

Method Path Extra perm
POST /posts access_posts; publish needs access_publish
PUT /posts/{id} Where("id", [0-9]+); canEdit
DELETE /posts/{id} canEdit
POST /posts/{id}/featured-images access_posts
DELETE /posts/{id}/featured-images/{fileId} access_posts
POST /media/upload access_posts (JOURNAL-006)

Go mux: register write /posts/{id} and public /posts/{slug} carefully. Numeric show is the public slug route with ctype_digit fallback [VERIFIED: PostApiController.php:192-201]. Writes use {id} digits only.

Optional editor on public GET

PHP: public group still consults BackendAuth::getUser() so an editor with access_posts sees drafts [VERIFIED: routes.php:16-17 and PostApiController.php:26-28]. Do not attach middleware "backend" to the public group (missing token would 401). In the handler, if Authorization: Bearer is present, verify with the same backend JWT (audience backend) and set principal; otherwise anonymous.

Do not use cabana "backend" middleware on these routes

Cabana's guard writes WriteError 401 unauthenticated / "Unauthenticated" [VERIFIED: modules/cabana/http.go:200-201]. PHP Journal API is flat {"error":"..."}. Implement plugin middleware or in-handler checks that:

  1. Verify the cabana backend JWT (HS256, same secret, same blacklist table backend_jwt_blacklist).
  2. Write PHP JSON: 401 {"error":"Authentication required"}, 403 {"error":"Insufficient permissions"} / {"error":"You do not have permission to publish posts"}, 404 {"error":"Post not found"}, 422 {"error":"Validation failed","errors":{...}}.

D-15 is satisfied by which token is accepted, not by wrapping cabana's admin envelope.

Draft visibility (JOURNAL-005)

Unpublished or published_at in the future is unpublished. Show returns 404 with no data key unless caller is owner (user_id) or holds golem15.journal.access_other_posts [VERIFIED: PostApiController.php:207-217 and AccessControlTest.php:49-69]. Anonymous always 404 for drafts.

Index: anonymous published=true; editor default includes drafts unless ?published=true [VERIFIED: PostApiController.php:49-58]. author filter editors only.

Serialize

Do not apply UNPUBLISHED_TITLE_PREFIX = '🔒 ' [VERIFIED: Post.php:43] on API JSON (PHP uses raw attributes). Include previous_post, next_post, related_posts on show (controller adds them; API.md list shape is incomplete).

Translations object on write: translations.*.title|slug|content|excerpt|metadata via translate plugin helpers, not extra columns.

Limiters (D-17)

[VERIFIED: routes.php:7-14]

DATA_p4c1v8qm_START

RateLimiter::for('journal-api', function (Request $request) {
    $user = \Backend\Facades\BackendAuth::getUser();
    return Limit::perMinute(120)->by($user?->id ?: $request->ip());
});

RateLimiter::for('journal-public-api', function (Request $request) {
    return Limit::perMinute(120)->by($request->ip());
});

DATA_p4c1v8qm_END

Go surf.BucketProvider [VERIFIED: modules/surf/limiter.go:21-33]:

func (p *Plugin) Buckets() map[string]surf.Bucket {
	trusted := surf.TrustedProxies(p.app.Config)
	return map[string]surf.Bucket{
		"journal-public-api": {
			Name:  "journal-public-api",
			Max:   120,
			Decay: time.Minute,
			Key:   func(r *http.Request) string { return "journal-public|" + surf.ClientIP(r, trusted) },
		},
		"journal-api": {
			Name:  "journal-api",
			Max:   120,
			Decay: time.Minute,
			Key: func(r *http.Request) string {
				if pr := bouncer.PrincipalFrom(/* ctx */); pr != nil && pr.Backend {
					return "journal-api|u:" + strconv.FormatUint(uint64(pr.ID), 10)
				}
				return "journal-api|" + surf.ClientIP(r, trusted)
			},
		},
	}
}

429 body is surf's {"message":"Too Many Attempts."} [VERIFIED: modules/surf/limiter.go:17]. Keep it (framework-wide), do not invent a Journal 429 shape.

CORS (D-17)

Host config/http.yaml currently only _user/api/* [VERIFIED: sm-grzybyfunkcjonalne-app/config/http.yaml:1-3]. Add _journal/api/*. Plugin tests that read host CORS must be updated the way user plugin TestRegisterCORSPath does.

Media upload (JOURNAL-006)

[VERIFIED: MediaApiController.php:21-62]: require backend user + access_posts; folder regex ^[A-Za-z0-9_\-\/]*$; force under journal/; strip leading journal segment; reject ... Image mimes jpg/jpeg/png/gif/webp max 10240 KB. 201 {data:{url,path}}. Use gocloud blob + host upload_bytes but still enforce 10MB in the handler.

6. Search (D-12)

PHP shouldBeSearchable returns the settings flag (default false) [VERIFIED: Post.php:245-248]. searchableAs is golem15_journal_posts [VERIFIED: Post.php:234-237].

Go: implement beachcomber.Searchable. Set a beachcomber.Gate that reads golem15_journal_settings.search_use_typesense for ID=1 and treats read errors as off (fonoteka pattern). ShouldBeSearchable false for unpublished posts and when the gate is off. Fresh install never contacts Typesense.

List search query: if length ≥ 3 and gate on, SearchPage then SQL re-gate; on engine error log and fall back to ILIKE (PHP try/catch). Weights from settings.

7. Host wiring (D-18–D-23)

Copy translate-phase host edits. Current host:

summer.yaml plugins user + translate only [VERIFIED: sm-grzybyfunkcjonalne-app/summer.yaml:3-7].

go.work uses ./plugins/golem15/user and ./plugins/golem15/translate [VERIFIED: go.work:5-9]. Add ./plugins/golem15/journal.

go.mod replace translate/user to submodule paths [VERIFIED: go.mod:85-87]. Add require + replace for git.golem15.com/golem15/sm-journal-plugin => ./plugins/golem15/journal.

Plugin go.mod (sibling checkout): module git.golem15.com/golem15/sm-journal-plugin, go 1.27.0, replace git.golem15.com/golem15/summercms => ../summercms.go like translate [VERIFIED: sm-translate-plugin/go.mod:122]. Do not copy PATTERNS' ../../../../summercms.go into the sibling checkout; that path is for a plugin that only lives under plugins/golem15/journal without a sibling go.mod replace. Host go.work covers the mounted tree.

.gitmodules: add journal submodule.

boot_test.go currently len(plugins) != 2, forbids golem15.journal [VERIFIED: boot_test.go:33-42]. Change to 3 plugins, allow journal, assert controller IDs and /_journal/api/v1 routes exist.

plugins.gen.go / main.go: regenerate with summer build; do not hand-author after first boot.

README: host may name itself; plugin README must not name grzybyfunkcjonalne or Płytarium.

8. PHPUnit → Go test map (D-05)

PHP test Finding Go case (last plan)
AccessControlTest::test_journal_005_api_show_returns_unpublished JOURNAL-005 httptest show draft: other user 404 no data; owner 200; access_other_posts 200; anonymous 404
AccessControlTest::test_journal_006_media_upload_no_permission_check JOURNAL-006 authenticated backend without access_posts → 403; with permission → 201; folder ../ rejected
MassAssignmentTest::test_journal_001_tag_fillable JOURNAL-001 Tag Fillable() is name/slug/description only; extra JSON keys dropped
MassAssignmentTest::test_journal_002_category_fillable JOURNAL-002 Category fillable allow-list; nest_left not mass-assigned
XssTest::test_journal_003_post_xss_filter JOURNAL-003 PHP asserts Twig |raw_safe on component templates (Phase 16). Go this phase: FormatHTML + rejectUnsafe on content_html; do not port .htm files
XssTest::test_journal_004_summary_xss_filter JOURNAL-004 Same: Phase 16 templates. This phase: summary accessor does not wrap unsanitized HTML into API JSON as executable markup
PostRedactorTest redactor_id Column exists, not in Fillable, set via explicit assign; null on backend-only rows. Do not import user plugin in journal go.mod; host test may join users if present

Also last-plan: migration table names, limiter names, CORS path, anonymous list hides drafts, write without Bearer 401 PHP shape, publish without access_publish 403, cabana YAML compile of adapted fields, mlmarkdown save through TranslationWriter, Typesense gate off (zero HTTP), go vet/go test ./....

9. Suggested plan split (recommendation, not PLAN.md)

Present at the plan-count checkpoint. Four plans, tests last:

  1. 15-01 — Clone sm-journal-plugin; squashed schema + models + Translatable + host submodule/summer.yaml/go.work/http.yaml CORS + boot_test 3 plugins.
  2. 15-02 — Admin controllers, adapted YAML, settings, permissions, nav SVG, FormatHTML, import/export commands + toolbar.
  3. 15-03 — /_journal/api/v1, buckets, optional editor on GET, backend Bearer writes, media upload, Typesense gate.
  4. 15-04 — Unit/integration tests last (PHPUnit map + smoke).

No separate framework-markdown plan: D-11 is done. Skip optional agents except security-review after implementation (CONTEXT discretion).

Architecture Patterns

System Architecture Diagram

Admin SPA  --backend JWT-->  cabana admin API  -->  Journal YAML controllers
                                               \->  HasSettings journal

Anonymous GET /_journal/api/v1/*  -->  throttle:journal-public-api
        \ optional Bearer          -->  Post/Category/Tag GORM
                                       \ if search+gate on --> beachcomber
                                       \ FormatHTML on save

Editor write /_journal/api/v1/*   -->  throttle:journal-api
        \ required backend JWT     -->  permission + canEdit
                                       \ featured_images / media blob

Host sm-grzybyfunkcjonalne-app
  party.Activate(user, translate, journal)
  journal Requires translate
sm-journal-plugin/
├── go.mod
├── plugin.go              # ID, Requires, Register, Buckets, Routes
├── README.md
├── admin.go / admin_permissions.go / admin_navigation.go
├── models/                # post, category, tag, settings + Fillable/Rules
├── updates/               # squashed gormigrate
├── controllers/           # admin + api
├── console/               # export/import
├── lang/en/lang.yaml
├── lang/pl/lang.yaml
├── assets/images/journal-icon.svg
└── controllers/{posts,categories,tags}/  # adapted YAML

sm-grzybyfunkcjonalne-app/
├── summer.yaml            # add golem15.journal
├── go.work / go.mod       # use + replace
├── plugins/golem15/journal  # submodule
└── config/http.yaml       # CORS _journal/api/*

Pattern 1: Compiled plugin init

What: party.Register in init, pact.Plugin + optional capabilities. When: every shared plugin. Analog: translate plugin.go, user plugin in host submodule.

Pattern 2: Squash PHP migrations

What: one DDL per final table named golem15_journal_*. When: always for Winter ports with rainlab history.

Pattern 3: Adapted YAML

What: rewrite fields to cabana-legal types/keys; FilterScopes instead of conditions. When: every PHP fields.yaml/columns.yaml/config_filter.yaml.

Pattern 4: PHP JSON API beside cabana admin

What: HasRoutes for /_journal/api/v1 with PHP error keys; cabana for SPA admin. When: public plugin APIs. Do not mix envelopes.

Anti-Patterns to Avoid

  • Byte-copy PHP YAML: boot fails on taglist, repeater, trigger, placeholder.
  • Attaching "backend" middleware to public GET: anonymous 401.
  • Using user.api_token or g15_: D-15.
  • Replaying rainlab table names.
  • Porting Translate or Pages or dashboard widgets.
  • Naming grzybyfunkcjonalne in the plugin README.
  • Adding Journal to tide 154-route harness (D-16).
  • Changing cabana.RenderMarkdown to enable footnotes globally.
  • Importing sm-user-plugin only to type redactor.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Admin CRUD Custom JSON admin cabana HasAdminControllers Schema pipeline already exists
Settings JSON blob system_settings port HasSettings singleton table docs/backend/settings.md
Rate limits Custom counters surf.BucketProvider Named buckets, 429 shape
CORS Handler headers host http.cors.paths surf OPTIONS 204
Markdown parse New library goldmark v1.8.6 Already pinned
Search client typesense-go beachcomber Gate + engine Phase 11; typesense-go was not added
Backend auth New JWT cabana backend JWT verify Same secret/audience
Translations title_pl columns sm-translate-plugin D-10
Nested filters Raw SQL in YAML pact.FilterScope conditions key refused
File attachments Ad-hoc disk system_files + fileupload cabana

Key insight: Journal is a compiled plugin over existing framework seams. The only new behavior is the PHP API/domain, not a new stack.

Runtime State Inventory

This is a port/migration (new Go plugin + host mount), not a rename of running production. Greenfield Go side; PHP Winter sites unchanged.

Category Items Found Action Required
Stored data PHP tables golem15_journal_* on live Winter sites; Go starts empty No data migration this phase. Cutover later (Phase 16/20). Squash Go DDL to final names.
Live service config Host http.yaml CORS _user/api/* only Add _journal/api/*
OS-registered state None — verified: no systemd unit in this phase's repos None
Secrets/env vars Host already has admin.jwt.secret (boot_test sets it) Writes reuse that secret. No new secret names. Typesense key already SUMMER_SEARCH__TYPESENSE__API_KEY — unused while gate off
Build artifacts sm-journal-plugin/ missing; host plugins.gen.go for 2 plugins Clone remote; summer build after summer.yaml change

Nothing found in category: OS-registered state — none in these checkouts.

Common Pitfalls

Pitfall 1: Byte-copy PHP YAML

What goes wrong: cabana.Activate boot error on taglist / repeater / trigger / placeholder / conditions. Why: fail-loud schema. How to avoid: rewrite YAML per §3. Warning signs: start-up log unknown field / unknown type.

Pitfall 2: conditions in config_filter.yaml

What goes wrong: boot error naming conditions. Why: Phase 9 already used this file as a negative example. How to avoid: FilterScopes. Warning signs: config_filter.yaml still contains conditions:.

Pitfall 3: Cabana 401 envelope on Journal API

What goes wrong: SPA-shaped {error:{code,message}} instead of {"error":"Authentication required"}. Why: wrapping "backend" middleware. How to avoid: in-handler / plugin middleware; still verify backend JWT. Warning signs: clients parsing error as a string fail.

Pitfall 4: Public GET requires Bearer

What goes wrong: anonymous list/show 401. Why: putting backend on the public group. How to avoid: optional verify only. Warning signs: curl without Authorization fails GET /posts.

Pitfall 5: Draft leak (JOURNAL-005)

What goes wrong: unpublished post JSON to strangers (200 or 401 instead of 404). Why: forgetting published_at in the future; returning 403 that confirms existence. How to avoid: 404 no data unless owner or access_other_posts. Warning signs: test_journal_005 analog fails.

Pitfall 6: Media folder traversal (JOURNAL-006)

What goes wrong: upload writes outside journal/. Why: concatenating folder unchecked. How to avoid: copy PHP regex + segment filter + forced prefix. Warning signs: path contains .. or /media/../.

Pitfall 7: Mass-assign redactor_id / nest_* / user_id

What goes wrong: API client becomes another author or frontend user. Why: Post has no PHP fillable; easy to lagoon.Fill the whole body. How to avoid: explicit allow-list in store/update matching PHP field-by-field assigns. Warning signs: PostRedactorTest fillable analog fails.

Pitfall 8: Host boot_test still wants 2 plugins

What goes wrong: TestBootUserTranslate fatals on journal. Why: 14.2.1 test forbids golem15.journal. How to avoid: update first in 15-01. Warning signs: unexpected plugin golem15.journal.

Pitfall 9: Sibling go.mod replace vs ../../../../

What goes wrong: plugin tests cannot compile from sm-journal-plugin/. Why: 14.2.1 PATTERNS mixed two replace paths. How to avoid: sibling ../summercms.go; host replace ./plugins/golem15/journal. Warning signs: module not found summercms.

Pitfall 10: MorphName mismatch

What goes wrong: translations and files attach to the wrong morph. Why: using journal.Post instead of Golem15\Journal\Models\Post. How to avoid: PHP class strings. Warning signs: empty golem15_translate_attributes after mlmarkdown save.

Pitfall 11: Typesense on by default

What goes wrong: seeder/create hits missing API key. Why: PHP Scout observer. How to avoid: Gate off; settings default 0; ShouldBeSearchable false. Warning signs: network to :8181 in tests.

Pitfall 12: API.md per_page 15/50

What goes wrong: pagination disagrees with PHP runtime. Why: stale API.md. How to avoid: controller 9/30. Warning signs: per_page 15 in Go.

Pitfall 13: Plugin README names the host

What goes wrong: docs checker / CLAUDE.md violation. How to avoid: the application, example blog. Warning signs: string grzyby in plugin README.

Pitfall 14: Editing wn-journal-plugin

What goes wrong: D-07. How to avoid: read-only PHP path. Warning signs: git status in fonoteka journal submodule.

Pitfall 15: Enabling footnotes in cabana.RenderMarkdown

What goes wrong: mail HTML / other markdown fields change. Why: shared goldmark.New(). How to avoid: plugin FormatHTML only. Warning signs: cabana markdown_test.go fails or mail snapshots change.

Code Examples

Plugin ID and require (PHP)

DATA_h9s2b6kd_START

public $require = [
    'Golem15.Apparatus',
    'Golem15.Translate',
];

DATA_h9s2b6kd_END

[VERIFIED: Plugin.php:15-18]

Go: Requires() []string { return []string{"golem15.translate"} }.

BackendUser table name

DATA_r5t0c3ny_START

func (BackendUser) TableName() string { return "backend_users" }

DATA_r5t0c3ny_END

[VERIFIED: modules/cabana/contracts.go:56]

Cabana Allows is OR

DATA_m8q1z4wp_START

// Allows reports whether principal satisfies any of the required permission
// codes, the way Winter's hasAnyAccess does.

DATA_m8q1z4wp_END

[VERIFIED: modules/cabana/contracts.go:143-145]

Host summer.yaml today (must gain journal)

DATA_v2j7f5la_START

plugins:
  - id: golem15.user
    module: git.golem15.com/golem15/sm-user-plugin
  - id: golem15.translate
    module: git.golem15.com/golem15/sm-translate-plugin

DATA_v2j7f5la_END

[VERIFIED: sm-grzybyfunkcjonalne-app/summer.yaml:3-7]

Translate plugin replace (copy for journal sibling checkout)

DATA_c6d9k2hx_START

replace git.golem15.com/golem15/summercms => ../summercms.go

DATA_c6d9k2hx_END

[VERIFIED: sm-translate-plugin/go.mod:122]

State of the Art

Old Approach Current Approach When Changed Impact
PHP JournalMarkdown widget cabana mlmarkdown 14.2.1 D-11 satisfied
PHP system_settings JSON cabana settings table ID=1 Phase 9 Dedicated golem15_journal_settings
Apparatus g15_ write tokens cabana backend Bearer D-15 API.md auth section is stale
Laravel Scout beachcomber Gate Phase 11 Off by default
Rainlab table names golem15_journal_* PHP v2.0.0 Squash in Go
Twig components Phase 16 views deferred Document successor only

Deprecated/outdated:

  • API.md "all endpoints require g15_": superseded by D-15 + routes.php public group.
  • CONTEXT "Existing Code Insights" still says cabana has no markdown field: stale after 14.2.1.

Assumptions Log

# Claim Section Risk if Wrong
A1 Goldmark footnote/table/attribute extensions are importable from the existing github.com/yuin/goldmark module without a new require FormatHTML If a separate module is required, stop and do not add it without a decision; fall back to cabana.RenderMarkdown (no footnotes)
A2 Omitting admin taglist/repeater/preview_page is acceptable MVP vs PHP UX YAML Editors need Tags admin + API for sources; Phase 16 may ask for taglist
A3 bouncer exposes a way to optionally parse a backend JWT from a request without the "backend" middleware Public GET editor If no helper exists, copy a few lines from cabana guard; do not invent a second JWT library
A4 PHP seeder "Uncategorized" still runs at this pin Seed Execute reads seed_all_tables.php; skip if empty

If this table is empty: not applicable — four assumptions remain.

Open Questions (RESOLVED)

  1. Should plan 01 still include "add cabana markdown"?

    • What we know: 14.2.1 already shipped the types and SPA controls.
    • What's unclear: whether the planner treats D-11 as a checkbox that needs a tagged no-op.
    • RESOLVED: no-op D-11; Journal uses mlmarkdown. Mention in plan 02 YAML only.
  2. On-the-fly tags (customTags: true)

    • What we know: cabana has no taglist type.
    • RESOLVED: relation picker + Tags admin. Do not add a framework type this phase.
  3. Optional backend principal helper

    • What we know: cabana guard is all-or-nothing middleware.
    • RESOLVED: small function in the journal plugin that Lookup's bouncer.Registry guard "backend" if the header is present.

These are execution details inside Claude's Discretion, not blockers.

Environment Availability

Dependency Required By Available Version Fallback
Go compile ✓ 1.27.0 —
Postgres migrations/tests ✓ 18.6 testcontainers
Docker testcontainers ✓ 29.7.2 -short skip last-plan Postgres tests
PHP journal tree contract ✓ SHA 02110eb Do not clone
sm-journal-plugin checkout D-23 ✗ missing Clone empty remote in 15-01
sm-journal-plugin remote D-22 ✓ empty public repo —
sm-grzybyfunkcjonalne-app D-18 ✓ user+translate mounted —
sm-translate-plugin D-09/D-10 ✓ sibling + host submodule Phase 14.2.1 must stay green
Typesense D-12 when on optional Phase 11 image Gate off; no contact

Missing dependencies with no fallback:

  • Local sm-journal-plugin/ directory — create in plan 01 (clone).

Missing dependencies with fallback:

  • Typesense server — feature off by default.

Validation Architecture

Test Framework

Property Value
Framework Go testing + testcontainers-go v0.44.0 (Postgres)
Config file none — go test ./...
Quick run command go test ./... -short -count=1 in sm-journal-plugin + host go test ./... -short -count=1
Full suite command go test ./... -count=1 in sm-journal-plugin, host, and summercms.go only if framework files change

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
D-01 Tables golem15_journal_posts|categories|tags after migrate integration go test ./updates -run TestJournalTables -count=1 ❌ Wave 0
D-10 Post Translatable list + MorphName PHP string unit go test ./models -run TestPostTranslatable -count=1 ❌ Wave 0
D-11 type: mlmarkdown compiles on posts form unit go test ./... -run TestPostsFormCompiles -count=1 ❌ Wave 0
D-12 Gate off → zero search HTTP on Post save unit go test ./... -run TestSearchGateOff -count=1 ❌ Wave 0
D-13 Commands registered journal:export-posts / journal:import-posts unit go test ./... -run TestJournalCommands -count=1 ❌ Wave 0
D-14 GET /_journal/api/v1/posts anonymous 200 integration go test ./... -run TestJournalPublicList -count=1 ❌ Wave 0
D-15 POST posts without Bearer 401 Authentication required integration go test ./... -run TestJournalWriteUnauthenticated -count=1 ❌ Wave 0
D-17 Buckets named journal-public-api and journal-api Max 120 unit go test ./... -run TestJournalBuckets -count=1 ❌ Wave 0
D-17 Host CORS includes _journal/api/* unit host go test ./... -run TestCORS -count=1 ❌ Wave 0
D-19 Host Activate 3 plugins including golem15.journal smoke host go test ./... -run TestBootUserTranslateJournal -count=1 ❌ Wave 0
JOURNAL-005 Draft show 404 to stranger integration go test ./... -run TestJournal005DraftShow -count=1 ❌ Wave 0
JOURNAL-006 Media upload 403 without access_posts; path prefix integration go test ./... -run TestJournal006MediaUpload -count=1 ❌ Wave 0
JOURNAL-001/002 Tag/Category fillable unit go test ./models -run TestFillable -count=1 ❌ Wave 0

Sampling Rate

  • Per task commit: go test ./... -short -count=1 in the repo that changed
  • Per wave merge: full go test ./... -count=1 in that repo + go vet ./...
  • Phase gate: plugin + host green before $gsd-verify-work

Wave 0 Gaps

  • sm-journal-plugin module and all test files listed above
  • Host boot_test updated for three plugins + CORS
  • Plugin Postgres harness (copy translate/user TestMain / testcontainers)
  • No new test framework install
  • PHPUnit XSS template tests deferred to Phase 16; FormatHTML unsafe-tag tests substitute this phase

Security Domain

security_enforcement absent in .planning/config.json → enabled.

Run the security-review agent after implementation (CONTEXT discretion). Phase touches pact, golem15.journal.*, draft visibility, public HTTP.

Applicable ASVS Categories

ASVS Category Applies Standard Control
V2 Authentication yes Backend JWT (cabana), not frontend user tokens, not g15_
V3 Session Management yes Same backend JWT blacklist backend_jwt_blacklist; public routes unauthenticated
V4 Access Control yes golem15.journal.*; canEdit; JOURNAL-005 404; JOURNAL-006 permission
V5 Input Validation yes fillable allow-lists; slug regex; media folder regex; goldmark rejectUnsafe; file mime/size
V6 Cryptography no Do not hand-roll tokens; reuse cabana backend JWT

Known Threat Patterns

Pattern STRIDE Standard Mitigation
Draft enumeration via 403 Information disclosure 404 without data (JOURNAL-005)
Media path traversal Tampering folder regex + forced journal/ prefix (JOURNAL-006)
Mass assignment nest_*/redactor_id Elevation Fillable + explicit API assigns
Stored XSS in content_html Tampering FormatHTML + rejectUnsafe; Phase 16 raw_safe
Cross-user post edit Elevation canEdit + ListExtendQuery
Publish without permission Elevation access_publish 403
Frontend token on write API Spoofing Reject user.api_token; backend aud only
Typesense data leak of drafts Information disclosure ShouldBeSearchable false when unpublished or gate off
Rate-limit bypass via X-Forwarded-For DoS surf.TrustedProxies + ClientIP
CORS credentialed * Information disclosure keep supports_credentials: false as host already has

Sources

Primary (HIGH confidence)

  • Frozen PHP /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal at 02110eb1c0c3861370b0b9b47b209a0702ac5d88 — Plugin.php, routes.php, API.md, models, YAML, tests, migrations
  • 15-CONTEXT.md D-01–D-23
  • modules/cabana/form_schema.go, field_markdown.go, filter_schema.go, contracts.go, http.go, docs/backend/forms.md, docs/backend/settings.md, docs/backend/admin-controllers.md
  • modules/surf/limiter.go, docs/services/routing.md, docs/services/rate-limiting.md
  • modules/pact/capabilities.go
  • sm-translate-plugin/go.mod, sm-grzybyfunkcjonalne-app/{summer.yaml,go.work,go.mod,boot_test.go,config/http.yaml}
  • .planning/notes/core-plugins-own-repos.md, apparatus-dissolved-into-framework.md
  • 14.2.1 RESEARCH/PATTERNS pitfalls (YAML, replace paths, host boot)

Secondary (MEDIUM confidence)

  • None required for stack versions; go.mod and go version read directly

Tertiary (LOW confidence)

  • A1–A4 in Assumptions Log

Metadata

Confidence breakdown:

  • Standard stack: HIGH — no new packages; versions from go.mod / go version
  • Architecture: HIGH — analog ports + PHP pin read this session
  • Pitfalls: HIGH — 14.2.1/12.1 plus journal-specific YAML/auth/draft/media

Research date: 2026-10-06 Valid until: SHA re-pin (D-02) or cabana field-type change Research-plan seam: skipped (in-repo PHP→Go port; same as 14.2.1) Graphify: disabled this session