Demo content from demo/developer-guide (0c17989)
This commit is contained in:
parent
e7b15ff117
commit
4e32950ceb
12 changed files with 676 additions and 2 deletions
9
.order
Normal file
9
.order
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
architecture
|
||||||
|
principles
|
||||||
|
local-setup
|
||||||
|
repository-layout
|
||||||
|
api
|
||||||
|
web-frontend
|
||||||
|
markdown-and-editor
|
||||||
|
extending
|
||||||
|
testing-and-contributing
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# developer-guide
|
|
||||||
|
|
||||||
77
api/index.md
Normal file
77
api/index.md
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
id: api
|
||||||
|
title: The API
|
||||||
|
description: Route groups, authentication, and the review workflow's SHA contract.
|
||||||
|
tags: [api, architecture]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# The API
|
||||||
|
|
||||||
|
`apps/api` is a Fastify server. It is the only part of f451 that checks
|
||||||
|
permissions, runs the review workflow and writes to the index — see
|
||||||
|
[[principles]]. Everything below is registered in `src/app.ts`.
|
||||||
|
|
||||||
|
## Route groups
|
||||||
|
|
||||||
|
| Group | Examples | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| System | `GET /healthz`, `GET /readyz` | Unauthenticated, for orchestrators. |
|
||||||
|
| Docs | `GET /api/docs`, `GET /api/openapi.json` | Swagger UI and the generated OpenAPI spec, open without a session. |
|
||||||
|
| Auth | `/auth/*` (OIDC login/callback/logout), `/auth/connect/*` (link a provider account) | Open — a login flow can't require a session to start one. |
|
||||||
|
| Session & tokens | `GET /api/me`, `/api/tokens/*` | Personal API token management for agents (see [[extending]]). |
|
||||||
|
| Read | `GET /api/spaces/:space/*` (pages, tree, search, broken links, graph, metadata schema, templates), `GET /api/pages/:id`, `GET /api/pages/:id/versions*` | Filtered by the caller's read access to the underlying repository. |
|
||||||
|
| Write / drafts | `POST /api/pages`, `.../draft`, `.../draft/media`, `.../locks`, `.../move`, `.../unarchive`, `DELETE /api/pages/:id`, `PUT /api/spaces/:space/order` | Require a linked provider account with write access; committed under the user's own token. |
|
||||||
|
| Review workflow | `/api/pages/:id/draft`, `/api/pages/:id/review`, `/api/pages/:id/review/request-changes`, `/api/pages/:id/release` (MCP tools `request_review` / `release_page`, see [[extending]]) | Turns a draft branch into a pull request, then merges it. |
|
||||||
|
| Media | `/media/*` | Binary asset delivery, gated like page reads. |
|
||||||
|
| Webhooks | `/webhooks/*` | Provider push events trigger incremental reindexing; HMAC-verified, no session. |
|
||||||
|
| Admin | `/admin/reindex`, `/admin/status`, `/admin/backfill-ids` | Bearer-token gated (`F451_ADMIN_TOKEN`), fail-closed if the token isn't configured. |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Two independent mechanisms protect `/api/*`, `/admin/*` and `/media/*`:
|
||||||
|
|
||||||
|
- **Session cookies**, established via OIDC login (Entra, Forgejo, or any
|
||||||
|
other OIDC provider). This is how the browser UI authenticates.
|
||||||
|
- **Personal API tokens** (`f451_pat_…`), issued through `/api/tokens` and
|
||||||
|
used by the MCP service and other automation. Tokens carry a scope —
|
||||||
|
`read` or `write` — and a read-scoped token gets a `403` on anything but
|
||||||
|
`GET`.
|
||||||
|
|
||||||
|
`/admin/*` additionally accepts a separate bearer token
|
||||||
|
(`F451_ADMIN_TOKEN`) instead of a session, so ops automation can call
|
||||||
|
`POST /admin/reindex` from a plain script.
|
||||||
|
|
||||||
|
A CSRF origin check runs on every state-changing request (`POST`, `PUT`,
|
||||||
|
`PATCH`, `DELETE`): a present-but-foreign `Origin` header is rejected. A
|
||||||
|
missing header is allowed through deliberately — that's the shape of
|
||||||
|
legitimate non-browser clients (CLI, CI, webhook senders), which
|
||||||
|
`SameSite=Lax` cookies don't protect against by themselves.
|
||||||
|
|
||||||
|
## `404` is deliberately ambiguous
|
||||||
|
|
||||||
|
Read routes never distinguish "this page doesn't exist" from "you don't
|
||||||
|
have access to it" — both are a plain `404`. See [[principles]] for why.
|
||||||
|
|
||||||
|
## The review workflow and its SHA contract
|
||||||
|
|
||||||
|
Nothing writes to the published version directly. The sequence is always:
|
||||||
|
|
||||||
|
1. **Create a draft** — a branch, from the current published state.
|
||||||
|
2. **Write** — save content to the draft branch, repeatedly.
|
||||||
|
3. **Request review** — opens a pull request.
|
||||||
|
4. **Release** — merges the pull request.
|
||||||
|
5. **Reindex** — the merge webhook (or a manual `/admin/reindex`) updates
|
||||||
|
the Postgres index from the new Git state.
|
||||||
|
|
||||||
|
Every write to a draft names a `baseSha`: the commit SHA the writer started
|
||||||
|
from. If the draft has moved on since — someone else saved a change, or a
|
||||||
|
previous save from the same writer landed — the API responds `409` with
|
||||||
|
the *current* SHA and content instead of silently overwriting it. The
|
||||||
|
caller is expected to re-fetch, reconcile, and retry with the new
|
||||||
|
`baseSha`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This is the same mechanism whether the writer is a human in the browser
|
||||||
|
> editor or an AI agent calling `update_page_draft` through MCP — see
|
||||||
|
> [[extending]]. There is no privileged, SHA-check-free path for agents.
|
||||||
49
architecture/index.md
Normal file
49
architecture/index.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
id: architecture
|
||||||
|
title: Architecture
|
||||||
|
description: The three applications and four packages, and how a request travels through them.
|
||||||
|
tags: [architecture, overview]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
f451 is a pnpm monorepo on Node 22. Three applications and four shared
|
||||||
|
packages divide the work cleanly along one line: **`apps/api` is the only
|
||||||
|
place that checks permissions, runs the review workflow and writes to the
|
||||||
|
index.** Everything else either renders what the API gives it, or talks to
|
||||||
|
the API on someone's behalf.
|
||||||
|
|
||||||
|
## Request flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser ──► apps/web (Next.js) ──► apps/api (Fastify) ──► Forgejo / GitHub
|
||||||
|
│ (content, permissions)
|
||||||
|
└──► PostgreSQL (index, sessions)
|
||||||
|
|
||||||
|
AI agent ──► apps/mcp (MCP↔HTTP) ──────────► apps/api (same HTTP API)
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing skips `apps/api`. The web app never talks to Git directly, and the
|
||||||
|
MCP service never talks to Postgres or Git directly — both reach content
|
||||||
|
and permissions exclusively through the API's HTTP surface.
|
||||||
|
|
||||||
|
## The three applications
|
||||||
|
|
||||||
|
| App | Role |
|
||||||
|
|---|---|
|
||||||
|
| `apps/api` (Fastify) | Indexing, read and write endpoints, webhooks, drift reconciliation, admin. The only place with permission checks, the review workflow and the index. |
|
||||||
|
| `apps/web` (Next.js App Router) | The user interface. Proxies `/api`, `/auth`, `/admin`, `/media` to the API, `/drawio` to the draw.io container, `/mcp` to the MCP service. Server Components by default; client islands stay small. |
|
||||||
|
| `apps/mcp` | Access for AI agents: a stateless translator between MCP and the HTTP API. It wraps only the HTTP API, holds no secret of its own, and passes the calling user's token through on every request. |
|
||||||
|
|
||||||
|
## The four packages
|
||||||
|
|
||||||
|
| Package | Role |
|
||||||
|
|---|---|
|
||||||
|
| `packages/markdown` | Renders Markdown to sanitized HTML. Shared by reading and writing so both sides agree on what a construct means. |
|
||||||
|
| `packages/editor` | The ProseMirror schema behind the WYSIWYG editor. Round-trip tests with `packages/markdown` keep reading and editing aligned — a document that survives Markdown → editor → Markdown unchanged. |
|
||||||
|
| `packages/design-tokens` | The catalog of design tokens that drives the look and feel — see [[web-frontend]]. |
|
||||||
|
| `packages/git-provider` | One interface, two implementations (Forgejo, GitHub): read/write files, commits, branches, pull requests, reviews. |
|
||||||
|
|
||||||
|
See [[principles]] for why the lines are drawn exactly here, and
|
||||||
|
[[repository-layout]] for the directories underneath each of these.
|
||||||
103
extending/index.md
Normal file
103
extending/index.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
id: extending
|
||||||
|
title: Extending f451
|
||||||
|
description: Recipes for the most common kinds of change — an API route, a UI string, a design token, a Markdown construct, an MCP tool, a Git provider.
|
||||||
|
tags: [howto, architecture]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extending f451
|
||||||
|
|
||||||
|
Six recipes for changes that come up repeatedly. Each one names where the
|
||||||
|
change starts and what it touches on the way.
|
||||||
|
|
||||||
|
## Add an API route
|
||||||
|
|
||||||
|
1. Add a `register*Routes` function under `apps/api/src/routes/`, following
|
||||||
|
the existing pattern: it takes `{ db, spaces, providerRegistry, access,
|
||||||
|
canWrite, getUserProvider }` as needed, not the raw request context.
|
||||||
|
2. Register it in `apps/api/src/app.ts`, in the block that only runs when
|
||||||
|
`db && opts.spaces && opts.providerRegistry` are present — read-only
|
||||||
|
routes need `access`; anything that writes needs `canWrite` and
|
||||||
|
`getUserProvider` as well, since writes commit under the calling user's
|
||||||
|
own provider token (see [[principles]]).
|
||||||
|
3. Decide read or write semantics up front: read routes are filtered by
|
||||||
|
`access.canRead`, write routes additionally need a linked provider
|
||||||
|
account and go through the draft/SHA-contract machinery described in
|
||||||
|
[[api]] — don't bypass it for a "simple" new route.
|
||||||
|
4. Add a Swagger/JSON schema for the response; the OpenAPI spec at
|
||||||
|
`/api/openapi.json` is generated from it.
|
||||||
|
|
||||||
|
## Add a UI string
|
||||||
|
|
||||||
|
Add the key to both `apps/web/lib/i18n/messages/en/` and the matching
|
||||||
|
`de/` file in the same change — f451's interface is bilingual, and a
|
||||||
|
string that exists in only one language is a regression, not a partial
|
||||||
|
feature. See [[web-frontend]].
|
||||||
|
|
||||||
|
## Add a design token
|
||||||
|
|
||||||
|
1. Add the value to `packages/design-tokens/src/tokens.ts`.
|
||||||
|
2. Describe it in `catalog.ts`: its level (`theme` / `derived` /
|
||||||
|
`structure` / `switch`), its settings-page group, its role in plain
|
||||||
|
language, and whether a user theme is allowed to set it (`settable`,
|
||||||
|
with a `lockReason` if not).
|
||||||
|
3. Update the token-count tests for the affected level and group — they
|
||||||
|
are pinned deliberately, so the update should be a conscious edit, not
|
||||||
|
a number you copy without reading why it existed. See [[web-frontend]].
|
||||||
|
|
||||||
|
## Add a Markdown construct
|
||||||
|
|
||||||
|
Full recipe in [[markdown-and-editor]]. In short: parse and render it in
|
||||||
|
`packages/markdown`, make `stringify.ts` reproduce the same syntax, add the
|
||||||
|
matching node to `packages/editor` and its two conversion directions, then
|
||||||
|
add a round-trip test that proves Markdown → editor → Markdown is
|
||||||
|
lossless.
|
||||||
|
|
||||||
|
## Add an MCP tool
|
||||||
|
|
||||||
|
MCP tools in `apps/mcp/src/tools/` are thin wrappers: each one calls the
|
||||||
|
HTTP API (`apps/mcp/src/client.ts`) with the token passed through from the
|
||||||
|
calling agent, and does not hold state or a secret of its own. The
|
||||||
|
existing set is a useful map of scope:
|
||||||
|
|
||||||
|
- **Read** (`tools/read.ts`): `search_wiki`, `list_spaces`, `get_tree`,
|
||||||
|
`read_page`, `get_page_source`, `get_graph`, `list_broken_links`.
|
||||||
|
- **Write** (`tools/write.ts`): `create_page`, `edit_page`,
|
||||||
|
`update_page_draft`, `discard_page_draft`, `request_review`,
|
||||||
|
`release_page`, `request_changes` — the review workflow from [[api]],
|
||||||
|
exposed one step at a time.
|
||||||
|
- **Attachments** (`tools/attachments.ts`): `save_diagram`, `attach_file`.
|
||||||
|
|
||||||
|
A new tool follows the same shape: register it with `server.registerTool`,
|
||||||
|
give it a clear `title` and input schema, and implement it by calling an
|
||||||
|
existing API route — never by reaching around the API into the database or
|
||||||
|
Git directly. If no suitable API route exists yet, add one first (see
|
||||||
|
above); the MCP service is not the place to grow API-shaped logic of its
|
||||||
|
own.
|
||||||
|
|
||||||
|
## Add a Git provider
|
||||||
|
|
||||||
|
`packages/git-provider/src/types.ts` defines one interface,
|
||||||
|
`GitProvider` — file reads (`readFile`, `readFileBinary`, `listTree`),
|
||||||
|
writes (`writeFile`, `writeFileBinary`, `deleteFile`, the batched
|
||||||
|
`commitFiles`), branches, and the pull request lifecycle
|
||||||
|
(`createPullRequest`, `requestReviewers`, `submitPullRequestReview`,
|
||||||
|
`mergePullRequest`). `forgejo.ts` and `github.ts` are the two existing
|
||||||
|
implementations.
|
||||||
|
|
||||||
|
A third provider means a new file implementing every method of
|
||||||
|
`GitProvider`, throwing the same three error types
|
||||||
|
(`NotFoundError`/`ConflictError`/`ProviderError`) the existing
|
||||||
|
implementations use — callers in `apps/api` branch on those errors, not on
|
||||||
|
provider-specific ones. `commitFiles` is worth reading closely before
|
||||||
|
implementing it: it exists because writing many files one call at a time
|
||||||
|
was measured to take minutes on a large move, and its batching behavior
|
||||||
|
(splitting large change sets across multiple underlying commits, returning
|
||||||
|
the last commit's SHA, treating an empty list as a no-op) is part of the
|
||||||
|
contract, not an implementation detail.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> None of these recipes need a browser or a full test run to make progress
|
||||||
|
> on. See [[testing-and-contributing]] for what to actually check before
|
||||||
|
> opening a pull request.
|
||||||
46
index.md
Normal file
46
index.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
id: overview
|
||||||
|
title: Developer Guide
|
||||||
|
description: How to understand, run and extend f451, the git-native documentation wiki.
|
||||||
|
tags: [overview, getting-started]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# f451 Developer Guide
|
||||||
|
|
||||||
|
f451 is a wiki whose content lives entirely in Git. Every space is a
|
||||||
|
repository on Forgejo or GitHub, every page a Markdown file with YAML
|
||||||
|
frontmatter. PostgreSQL only holds a derived index — search vectors,
|
||||||
|
rendered HTML, the knowledge-graph edges — and can always be rebuilt from
|
||||||
|
Git. This demo space is for developers who want to run f451 locally,
|
||||||
|
understand how the pieces fit together, and add something to it.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This is a demo space, not the project's own documentation. It exists to
|
||||||
|
> show what a well-structured f451 space looks like while explaining f451
|
||||||
|
> itself.
|
||||||
|
|
||||||
|
## Where to start
|
||||||
|
|
||||||
|
- [[principles]] — the four architectural rules that shape every decision
|
||||||
|
in the codebase, and why they exist
|
||||||
|
- [[architecture]] — the three applications and four packages, and how a
|
||||||
|
request travels through them
|
||||||
|
- [[local-setup]] — get a working stack on your machine
|
||||||
|
- [[repository-layout]] — what lives where in the monorepo
|
||||||
|
|
||||||
|
## Going deeper
|
||||||
|
|
||||||
|
- [[api]] — route groups, authentication, the review workflow and its SHA
|
||||||
|
contract
|
||||||
|
- [[web-frontend]] — routing, the proxy, internationalization, styling and
|
||||||
|
design tokens
|
||||||
|
- [[markdown-and-editor]] — the Markdown pipeline and the editor that has
|
||||||
|
to stay in sync with it
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
- [[extending]] — recipes for the six most common kinds of change
|
||||||
|
- [[testing-and-contributing]] — what to run before you open a pull
|
||||||
|
request, what CI runs, and the license terms your contribution falls
|
||||||
|
under
|
||||||
84
local-setup/index.md
Normal file
84
local-setup/index.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
id: local-setup
|
||||||
|
title: Local setup
|
||||||
|
description: Get a working f451 stack on your machine, and the commands you'll use daily.
|
||||||
|
tags: [setup, getting-started]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Local setup
|
||||||
|
|
||||||
|
f451 needs Node 22 and pnpm 9, plus Docker (or Podman) for the two stacks
|
||||||
|
it runs locally: a Forgejo instance for content and sign-in, and the wiki
|
||||||
|
itself.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This mirrors the top-level `README.md`. If the two ever disagree, the
|
||||||
|
> README is authoritative — this page exists so the setup steps sit next
|
||||||
|
> to the rest of the developer documentation.
|
||||||
|
|
||||||
|
## Why not `localhost`
|
||||||
|
|
||||||
|
The browser and the containers must reach Forgejo and the wiki under the
|
||||||
|
*same* address, because inside a container `localhost` means the container
|
||||||
|
itself, not your machine. Pick your machine's LAN address once and use it
|
||||||
|
everywhere below (called `<IP>`).
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Start Forgejo first.** It must be running before the wiki stack, since
|
||||||
|
the wiki authenticates against it.
|
||||||
|
|
||||||
|
```
|
||||||
|
cat > deploy/git/docker-compose.override.yml <<EOF
|
||||||
|
services:
|
||||||
|
forgejo:
|
||||||
|
environment:
|
||||||
|
FORGEJO__server__ROOT_URL: http://<IP>:3300/
|
||||||
|
EOF
|
||||||
|
docker compose -f deploy/git/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the setup script.** It creates an admin account, an organization,
|
||||||
|
an OAuth app and a demo space, and writes `deploy/wiki/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
FORGEJO_URL=http://<IP>:3300 WEB_BASE=http://<IP>:8080 ./scripts/dev-local-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the wiki:**
|
||||||
|
|
||||||
|
```
|
||||||
|
cd deploy/wiki && docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open** `http://<IP>:8080`, sign in through Forgejo
|
||||||
|
(`wiki-admin` / `admin1234`), then link your Forgejo account under
|
||||||
|
*Settings → Connections*. Spaces only become visible and writable once
|
||||||
|
your account is linked — this is the "permissions come from the Git
|
||||||
|
provider" rule from [[principles]] in practice.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This setup is for local development only: plain HTTP, insecure cookies.
|
||||||
|
> It is not meant to be exposed beyond your machine.
|
||||||
|
|
||||||
|
Database migrations do not run automatically after an update; run
|
||||||
|
`docker compose run --rm api node dist/db/migrate-cli.js`.
|
||||||
|
|
||||||
|
## Everyday commands
|
||||||
|
|
||||||
|
Once the stack is up, most development happens outside Docker:
|
||||||
|
|
||||||
|
| Purpose | Command |
|
||||||
|
|---|---|
|
||||||
|
| Type-check every package | `pnpm -w typecheck` |
|
||||||
|
| Run every test | `pnpm -w test` |
|
||||||
|
| Run one test file | `pnpm --filter @f451/web test -- lib/urls.test.ts` |
|
||||||
|
| API in dev mode (port 3001) | `pnpm --filter @f451/api dev` |
|
||||||
|
| Web in dev mode (port 3000) | `pnpm --filter @f451/web dev` |
|
||||||
|
| Generate a database migration | `pnpm --filter @f451/api db:generate` |
|
||||||
|
| CSS metrics with bounds | `pnpm css:inventar` |
|
||||||
|
|
||||||
|
For what to actually run before opening a pull request, see
|
||||||
|
[[testing-and-contributing]] — it is a much shorter list than "everything
|
||||||
|
above".
|
||||||
76
markdown-and-editor/index.md
Normal file
76
markdown-and-editor/index.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
---
|
||||||
|
id: markdown-and-editor
|
||||||
|
title: Markdown and the editor
|
||||||
|
description: The Markdown pipeline, its constructs, and adding a construct in both packages.
|
||||||
|
tags: [markdown, editor, architecture]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Markdown and the editor
|
||||||
|
|
||||||
|
Two packages share the job of understanding a page's content:
|
||||||
|
`packages/markdown` turns Markdown into sanitized HTML for reading;
|
||||||
|
`packages/editor` provides the ProseMirror schema behind the WYSIWYG
|
||||||
|
editor. They have to agree, because a page can be opened in either mode at
|
||||||
|
any time and must come back unchanged.
|
||||||
|
|
||||||
|
## The Markdown pipeline
|
||||||
|
|
||||||
|
`packages/markdown/src/`:
|
||||||
|
|
||||||
|
- `parse.ts` / `render.ts` / `stringify.ts` — Markdown → AST → HTML, and
|
||||||
|
back to Markdown text.
|
||||||
|
- `frontmatter.ts` / `frontmatter-split.ts` / `frontmatter-metadata.ts` —
|
||||||
|
the YAML header every page carries.
|
||||||
|
- `alerts.ts` — the `> [!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]`
|
||||||
|
callouts used throughout this space.
|
||||||
|
- `image-size.ts`, `youtube.ts` — extra constructs beyond plain CommonMark.
|
||||||
|
- `slug.ts`, `diff.ts`, `version.ts` — heading slugs, and support for page
|
||||||
|
version comparison.
|
||||||
|
|
||||||
|
## The editor
|
||||||
|
|
||||||
|
`packages/editor/src/`:
|
||||||
|
|
||||||
|
- `extensions.ts` — assembles the ProseMirror schema.
|
||||||
|
- `nodes/alert.ts`, `nodes/image.ts`, `nodes/wiki-link.ts`,
|
||||||
|
`nodes/youtube-embed.ts` — one node per construct that needs editor
|
||||||
|
support beyond stock ProseMirror.
|
||||||
|
- `from-markdown.ts` / `to-markdown.ts` — the two directions of
|
||||||
|
conversion between editor document and Markdown text.
|
||||||
|
|
||||||
|
## Constructs shared by both packages
|
||||||
|
|
||||||
|
Wikilinks (`[[page-id]]`, `[[page-id|label]]`) and callouts are the two
|
||||||
|
constructs every page in this space uses, and both need a matching node in
|
||||||
|
`packages/editor` (`wiki-link.ts`, `alert.ts`) so that opening a page in
|
||||||
|
the WYSIWYG editor round-trips it correctly.
|
||||||
|
|
||||||
|
## Adding a construct to both packages
|
||||||
|
|
||||||
|
A new Markdown construct (say, a new kind of callout, or a new inline
|
||||||
|
element) needs changes in four places, in this order:
|
||||||
|
|
||||||
|
1. **`packages/markdown`** — teach the parser to recognize the syntax and
|
||||||
|
the renderer to produce sanitized HTML for it.
|
||||||
|
2. **`packages/markdown`** — teach `stringify.ts` to produce the same
|
||||||
|
syntax back out, so a document that was never touched in the editor is
|
||||||
|
byte-for-byte stable.
|
||||||
|
3. **`packages/editor`** — add a node (or mark) under `nodes/` and wire it
|
||||||
|
into `extensions.ts`, then extend `from-markdown.ts` and
|
||||||
|
`to-markdown.ts` for the two conversion directions.
|
||||||
|
4. **Round-trip test** — Markdown → editor document → Markdown must
|
||||||
|
reproduce the original. This is the test that actually catches drift
|
||||||
|
between the two packages; skipping it is how "reading shows X, editing
|
||||||
|
shows Y" bugs get in.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> If the sanitizer (used by `packages/markdown`'s HTML output) doesn't know
|
||||||
|
> about the new construct's tags or attributes, it will silently strip
|
||||||
|
> them. This is exactly the failure mode the diagram sanitizer test guards
|
||||||
|
> against for `.drawio.svg`/`.excalidraw.svg` — the `content` attribute and
|
||||||
|
> wrapping text elements must survive sanitization, or diagrams lose their
|
||||||
|
> source and their full labels.
|
||||||
|
|
||||||
|
See [[extending]] for the same recipe phrased as a short checklist, and
|
||||||
|
[[api]] for how the rendered result reaches the reader.
|
||||||
65
principles/index.md
Normal file
65
principles/index.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
---
|
||||||
|
id: principles
|
||||||
|
title: Principles
|
||||||
|
description: The four rules that shape f451's design, and why they exist.
|
||||||
|
tags: [architecture, principles]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Principles
|
||||||
|
|
||||||
|
Four rules recur throughout the codebase. They are not style preferences —
|
||||||
|
each one closes off a class of bugs or design mistakes that the project has
|
||||||
|
already made once.
|
||||||
|
|
||||||
|
## Git is the source of truth, Postgres is only an index
|
||||||
|
|
||||||
|
Every space is a Git repository; every page is a Markdown file with
|
||||||
|
frontmatter. Postgres holds the rendered content, the search vector, the
|
||||||
|
knowledge-graph edges, sessions and encrypted provider tokens — nothing
|
||||||
|
that can be derived from Git is allowed to live *only* in the database.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Losing `pg-data` is recoverable: `POST /admin/reindex` or the drift job
|
||||||
|
> rebuilds the index from Git. Losing `forgejo-data` is not — that is the
|
||||||
|
> one thing backups exist for. If you find yourself adding a column that
|
||||||
|
> holds information not derivable from the Git content, stop and ask
|
||||||
|
> whether it belongs in Git instead.
|
||||||
|
|
||||||
|
## Permissions come from the Git provider
|
||||||
|
|
||||||
|
f451 has no role system of its own. Whether someone can see a space or
|
||||||
|
write to it is decided by their permissions on the underlying repository.
|
||||||
|
Writers always write with **their own** provider token — the API never
|
||||||
|
commits under a service account. Without a linked provider account, every
|
||||||
|
write attempt fails with 403; that is a precondition, not a bug.
|
||||||
|
|
||||||
|
This also shapes how "not found" behaves: a `404` deliberately means
|
||||||
|
*"doesn't exist, or you don't have access"* — never distinguish the two.
|
||||||
|
Doing so would let an unauthorized caller learn that a page exists, which
|
||||||
|
is exactly the kind of existence oracle this design avoids.
|
||||||
|
|
||||||
|
## Writing always goes through the review workflow
|
||||||
|
|
||||||
|
There is no route that writes directly to the published version — not for
|
||||||
|
people, not for agents. The path is always: create a draft (a branch) →
|
||||||
|
write → open a review (a pull request) → approve (merge) → reindex.
|
||||||
|
|
||||||
|
Draft routes carry a SHA contract: whoever writes names the `baseSha` of
|
||||||
|
the version they started from. If the content has changed underneath them,
|
||||||
|
they get a `409` back with the current state instead of a silent
|
||||||
|
overwrite. See [[api]] for the routes that implement this.
|
||||||
|
|
||||||
|
## Diagrams carry their own source
|
||||||
|
|
||||||
|
`.drawio.svg` and `.excalidraw.svg` files under `<page folder>/_media/` are
|
||||||
|
plain SVGs that also carry the diagram's source XML in a `content`
|
||||||
|
attribute. One file is both the picture people see and the thing the
|
||||||
|
diagram editor reopens. The SVG sanitizer keeps that attribute and the
|
||||||
|
wrapping text elements deliberately — stripping either would make the
|
||||||
|
diagram unreadable or unopenable.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> These four rules are the first thing to check when a design choice looks
|
||||||
|
> surprising: it is very likely a direct consequence of one of them, not an
|
||||||
|
> arbitrary decision.
|
||||||
44
repository-layout/index.md
Normal file
44
repository-layout/index.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
id: repository-layout
|
||||||
|
title: Repository layout
|
||||||
|
description: What lives where in the monorepo.
|
||||||
|
tags: [architecture, reference]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Repository layout
|
||||||
|
|
||||||
|
A pnpm workspace with three applications and four packages, plus
|
||||||
|
deployment and operational tooling around them.
|
||||||
|
|
||||||
|
## `apps/`
|
||||||
|
|
||||||
|
| Directory | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `apps/api` | Fastify server: route registration in `src/app.ts`, routes under `src/routes/`, the indexer under `src/indexer/`, draft/workflow logic under `src/drafts/`, auth under `src/auth/`. See [[api]]. |
|
||||||
|
| `apps/web` | Next.js App Router UI. Proxy configuration in `next.config.ts`, styles in `app/styles/*.css`, i18n messages in `lib/i18n/messages/`. See [[web-frontend]]. |
|
||||||
|
| `apps/mcp` | The stateless MCP↔HTTP translator. Tools live under `src/tools/` (`read.ts`, `write.ts`, `attachments.ts`), diagram generation under `src/diagram/`. |
|
||||||
|
|
||||||
|
## `packages/`
|
||||||
|
|
||||||
|
| Directory | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `packages/markdown` | Parsing, rendering and stringifying Markdown (`parse.ts`, `render.ts`, `stringify.ts`), plus the shared constructs: `alerts.ts` (callouts), `frontmatter.ts`, `youtube.ts`, `image-size.ts`, `slug.ts`, `diff.ts`. |
|
||||||
|
| `packages/editor` | The ProseMirror schema: `extensions.ts`, `nodes/` (`alert.ts`, `image.ts`, `wiki-link.ts`, `youtube-embed.ts`), and the two directions of conversion, `from-markdown.ts` / `to-markdown.ts`. |
|
||||||
|
| `packages/design-tokens` | `tokens.ts` (generated values) and `catalog.ts` (one entry per token: level, settings-page group, role, whether a user theme may override it). |
|
||||||
|
| `packages/git-provider` | The `GitProvider` interface (`types.ts`) and its two implementations, `forgejo.ts` and `github.ts`. |
|
||||||
|
|
||||||
|
## Everything else
|
||||||
|
|
||||||
|
| Directory | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `deploy/git` | Forgejo stack — must be started before `deploy/wiki`. |
|
||||||
|
| `deploy/wiki` | The wiki stack (web, api, drawio, mcp, Postgres). |
|
||||||
|
| `docs/` | Domain vocabulary, architecture decisions, agent-facing docs, design specs. |
|
||||||
|
| `scripts/` | Local setup (`dev-local-setup.sh`), deployment E2E, CSS metrics, seed content for the local demo space. |
|
||||||
|
| `.github/workflows/ci.yml` | The CI pipeline — see [[testing-and-contributing]]. |
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> When you're not sure which package owns a piece of behavior, start from
|
||||||
|
> [[architecture]]'s table of roles rather than guessing from file names —
|
||||||
|
> the boundaries are drawn by responsibility, not by technology.
|
||||||
64
testing-and-contributing/index.md
Normal file
64
testing-and-contributing/index.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
id: testing-and-contributing
|
||||||
|
title: Testing and contributing
|
||||||
|
description: Targeted tests over full runs, what CI checks, and the license your contribution falls under.
|
||||||
|
tags: [testing, contributing, license]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing and contributing
|
||||||
|
|
||||||
|
## Run targeted checks, not the full suite
|
||||||
|
|
||||||
|
For a given change, run type-checking across the workspace and only the
|
||||||
|
test files of the modules you touched:
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm -w typecheck
|
||||||
|
pnpm --filter @f451/web test -- lib/urls.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Not the full `pnpm -w test` for every change — its output is the actual
|
||||||
|
cost driver on a change of any size, and it re-proves things your change
|
||||||
|
didn't touch. Full-suite runs and browser/E2E verification are what CI and
|
||||||
|
review are for, not every local iteration.
|
||||||
|
|
||||||
|
## What CI checks
|
||||||
|
|
||||||
|
`.github/workflows/ci.yml` runs on every push to `main`/`dev` and on every
|
||||||
|
pull request:
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
Two further jobs exist but only run on manual dispatch, because they are
|
||||||
|
expensive: a GitHub live-contract test against `packages/git-provider`
|
||||||
|
(needs real credentials), and a Playwright end-to-end read-flow test
|
||||||
|
(needs a browser install and container startup).
|
||||||
|
|
||||||
|
## Before opening a pull request
|
||||||
|
|
||||||
|
- Type-check, and run the tests for the files you changed (above).
|
||||||
|
- Keep the change to what the task asked for; unrelated cleanup belongs in
|
||||||
|
its own pull request.
|
||||||
|
- New code, comments, commit messages and documentation are written in
|
||||||
|
English. f451's UI itself stays bilingual — see [[web-frontend]] — but
|
||||||
|
written material *about* the project is English going forward.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
f451 is source-available, not Open Source in the OSI sense: noncommercial
|
||||||
|
use, modification and redistribution are free, with attribution to rote
|
||||||
|
code fraktion and a link to the project, both in the code and in the
|
||||||
|
running application's interface. Commercial use requires a separate
|
||||||
|
license on request. Full terms, including named exclusions, are in
|
||||||
|
`LICENSE.md` at the repository root — read it before assuming a use case
|
||||||
|
is covered.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Contribution guidelines beyond this page live in `CONTRIBUTING.md` at the
|
||||||
|
> repository root.
|
||||||
59
web-frontend/index.md
Normal file
59
web-frontend/index.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
id: web-frontend
|
||||||
|
title: The web frontend
|
||||||
|
description: Routing, the proxy, internationalization, and styling with design tokens.
|
||||||
|
tags: [frontend, architecture]
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# The web frontend
|
||||||
|
|
||||||
|
`apps/web` is a Next.js App Router application. Server Components are the
|
||||||
|
default; client islands are kept as small as the interaction requires
|
||||||
|
(theme toggle, search dialog, tree expansion are the reference examples).
|
||||||
|
|
||||||
|
## The proxy
|
||||||
|
|
||||||
|
`apps/web` is not a client of the API in the usual sense — it *proxies*
|
||||||
|
several path prefixes straight through, configured in `next.config.ts`:
|
||||||
|
|
||||||
|
| Prefix | Destination |
|
||||||
|
|---|---|
|
||||||
|
| `/api/:path*` | `apps/api` |
|
||||||
|
| `/auth/:path*` | `apps/api` |
|
||||||
|
| `/admin/:path*` | `apps/api` |
|
||||||
|
| `/media/:path*` | `apps/api` |
|
||||||
|
| `/drawio/:path*` | the draw.io container |
|
||||||
|
| `/mcp` | the MCP service |
|
||||||
|
|
||||||
|
Because these are rewrites, not client-side fetches, requests to them carry
|
||||||
|
the API's own response headers (its `onSend` security hook, or the media
|
||||||
|
route's sandboxed CSP) rather than anything `apps/web` adds — `headers()`
|
||||||
|
in `next.config.ts` only applies to responses Next.js generates itself.
|
||||||
|
|
||||||
|
## Internationalization
|
||||||
|
|
||||||
|
The interface is bilingual (German/English). UI strings live under
|
||||||
|
`apps/web/lib/i18n/messages/{de,en}/`. This is separate from page content
|
||||||
|
language: a page's own `lang` frontmatter field (as in this space, `en`)
|
||||||
|
describes the content, not the chrome around it.
|
||||||
|
|
||||||
|
## Styling and design tokens
|
||||||
|
|
||||||
|
`apps/web/app/globals.css` contains no rules of its own — only the list of
|
||||||
|
imports from `app/styles/*.css`. **That import order is the cascade**; the
|
||||||
|
numeric prefixes in the filenames are not what determines it.
|
||||||
|
|
||||||
|
The token values themselves are generated, not hand-maintained: `sync-tokens`
|
||||||
|
runs automatically as `predev`/`prebuild` and produces the CSS variables
|
||||||
|
from `packages/design-tokens`. See [[markdown-and-editor]] for the
|
||||||
|
package's counterpart on the content side, and [[extending]] for the steps
|
||||||
|
to add a token.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `packages/design-tokens/src/catalog.ts` separates *values* (`tokens.ts`)
|
||||||
|
> from *description* (`catalog.ts`: which settings-page group a token
|
||||||
|
> belongs to, its role in plain language, and whether a user theme is
|
||||||
|
> allowed to override it). Tests pin the count of tokens per level and
|
||||||
|
> group, so adding one means updating those counts deliberately, not
|
||||||
|
> incidentally.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue