Skip to content

Commit 2a31317

Browse files
pcamarajrclaude
andauthored
docs(astro-builder): APoSD architecture reference (v1.8.1) (#50)
Add docs/architecture.md — the A Philosophy of Software Design criteria behind the plugin's decisions, in two parts: the code the builder generates (deep modules with the BaseLayout <head> as canonical example, narrow lib/ interfaces, information hiding, when NOT to abstract, design it twice, comments-as-design) and how to evolve the plugin (audit as thin orchestrator, rules+checks co-located in skills, the one-line domain cost target). Wire it in: astro-architect plans now carry a Design rationale section justified by the doc's criteria, astro-builder references it before adding abstractions, and the README points to it from the Architecture Rules section. Attribution: concepts from John Ousterhout's book; seam/adapter framing and the design-it-twice process adapted from mattpocock/skills (MIT), recorded in NOTICE.md. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7ab6127 commit 2a31317

6 files changed

Lines changed: 253 additions & 2 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
{
3131
"name": "astro-builder",
3232
"description": "Build Astro 6 static content sites with Claude. Enforces best practices for i18n, content collections, the page-views pattern, and design systems. Starts with /astro-builder:init to scaffold a working, buildable site.",
33-
"version": "1.8.0",
33+
"version": "1.8.1",
3434
"author": {
3535
"name": "pcamarajr",
3636
"email": "eu@pcamarajr.dev"

astro-builder/NOTICE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,20 @@ acknowledged here per the upstream `NOTICE`.
2727

2828
A copy of the Apache 2.0 license terms is available at the URL above. This file satisfies the
2929
attribution requirement of Section 4 of that license for the adapted portions listed.
30+
31+
Portions of the following component are **adapted** from **mattpocock/skills** by Matt Pocock,
32+
licensed under the MIT License:
33+
34+
- `docs/architecture.md` — the seam/adapter framing ("one adapter is a hypothetical seam") and
35+
the design-it-twice comparison process (contrast candidate interfaces by depth and locality)
36+
are adapted from the `improve-codebase-architecture` skill (`DEEPENING.md`,
37+
`INTERFACE-DESIGN.md`); the document's overall structure is inspired by those files. The
38+
conceptual source for the document is John Ousterhout's *A Philosophy of Software Design*,
39+
cited in the document itself.
40+
41+
The material was rewritten for the Astro 6 domain this plugin enforces; the original work is
42+
**not** vendored verbatim.
43+
44+
- Source: https://github.com/mattpocock/skills
45+
- License: MIT — Copyright (c) 2026 Matt Pocock
46+
(https://github.com/mattpocock/skills/blob/main/LICENSE)

astro-builder/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Requires `jq` as a system tool: `brew install jq` (macOS) or `apt install jq` (L
142142

143143
Two agents work behind the scenes:
144144

145-
**`astro-architect`** — Planning agent. Reads project context, designs solutions, presents implementation plans before execution. Use when you want to understand what will happen before it happens.
145+
**`astro-architect`** — Planning agent. Reads project context, designs solutions, presents implementation plans before execution — each plan justifies its structural decisions against the criteria in [`docs/architecture.md`](./docs/architecture.md). Use when you want to understand what will happen before it happens.
146146

147147
**`astro-builder`** — Build agent. Fully autonomous. Writes files, runs builds, fixes errors, commits. Operates without interruption until all quality gates pass.
148148

@@ -194,6 +194,11 @@ your-project/
194194

195195
## Architecture Rules Enforced
196196

197+
These rules are not arbitrary: every one is argued from the design criteria in
198+
[`docs/architecture.md`](./docs/architecture.md) — deep modules, information hiding, narrow
199+
interfaces, design it twice — applied both to the sites the plugin generates and to the plugin
200+
itself (based on John Ousterhout's *A Philosophy of Software Design*).
201+
197202
### Astro 6 patterns
198203

199204
- `src/content.config.ts` with `glob()` loaders (never `src/content/config.ts`)

astro-builder/agents/astro-architect.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ You design solutions. You do not write code. When invoked, you:
1818

1919
- **Astro 6 docs**: https://docs.astro.build/llms-small.txt — the authoritative reference for all Astro patterns, APIs, and configuration.
2020
- **MDN Web API**: https://developer.mozilla.org/en-US/ — for any browser or web API usage (fetch, URL, IntersectionObserver, etc.).
21+
- **Design criteria**: `docs/architecture.md` (plugin root) — the APoSD criteria every plan must be argued from: deep modules, information hiding, narrow interfaces, design it twice, when NOT to abstract.
2122
- **Project context**: `CLAUDE.md`, `.astro-builder/` folder, and existing source files in this repo.
2223
- **Astro LSP** (requires astro-lsp plugin): Available to the builder agent for diagnostics on `.astro` files via `mcp__ide__getDiagnostics` — note this in your plans when `.astro` files are being created or modified.
2324

@@ -101,6 +102,11 @@ When presenting a plan, use this structure:
101102
### Implementation notes
102103
{Key decisions, trade-offs, or caveats}
103104
105+
### Design rationale
106+
{Justify each structural decision by the criteria in docs/architecture.md — which module gets
107+
deeper, what information stays hidden where, which alternative interfaces were sketched and why
108+
the winner won. A plan that introduces a new abstraction must say what the abstraction adds.}
109+
104110
### Quality gate
105111
{Which build/lint commands will verify this works}
106112
```

astro-builder/agents/astro-builder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ You do not ask for permission at each step. You ask only when you encounter a ge
2323
- **Astro LSP** (requires astro-lsp plugin): Run `mcp__ide__getDiagnostics` on `.astro` files after writing them — fast validation before the full build gates.
2424
- **Astro 6 docs**: https://docs.astro.build/llms-small.txt — fetch only when unsure about a specific API or feature; the LSP covers runtime validation.
2525
- **MDN Web API**: https://developer.mozilla.org/en-US/ — for any browser or web platform API.
26+
- **Design criteria**: `docs/architecture.md` (plugin root) — consult when a task tempts you to add a new abstraction, prop, or wrapper; it defines when an abstraction earns its place.
2627
- **Project context**: `CLAUDE.md`, `.astro-builder/` folder contents.
2728

2829
## Before writing any code

astro-builder/docs/architecture.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Architecture — the design criteria behind astro-builder
2+
3+
Every structural decision in this plugin — in the sites it generates and in the plugin itself —
4+
is justified by the criteria in John Ousterhout's *A Philosophy of Software Design* (APoSD).
5+
This document makes those criteria explicit so that future decisions can be argued from them,
6+
not from taste. Two audiences:
7+
8+
1. **Code the builder generates** — what a well-designed astro-builder site looks like and why.
9+
2. **The plugin itself** — why the audit, skills, and scaffolds are shaped the way they are.
10+
11+
The unit of judgment throughout is the **module**: anything with an interface (what callers must
12+
know) and an implementation (what it hides). A layout, a `lib/` file, a skill, the audit — all
13+
modules.
14+
15+
---
16+
17+
## Part 1 — The code the builder generates
18+
19+
### 1.1 — Deep modules: small interface, large functionality
20+
21+
A module's cost to its callers is its interface; its value is the functionality behind it. A
22+
**deep** module gives a lot for a little. A **shallow** module makes you learn an interface
23+
nearly as big as the implementation it fronts.
24+
25+
The canonical deep module in every generated site is the **BaseLayout `<head>`**
26+
(`docs/init-templates/BaseLayout.astro.template`, rules in `skills/seo-conventions/SKILL.md`).
27+
Its entire interface is five props:
28+
29+
```ts
30+
interface Props {
31+
title: string;
32+
description: string;
33+
image?: string;
34+
type?: "website" | "article";
35+
publishedAt?: Date;
36+
}
37+
```
38+
39+
Behind those five props: the full title format, the canonical URL, the complete OG/Twitter set,
40+
JSON-LD (`WebSite` / `Article`), hreflang alternates including `x-default`, and the RSS and
41+
sitemap `<link>`s — all derived from `Astro.site`, `Astro.url`, `Astro.currentLocale`, and the
42+
i18n config.
43+
44+
```astro
45+
<!-- 🔴 Shallow: the interface restates the implementation — every page must know
46+
everything the layout knows, and each page can get it differently wrong -->
47+
<BaseLayout
48+
title={title}
49+
canonical={`https://example.com/en/articles/${slug}`}
50+
ogTags={{ "og:title": title, "og:url": `https://example.com/en/articles/${slug}`, ... }}
51+
alternates={[{ lang: "en", href: "..." }, { lang: "it", href: "..." }]}
52+
jsonLd={{ "@type": "Article", headline: title, ... }}
53+
>
54+
```
55+
56+
```astro
57+
<!-- ✅ Deep: pages state the five facts only they know; the layout derives the rest -->
58+
<BaseLayout title={title} description={description} type="article" publishedAt={date}>
59+
```
60+
61+
The test: **does the caller pass anything the module could derive?** If yes, the interface is
62+
leaking implementation. The five props are exactly the facts only the page knows.
63+
64+
### 1.2 — Narrow interfaces pull complexity downward
65+
66+
When complexity must exist, put it inside the module so callers don't carry it. The generated
67+
`src/lib/` layer is built on this:
68+
69+
- **`lib/i18n.ts`**`createTranslator(Astro.currentLocale)` returns `tl(key)`. Nothing else
70+
knows where strings live, the fallback order, or the default locale. The `MessageKey` union
71+
turns a typo into a compile error instead of a blank string in production.
72+
- **`lib/urls.ts`**`buildLocaleUrl(locale, ...segments)` is the one place that knows every
73+
route carries a locale prefix and a trailing slash. Components never concatenate paths.
74+
75+
```astro
76+
<!-- 🔴 Every component re-derives routing policy; change the URL scheme, sweep the codebase -->
77+
<a href={`/${Astro.currentLocale}/articles/${article.id.replace(`${Astro.currentLocale}/`, "")}/`}>
78+
79+
<!-- ✅ Policy lives once, behind one function -->
80+
<a href={buildArticleUrl(article.id, locale)}>
81+
```
82+
83+
The complexity (locale prefixes, slug stripping, trailing slashes) didn't disappear — it moved
84+
down, where it is written once and tested once.
85+
86+
### 1.3 — Information hiding: one decision, one place
87+
88+
Each design decision should live in exactly one module; everything else consumes its result.
89+
In generated sites: `prefixDefaultLocale: true` is *known* only by `lib/urls.ts` (building
90+
prefixes) and BaseLayout's `localePath` strip (removing the one the framework reports). No
91+
component parses a URL to detect the locale — that would copy the routing decision into every
92+
parser.
93+
94+
The same principle shapes this plugin: **a convention rule and its mechanical check live
95+
together.** Each convention skill defines its rules in `SKILL.md` and ships their checks in
96+
`references/audit.md`, side by side in one directory. Changing a rule is a one-directory edit.
97+
If the audit owned the checks, every rule change would be a two-place edit that someone forgets
98+
half of — the classic information leak.
99+
100+
### 1.4 — When NOT to create an abstraction
101+
102+
Depth cuts both ways: an abstraction that adds interface without adding functionality is pure
103+
cost.
104+
105+
- **One adapter is a hypothetical seam.** Don't introduce an interface/port until at least two
106+
real implementations exist (e.g. production + test). With one, the "seam" is just
107+
indirection — collapse it.
108+
109+
```ts
110+
// 🔴 A ContentSource interface with exactly one implementation, "in case we
111+
// swap the CMS later" — callers now learn two surfaces to reach one behavior
112+
interface ContentSource { getArticles(lang: string): Promise<Article[]> }
113+
class CollectionContentSource implements ContentSource { ... }
114+
115+
// ✅ The concrete function, until a second source is real
116+
export async function getArticlesByLang(lang: string): Promise<Article[]> { ... }
117+
```
118+
119+
- **Passthrough wrappers are shallow by definition.** A function that forwards to another
120+
function with the same signature gives callers nothing.
121+
122+
```ts
123+
// 🔴 New surface, zero new functionality
124+
export function getTranslation(locale: string, key: MessageKey): string {
125+
return createTranslator(locale)(key);
126+
}
127+
```
128+
129+
- **Real plugin example: there is no `new-component` skill.** Writing a component is already
130+
fully governed by `css-conventions`, `html-conventions`, and `ux-writing` — they auto-apply.
131+
A `new-component` skill would restate their rules behind a second entry point: a shallow
132+
wrapper over three deep modules. The decision *not* to build it was an architecture decision.
133+
134+
### 1.5 — Design it twice
135+
136+
Your first interface is unlikely to be the best one. Before fixing any interface — a `lib/`
137+
function, a layout prop contract, a new skill — sketch **at least two radically different**
138+
designs and compare them by depth (leverage per entry point), locality (where future change
139+
concentrates), and what callers must know.
140+
141+
The BaseLayout contract went through exactly this. Alternatives considered:
142+
143+
| Design | Verdict |
144+
|---|---|
145+
| Per-tag props (`canonical`, `ogImage`, `alternates`, ...) | Shallow — interface grows with every SEO feature; callers restate derivable facts |
146+
| One `seo: SeoConfig` object prop | Same width, hidden in a bag — and invites pages to construct policy |
147+
| **Five scalar props, everything else derived** | Deep — interface only grows when a page learns a genuinely new fact |
148+
149+
Write the comparison down (a few lines in the plan is enough). If the second design never beats
150+
the first, you at least know *why* the first is right.
151+
152+
### 1.6 — Comments are part of the design
153+
154+
A comment earns its place by saying what the code *cannot* say: intent, invariants, the reason a
155+
derivation is shaped the way it is. The generated templates practice this:
156+
157+
```ts
158+
// ✅ States an invariant the signature can't express (from urls.ts):
159+
/** RSS feed for a locale: `/en/rss.xml`. No trailing slash — it is a file, not a page. */
160+
161+
// ✅ States the design rule and its boundary (from BaseLayout):
162+
// Hand-written SEO tags anywhere else are a violation.
163+
```
164+
165+
```ts
166+
// 🔴 Restates the code — noise that will drift
167+
// loop over the locales and create a link for each
168+
locales.map((locale) => ...)
169+
```
170+
171+
If you can't write a comment that adds information, the abstraction may not need one — or the
172+
abstraction itself may be too thin to describe.
173+
174+
---
175+
176+
## Part 2 — Evolving the plugin
177+
178+
The plugin is judged by the same criteria as the code it generates.
179+
180+
### 2.1 — The audit is a thin orchestrator
181+
182+
`skills/audit/SKILL.md` defines the report contract, the run order, and the checks that have no
183+
backing skill (architecture, i18n, schema, build). It never restates a convention rule. Each
184+
domain's rules live in its skill's `SKILL.md`; the mechanical checks live in that skill's
185+
`references/audit.md`; the audit reads a table and runs what it finds.
186+
187+
**The cost target is the design's acceptance test: adding a domain to the audit costs one
188+
row in one table.** When the css-conventions skill shipped, the audit gained one line. If a
189+
future change makes a new domain cost more than that, the orchestrator has started absorbing
190+
knowledge that belongs in a skill — that is the regression to catch in review.
191+
192+
### 2.2 — Skills are deep modules
193+
194+
A skill's frontmatter `description` is its interface — it must carry everything the trigger
195+
decision needs, because it is all the model sees before loading the body. The body is the
196+
implementation: rules, rationale, good/bad contrasts, verification list. The same shallow-module
197+
smell applies: a skill whose body merely restates other skills (see the `new-component`
198+
non-decision, §1.4) should not exist.
199+
200+
### 2.3 — Checklist for any structural change
201+
202+
Before adding a skill, agent, scaffold template, or audit domain:
203+
204+
- [ ] Is the new module deep — interface meaningfully smaller than what it hides?
205+
- [ ] Does each piece of knowledge it introduces live in exactly one place?
206+
- [ ] Did you sketch at least two interface designs and record why the winner won?
207+
- [ ] If it wraps something that exists, what functionality does the wrapper *add*?
208+
- [ ] Does "add the next instance of this" still cost one line / one file / one row?
209+
210+
---
211+
212+
## Attribution
213+
214+
The concepts here — deep vs. shallow modules, information hiding, pulling complexity downward,
215+
design it twice, comments as design — are from **John Ousterhout, *A Philosophy of Software
216+
Design*** (Yaknyam Press). This document adapts them to the astro-builder domain; read the book
217+
for the full argument.
218+
219+
The seam/adapter framing in §1.4 and the compare-by-depth/locality process in §1.5 are adapted
220+
from the `improve-codebase-architecture` skill (`DEEPENING.md`, `INTERFACE-DESIGN.md`) in
221+
[mattpocock/skills](https://github.com/mattpocock/skills) (MIT). See `NOTICE.md` at the plugin
222+
root.

0 commit comments

Comments
 (0)