Naming is free before a module is published, a URL is shared, or a binary is deployed; every day it waits costs more. Swept every form: module path, binary, cmd/ directory, KHOSRA_SITE, the feature-loop skill directory, and the prose in earlier ADRs — which describe this project under its old name, not a different project. Recorded as ADR-0030. go mod init lands here rather than with the first feature because the module path is what the rename is about. x/text and yaml.v3 are required but not yet imported, so both are indirect and no direct dependency is claimed yet.
96 lines
4.7 KiB
Markdown
96 lines
4.7 KiB
Markdown
# Extensions
|
||
|
||
The plugin story, and the gate keeping it from arriving early.
|
||
|
||
**STATUS: not buildable yet.** A feature is its own directory under `internal/ext/<name>/`, called
|
||
explicitly from `wire.go` (ADR-0027) — correct and sufficient until the counters say otherwise. This document exists so the eventual shape is known, not
|
||
so it can be built now.
|
||
|
||
## The gate
|
||
|
||
| Stage of growth | What a feature looks like | Trigger to advance |
|
||
|---|---|---|
|
||
| Now (0–2 features) | Its own directory under `internal/ext/<name>/`, called explicitly from `wire.go` | — |
|
||
| Transform counter due | Extract the Stage pipeline: an ordered `[]Stage` in one wire file | `state.md` |
|
||
| Extension counter due | Extract the `Extension` struct below; move each into `internal/ext/<name>` | `state.md` |
|
||
| After Arc 2 | Composition only; the core no longer grows | Arc 2 closes |
|
||
|
||
`state.md` holds the thresholds and is the only place they are written. Do not extract early. Do not
|
||
"prepare".
|
||
|
||
## Target shape
|
||
|
||
Compile-time registry. No `plugin.so`, no `init()` side effects, no discovery, no config file listing
|
||
plugins. One slice, one file, source order — the order *is* the semantics.
|
||
|
||
Compile-time is not a preference: Go's `plugin` package forbids a static binary and demands an exact
|
||
toolchain match, which the container target (ADR-0010) rules out. Dynamic loading buys only
|
||
extension-without-recompiling, worth nothing to the single author (ADR-0006) holding commit access.
|
||
The registry costs a few hundred lines that render zero pages — hence real callers before a contract.
|
||
|
||
```go
|
||
// internal/ext/ext.go — the whole contract, once earned.
|
||
type Extension struct {
|
||
Name string
|
||
Stages []Stage // ordered; Phase decides placement
|
||
Views map[string]View // named, referenced by frontmatter `view:`
|
||
Shortcodes map[string]Shortcode // trusted content only (ADR-0003)
|
||
Effects []Effect // derived artifacts and outbound calls, off the request path
|
||
Adapters []Adapter // Interaction sources, Arc 3
|
||
Routes []Route // additional URL cases, via the resolver
|
||
}
|
||
```
|
||
|
||
`cmd/khosra/wire.go` holds the only list of enabled extensions. Enabling or disabling one is a
|
||
one-line diff and a rebuild. Removing one leaves no trace elsewhere — that property is the test of
|
||
whether the contract is right.
|
||
|
||
## Stage phases
|
||
|
||
An ordered list, not a dependency graph. Two stages needing a graph to be correct are one stage
|
||
wearing a disguise.
|
||
|
||
| Phase | Operates on | Examples |
|
||
|---|---|---|
|
||
| `PhaseLoad` | raw bytes + frontmatter | includes, translation fallback |
|
||
| `PhaseParse` | the parsed Markdown tree | shortcodes, transclusion, image derivatives |
|
||
| `PhaseMarkup` | rendered HTML fragments, code spans skipped | smart quotes, dashes, widows, Bengali numerals |
|
||
| `PhasePage` | the assembled page object | OpenGraph, JSON-LD, related posts, series nav |
|
||
| `PhaseOutput` | the final byte stream | minification, dithering, gemtext conversion |
|
||
|
||
Every Stage runs on every bundle unless a cascade key disables it (`ideas/deferred-decisions.md`), and declares its trust
|
||
requirement. A Stage evaluating templates or shortcodes runs in trusted mode only, and the pipeline
|
||
refuses it otherwise — enforced in code, not by convention, and
|
||
tested with an untrusted-input case.
|
||
|
||
## Rules for any extension
|
||
|
||
1. Deletable without trauma. Removing the package leaves the engine building and serving.
|
||
2. Reads only what already exists on the page; adds through the `Extra` bag, never by widening the
|
||
core struct for its own convenience.
|
||
3. Owns its output files under a namespaced path, or none.
|
||
4. No new dependency without an ADR — extensions get no looser budget than the core, and their Go
|
||
lines count against `EXT_LOC_MAX`. Presentation features (OpenGraph, galleries, series nav,
|
||
related posts) belong in templates and frontmatter where they cost nothing; reach for Go only when
|
||
there is real logic.
|
||
5. Failure degrades: a broken extension logs and is skipped, never takes a request down.
|
||
6. Needing a permanent external service or a primitive change makes it a **trunk** — see
|
||
`exploration.md`.
|
||
7. One directory, no sibling imports, and a `doc.go` in this shape (ADR-0027):
|
||
|
||
```go
|
||
// Package feeds emits RSS and Atom for the primary feed and each section.
|
||
//
|
||
// Contributes: Effect (on change).
|
||
// Cascade keys: feeds.enabled, feeds.limit.
|
||
// Contract fields: none.
|
||
// Not doing: JSON Feed, WebSub — separate features if wanted.
|
||
package feeds
|
||
```
|
||
|
||
## ContentAPI
|
||
|
||
A thin internal write path, introduced **with comments** in Arc 3 and not before. It exists so a
|
||
second client (admin panel, Micropub endpoint) becomes possible without the engine growing a UI.
|
||
Read paths keep going straight to the filesystem; git remains the source of truth.
|