diff --git a/.claude/settings.json b/.claude/settings.json index 25fc1af..ac4bff5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -19,6 +19,8 @@ "Bash(git diff:*)", "Bash(git log:*)", "Bash(git show:*)", + "Bash(git add:*)", + "Bash(git commit:*)", "Bash(rg:*)", "Bash(wc:*)", "Bash(curl -s http://localhost:*)", diff --git a/.claude/skills/atelier-feature-loop/SKILL.md b/.claude/skills/khosra-feature-loop/SKILL.md similarity index 100% rename from .claude/skills/atelier-feature-loop/SKILL.md rename to .claude/skills/khosra-feature-loop/SKILL.md diff --git a/.gitignore b/.gitignore index c35c445..0194520 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/atelier +/khosra *.test *.out .DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index fcd8221..61ab99c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# atelier — agent constitution +# khosra — agent constitution -`atelier` is a flat-file personal publishing engine in Go, built solo, one feature at a time. +`khosra` is a flat-file personal publishing engine in Go, built solo, one feature at a time. You implement; the human owns scope and taste. This file overrides your defaults. A **personal publishing substrate**: a directory of Markdown becomes an owned, networked home for @@ -74,7 +74,7 @@ service or a core-model change): stop, say so in a paragraph, propose the leaf, ## 4. The loop (every request, no exceptions) `Clarify → Plan → Implement → Verify → Document → Report`. -Procedure: `.claude/skills/atelier-feature-loop/SKILL.md`. The two gates people skip: +Procedure: `.claude/skills/khosra-feature-loop/SKILL.md`. The two gates people skip: **Clarify.** Only questions whose answer changes the code or the bytes on disk. Max three, batched, up front, each with a **bold** default so silence answers. Never about naming, formatting, diff --git a/HARNESS.md b/HARNESS.md index a2c8276..c7a9aae 100644 --- a/HARNESS.md +++ b/HARNESS.md @@ -1,6 +1,6 @@ -# The atelier harness — human's guide +# The khosra harness — human's guide -Scaffolding that makes an agent build `atelier` the way you want: minimally, surgically, questions +Scaffolding that makes an agent build `khosra` the way you want: minimally, surgically, questions before code, docs that stay true. `README.md` is a two-line signpost — human here, agent to `CLAUDE.md`. `CLAUDE.md` is the @@ -107,7 +107,7 @@ imports `cmd`. One convenient sibling import is what turns a layered engine into it always looks locally reasonable — so it is a hard failure, not a review note. **Feature locality is enforced, not hoped for.** A feature is one directory under `internal/ext//` -plus one line in `cmd/atelier/wire.go`. `verify.sh` fails on a feature importing a sibling and on a +plus one line in `cmd/khosra/wire.go`. `verify.sh` fails on a feature importing a sibling and on a feature package without a `doc.go` — the first because sibling imports make an agent's read set compound, the second because a four-line `doc.go` turns orientation into a fifteen-line read (ADR-0027). diff --git a/README.md b/README.md index 78f381b..3533a15 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# atelier +# khosra A flat-file personal publishing engine in Go. Point the binary at a directory of Markdown and it becomes an owned, networked home for fiction, webcomics, art, and essays, in English and Bengali. diff --git a/docs/content-model.md b/docs/content-model.md index e392322..4c8ebed 100644 --- a/docs/content-model.md +++ b/docs/content-model.md @@ -12,7 +12,7 @@ whatever builds it. What exists is `state.md`'s job. Never build a `[spec]` sect ## The site root -The engine is pointed at a site root outside this repository (`-site `, or `ATELIER_SITE`), with +The engine is pointed at a site root outside this repository (`-site `, or `KHOSRA_SITE`), with its own git history (ADR-0011). Nothing in the engine repo is content. `templates/` in the site root overrides the defaults the binary embeds, so a bare root still renders. diff --git a/docs/conventions.md b/docs/conventions.md index 6f5ba6f..6558d31 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -8,7 +8,7 @@ goes through an ADR, not a diff. - `net/http`, `html/template`, `log/slog`, `os`, `io/fs`, `embed`. Routing is `http.ServeMux` with Go 1.22+ patterns — `GET /{section}/{slug}/` and `{path...}` for multi-segment slugs — so no router library is needed. No web framework, ORM, or config library — flags plus environment variables, parsed in one place. The site root - (`-site`, `ATELIER_SITE`) is the only required setting; default templates are `embed`ded so a bare + (`-site`, `KHOSRA_SITE`) is the only required setting; default templates are `embed`ded so a bare site root renders (ADR-0011). - New dependency = ADR + human approval + `scripts/allowed-deps.txt`. `verify.sh` enforces it. - Prefer 40 lines of obvious code over a dependency doing it in one call — unless the 40 lines would be @@ -16,7 +16,7 @@ goes through an ADR, not a diff. ## Package layout ``` -cmd/atelier/ main, flag parsing, explicit wiring — the only place things are assembled +cmd/khosra/ main, flag parsing, explicit wiring — the only place things are assembled internal/content/ bundles, frontmatter, slugs, queries — knows the disk, not HTTP internal/render/ markdown, transforms, templates — knows content, not HTTP internal/web/ handlers, routing, headers, caching — knows both, exposes neither @@ -26,7 +26,7 @@ Dependencies point inward. `internal/content` imports nothing from the others; ` and is imported by nothing. A feature under `internal/ext/` may import `internal/content` and `internal/render`, and must not import `internal/web`, `cmd/`, or **another feature** — sibling imports are what make an agent's read set compound (ADR-0027). Every `internal/ext/*` package carries a `doc.go`; -`verify.sh` fails without one. Routes reach `web` by assembly in `cmd/atelier/wire.go`, so `web` never +`verify.sh` fails without one. Routes reach `web` by assembly in `cmd/khosra/wire.go`, so `web` never learns features exist. No `utils`, `helpers`, `common`, `shared`, `manager`, `base`, `impl`, `core` — a package name not describing a domain is a smell. Flat until a package exceeds `FILE_LOC_WARN`; do not pre-partition, and a single-file package therefore warns at the same point it diff --git a/docs/decisions.md b/docs/decisions.md index 647557f..33b8e4e 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -116,7 +116,7 @@ Revisit if: the engine can no longer boot and serve correctly with every externa Date: 2026-07-28 · Status: accepted Decision: the engine is pointed at a **site root** — a directory outside this repository, versioned in its own git repo — holding `content/`, `static/`, and optionally `templates/` overriding the defaults -the binary embeds. Selected by `-site ` or `ATELIER_SITE`. This repository contains engine source +the binary embeds. Selected by `-site ` or `KHOSRA_SITE`. This repository contains engine source only; no content, ever, not even an example. Why: content is the database, and a database does not live in the application's repo. Mixing them interleaves "fixed a typo in a poem" with "extracted the resolver" in one history, makes every typo a @@ -298,7 +298,7 @@ Revisit if: it starts accumulating design decisions — then split it into a ref ## ADR-0027 — Feature locality: one directory, no sibling imports, a mandatory `doc.go` Date: 2026-07-30 · Status: accepted Decision: a feature is one directory under `internal/ext//` from its first use, plus one line in -`cmd/atelier/wire.go` — the only file that knows every feature. No `internal/ext` package may import +`cmd/khosra/wire.go` — the only file that knows every feature. No `internal/ext` package may import another. Every one carries a `doc.go` stating, in four lines: what it contributes, which cascade keys it reads, which theme-contract fields it adds, and what it deliberately does not do. A feature may import `internal/content` and `internal/render`; never `internal/web`, never `cmd/`, never a sibling. @@ -313,3 +313,44 @@ inward (earning it under the counters) or stay duplicated until the third use, w rather than a new one. Expensive — `wire.go` absorbs all the coupling on purpose and will look repetitive; and this buys cheap *features*, not cheap spine changes, which still need the core read. Revisit if: `wire.go` becomes hard to read, which means the registry from `extensions.md` is due. + +## ADR-0028 — Pagination lives in the path: `/{section}/page/2/` +Date: 2026-07-30 · Status: accepted +Decision: page two of a listing is `/{section}/page/2/`; page one is the bare listing URL and never +`/page/1/`, which permanently redirects to it. Tag listings compose the same way: +`/tags/{tag}/page/2/`. `page` is therefore a reserved segment inside a section. +Why: a query parameter is not a permalink — it reads as ephemeral, is dropped by careless sharing, and +splits caching by URL shape rather than by content. Putting it in the path keeps every listing page +linkable and cacheable on the same terms as a bundle. +Consequence: cheap — one more resolver case, and every paginated listing is a first-class URL. Expensive +— no bundle may be slugged `page` inside a paginated section, and changing the page size renumbers pages, +so page URLs are stable only while the size is. +Revisit if: never for the shape. Page size is a separate setting and changing it is a URL event. + +## ADR-0029 — A bundle that fails to parse is skipped loudly, not fatal +Date: 2026-07-30 · Status: accepted +Decision: a bundle whose frontmatter or filename cannot be parsed is logged at error level with its path +and line, excluded from the site, and does not stop startup or any request. The `check` command reports +the same failures as errors, and that is where a non-zero exit belongs. +Why: `conventions.md` says startup failure is fatal and loud, and separately that request-time failure +degrades. A single mistyped colon in one post is not a startup failure — treating it as one means one +typo takes the whole site down, which is the worst possible failure mode for a personal site published +from a text editor. Fatal is right for a broken site root or an unreadable directory, not for one file. +Consequence: cheap — the site always serves whatever is valid, and the author sees the error in the log +and in `check`. Expensive — a silently absent page is a real failure mode, so the log line must name the +file and the reason plainly, and `check` must exist early enough to be the place you look. +Revisit if: skipped bundles start going unnoticed in practice — then `check` runs in CI, rather than the +engine becoming fatal. + +## ADR-0030 — The project is named khosra +Date: 2026-07-30 · Status: accepted +Decision: the engine is `khosra`. Module path `khosra`, binary `khosra`, command directory +`cmd/khosra/`, environment variable `KHOSRA_SITE`, feature-loop skill `khosra-feature-loop`. The former +name `atelier` appears nowhere, including in earlier ADRs, which describe this project under its old +name rather than a different project. +Why: naming is the author's, and it is free now — before a module is published, a URL is shared, or a +binary is deployed. Every day it waits, it costs more. +Consequence: cheap now, and permanent-ish once the module path is fetched by anything. Expensive later — +a module rename after publication needs a redirect or a major-version bump, and a deployed binary name +appears in service files and container tags. +Revisit if: never. Renaming again costs strictly more than this did. diff --git a/docs/extensions.md b/docs/extensions.md index 6373bd0..dd7539b 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -41,7 +41,7 @@ type Extension struct { } ``` -`cmd/atelier/wire.go` holds the only list of enabled extensions. Enabling or disabling one is a +`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. diff --git a/docs/state.md b/docs/state.md index cd89a5c..b640de8 100644 --- a/docs/state.md +++ b/docs/state.md @@ -5,8 +5,12 @@ If this file disagrees with the code, the code is right and this file is a bug. ## Inventory -No Go source, no `go.mod`. The harness is installed; the engine is unwritten. This repo holds engine -source only — the site root is external and passed with `-site` (ADR-0011). +| File | Purpose | LOC | +|---|---|---| +| `go.mod` | module `khosra`; `x/text` and `yaml.v3` required, not yet imported | 8 | + +No Go source yet. This repo holds engine source only — the site root is external and passed with `-site` +(ADR-0011). Dependencies: none. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9af949b --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module khosra + +go 1.26.1 + +require ( + golang.org/x/text v0.40.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d9d8e1a --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/verify.sh b/scripts/verify.sh index 57bd4a7..6f4d399 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -333,7 +333,7 @@ longfuncs=$(echo "$gofiles" | xargs awk -v m="$FUNC_LOC_WARN" ' if [ -f Dockerfile ] && [ "${VERIFY_DOCKER:-0}" = "1" ]; then head_ "container (VERIFY_DOCKER=1)" if command -v docker >/dev/null 2>&1; then - if docker build -q -t atelier:verify . >/tmp/docker.log 2>&1; then + if docker build -q -t khosra:verify . >/tmp/docker.log 2>&1; then pass "docker build" else bad "docker build"; tail -20 /tmp/docker.log | sed 's/^/ /'