A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1# Working agreement for SkyPress
2
3SkyPress is built from a detailed brief (see `docs/` and the original build brief). This
4file captures the durable, non-obvious constraints an agent needs before touching code.
5Read the relevant `docs/decisions/*` and `docs/specs/*` before working in an area.
6
7## Process
8
9- **Brainstorm → spec → plan → build, test-driven.** Write the failing test first.
10- **Record non-obvious calls** in `docs/decisions/NNNN-title.md` (context, options,
11 choice, why). These are first-class deliverables.
12- **Vertical slices over horizontal layers.** Keep the app runnable at every step.
13- **Where specs live.** `docs/specs/sp0`–`sp12` are the **archive of the initial v1 build**
14 (all Built/Complete) — read them for the "how" and the cross-referenced `Depends on`
15 history, but don't extend the SP numbering. New, post-v1 work gets a dated design + plan
16 pair under `docs/superpowers/specs/` and `docs/superpowers/plans/`
17 (e.g. `2026-06-09-editor-page-rework-design.md`). Durable rationale still graduates to a
18 `docs/decisions/NNNN-*.md`.
19
20## Hard constraints (learned the hard way — see decisions)
21
221. **React 18 only.** `@wordpress/block-editor@15.x` peer-depends on `react@^18`. Do not
23 introduce React 19. (Decision 0001)
242. **SkyPress depends on `@wordpress/block-editor` and friends directly** at the current
25 release line; there is **no `@wordpress/*` `overrides` map** (only `react`/`react-dom`
26 stay pinned). Depending directly, the tree resolves to a single coherent copy of every
27 store singleton — `@wordpress/data`, `core-data`, `element`, `blocks`, `block-editor` —
28 without any overrides. The old duplicate-registry crash (`reading 'get' of undefined`)
29 came specifically from IBE pinning an old line while transitive caret ranges floated a
30 newer one; that root cause is gone. Upgrading is now a normal `@wordpress` version bump,
31 **not** regenerating an override map. One caveat: `core-data` / `notices` / `date` install
32 as multiple nested copies (no hoisted top-level one) and each registers its store →
33 `Store "core" is already registered` + a split registry. The fix is the bundler dedupe
34 backstop — run `npm dedupe`, then list them in `resolve.dedupe` in **both**
35 `astro.config.mjs` and `vitest.config.ts` (deduping before the hoist *breaks* the build).
36 (Decision 0021 — supersedes the pinning half of Decision 0003.)
373. **Reading pages must never import `@wordpress/*`.** The editor stack is browser-only
38 (touches `window`/`moment`/registries at import) and cannot render server-side. Use
39 the dependency-free `src/lib/blocks/render.ts`. `@wordpress` belongs only in the
40 editor island and in tests. (Decision 0003)
414. **Render fidelity is test-locked.** `render.ts` must match
42 `@wordpress/blocks.serialize()` for the curated blocks — `render.test.ts` enforces it.
43 Adding a block means adding a `render.ts` case **and** a fidelity assertion.
445. **Curated block allowlist is the content model.** Add blocks deliberately; removing
45 one after content exists is a breaking change. (Decision 0002)
466. **Untrusted content:** stored block trees come from arbitrary PDSes. The reader
47 **sanitises** HTML before injecting it (`src/lib/reader/sanitize.ts`). Three standing
48 rules for the read path: (a) any server-side `fetch` to a host derived from user input
49 (a handle, `did:web`, a PDS `serviceEndpoint`) MUST go through `src/lib/net/safe-fetch.ts`
50 (SSRF guard); (b) never inject PDS-sourced HTML without sanitising — turn document
51 blocks into HTML through `src/lib/reader/render-article.ts`, which runs
52 blob-resolve → render → sanitise in the one safe order (Decision 0018); (c) read routes
53 resolve author/publication/document through `src/lib/reader/read-context.ts` — don't
54 re-assemble the handle → DID → PDS → slug-match → site-join chain in page frontmatter.
55 (Decision 0016)
567. **OAuth is a browser public client** (`@atproto/oauth-client-browser`, Decision 0004).
57 In **dev you must serve on `http://127.0.0.1:<port>`, not `localhost`** (atproto
58 loopback requirement), and the loopback `client_id` must be path-less — see
59 `src/lib/auth/oauth.ts`. Auth + editor live in the `Studio` client-only
60 (`client:only="react"`) island — the editor is composed directly from
61 `@wordpress/block-editor`, not by wrapping `IsolatedBlockEditor`.
628. **Colocated tests under `src/pages/` MUST be underscore-prefixed** (e.g.
63 `_index.meta.test.ts`). Astro's file router imports every `.ts` in `src/pages/` during
64 static-path collection; a `*.test.ts` there runs its top-level `import … from 'vitest'`,
65 which throws outside the vitest runner → the build's prerender server 500s
66 (`Vitest failed to access its internal state`). A leading `_` makes Astro ignore the
67 file as a route while vitest's `src/**/*.test.ts` glob still finds it.
689. **`@wordpress/*` is inlined via `ssr.noExternal` in the vitest config, but
69 `moment`/`moment-timezone` stay external** (native CJS). Inlining moment breaks
70 moment-timezone's augmentation of it (`moment.tz` ends up undefined). Separately,
71 `@wordpress/block-editor` ships no types, so its surface is declared in
72 `src/types/wordpress.d.ts`.
73
74## Product guardrails (from the brief)
75
76- SkyPress is an **editor + OAuth client + public renderer** — never a PDS or relay.
77- **OAuth only** (no app passwords). Secrets never in the client.
78- **Don't surprise users**: publishing also creates a Bluesky post — the UI must say so.
79- Lexicon discipline: prefer optional fields + open unions; treat shipped constraints as
80 frozen (additions only, else `-v2`). License: **GPL-2.0-or-later**.
81
82## Commands
83
84```sh
85npm run dev | build | preview | test | check
86```