Files
summercms/.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md
2026-09-16 12:03:21 +02:00

14 KiB

Phase 1: Framework kernel foundation - Context

Gathered: 2026-09-16 Status: Ready for planning

## Phase Boundary

Phase 1 delivers the framework kernel in summercms.go only: config (compass), plugin descriptor and registry with the generated import list and summer build (party), typed event bus (festival), app container and service registry (backpack), console command framework with rich output (bonfire), and a dev watch loop. It is built only as far as Phase 3's GET /_fonoteka/api/v1/genres slice will need, per the interleaved-not-sequential rule. No HTTP router (surf), no ORM (lagoon), no auth (bouncer), no i18n (phrasebook) and no scaffolding beyond what summer build and the examples/hello testbed require. Requirements: KERN-01 through KERN-09 and CLI-01.

## Implementation Decisions

Binary and repo layout

  • D-01: Two binaries, xcaddy-style. summer is a framework tool installed once (go install git.golem15.com/golem15/summercms/cmd/summer) providing make:plugin, plugin:add, build and dev. It generates main.go and plugins.gen.go in an app repo and runs go build there. The app binary (for example fonoteka) embeds the framework's runtime commands plus plugin commands. The framework never links an app.
  • D-02: One command kernel, two entry points. The framework exposes a bonfire kernel function that builds the root cobra command with runtime commands (serve, migrate:*, queue:work as later phases add them). The summer tool wraps that kernel plus the build and scaffold commands; the generated app main.go calls the same kernel. No duplicated command wiring.
  • D-03: The Phase 1 testbed is examples/hello, kept permanently: a tiny real app inside the framework repo with its own go.mod, linked through a root go.work, containing two or three trivial plugins (enough to prove Register-before-Boot, Requires() ordering and an optional capability interface). summer build and summer dev are exercised against it, it is the template source for make:plugin, and integration tests run it.
  • D-04: Canonical module path is git.golem15.com/golem15/summercms (go.mod as committed; origin added and master pushed). Research docs that say github.com/golem15/summercms get a one-line correction. Apps require this path and use go.work or a local replace during development.
  • D-05: Pin the toolchain: add toolchain go1.27.x to go.mod so every developer and agent builds with the same compiler. Generated app go.mod files copy the same line.

Config layout and keys

  • D-06: Plugin config keys use the bare plugin ID as namespace: golem15.fonoteka.posts_per_page. App-level sections (app, database) stay top-level. Rule: a top-level key containing a dot-separated vendor prefix is a plugin namespace. This follows the summer-compass design and reads like WinterCMS's golem15.fonoteka::key; STACK.md's plugins.<name>.* sketch is superseded.
  • D-07: File layout follows summer-compass: config/<section>.yaml for base config, config/env/<env>/<section>.yaml for environment overlays, filename becomes the section key. Plugins ship config/*.yaml inside their module (embedded FS via the HasConfig capability) and are loaded under their namespace. Deep merge, not top-level replace.
  • D-08: Environment variables use the SUMMER_ prefix with __ as the path separator: SUMMER_DATABASE__HOST maps to database.host, SUMMER_GOLEM15__FONOTEKA__POSTS_PER_PAGE maps to golem15.fonoteka.posts_per_page. Single underscores stay literal so snake_case leaf keys are unambiguous. A .env file in the app root is loaded when present and never overrides variables already set in the real environment.
  • D-09: The full compass override surface is carried over: in-memory Set (highest priority), Persist writing config/env/<env>/overrides.yaml, and Reload that clears runtime overrides and re-reads disk. Priority, highest wins: runtime Set, persisted overrides, env vars, env overlay file, base file, plugin defaults.

Plugin lifecycle and events

  • D-10: A plugin whose Requires() names an unregistered plugin fails boot with an error naming both the plugin and the missing dependency; the binary exits non-zero. Cycles in Requires() fail the same way. No warn-and-disable behavior.
  • D-11: Optional integration (KERN-05) has two mechanisms, both free of imports of the optional plugin's package: app.HasPlugin("golem15.translate") for coarse checks mirroring PHP class_exists guards, and a typed lookup on the backpack container returning (value, ok) for interface-based integration where the optional plugin publishes a service under a pact interface.
  • D-12: Event listeners have an optional integer priority, default 0. Higher priority runs first; ties keep registration order (stable). Both a plain Listen and a priority-taking variant exist. This makes fire-until-handled deterministic and lets ported PHP listeners keep their priorities.
  • D-13: Listener error handling per dispatch mode. Fire-and-forget: run every listener, return the joined errors (errors.Join). Fire-and-collect: run every listener, return the partial merged payload together with the joined error. Fire-until-handled: the first error stops dispatch and is returned. Panics inside a listener are recovered and wrapped as an error carrying the listener's owning plugin ID.

CLI and dev loop

  • D-14: Rich output widgets are hand-rolled on the standard library plus golang.org/x/term for TTY detection and raw mode: braille spinner, gradient progress bar, box-drawing table, ask/confirm/choice/secret prompts, with the non-TTY degradation table from the bonfire plan (static [...] line, [N/M] pct% updates, tab-separated tables, stdin prompts, NO_COLOR/FORCE_COLOR/TERM=dumb respected). No Charm stack, no pterm.
  • D-15: Command names are colon-style like WinterCMS/Artisan: summer fonoteka:reindex, summer make:plugin, summer migrate:rollback. Plugin commands must carry a namespace prefix; kernel commands use their group prefix. cobra accepts colons in Use.
  • D-16: The dev watch loop is built into the tool as summer dev using fsnotify: watch the app workspace (Go sources, config, plugin manifests), run the same build step as summer build, restart the app binary with a debounce, and print the measured rebuild latency after every cycle. No dependency on an external air install; STACK.md's air recommendation is superseded for this purpose.
  • D-17: Plugins implement a bonfire.Command interface (name, description, flag and argument definition, Run(ctx, Input, Output) error). The kernel wraps each into a cobra.Command. Output is an injected value so tests capture it and non-TTY degradation is decided once, not per command.

Claude's Discretion

  • Event identity: typed Go struct events (generic Listen[T]), as sketched in ARCHITECTURE.md Pattern 3a; no string-named events. Dispatch is synchronous on the caller's goroutine; async fan-out is a listener's own concern.
  • The event bus and the service registry are fields of the app instance, not package globals, so tests build isolated apps. Only party's init() self-registration uses a package-level ordered slice, drained into the app at boot.
  • Plugin IDs are lowercase vendor.plugin (golem15.fonoteka); matching is exact.
  • Environment detection reads SUMMER_ENV, defaulting to production as compass did; an explicit constructor argument overrides it.
  • Typed section loading uses koanf's Unmarshal into a struct with a koanf tag; dot-path getters mirror the compass API (String, Int, Bool, Has, plus a Lookup returning ok).
  • plugins.gen.go is regenerated from an explicit ordered manifest (a summer.yaml or the go.work use list), never from a map, per the init-order pitfall. summer build measures and prints build time.
  • What make:plugin scaffolds in Phase 1: the minimum that compiles and registers (go.mod, plugin.go with the required interface, an empty config dir). Model, migration, controller and command stubs belong to Phase 4 (CLI-02).
  • Package layout follows ARCHITECTURE.md's list (pact, towel, compass, festival, backpack, party, bonfire, cmd/summer, examples/hello); internal/ is used for build codegen helpers not meant as public API.

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Kernel architecture and pitfalls

  • .planning/research/ARCHITECTURE.md §Component Responsibilities, §Recommended Project Structure, §Pattern 1, §Pattern 2, §Pattern 3, §Anti-Patterns, §Framework vs Application Repo Boundary, §Suggested Build Order — plugin interface sketch, capability interfaces, generated import list, container usage rules, package names. Note: it says github.com/golem15/summercms and cmd/summer lives in the app repo; D-01, D-02 and D-04 supersede those two points.
  • .planning/research/PITFALLS.md §Pitfall 1 (bottom-up kernel), §Pitfall 2 (globals vs context), §Init-order plugin registration nondeterminism, §Dev rebuild loop friction, §Pitfall-to-Phase Mapping — the tests and acceptance criteria Phase 1 must include (go test -race, reordered-input test, measured rebuild latency).
  • .planning/research/SUMMARY.md §Architecture Approach, §Phase 1, §Research Flags — kernel phase is flagged as safe to plan without deep research.
  • .planning/research/STACK.md §Core Technologies (koanf, cobra, goccy/go-yaml versions), §Config: koanf + YAML — the plugins.<name>.* sketch there is superseded by D-06; air for the watch loop is superseded by D-16.
  • .planning/research/go-ecosystem.md §2 Plugin architecture — the Caddy/xcaddy verdict and the hand-rolled event bus verdict.

Design carry-overs from the Scala modules

  • ../modules/summer-compass/README.md — config file layout, environment layering priority, plugin namespaces, set/persist/reload API and thread-safety notes; port the design, not the code.
  • ../modules/summer-bonfire/README.md and ../modules/summer-bonfire/PLAN.md §Input trait, §Output trait, §Command trait, §CommandRunner, §Non-TTY Graceful Degradation — the command and output interfaces and the degradation table to port.
  • ../IDEA_LIB_NAMES.md — Go package names (backpack, compass, festival, party, bonfire, pact, towel).

Project-level decisions

  • .planning/PROJECT.md §Constraints, §Key Decisions — stdlib-first rule, two-repo split, compiled plugins only.
  • .planning/REQUIREMENTS.md KERN-01..KERN-09, CLI-01 — the phase's requirement text.
  • .planning/notes/why-go-not-scala.md — the bottom-up failure mode this phase must not repeat.
  • CLAUDE.md (repo root) §GSD workflow rules — lean planning, plan-count checkpoint, unit tests as the last plan, go vet and go test ./... green at every commit.

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • No Go code exists yet; the repo holds go.mod (module git.golem15.com/golem15/summercms, go 1.27.0), README.md, CLAUDE.md and .planning/. Everything in Phase 1 is greenfield.
  • Design assets to port: summer-compass (config layering, namespaces, overrides) and summer-bonfire (command interfaces, widgets, degradation table). Their READMEs are the spec; their Scala code is not ported.

Established Patterns

  • Stdlib-first: new dependencies in Phase 1 are limited to koanf v2 with its file, env and confmap providers and yaml parser, goccy/go-yaml, cobra, golang.org/x/term, fsnotify, and testify for tests. Anything else needs a decision note.
  • Request state travels only in context.Context with unexported struct key types and exported accessor functions.
  • Container used only at Register and Boot for constructor injection; application code holds typed fields.

Integration Points

  • examples/hello is the first consumer of every kernel package and the target of summer build and summer dev.
  • Phase 3 will add surf, lagoon and bouncer on top of backpack and party; the capability interfaces they need (HasRoutes, HasModels, HasMigrations) may be declared in pact now but must not be implemented before Phase 3 pulls them.
  • Phase 2's parity harness has no dependency on Phase 1 and runs in parallel.

</code_context>

## Specific Ideas
  • "It should feel like WinterCMS's drop-in loop": summer dev prints rebuild latency every cycle so the friction pitfall is visible, and the phase's acceptance includes a measured rebuild time for a single-plugin change in examples/hello.
  • Command spelling must let ported PHP commands keep their names (fonoteka:reindex, fonoteka:oauth-client).
  • The user asked whether __ is common in Go; answer recorded: single underscore is more common in Go libraries but ambiguous with snake_case keys, __ is the cross-ecosystem convention (ASP.NET Core, Rust config, dynaconf) and was chosen deliberately.
## Deferred Ideas
  • Scaffolding of model, migration, command, job and admin-controller stubs by make:* commands — Phase 4 (CLI-02). Phase 1's make:plugin produces only the compiling minimum.
  • Admin config editor consuming Persist overrides — later admin phase; Phase 1 only provides the API.
  • Extraction of shared stack plugins into their own repos — only when a second app (keios.eu) needs one.
  • One-line corrections to research docs (module path, air, plugins.<name> sketch) — do as a docs commit alongside Phase 1 planning, not as code.

Phase: 01-framework-kernel-foundation Context gathered: 2026-09-16