Files
khosra/docs/extensions.md
T
bdeshi c468cf24c3 rename the project to khosra, initialise the module
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.
2026-08-01 02:23:34 +06:00

4.7 KiB
Raw Blame History

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 (02 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.

// 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):
// 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.