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