Commit Graph
15 Commits
Author SHA1 Message Date
bdeshi 61af326474 let a site root override the theme, and serve static/
A site's templates/ is parsed after the embedded set, so the last definition of a
name wins and a theme redefines one block while inheriting the document
(ADR-0019). Per kind exactly two files are overlaid — base.html and that kind's
block — because overlaying every site template into every set lets a listing's
"main" leak into bundle pages, which is the collision per-kind sets exist to
prevent. Both directions are tested.

templates/theme.css in the site root replaces the reference stylesheet outright;
there is no merging to reason about. static/ is served verbatim under /static/,
through the same os.Root-backed fs.FS, and directory paths answer 404 so it never
indexes its own contents.

Queue entries 6 and 7 are deferred again with triggers: the cascade's consumers are
stage toggles, view selection and cache flags — none of which exist — and declared
types are read by ordering, feeds and check, none of which have landed. Building
either now is the speculation that justified withdrawing them.

Evidence: a real site override renders <section class="mine"> inside the embedded
document with canonical intact, the listing still uses the embedded block, and
/static/site.css and /static/img/logo.svg are 200 while /static/ and /static/img/
are 404.
2026-08-01 02:23:34 +06:00
bdeshi 60a7e10aee add paginated section index pages
The first collection page earns the Query primitive: content.Query{Section, Lang}
with Site.Run, newest first, undated after dated, ties broken by key so the same
query always answers in the same order. No cache signature — nothing caches, and a
signature with no consumer is speculation.

Pagination lives in the path (ADR-0028): page one is the bare listing URL,
/page/1/ redirects to it, and a page past the end is 404 rather than an empty page,
because an empty page is a URL that means nothing. `page` is therefore a reserved
segment inside a section, now recorded in content-model.md.

Two kinds of page means two parsed template sets already — base plus the block that
kind defines — which is ADR-0019's per-type shape arriving by need rather than by
anticipation. A head struct is embedded in both Page and List so base.html has one
contract, and theme-contract.md gains the listing fields.

Bundle gains Date, accepting an unquoted YAML date or an RFC 3339 string, since
yaml.v3 hands back time.Time for one and a string for the other.

Evidence: 12 posts → /posts/ shows 10 with rel=next to /posts/page/2/,
/posts/page/2/ shows 3 with rel=prev to /posts/, ordering is post-12 11 10,
/posts/page/1/ 301s to /posts/, /posts/page/9/ is 404, /bn/posts/ is 200.
2026-08-01 02:23:34 +06:00
bdeshi 9dba59be2f close Arc 1: invariants checked, latent list triaged, retro written
/invariants at the arc boundary: 1, 3 and 5 held and were checked rather than
assumed; 2, 4, 6, 7, 8 and 9 are not yet applicable, with the pre-freeze core
figure recorded as 619.

Invariant 2 turned up a load-bearing default nobody had written down: goldmark
omits raw HTML unless WithUnsafe is set, verified, and that is the only reason
authored Markdown cannot inject script today. The obvious move when a shortcode
needs to emit HTML removes it, so it is now a latent item triggered by queue
entry 12 rather than a surprise.

The other two latent items are triaged as the arc-close rule requires: the
counter-correctness gap is accepted with a reason, the untrusted-boundary gate is
scheduled to Arc 3.
2026-08-01 02:23:34 +06:00
bdeshi 97b01bea05 harness: a standing advisory is a defect
The %w advisory counted every fmt.Errorf without %w, including calls that create
an error rather than wrap one — so it fired permanently on correct code. It now
looks for a call that passes an err and drops the %w, which is the actual rule.

Recorded the general form in the loop's Verify step and in HARNESS.md: a warning
that fires on correct code and keeps firing is a defect in the check or the code,
resolved in that change. Two advisories have now been narrowed after firing on
code the harness itself mandates, and the count creeping from one to two was the
only signal either time.

verify.sh is at zero warnings, which is what makes the next one legible.
2026-08-01 02:23:34 +06:00
bdeshi 2f253f94dd honour aliases as permanent redirects
An alias is a promise that an old URL keeps working, so it answers 301 to the
canonical one rather than serving the same content twice (ADR-0008). Frontmatter
takes a scalar or a list and tolerates surrounding slashes, because authors write
both.

Ambiguity is dropped, not resolved: an alias naming a real bundle, or claimed by
two bundles, is logged and ignored so the real bundle keeps its URL. Aliases
compose with language prefixes for free, since the resolver splits the language
before the key is looked up.

The redirect still fires only for an alias that exists, so a nonexistent path
cannot be probed by 301 — the property prompt 3 established.

Evidence: /pages/bio/ and /about/ both 301 to /pages/about/, /bn/pages/bio/ 301s
to /bn/pages/about/, and /pages/nothing/ is 404.
2026-08-01 02:23:34 +06:00
bdeshi 80057aa183 serve language variants under a prefix
The default locale stays at the root; every other language is the same key under
/{lang}/ (ADR-0009). /en/… is never live and redirects to the root form so the URL
space cannot fork. Lookup now takes a language and reports which one it served,
following requested → default → any rather than 404ing when a translation is
missing.

That is the second routing case, so the resolver is extracted to resolve.go and
the mux keeps one entry: URL shape is the resolver's business. A leading segment
counts as a language only when some bundle is written in it, so an unknown prefix
is a 404 rather than a stripped path — and a section may not be named after a
language in use, now recorded in content-model.md.

Because the served variant can differ from the URL requested, Page gained
.Canonical (the variant actually served) and .Alternates for hreflang. A theme
must never build a path, so both come from the engine.

Evidence: /bn/pages/about/ serves the Bengali body with lang="bn" and canonical
/bn/pages/about/; /bn/posts/hello-world/ falls back to English with canonical
/posts/hello-world/; /en/pages/about/ 301s to /pages/about/; /fr/… is 404.
2026-08-01 02:23:34 +06:00
bdeshi 9359478789 add a Makefile and build instructions
So the thing can be built and run without reading the harness. Eight targets,
each one go command or verify.sh; make help lists them. README gains the four
lines someone actually needs, and says a site root is a directory the binary is
pointed at rather than something in this repo.

make is recorded in toolchain.md as a convenience: the gate never invokes it, so
a machine without make loses nothing but typing.
2026-08-01 02:23:34 +06:00
bdeshi 605359a495 serve a bundle at its permalink
-site (or KHOSRA_SITE) opens the site root through content.OpenSite, so every
read keeps the os.Root guarantee. A path is a bundle key: /{section}/{slug}/
serves, the slashless form redirects permanently to it (ADR-0008), anything
unknown is 404. Render failure logs and returns a bare 500 rather than leaking a
template or filesystem detail.

internal/render holds goldmark plus the embedded reference theme (ADR-0026):
base.html with a redefinable "main" block, and one stylesheet inlined through
.Style. Serving it at an asset route would have been a second routing case for no
gain, and static serving belongs to a later entry.

Evidence beyond the tests: the binary against a real site root returns 200 with
<h1>About</h1> and the rendered body, 301 from /pages/about to /pages/about/, and
404 for /nope/. A Bengali variant is scanned but not yet reachable — that is the
next entry.

theme-contract.md gains a "Live today" section listing the six fields and two
named templates a theme may now rely on; the rest stays marked as shape.
2026-08-01 02:23:34 +06:00
bdeshi 0314179ec2 keep the build queue in an uncommitted .scratch/
The queue is a working plan, not a record: git log already carries what landed on
each step, so committing the plan duplicates it and invites the two to disagree.
.scratch/ is gitignored and skipped by the gate the way ideas/ and reference/ are,
so exploratory files there need not compile.

state.md keeps a conditional pointer rather than a path it cannot promise: if the
scratch file is absent, the log is the plan.
2026-08-01 02:23:34 +06:00
bdeshi 4ba70b0459 content-model: state the language-suffix shape and the collision rule
The parser treats two or three lowercase letters before .md as a language and
rejects a key claimed by two spellings of one variant. Both were implemented and
neither was in the doc that owns the disk contract.

Folded from a separate state commit:
state: point verified-against at the commit that introduced code
2026-08-01 02:23:34 +06:00
bdeshi eb9f408dd7 content: read a site root into bundles
Bundle loading with no HTTP: walk content/, split YAML frontmatter, derive an
NFC-normalised key and a language from the filename, and lift only title out of
frontmatter so every other key stays readable through Extra (ADR-0002).

Path safety is os.Root rather than a hand-rolled cleaner (ADR-0031). os.DirFS
documents that it does not prevent symlink escape; os.Root refuses any name
resolving outside the root, so the guard is a property of the type instead of a
check to remember at each call site. Test: a symlink to a file above the root
cannot be read. This clears the traversal item off the latent list.

A bundle that will not parse is logged and skipped, never fatal (ADR-0029), as
is a key claimed by two spellings of one variant (ADR-0021).

Bundle carries only Key, Lang, Path, Title, Body and Extra; Date, Slug, Draft
and Aliases arrive with the features that read them.
2026-08-01 02:23:34 +06:00
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
bdeshi 39dcb8cc34 harness: soften two gates that would have taught --no-verify
The test-coupling gate failed comment-only and gofmt-only .go diffs, which
ship no behaviour and owe no test. Bypassing it would have cost every gate at
once, so it now compares added and removed lines with comments, blanks and
whitespace runs stripped: equal sets mean nothing happened. A new .go file is
never exempt.

The exported-doc-comment check is now a warning. No gate can tell
"// Load loads." from a useful sentence, so as a hard failure its cheapest
satisfaction was exactly the noise conventions.md calls worse than nothing.
Package comments and ADR citations in code stay hard.

Also fills in state.md's verified-against line and drops a stray blank line
left in roadmap.md by an earlier gate test.
2026-08-01 02:23:34 +06:00
bdeshi 581d6c08ee harness: withdraw five pre-code ADRs to deferred ideas
Twenty-seven ADRs existed before a line of Go. Five specified the shape of
unbuilt mechanisms — cache validity, declared types, the settings cascade,
taxonomies, extras — which breaks the rule against abstraction before a
second concrete use, in prose where the counters cannot see it. They move to
ideas/deferred-decisions.md as recorded intent and return shaped by whatever
implements them.

Citations retargeted throughout; where one was decoration the rule now stands
on its own reasoning. Type declarations and the cascade drop to [spec] with
the MVP behaviour stated instead, so the first prompts have less to build.
conventions.md names http.ServeMux as the router, closing a hole that invited
hand-rolling a path splitter. The ADR gate now checks a number is registered
in the log rather than headed by an entry, so withdrawals resolve and invented
numbers still fail. Two architecture invariants corrected: identity no longer
implies a required language suffix, and the duplicated permalink clause is gone.
2026-08-01 02:23:33 +06:00
bdeshi 54c14a20be init 2026-07-30 00:34:18 +06:00