pike/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files draw
1
# Working on this app
2
3
You are one of the crew on this repository. The forge runs the app for you:
4
production follows the default branch, and every pull you open gets a
5
preview at its own address, deployed from your branch as you push.
6
7
## The rules
8
9
- Work on the branch you were given. You cannot move the default branch;
10
  a person merges your pull once it is reviewed.
11
- Keep state under `FORGE_APP_DATA`. Never write elsewhere at runtime.
12
- Run `bun test` before you push. `forge.json`'s `app.test` runs it beside
13
  the app on every deploy, not before it: the preview comes up the moment
14
  the app answers and says `live`, or `serving · tests failed` the moment
15
  the suite disagrees. Every behaviour you add gets a test that would fail
16
  without it.
17
- `src/tty.ts` is the app's terminal. The exact command that reaches it is
18
  `FORGE_APP_SSH` at runtime; print that verbatim wherever the app tells
19
  people how to connect. What a person can do on the page they can do here,
20
  and nothing more: the terminal gates its reads and writes exactly as the
21
  page does (`FORGE_APP_USER_ROLE` is the forge's word on who is there), and a
22
  write made there reaches web viewers live like any other.
23
- Keep it dependency-free unless a dependency is clearly worth it, and say
24
  why in the pull. A dependency is code you now maintain.
25
- When you are done, push and stop. Your last message becomes the pull's
26
  description: say what changed, what you chose not to do, and what to look
27
  at in the preview.
28
29
## The shape
30
31
One implementation, many doors. Every operation is a function in
32
`src/actions.ts` — `(db, who, input)` in, data or a named failure out — and
33
the page (`src/routes.ts`, `src/page.ts`), the JSON API (`src/routes.ts`),
34
the MCP server (`src/mcp.ts`) and the terminal (`src/tty.ts`) call it. A
35
door reads its wire format and shapes the answer; it decides nothing. A
36
rule in an action holds at every door at once; a rule anywhere else is a
37
bug waiting for the door you forgot.
38
39
Identity comes from the forge (`src/who.ts`): `anon`, `user` or `admin`,
40
verified by signature, never by a cookie the app invents.
41
42
Every request passes the door (`src/security.ts`) before a route sees it:
43
writes are rationed, a write from another site is refused, and a request
44
under a resource's `api` path is refused — before its body is read — when
45
the table lets that person through that door for no row at all. The door
46
asks the same table the actions do; it never compares roles by hand, and
47
the action still decides about the real row. Do not route around it.
48
49
Every change is written down. An action that creates, updates or deletes
50
returns through `audited` (`src/actions.ts`), which appends a row to the
51
`audit` table — who, the resource and verb, the row, when, and whether
52
they were let — refused attempts included; the door records the writes it
53
turns away. Admins read it at `GET /api/audit` (the `audit` resource in
54
`contract.json`). Recording never fails a request: a row that cannot be
55
written is a log line.
56
57
Who may do what is a table, not code: `contract.json`. It declares the
58
roles — each names the roles below it, and how it is held: by everyone, by
59
the identity the forge signed, or by a row's column naming the person — and,
60
for every resource, the least role each of create, read, update, delete and
61
list needs. An action asks `permit(who, resource, verb, row)`
62
(`src/contract.ts`) and obeys the answer; it never compares `who.role`
63
itself. `src/can.ts` is the forge's evaluator, carried verbatim: do not
64
edit it. `src/contract.test.ts` acts as every identity at every door and
65
fails when a door and the table disagree.
66
67
A new kind of person is a new role in the table, under an existing one. A
68
change to a cell is a change of policy: say so in the pull's description.
69
A cell loosened — anything an identity could not do before that it can
70
after — is a wall: the forge shows the pull to a person and never lands it
71
on its own. Do not loosen a cell unless the issue asks for exactly that;
72
prefer a new role under an existing one, or a new resource, to widening a
73
door that was decided.
74
75
To add a concept:
76
77
- **An action** — a function in `src/actions.ts` that asks `permit`, its
78
  resource's row in `contract.json`, its door in `src/contract.test.ts`,
79
  and a test in `src/actions.test.ts` for what the table cannot say.
80
- **A migration** — append to `migrations` in `src/db.ts`. Never edit one
81
  that has run. `bun run src/db.ts --check` shows the version.
82
- **A page** — a row in `src/routes.ts` and a function in `src/page.ts`.
83
  With a second page, move them under `src/pages/` and keep one `layout`.
84
- **An API route** — a row in `src/routes.ts` that calls the action and
85
  answers with `answer(...)`; the status follows the failure. Give it an
86
  `api` entry beside it (a sentence, the body and answer as JSON Schema)
87
  and run `bun run contract:emit`: `contract/openapi.json` is committed,
88
  served at `/openapi.json`, and `bun test` fails while it is stale. A
89
  resource's routes live under the `api` path its `contract.json` entry
90
  names, so the door and the forge's probe find them.
91
- **A tool** — an entry in `tools` in `src/mcp.ts`: a name, one sentence,
92
  a JSON schema, the action.
93
- **A terminal command** — a key in `src/tty.ts` that calls the action and
94
  reports on the status line.
95
- **Something live** — `emit(type, data)` in the action, and
96
  `data-live="<types>"` on the region of the page that shows it.
97
- **A colour, a space, a size** — a token in `design/tokens.json` (a
98
  colour needs its dark value in `$extensions.mode.dark`), used as
99
  `var(--…)`. `bun run lint:design` refuses a raw `#hex`, `rgb(` or
100
  `hsl(` anywhere in `src/` or `static/`, and a `var(--…)` that names no
101
  token; `src/design.test.ts` runs it, and holds every pair of colours a
102
  person reads to 4.5:1 in both themes.
103
104
## The bar
105
106
- Security: every input is validated at the boundary and every output is
107
  escaped; every response carries the floor in `src/http.ts` (keep it,
108
  extend it); a POST that is not same-origin is refused; writes are
109
  rationed per person; secrets come from the environment (Settings → App →
110
  Environment), never the repository; no `eval`, no shell built from user
111
  text. A request body is capped by the bytes actually read (`readBody`),
112
  never by a Content-Length. `HEAD` answers like `GET`. A private thing
113
  needs an identity from `who`, and a cookie the app sets itself is
114
  `HttpOnly; SameSite=Lax`.
115
- Speed: the first paint is server-rendered HTML; JavaScript is small, in
116
  `static/`, linked by content (`asset()`) and cached for a year; no
117
  layout shift; what changes arrives over `/events`, never by polling;
118
  work that can wait is done after the response.
119
- A phone is a first-class client: the page works at 360px wide with no
120
  horizontal scroll, every control is at least 44px on a side
121
  (`--size-tap`), an input's text is never below 16px (a phone zooms into
122
  a smaller one), focus is always visible, whoever asked for less motion
123
  gets none, and the keyboard reaches everything a mouse does.
124
- Beauty is the tokens': light and dark both, from one file. A new page
125
  uses `layout`, which carries the tokens, the description and the Open
126
  Graph and card metadata a shared link unfurls with — pass the page's
127
  path so `og:url` is its own.
128
- Finish: the app boots, `/healthz` answers `{ ok, version, uptime }`
129
  (the forge polls it; keep it cheap and apart from the page), `/`
130
  answers, `bun test` is green, the README says how to use it and what is
131
  real versus simulated, `forge.json` says how to run it.
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts