60 KiB
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-pluginat git SHA02110eb(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 newermaster. — 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/v1request/response shapes. Same bar as the user plugin port. - D-05: PHPUnit under
tests/(security access/XSS/mass-assignment plusPostRedactor) 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.Translateinside Phase 15. Insertsm-translate-pluginas its own roadmap phase before Phase 15 ($gsd-phaseafter 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
$translatableattributes 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.gothis 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-postscommands 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/v1surface this phase: anonymous GETposts,posts/{slug},categories,tags,rss; backend-authenticated POST/PUT/DELETE posts, featured-image and media upload. Shapes followAPI.mdandroutes.phpin 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-pluginuser.api_token(wrong identity) and they do not fold ApparatusPersonalApiToken/g15_tokens (still.planning/todos/pending/backend-admin-api-tokens.md). — Reversibility: costly — clients that later expectg15_tokens would need a second guard. - D-16: Proof is
API.mdplus 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-apiandjournal-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 notfonoteka.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/v1answers. 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 ofsummercms.go, same asfonoteka.goandsm-summercmsio-app). - D-21: The app mounts
sm-journal-plugin,sm-translate-pluginandsm-user-plugin(submodules atplugins/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.gitalready exists and is public. Module pathgit.golem15.com/golem15/sm-journal-plugin, packagejournal, plugin IDgolem15.journal. - D-23: Local plugin checkout:
/media/nvme/dev/golem15/summercms.io/summercms/sm-journal-plugin/(sibling ofsummercms.go). The host mounts it as a submodule atplugins/golem15/journalwith ago.workreplace, same assm-user-plugininfonoteka.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 committeddist/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) insidesm-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-pluginas 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_) — todobackend-admin-api-tokens.md. - Remaining 19 PHP locales beyond
enandpl. - 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/httpServeMux,html/template,encoding/json). Add a dependency only when this research or a phase decision names it.go vetandgo 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.gois framework only and knows nothing about Płytarium. This phase writes the plugin insm-journal-plugin, the proof host insm-grzybyfunkcjonalne-app, andsummercms.goonly if a framework feature is missing. Planning docs stay insummercms.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.mdand the affecteddocs/pages in the same change. Cabana field types also update admin OpenAPI, generated TS types, and committedboardwalkdist/. - 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\PostGolem15\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_settingsgolem15.journal.access_postsgolem15.journal.access_categoriesgolem15.journal.access_other_postsgolem15.journal.access_import_exportgolem15.journal.access_publishgolem15.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:
publishedswitch →FilterScopesname matching YAMLscope:(e.g.FilterPublished) applyingpublished <> true/published = true.published_datedaterange → scope oncreated_atbetween:after/:before.categoryalready hasscope: FilterCategories— implementFilterCategories(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:
- CLI
journal:export-posts/journal:import-postsviapact.HasCommands(PHPPlugin.php:169-170). - 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:
- Verify the cabana backend JWT (HS256, same secret, same blacklist table
backend_jwt_blacklist). - 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:
- 15-01 — Clone
sm-journal-plugin; squashed schema + models + Translatable + host submodule/summer.yaml/go.work/http.yamlCORS + boot_test 3 plugins. - 15-02 — Admin controllers, adapted YAML, settings, permissions, nav SVG, FormatHTML, import/export commands + toolbar.
- 15-03 —
/_journal/api/v1, buckets, optional editor on GET, backend Bearer writes, media upload, Typesense gate. - 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
Recommended Project Structure
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_tokenorg15_: 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
-
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.
- Recommendation: no-op D-11; Journal uses
mlmarkdown. Mention in plan 02 YAML only.
-
On-the-fly tags (
customTags: true)- What we know: cabana has no taglist type.
- Recommendation: relation picker + Tags admin. Do not add a framework type this phase.
-
Optional backend principal helper
- What we know: cabana guard is all-or-nothing middleware.
- Recommendation: small function in the journal plugin that Lookup's
bouncer.Registryguard"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=1in the repo that changed - Per wave merge: full
go test ./... -count=1in that repo +go vet ./... - Phase gate: plugin + host green before
$gsd-verify-work
Wave 0 Gaps
sm-journal-pluginmodule 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/journalat02110eb1c0c3861370b0b9b47b209a0702ac5d88— Plugin.php, routes.php, API.md, models, YAML, tests, migrations 15-CONTEXT.mdD-01–D-23modules/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.mdmodules/surf/limiter.go,docs/services/routing.md,docs/services/rate-limiting.mdmodules/pact/capabilities.gosm-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 versionread 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