@panaversity/ksor 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,15 +3,40 @@
3
3
  A **Knowledge System of Record**: the governed source of knowledge this
4
4
  project's people and AI agents operate from.
5
5
 
6
- Two worlds live here:
6
+ Three things live here, and they are not the same kind of thing:
7
7
 
8
8
  - **`knowledge/` — the record.** Plain governed markdown (plus the optional
9
9
  study attachments a document may carry). Yours forever, readable anywhere,
10
10
  portable without this repository's code.
11
- - **`system/` — the system.** The site (and later, services) that serve the
11
+ - **`system/` — the system.** The site, and later the services, that serve the
12
12
  record. Replaceable machinery.
13
-
14
- ## Working here
13
+ - **`.ksor/governance.yaml` — the authority.** Which audiences exist, who may
14
+ approve a document, who may take one down. Every governance act is checked
15
+ against it, so the record never claims authority nobody granted.
16
+
17
+ The point of all this is the **agent interface**: an MCP door that answers from
18
+ the record with citations, and declines — in as many words — when the record
19
+ does not cover the question. That refusal is the feature; an agent that
20
+ improvises over a gap is the thing a system of record exists to prevent.
21
+ Beside it, the **human interface** is a site your readers browse. Same record,
22
+ same governance, different machinery: the site is files, the door is a process,
23
+ and they deploy separately.
24
+
25
+ **Contents**
26
+
27
+ - [Quick start](#quick-start)
28
+ - [Explore the human interface](#explore-the-human-interface)
29
+ - [Serving to agents](#serving-to-agents)
30
+ - [Quick deployment](#quick-deployment)
31
+ - [Make the record yours: replace the starters](#make-the-record-yours-replace-the-starters)
32
+ - [Writing knowledge](#writing-knowledge)
33
+ - [Deploying](#deploying)
34
+ - [Reference](#reference) — [commands](#commands) · [the files](#the-files-explained) · [when something refuses you](#when-something-refuses-you) · [dependencies](#dependencies-and-advisories)
35
+ - [Ownership](#ownership)
36
+
37
+ ---
38
+
39
+ ## Quick start
15
40
 
16
41
  ```sh
17
42
  pnpm install
@@ -19,155 +44,181 @@ pnpm dev # browse the knowledge at http://localhost:3000
19
44
  ```
20
45
 
21
46
  <!-- ksor:pm pnpm -->
47
+
22
48
  No pnpm? Run `npm install -g pnpm` — or `corepack enable pnpm` on Node
23
49
  versions that bundle corepack.
50
+
24
51
  <!-- /ksor:pm -->
25
- The first `pnpm install` also fetches the
26
- `ksor` tool (pinned in `package.json`) and writes it into your lockfile —
27
- commit the updated lockfile.
28
-
29
- **The five starter documents publish on the first build.** They ship
30
- `status: stable`, so `pnpm dev` and `pnpm build` both give you a working
31
- record straight away — pages, a sidebar, a `/llms.txt` an agent can read —
32
- instead of an empty shelf. They are approved by `ksor-starter/KSOR-STAMP-VERSION`:
33
- the tool that wrote them, named as a producer rather than as a person, because
34
- no person reviewed a word of it. That is what the trust tier _unverified_ on
35
- every one of those pages says, and it is true.
36
-
37
- **So your first act here is replacing them.** They describe KSoR, not your
38
- organisation, and a record that describes the wrong thing describes it on every
39
- surface. Delete each one as your own knowledge arrives — and when the last is
40
- gone, delete `ksor-starter/KSOR-STAMP-VERSION` from `approval_authorities` in
41
- `.ksor/governance.yaml` too. Nothing of yours should be approved by a tool. Ask
42
- your coding agent to run the intake interview: it replaces the `human:you`
43
- placeholder in that file with your real handle and writes `instance.md` with
44
- you.
45
-
46
- **What you write starts unpublished.** A new document is `status: draft`, and
47
- `pnpm build` admits a draft to no surface at all: no page, no sidebar row, no
48
- `/llms.txt` entry, nothing for an agent to read. `pnpm dev` shows it, marked —
49
- the preview is where drafts live.
50
-
51
- Publishing one adds two keys beside `status: stable` — what produced the text,
52
- and who approved it. Both, or `pnpm check` refuses the document:
53
52
 
54
- ```yaml
55
- status: stable
56
- generated: { by: "human:you", at: 2026-01-31T09:00:00Z }
57
- ksor:
58
- audience: [public] # already there — every document carries it, drafts too
59
- approval: { by: "human:you", at: 2026-01-31T09:00:00Z }
60
- ```
53
+ **Commit the lockfile your first install writes.**
61
54
 
62
- `generated` is provenance: it names whatever produced the text — a person, or
63
- the agent that drafted it — and nothing has to authorise it. `approval.by` is
64
- authority, so it must name an actor `.ksor/governance.yaml` lists, and its `at`
65
- may not be earlier than `generated.at` — the text that was approved has to be
66
- the text that was written. That act is yours, so the record never claims
67
- authority nobody granted.
55
+ <!-- ksor:pm pnpm -->
68
56
 
69
- ### Presenting a document
57
+ The committed `pnpm-lock.yaml` covers the site but not `@panaversity/ksor`
58
+ itself: the version pinned in `package.json` was stamped by the CLI that
59
+ scaffolded this project and could not be resolved before that happened. Your
60
+ first `pnpm install` writes it in. Commit the result before you push — pnpm
61
+ turns on `--frozen-lockfile` automatically whenever `CI` is set, so an
62
+ uncommitted pin fails your CI rather than warning you. (`vercel.json` already
63
+ installs with `--no-frozen-lockfile`, and the shipped `validate.yml` runs no
64
+ install.)
70
65
 
71
- Ask your coding agent for slides and it writes them, from the document, into
72
- the record:
66
+ <!-- /ksor:pm -->
67
+ <!-- ksor:pm npm -->
73
68
 
74
- ```
75
- make slides for knowledge/expenses/approvals.md
76
- ```
69
+ No lockfile ships with this scaffold: npm keeps ONE lock for the whole
70
+ workspace, and the `@panaversity/ksor` version pinned in `package.json` was
71
+ stamped by the CLI that scaffolded this project — it could not be resolved into
72
+ a lock before it existed. Your FIRST `npm install` writes `package-lock.json`;
73
+ run it before you push, and COMMIT the result — that lock is why two machines
74
+ build the same site.
77
75
 
78
- The `make-slides` skill reads the document whole, writes the deck into
79
- `knowledge/expenses/approvals.slides.yaml`, checks every claim and every
80
- number back against the document, and tells you what it left out because the
81
- document did not support it which is usually how you find out a document has
82
- a gap. The deck then renders on that document's page, straight after its
83
- introduction: click through it inline, or **Present** for fullscreen.
84
- Presenter notes stay off the screen.
85
-
86
- The slides live in the record, so they are reviewed in the same pull request
87
- as the document, versioned with it, and withdrawn when it is withdrawn. There
88
- is no third party and no link to rot. If you already keep a deck in Google
89
- Slides, Canva or SlideShare you can point at it instead — `slides.url:` rather
90
- than `deck:` — and the page will offer it as a link with a frame the reader
91
- loads on click, so nothing is requested from the host until somebody asks.
76
+ One honest difference from the pnpm scaffold: pnpm quarantines newly published
77
+ dependency versions for 48 hours (`minimumReleaseAge`), so a routine install
78
+ never picks up a day-zero compromised release. npm has no equivalent the
79
+ `.npmrc` here carries the install-script denial half of that posture, and this
80
+ sentence is the disclosure of the half it cannot.
92
81
 
93
- ### Summarising a document
82
+ <!-- /ksor:pm -->
83
+ <!-- ksor:pm bun -->
94
84
 
95
- Long documents get a **Summary** tab beside their own words, and your agent
96
- writes it the same way:
85
+ No lockfile ships with this scaffold: the `@panaversity/ksor` version pinned in
86
+ `package.json` was stamped by the CLI that scaffolded this project — it could
87
+ not be resolved into a lock before it existed. Your FIRST `bun install` writes
88
+ `bun.lock`; run it before you push, and COMMIT the result — that lock is why two
89
+ machines build the same site.
97
90
 
98
- ```
99
- summarise knowledge/expenses/approvals.md
91
+ One honest difference from the pnpm scaffold: pnpm quarantines newly published
92
+ dependency versions for 48 hours (`minimumReleaseAge`), so a routine install
93
+ never picks up a day-zero compromised release. bun has no equivalent — its
94
+ default refusal of dependency install scripts covers the OTHER half of that
95
+ posture, and this sentence is the disclosure.
96
+
97
+ <!-- /ksor:pm -->
98
+
99
+ ---
100
+
101
+ ## Explore the human interface
102
+
103
+ Before changing anything, get a feel for how the record behaves. With
104
+ `pnpm dev` running:
105
+
106
+ - **Edit a starter document.** Change its body, save, watch the page update.
107
+ - **Add a new document.** It appears on the dev site, marked — and it would be
108
+ on no built surface at all, because a new document is `status: draft`. No
109
+ page, no sidebar row, no `/llms.txt` entry. That is the system working, not a
110
+ broken build.
111
+ - **Ask your coding agent to do the same.** `AGENTS.md` carries the working
112
+ rules; read it before you change how documents are written here. The kit in
113
+ `.agents/skills/` already knows this project: `intake-interview` (define the
114
+ record with you), `add-sources` (turn source material into governed
115
+ documents), `make-slides`, `make-summary`, and `format-checker` (the rules,
116
+ as a program — also what `pnpm check` runs).
117
+
118
+ **Treat the starters as scratch paper.** They ship approved by
119
+ `ksor-starter/KSOR-STAMP-VERSION` — a tool, not a person. Edit the body and that
120
+ approval stays stamped on text nobody reviewed, and `pnpm check` will not catch
121
+ it, because the frontmatter is still internally consistent. So explore in them
122
+ freely, but start nothing you intend to keep in one of them. Replacing them
123
+ properly is [below](#make-the-record-yours-replace-the-starters).
124
+
125
+ To see what a build actually produces:
126
+
127
+ ```sh
128
+ pnpm build # the static site, into system/site/out/
129
+ pnpm preview # serve exactly those bytes
100
130
  ```
101
131
 
102
- The `make-summary` skill reads the document whole, writes
103
- `knowledge/expenses/approvals.summary.md`, and checks every line back against
104
- the documentevery number, every rule, and every `##` section, because a
105
- summary that covers the opening and trails off is worse than none: a reader who
106
- used it believes they have the whole document. It reports what it left out
107
- because the document did not support it.
132
+ There is no `start` script, and that is not an omission: the site is a static
133
+ export, so nothing serves it at runtime. `pnpm preview` is `node:http` and
134
+ nothing elseno dependency, no network fetch so it works offline and behind
135
+ a firewall, like the build itself.
108
136
 
109
- The summary is part of its document, not a document of its own: no route, no
110
- sidebar row, no line in `llms.txt`, and it takes its governance from its
111
- parent. Ask for one only where there is something to compress — under about two
112
- screens, a summary that restates the page teaches readers the tab is not worth
113
- opening, and the skill will say so rather than write one.
137
+ ---
114
138
 
115
- ### Serving to agents
139
+ ## Serving to agents
116
140
 
117
- The record's other surface is an MCP server for AI agents — the same
118
- knowledge, cited, with honest abstention. It is the climbed rung: it needs a
119
- Postgres store (with pgvector) and an embedding provider key, so it is not
120
- part of `pnpm dev`. Three steps, and the order is load-bearing: the command
121
- block is last because it needs both of the things above it. Skip ahead to it
122
- and `pnpm provision` refuses, naming the config step 1 writes.
141
+ **This is the point of the whole thing.** The record's other surface is an MCP
142
+ server: the same knowledge, cited, with a measured floor under which it
143
+ declines. An agent connected to it answers from your governed documents or says
144
+ the record does not cover the question — and never quietly fills the gap from
145
+ its training.
123
146
 
124
- **1. Uncomment the `database:` block already in `instance.md`.** It names the
125
- VARIABLE holding your DSN, never the DSN itself:
147
+ It asks for two things `pnpm dev` does not: a Postgres store with pgvector, and
148
+ an embedding provider key. Three steps, and the order is load-bearing — the
149
+ command block is last because it needs both of the things above it.
150
+
151
+ ### 1. Nothing to configure
152
+
153
+ `instance.md` already names the VARIABLE holding your DSN — never the DSN
154
+ itself:
126
155
 
127
156
  ```yaml
128
157
  database:
129
158
  dsn_env: KSOR_DB_URL
130
159
  ```
131
160
 
132
- Nothing below works until it is there `pnpm provision` refuses with
133
- `instance.md declares no database: block`. That is also the whole required
134
- config: `embedding:` already defaults to Gemini at 1536 dimensions, and leaving
135
- `retrieval:` out starts you with the abstention gate off and honest about it
136
- (turn it on afterwards with `ksor calibrate`, once the record is serving).
161
+ That is the whole required config. `embedding:` defaults to Gemini at 1536
162
+ dimensions, and `retrieval:` is written for you by step 3's `calibrate`. Change
163
+ the variable name here only if you want a different one.
137
164
 
138
- **2. Copy the environment file**, then fill in `KSOR_DB_URL`, `GEMINI_API_KEY`
139
- and `KSOR_AUTH=disabled-local`:
165
+ ### 2. Fill in the environment
140
166
 
141
167
  ```sh
142
168
  cp .env.example .env
143
169
  ```
144
170
 
171
+ Then set `KSOR_DB_URL`, `GEMINI_API_KEY` and `KSOR_AUTH=disabled-local`.
172
+
173
+ **Point `KSOR_DB_URL` at a hosted Postgres now if you intend to deploy** — the
174
+ same one your host will use. [Neon](https://neon.com) is what this project's own
175
+ docs are measured against; pgvector is on every plan including the free one, and
176
+ you enable it once per database with `CREATE EXTENSION IF NOT EXISTS vector;`.
177
+ Anything with pgvector works. [Quick deployment](#quick-deployment) explains why
178
+ one shared database saves you a step.
179
+
145
180
  `ksor` reads `.env` automatically — there is nothing to export, and where a
146
- refusal tells you to _export_ that variable, putting it in `.env` is the same
181
+ refusal tells you to _export_ a variable, putting it in `.env` is the same
147
182
  thing. `KSOR_AUTH=disabled-local` is required for a local run: serve refuses to
148
183
  boot unauthenticated on purpose, so a server is never open by accident.
149
184
 
150
- **3. Bring it up.**
185
+ ### 3. Bring it up
151
186
 
152
187
  ```sh
153
188
  pnpm provision # once: apply the schema, authorize ingest
154
189
  pnpm refresh # build, ingest the record, collect retired generations
155
- pnpm serve # the MCP server
190
+ pnpm exec ksor calibrate --instance instance.md # measure the floor; paste the printed block into instance.md
191
+ pnpm serve # the MCP server, with the gate already on
156
192
  ```
157
193
 
158
194
  `pnpm provision` runs once — it applies the schema (or migrates it forward) and
159
- authorizes ingest, the two privileged acts that should not happen on every
160
- boot. After that: `pnpm refresh` publishes what you have edited, and `pnpm serve`
161
- runs the server. They are separate because publishing is an act, not a side
162
- effect of starting a process. A rerun on an unchanged record
163
- costs nothing: no new generation, no embedding, no rows. Edit a document and
164
- the next run picks up exactly that change. `AGENTS.md` "Serving to agents" is the
165
- full runbook; your coding agent reads it first. A public bind needs a
166
- configured SSO door rather than `disabled-local`see step 2 and
167
- "The agent surface deploys separately" below. Any other operation is
195
+ authorizes ingest, the two privileged acts that should not happen on every boot.
196
+ After that, `pnpm refresh` publishes what you have edited and `pnpm serve` runs
197
+ the server. They are separate because **publishing is an act, not a side effect
198
+ of starting a process.**
199
+
200
+ **`calibrate` is what makes "not in this corpus" a real answer**, and it belongs
201
+ here rather than later: it needs an ingested corpus but no running server, and
202
+ the floor is read when the door boots so measuring before `pnpm serve` means
203
+ the door comes up gated the first time. It prints a `retrieval:` block for THIS
204
+ corpus in THIS embedding space; paste it into `instance.md` exactly as printed.
205
+ Never copy a floor from another corpus.
206
+
207
+ It also prints its own caveat, and it is worth reading: the probes it writes are
208
+ derived from your passages, so they share vocabulary a real question will not.
209
+ The floor it reports is an upper bound on separation until you check it against
210
+ questions the corpus did not write (`--queries-file`).
211
+
212
+ A rerun on an unchanged record costs nothing: no new generation, no embedding,
213
+ no rows. Edit a document and the next run picks up exactly that change.
214
+
215
+ `AGENTS.md` → "Serving to agents" is the full runbook; your coding agent reads
216
+ it first. A public bind needs a configured SSO door rather than
217
+ `disabled-local` — see [The agent surface deploys
218
+ separately](#the-agent-surface-deploys-separately). Any other operation is
168
219
  `pnpm exec ksor <verb>`.
169
220
 
170
- ### Test the agent surface with an actual agent
221
+ ### Test the door with an actual agent
171
222
 
172
223
  The MCP door is meant to be read by agents, so check it with one rather than
173
224
  with `curl`. With `pnpm serve` running, write `.mcp.json` at the repo root:
@@ -183,6 +234,12 @@ with `curl`. With `pnpm serve` running, write `.mcp.json` at the repo root:
183
234
  }
184
235
  ```
185
236
 
237
+ **If you skipped `calibrate`, expect answers where this test wants refusals** —
238
+ the gate is off until a floor is measured, which the server says at boot
239
+ (`abstain OFF`) and in every search envelope (`gate: "off"`). That is honest,
240
+ not broken: a floor nobody measured would be a number pretending to be a
241
+ guarantee.
242
+
186
243
  Open a new session of your coding agent, confirm it lists the server, then ask
187
244
  it three questions **in this order** — the order is the test:
188
245
 
@@ -191,198 +248,215 @@ it three questions **in this order** — the order is the test:
191
248
  arrive with a citation.
192
249
  2. Something **adjacent but not covered** — your record's own subject area, a
193
250
  question it genuinely does not answer. It should decline.
194
- 3. Something far outside the record. It should decline, and must not answer
195
- from its own knowledge.
251
+ 3. Something far outside the record. It should decline, and must not answer from
252
+ its own knowledge.
196
253
 
197
254
  Question 2 is the one that matters. Anything can answer questions it has the
198
255
  text for; refusing a plausible near-miss is the property that makes a system of
199
256
  record worth trusting, and it is the one that breaks quietly.
200
257
 
201
- **On a fresh record, 2 and 3 will not refuse and that is honest, not broken.**
202
- The abstention gate is off until you measure a floor for this corpus, which the
203
- server says out loud at boot (`abstain OFF`) and in every search envelope
204
- (`gate: "off"`). Run `pnpm exec ksor calibrate --instance instance.md` first if
205
- you want to test refusal. Delete `.mcp.json`, or keep it — it holds no secret.
258
+ Delete `.mcp.json`, or keep itit holds no secret.
206
259
 
207
- Then talk to your coding agent — `AGENTS.md` carries the working rules, and
208
- the agent kit in `.agents/skills/` knows how to interview you
209
- (`intake-interview`), convert your source material (`add-sources`), and keep
210
- the record well-formed (`format-checker`, also `pnpm check`).
260
+ ---
211
261
 
212
- ### A note on the lockfile
262
+ ## Quick deployment
213
263
 
214
- <!-- ksor:pm pnpm -->
215
- The committed `pnpm-lock.yaml` covers the site. It cannot cover
216
- `@panaversity/ksor` itself, because the version pinned in `package.json` is
217
- stamped by the CLI that scaffolded this project and could not be resolved before
218
- that happened. So your FIRST `pnpm install` writes it — run it before you push,
219
- and commit the result.
220
-
221
- The deploy config already accounts for this (`vercel.json` installs with
222
- `--no-frozen-lockfile`), and the shipped `validate.yml` runs no install. If you
223
- add CI of your own, note that pnpm turns on `--frozen-lockfile` automatically
224
- whenever `CI` is set.
225
- <!-- /ksor:pm -->
226
- <!-- ksor:pm npm -->
227
- No lockfile ships with this scaffold: npm keeps ONE lock for the whole
228
- workspace, and the `@panaversity/ksor` version pinned in `package.json` was
229
- stamped by the CLI that scaffolded this project — it could not be resolved
230
- into a lock before it existed. Your FIRST `npm install` writes
231
- `package-lock.json`; run it before you push, and COMMIT the result — that
232
- lock is why two machines build the same site.
233
-
234
- One honest difference from the pnpm scaffold: pnpm quarantines newly
235
- published dependency versions for 48 hours (`minimumReleaseAge`), so a
236
- routine install never picks up a day-zero compromised release. npm has no
237
- equivalent — `.npmrc` here carries the install-script denial half of that
238
- posture, and this sentence is the disclosure of the half it cannot.
239
- <!-- /ksor:pm -->
240
- <!-- ksor:pm bun -->
241
- No lockfile ships with this scaffold: the `@panaversity/ksor` version pinned
242
- in `package.json` was stamped by the CLI that scaffolded this project — it
243
- could not be resolved into a lock before it existed. Your FIRST
244
- `bun install` writes `bun.lock`; run it before you push, and COMMIT the
245
- result — that lock is why two machines build the same site.
246
-
247
- One honest difference from the pnpm scaffold: pnpm quarantines newly
248
- published dependency versions for 48 hours (`minimumReleaseAge`), so a
249
- routine install never picks up a day-zero compromised release. bun has no
250
- equivalent (its default refusal of dependency install scripts covers the
251
- OTHER half of that posture), and this sentence is the disclosure.
252
- <!-- /ksor:pm -->
264
+ Both surfaces on one domain, in about ten minutes:
253
265
 
254
- ### A note on `audit`
266
+ 1. **Push the repository to GitHub.**
267
+ 2. **Import it in Vercel**, then **set Root Directory to `./`.** Vercel
268
+ auto-fills it with `system/site`, because that is where it finds a framework
269
+ — and the build then reads `system/site/vercel.json`, which does not exist,
270
+ and fails with `Project framework is set to "services", but no services are
271
+ declared`. The services ARE declared, in `vercel.json` at the repo root,
272
+ which is the only place they can be: one builds the site, the other builds a
273
+ container from the root `Dockerfile`.
274
+ 3. **Set three environment variables** in Vercel: `KSOR_DB_URL`,
275
+ `GEMINI_API_KEY`, and `KSOR_AUTH=disabled-public`.
255
276
 
256
- An audit of this scaffold reports vulnerabilities in `next`, and will keep
257
- doing so: a framework that large always has open advisories against whatever
258
- version you have pinned.
259
- <!-- ksor:pm npm -->
260
- `npm install` prints the count at the end of every install, so you meet it
261
- before you have run anything, next to an invitation to run
262
- `npm audit fix --force`.
263
- <!-- /ksor:pm -->
264
- <!-- ksor:pm pnpm -->
265
- pnpm reports it only when you run `pnpm audit`.
266
- <!-- /ksor:pm -->
267
- <!-- ksor:pm bun -->
268
- bun reports it only when you run `bun audit`.
269
- <!-- /ksor:pm -->
277
+ Two things catch people here, and both are the system being deliberate:
270
278
 
271
- **Never let an audit tool raise the pin for you.** It moves off the version
272
- this scaffold was built and tested against, and that pin is the whole reason
273
- two machines produce the same site. Bump it deliberately instead take the
274
- newer pin a newer `ksor init` emits, or raise it yourself and re-run
275
- `pnpm build` and the deploy check above.
276
-
277
- It also reads worse than it is, for one structural reason worth knowing:
278
- **this site is a static export.** `pnpm build` writes HTML, JS and CSS to
279
- `system/site/out/`, and no framework server ever runs in front of your
280
- readersno middleware, no server actions, no rewrites, no image optimizer.
281
- Most framework advisories describe exactly those request paths, so they have
282
- nothing here to reach. Two things that argument does NOT cover, and you should
283
- treat as real: an advisory in the **build** toolchain, which does run, on your
284
- machine and in your CI; and any advisory at all if you later add a served route
285
- and stop exporting. Read what an advisory affects before deciding it is inert —
286
- the static export is a reason, not a blanket.
287
-
288
- ## The files, explained
279
+ - **`disabled-local` will not deploy.** The container sets `$PORT`, so the door
280
+ binds `0.0.0.0` a PUBLIC bind and refuses that value by design, saying so
281
+ in as many words. `disabled-public` is you saying you know the door is
282
+ reachable from outside itself. It belongs in Vercel's environment, not in your
283
+ `.env`, so your local `pnpm serve` keeps its loopback posture. It is a
284
+ starting posture, not a destination: [secure it
285
+ properly](#audiences-decide-what-a-build-contains) with the SSO variables once
286
+ the thing is up.
287
+ - **Point `KSOR_DB_URL` at a Postgres your host can reach.** Deploying does not
288
+ publishthe door serves whatever generation is already in the database. If
289
+ your DSN pointed at a database on your laptop, the site comes up fine and the
290
+ door comes up empty. Use one hosted Postgres for both and the `pnpm refresh`
291
+ you already ran is the generation Vercel serves.
289
292
 
290
- Nothing here is decoration, and the dotfiles are not ceremony each one is a
291
- different coding agent's way of finding the same working contract.
293
+ If you only want readers served, the site is a static export with no database in
294
+ the loop: deploy with no environment variables at all and add the door later.
292
295
 
293
- | Entry | What it is |
294
- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
295
- | `knowledge/` | **the record** — your governed markdown. The product; everything else serves it. |
296
- | `system/` | the code that serves the record: the site today, more as you need it. |
297
- | `instance.md` | what this record is authoritative for; its `name:` is the identity every surface publishes and its `title:` the display title every page leads with (both read at server/build start — restart `pnpm dev` after changing either). Its BODY is the agent surface's system prompt — `ksor serve` wires it into the MCP server's instructions. |
298
- | `.ksor/governance.yaml` | **the root of authority** — which audiences exist, who may approve a document, who may take one down. Committed; every governance act is checked against it. |
299
- | `.ksor/takedowns.yaml` | the takedown ledger: every withdrawal and every lift, append-only and committed, so the site honours a takedown with no database in the loop. It appears at your first `ksor takedown` — an empty ledger would assert an act nobody performed. |
300
- | `build.lock.json` | what the last `ksor build` published — the corpus, the commit, the toolchain — and what every machine surface stamps. Committed; written by `ksor build`, never by hand. |
301
- | `Dockerfile`, `.dockerignore` | how the agent surface reaches a host. The Dockerfile names no host; `vercel.json` points at it rather than replacing it, so moving hosts is a redeploy. |
302
- | `vercel.json` | one domain, two services — the static site and the MCP door — for the host this scaffold answers the setup interview for. Delete it if you deploy elsewhere. |
303
- | `AGENTS.md` | the working contract every coding agent reads first — the rules for writing knowledge here. |
304
- | `CLAUDE.md` | one line, pointing at `AGENTS.md`. Claude Code looks for this filename, not that one. |
305
- | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn source material into governed documents), `make-slides` (generate a presentation from a document and attach it), `make-summary` (write a document's summary and attach it), `format-checker` (the rules, as a program). |
306
- | `.claude/skills/` | byte-identical copies of the kit — Claude Code discovers skills only here. The checker enforces the mirror, so the two cannot drift. |
307
- | `.gemini/settings.json` | points Gemini CLI at `AGENTS.md`; Gemini does not read that filename on its own. |
308
- | `.github/workflows/validate.yml` | your CI: runs the same checker on every pull request and push to main. |
309
- | `.gitattributes` | markdown is checked out byte-stable on every platform, so the same commit hashes the same everywhere. |
310
- | `.env.example` | the variables the served rung needs; copy to `.env` (gitignored) and fill in. |
311
- | `.gitignore` | keeps build output, `node_modules/`, and `.env` out of the record's history — and negates two paths inside `.ksor/`, because the policy and the ledger ARE the record. |
312
- | `package.json` | the surface commands — `pnpm dev` (the site) and `pnpm provision` / `pnpm refresh` / `pnpm serve` (the agent surface: set up once, publish, then serve) — plus `pnpm build` / `pnpm check`, the pinned `@panaversity/ksor` tool and the workspace layout the manifest declares. |
313
- <!-- ksor:pm pnpm -->
314
- | `pnpm-workspace.yaml` | where the workspace looks for code (`system/site`, plus reserved `system/gateways/*` and `system/packages/*`), and the supply-chain policy for installs. |
315
- | `pnpm-lock.yaml` | the exact dependency versions — the reason two machines build the same site. |
316
- <!-- /ksor:pm -->
317
- <!-- ksor:pm npm -->
318
- | `.npmrc` | dependency install scripts are denied; the comment inside discloses the one protection this scaffold lacks (a 48-hour quarantine on new releases). |
319
- | `package-lock.json` | the exact dependency versions — written by your FIRST install; commit it, it is the reason two machines build the same site. |
320
- <!-- /ksor:pm -->
321
- <!-- ksor:pm bun -->
322
- | `bun.lock` | the exact dependency versions — written by your FIRST install; commit it, it is the reason two machines build the same site. |
323
- <!-- /ksor:pm -->
296
+ ---
324
297
 
325
- `format-checker` deliberately contains a program, `check.mjs`, and not only
326
- prose: rules that are only written down cannot refuse anything. `pnpm check`
327
- runs it, and every failure it reports says what is wrong, why the rule exists,
328
- and how to fix it.
298
+ ## Make the record yours: replace the starters
329
299
 
330
- Everything here is yours to change. The kit exists so that any coding agent can
331
- operate this project without being taught it first.
300
+ **Five starter documents publish on the first build.** They ship
301
+ `status: stable`, so `pnpm dev` and `pnpm build` both give you a working record
302
+ straight away — pages, a sidebar, a `/llms.txt` an agent can read — instead of
303
+ an empty shelf. They are approved by `ksor-starter/KSOR-STAMP-VERSION`: the tool
304
+ that wrote them, named as a producer rather than as a person, because no person
305
+ reviewed a word of it. That is what the trust tier _unverified_ on every one of
306
+ those pages says, and it is true.
307
+
308
+ They also describe KSoR, not your organisation — and a record that describes the
309
+ wrong thing describes it on every surface. So replace them, in this order:
310
+
311
+ 1. **Run the intake interview.** Ask your coding agent for it. Three questions —
312
+ what this record is the final word on, what sits just outside it, and who may
313
+ approve or withdraw a document — then it writes `instance.md` with you and
314
+ replaces the `human:you` placeholder in `.ksor/governance.yaml` with your
315
+ real handle.
316
+ 2. **Delete each starter document** as your own knowledge arrives.
317
+ 3. **When the last one is gone, delete `ksor-starter/KSOR-STAMP-VERSION` from
318
+ `approval_authorities` in `.ksor/governance.yaml`.** Nothing of yours should
319
+ be approved by a tool.
320
+
321
+ ---
322
+
323
+ ## Writing knowledge
324
+
325
+ ### A new document publishes nothing
326
+
327
+ A new document is `status: draft`, and `pnpm build` admits a draft to no surface
328
+ at all: no page, no sidebar row, no `/llms.txt` entry, nothing for an agent to
329
+ read. `pnpm dev` shows it, marked — the preview is where drafts live.
330
+
331
+ ### Publishing adds two keys
332
+
333
+ Beside `status: stable`, name what produced the text and who approved it. Both,
334
+ or `pnpm check` refuses the document:
335
+
336
+ ```yaml
337
+ status: stable
338
+ generated: { by: "human:you", at: 2026-01-31T09:00:00Z }
339
+ ksor:
340
+ audience: [public] # already there — every document carries it, drafts too
341
+ approval: { by: "human:you", at: 2026-01-31T09:00:00Z }
342
+ ```
343
+
344
+ `generated` is **provenance**: it names whatever produced the text — a person,
345
+ or the agent that drafted it — and nothing has to authorise it.
346
+
347
+ `approval.by` is **authority**, so it must name an actor `.ksor/governance.yaml`
348
+ lists, and its `at` may not be earlier than `generated.at` — the text that was
349
+ approved has to be the text that was written. That act is yours.
350
+
351
+ Run `pnpm check` before you commit. It runs the rules as a program, and every
352
+ failure it reports says what is wrong, why the rule exists, and how to fix it.
353
+
354
+ ### Presenting a document
355
+
356
+ Ask your coding agent for slides and it writes them, from the document, into the
357
+ record:
358
+
359
+ ```
360
+ make slides for knowledge/expenses/approvals.md
361
+ ```
362
+
363
+ The `make-slides` skill reads the document whole, writes the deck into
364
+ `knowledge/expenses/approvals.slides.yaml`, checks every claim and every number
365
+ back against the document, and tells you what it left out because the document
366
+ did not support it — which is usually how you find out a document has a gap. The
367
+ deck then renders on that document's page, straight after its introduction:
368
+ click through it inline, or **Present** for fullscreen. Presenter notes stay off
369
+ the screen.
370
+
371
+ The slides live in the record, so they are reviewed in the same pull request as
372
+ the document, versioned with it, and withdrawn when it is withdrawn. There is no
373
+ third party and no link to rot. If you already keep a deck in Google Slides,
374
+ Canva or SlideShare you can point at it instead — `slides.url:` rather than
375
+ `deck:` — and the page will offer it as a link with a frame the reader loads on
376
+ click, so nothing is requested from the host until somebody asks.
377
+
378
+ ### Summarising a document
379
+
380
+ Long documents get a **Summary** tab beside their own words, written the same
381
+ way:
382
+
383
+ ```
384
+ summarise knowledge/expenses/approvals.md
385
+ ```
386
+
387
+ The `make-summary` skill reads the document whole, writes
388
+ `knowledge/expenses/approvals.summary.md`, and checks every line back against
389
+ the document — every number, every rule, and every `##` section, because a
390
+ summary that covers the opening and trails off is worse than none: a reader who
391
+ used it believes they have the whole document. It reports what it left out
392
+ because the document did not support it.
393
+
394
+ The summary is part of its document, not a document of its own: no route, no
395
+ sidebar row, no line in `llms.txt`, and it takes its governance from its parent.
396
+ Ask for one only where there is something to compress — under about two screens,
397
+ a summary that restates the page teaches readers the tab is not worth opening,
398
+ and the skill will say so rather than write one.
399
+
400
+ ---
332
401
 
333
402
  ## Deploying
334
403
 
335
- The built site is a folder of files 2 MB of HTML, JS and CSS with zero
336
- host-specific dependencies. `pnpm build` writes it to `system/site/out/`,
337
- and anything that can serve files can serve it.
404
+ [Quick deployment](#quick-deployment) covers the common path. This is the rest
405
+ of it.
338
406
 
339
- - **Vercel** — connect the repository (or run `vercel`); the shipped
340
- `vercel.json` answers the setup interview: deploy from the repo root
341
- (never pin `system/site` as the root directory — the record lives
342
- outside it), build with `pnpm build`, serve `system/site/out/`. It also
343
- declares the MCP **door** as a second service built from the shipped
344
- `Dockerfile`, so `/mcp` and the site share one domain.
345
- <!-- ksor:pm pnpm -->
407
+ ### The site
408
+
409
+ `pnpm build` writes HTML, JS and CSS to `system/site/out/` about 2 MB with
410
+ zero host-specific dependencies. Anything that can serve files can serve it, and
411
+ `pnpm preview` serves exactly those bytes locally.
412
+
413
+ **`pnpm build` runs `ksor build` first.** It generates every `index.md`, runs
414
+ the record checker, and writes `build.lock.json` — the committed record of what
415
+ was published, from which commit, with which toolchain — and only then builds
416
+ the site. A checker refusal stops the build before anything is written.
417
+
418
+ - **Vercel** — the shipped `vercel.json` deploys from the repo root, builds with
419
+ `pnpm build`, and serves `system/site/out/`. It also declares the MCP **door**
420
+ as a second service built from the shipped `Dockerfile`, so `/mcp` and the
421
+ site share one domain.
422
+ <!-- ksor:pm pnpm -->
346
423
  If the build image's pnpm predates the `packageManager` pin, set the
347
424
  `ENABLE_EXPERIMENTAL_COREPACK=1` build environment variable.
348
- <!-- /ksor:pm -->
349
- **`pnpm build` runs `ksor build` first.** It generates every `index.md`,
350
- runs the record checker, and writes `build.lock.json` — the committed record
351
- of what was published, from which commit, with which toolchain — and only
352
- then builds the site. A checker refusal stops the build before anything is
353
- written. Takedowns reach the site through `.ksor/takedowns.yaml`, the
354
- committed ledger a file in this repository, so the site build needs no
355
- database access at all.
356
-
357
- That is deliberate. The act that withdraws a document is one merged commit,
358
- and both surfaces read it: the door refuses immediately, the site at its next
359
- build. Merge the ledger entry, rebuild, redeploy.
425
+ <!-- /ksor:pm -->
426
+ - **GitHub Pages, nginx, S3, anything static** run `pnpm build` and upload
427
+ `system/site/out/`. Hosted under a sub-path (like `user.github.io/repo`)?
428
+ Build with `KSOR_BASE_PATH=/repo pnpm build`.
429
+
430
+ **Verify any deploy** the same way: the home page, one document page and
431
+ `/llms.txt` load, and each names the documents this record has approved. On a
432
+ record whose documents are all still drafts, the home page and `/llms.txt` come
433
+ up empty and there is no document page at all — which is the correct answer, not
434
+ a broken deploy. Approve a document and rebuild to see it change.
435
+
436
+ ### Takedowns reach the site through a committed file
437
+
438
+ Takedowns reach the site through `.ksor/takedowns.yaml`, the committed ledger —
439
+ a file in this repository, so the site build needs no database access at all.
440
+
441
+ That is deliberate. The act that withdraws a document is one merged commit, and
442
+ both surfaces read it: the door refuses immediately, the site at its next build.
443
+ Merge the ledger entry, rebuild, redeploy.
360
444
 
361
445
  **A withdrawal that arrives on a clock works the same way, and that one has to
362
446
  be scheduled.** `stale_after` and `ksor.effective_from` are evaluated once per
363
447
  build, at the instant that build ran, and the answer is written into
364
448
  `system/site/out/` — static files cannot re-decide themselves. So a document
365
449
  whose `stale_after` passes after your last build keeps appearing in `/llms.txt`
366
- and in its markdown twin, while `ksor serve` — a process, evaluating per
367
- request — already refuses it. `ksor build` prints the next instant at which
368
- this happens. Nothing here rebuilds for you: `validate.yml` runs on pull
369
- requests and `vercel.json` declares no cron. If this record uses either key,
370
- add a scheduled rebuild and redeploy.
371
-
372
- - **GitHub Pages, nginx, S3, anything static** — run `pnpm build` and
373
- upload `system/site/out/`. Hosted under a sub-path (like
374
- `user.github.io/repo`)? Build with `KSOR_BASE_PATH=/repo pnpm build`.
375
- - **Verify any deploy** the same way: the home page, one document page and
376
- `/llms.txt` load, and each names the documents this record has approved. On a
377
- record whose documents are all still drafts, the home page and `/llms.txt`
378
- come up empty and there is no document page at all — which is the correct
379
- answer, not a broken deploy. Approve a document and rebuild to see it change.
450
+ and in its markdown twin, while `ksor serve` — a process, evaluating per request
451
+ — already refuses it. `ksor build` prints the next instant at which this
452
+ happens. Nothing here rebuilds for you: `validate.yml` runs on pull requests and
453
+ `vercel.json` declares no cron. **If this record uses either key, add a scheduled
454
+ rebuild and redeploy.**
380
455
 
381
456
  ### The agent surface deploys separately
382
457
 
383
- The site is files; the MCP door is a process. `Dockerfile` and `.dockerignore`
384
- at the repo root build it, and they name no host the same image runs on
385
- Cloud Run, Fly, Render, ECS, Kubernetes or a VPS:
458
+ `Dockerfile` and `.dockerignore` at the repo root build it, and they name no
459
+ host the same image runs on Cloud Run, Fly, Render, ECS, Kubernetes or a VPS:
386
460
 
387
461
  ```sh
388
462
  docker build -t my-record .
@@ -392,13 +466,13 @@ docker run --rm -p 8080:80 --env-file .env \
392
466
 
393
467
  **That last flag is not boilerplate, and it is not a workaround.** The image
394
468
  sets `$PORT`, so the door binds `0.0.0.0` — a PUBLIC bind — and the
395
- `KSOR_AUTH=disabled-local` your `.env` carries refuses there by design, saying
396
- so in as many words. Your laptop is not the exception: a container really is
397
- reachable from outside itself, and `disabled-public` is you saying you know
398
- that. It goes on the command rather than into `.env` so your ordinary
399
- `pnpm serve` keeps the loopback posture — and a real deployment sets it (or,
400
- better, the SSO variables) in the host's environment, since `.dockerignore`
401
- keeps `.env` out of the image entirely.
469
+ `KSOR_AUTH=disabled-local` your `.env` carries refuses there by design. Your
470
+ laptop is not the exception: a container really is reachable from outside
471
+ itself, and `disabled-public` is you saying you know that. It goes on the
472
+ command rather than into `.env` so your ordinary `pnpm serve` keeps the loopback
473
+ posture — and a real deployment sets it (or, better, the SSO variables) in the
474
+ host's environment, since `.dockerignore` keeps `.env` out of the image
475
+ entirely.
402
476
 
403
477
  One thing surprises people: **deploying does not publish.** The door serves
404
478
  whatever generation is already in the database, so a first deploy with no
@@ -407,15 +481,16 @@ machine or from CI — and it is deliberately not something a booting container
407
481
  does. The full walkthrough, including what a cold start costs and where ingest
408
482
  belongs, is in `node_modules/@panaversity/ksor/docs/deploying.md`.
409
483
 
410
- If `.ksor/governance.yaml` registers audiences, what you deploy is a
411
- **viewer**. Plain `pnpm build` builds for `[public]` — safe for any host.
484
+ ### Audiences decide what a build contains
485
+
486
+ If `.ksor/governance.yaml` registers audiences, what you deploy is a **viewer**.
487
+ Plain `pnpm build` builds for `[public]` — safe for any host.
412
488
  `KSOR_AUDIENCE=public,<audience> pnpm build` — a comma list that must always
413
- include `public` — builds for a wider viewer's own deployment, and that build
414
- carries an
415
- "— not for publication" label because it must never reach a public host:
416
- put it behind access control you already trust (VPN, SSO proxy,
417
- authenticated host). The tiers govern what a build contains; where each
418
- build may be served is yours to enforce.
489
+ include `public` — builds for a wider viewer, and that build carries an "— not
490
+ for publication" label because it must never reach a public host: put it behind
491
+ access control you already trust (VPN, SSO proxy, authenticated host). The tiers
492
+ govern what a build contains; where each build may be served is yours to
493
+ enforce.
419
494
 
420
495
  The site can also show a **sign-in control** that names the reader in the
421
496
  navbar. It is off until you set three variables (see `.env.example`), and it
@@ -424,6 +499,156 @@ so it is worth having on a record already behind one of the answers above, and
424
499
  is not a substitute for them. Setup and the honest limits:
425
500
  `node_modules/@panaversity/ksor/docs/deploying.md`.
426
501
 
502
+ ---
503
+
504
+ ## Reference
505
+
506
+ ### Commands
507
+
508
+ | Command | What it does | When |
509
+ | ----------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------- |
510
+ | `pnpm install` | fetches dependencies and the pinned `ksor` tool | first — then commit the lockfile |
511
+ | `pnpm dev` | the site at `:3000`, drafts visible and marked | while you write |
512
+ | `pnpm check` | runs the record checker as a program | before every commit |
513
+ | `pnpm build` | `ksor build` (index pages, checker, `build.lock.json`), then the static site to `system/site/out/` | before a deploy |
514
+ | `pnpm preview` | serves `system/site/out/` — there is no `start`, the site is a static export | to check a build |
515
+ | `pnpm provision` | applies the schema, authorizes ingest | once, for the agent surface |
516
+ | `pnpm refresh` | builds, ingests the record, collects retired generations | every time you publish |
517
+ | `pnpm serve` | the MCP server on `:8080` | to run the door locally |
518
+ | `pnpm exec ksor <verb>` | everything else — `calibrate`, `takedown`, … | as needed |
519
+
520
+ ### The files, explained
521
+
522
+ Nothing here is decoration, and the dotfiles are not ceremony — each one is a
523
+ different coding agent's way of finding the same working contract.
524
+
525
+ **The record and its authority**
526
+
527
+ | Entry | What it is |
528
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
529
+ | `knowledge/` | **the record** — your governed markdown. The product; everything else serves it. |
530
+ | `instance.md` | what this record is authoritative for; its `name:` is the identity every surface publishes and its `title:` the display title every page leads with (both read at server/build start — restart `pnpm dev` after changing either). Its BODY is the agent surface's system prompt — `ksor serve` wires it into the MCP server's instructions. |
531
+ | `.ksor/governance.yaml` | **the root of authority** — which audiences exist, who may approve a document, who may take one down. Committed; every governance act is checked against it. |
532
+ | `.ksor/takedowns.yaml` | the takedown ledger: every withdrawal and every lift, append-only and committed, so the site honours a takedown with no database in the loop. It appears at your first `ksor takedown` — an empty ledger would assert an act nobody performed. |
533
+ | `build.lock.json` | what the last `ksor build` published — the corpus, the commit, the toolchain — and what every machine surface stamps. Committed; written by `ksor build`, never by hand. |
534
+
535
+ **The system that serves it**
536
+
537
+ | Entry | What it is |
538
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
539
+ | `system/` | the code that serves the record: the site today, more as you need it. |
540
+ | `package.json` | the surface commands — `pnpm dev` (the site) and `pnpm provision` / `pnpm refresh` / `pnpm serve` (the agent surface) — plus `pnpm build` / `pnpm preview` / `pnpm check`, the pinned `@panaversity/ksor` tool and the workspace layout. |
541
+ | `Dockerfile`, `.dockerignore` | how the agent surface reaches a host. The Dockerfile names no host; `vercel.json` points at it rather than replacing it, so moving hosts is a redeploy. |
542
+ | `vercel.json` | one domain, two services — the static site and the MCP door. Delete it if you deploy elsewhere. |
543
+ | `.env.example` | the variables the served rung needs; copy to `.env` (gitignored) and fill in. |
544
+
545
+ <!-- ksor:pm pnpm -->
546
+
547
+ | Entry | What it is |
548
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
549
+ | `pnpm-workspace.yaml` | where the workspace looks for code (`system/site`, plus reserved `system/gateways/*` and `system/packages/*`), and the supply-chain policy for installs. |
550
+ | `pnpm-lock.yaml` | the exact dependency versions — the reason two machines build the same site. |
551
+
552
+ <!-- /ksor:pm -->
553
+ <!-- ksor:pm npm -->
554
+
555
+ | Entry | What it is |
556
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
557
+ | `.npmrc` | dependency install scripts are denied; the comment inside discloses the one protection this scaffold lacks (a 48-hour quarantine on new releases). |
558
+ | `package-lock.json` | the exact dependency versions — written by your FIRST install; commit it, it is the reason two machines build the same site. |
559
+
560
+ <!-- /ksor:pm -->
561
+ <!-- ksor:pm bun -->
562
+
563
+ | Entry | What it is |
564
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
565
+ | `bun.lock` | the exact dependency versions — written by your FIRST install; commit it, it is the reason two machines build the same site. |
566
+
567
+ <!-- /ksor:pm -->
568
+
569
+ **The working contract**
570
+
571
+ | Entry | What it is |
572
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
573
+ | `AGENTS.md` | the working contract every coding agent reads first — the rules for writing knowledge here. |
574
+ | `CLAUDE.md` | one line, pointing at `AGENTS.md`. Claude Code looks for this filename, not that one. |
575
+ | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn source material into governed documents), `make-slides`, `make-summary`, `format-checker` (the rules, as a program). |
576
+ | `.claude/skills/` | byte-identical copies of the kit — Claude Code discovers skills only here. The checker enforces the mirror, so the two cannot drift. |
577
+ | `.gemini/settings.json` | points Gemini CLI at `AGENTS.md`; Gemini does not read that filename on its own. |
578
+ | `.github/workflows/validate.yml` | your CI: runs the same checker on every pull request and push to main. |
579
+ | `.gitattributes` | markdown is checked out byte-stable on every platform, so the same commit hashes the same everywhere. |
580
+ | `.gitignore` | keeps build output, `node_modules/`, and `.env` out of the record's history — and negates two paths inside `.ksor/`, because the policy and the ledger ARE the record. |
581
+
582
+ `format-checker` deliberately contains a program, `check.mjs`, and not only
583
+ prose: rules that are only written down cannot refuse anything.
584
+
585
+ Everything here is yours to change. The kit exists so that any coding agent can
586
+ operate this project without being taught it first.
587
+
588
+ ### When something refuses you
589
+
590
+ This project refuses loudly and on purpose. Most of what looks like a failure is
591
+ a rule doing its job — and every refusal names its own fix, so this table is a
592
+ map rather than a substitute.
593
+
594
+ | What you see | What it means | What to do |
595
+ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
596
+ | `pnpm check` refuses a document | `status: stable` without both `generated` and `ksor.approval`, or an approval earlier than the text it approves | add both keys; approval cannot precede what it approves |
597
+ | `start` — missing script | there is none: the site is a static export, so nothing serves it at runtime | `pnpm preview`, or upload the folder |
598
+ | `pnpm preview` exits `3` | there is no `system/site/out/` yet | run the build first |
599
+ | `pnpm serve` refuses to boot | it will not run unauthenticated by accident | `KSOR_AUTH=disabled-local` in `.env` for a loopback run |
600
+ | the deployed or containerised door refuses with `disabled-local` | it binds `0.0.0.0` — a public bind | `KSOR_AUTH=disabled-public` in the host environment, or configure the SSO variables |
601
+ | the agent answers questions 2 and 3 instead of declining | no floor is measured, so the gate is off (`abstain OFF`, `gate: "off"`) — step 3's `calibrate` was skipped | `pnpm exec ksor calibrate --instance instance.md`, paste the block, restart |
602
+ | a deployed door serves an empty record | deploying does not publish — and a laptop DSN is unreachable from the host | point both at one hosted Postgres, then `pnpm refresh` |
603
+ | the home page and `/llms.txt` are empty | every document is still a draft — correct, not broken | approve one and rebuild |
604
+ | a new document never appears on the built site | drafts reach no built surface at all | publish it — `status: stable` plus both governance keys |
605
+ | an expired document still shows on the site but not through the door | the static build evaluated `stale_after` at build time | rebuild and redeploy; schedule a rebuild if you use it |
606
+ | Vercel: `no services are declared` | Root Directory was auto-filled with `system/site` | set it to `./` |
607
+
608
+ ### Dependencies and advisories
609
+
610
+ An audit of this scaffold reports vulnerabilities in `next`, and will keep doing
611
+ so: a framework that large always has open advisories against whatever version
612
+ you have pinned.
613
+
614
+ <!-- ksor:pm npm -->
615
+
616
+ `npm install` prints the count at the end of every install, so you meet it
617
+ before you have run anything, next to an invitation to run
618
+ `npm audit fix --force`.
619
+
620
+ <!-- /ksor:pm -->
621
+ <!-- ksor:pm pnpm -->
622
+
623
+ pnpm reports it only when you run `pnpm audit`.
624
+
625
+ <!-- /ksor:pm -->
626
+ <!-- ksor:pm bun -->
627
+
628
+ bun reports it only when you run `bun audit`.
629
+
630
+ <!-- /ksor:pm -->
631
+
632
+ **Never let an audit tool raise the pin for you.** It moves off the version this
633
+ scaffold was built and tested against, and that pin is the whole reason two
634
+ machines produce the same site. Bump it deliberately instead — take the newer
635
+ pin a newer `ksor init` emits, or raise it yourself and re-run `pnpm build`.
636
+
637
+ It also reads worse than it is, for one structural reason worth knowing: **this
638
+ site is a static export.** `pnpm build` writes HTML, JS and CSS to
639
+ `system/site/out/`, and no framework server ever runs in front of your readers —
640
+ no middleware, no server actions, no rewrites, no image optimizer. Most
641
+ framework advisories describe exactly those request paths, so they have nothing
642
+ here to reach.
643
+
644
+ Two things that argument does NOT cover, and you should treat as real: an
645
+ advisory in the **build** toolchain, which does run, on your machine and in your
646
+ CI; and any advisory at all if you later add a served route and stop exporting.
647
+ Read what an advisory affects before deciding it is inert — the static export is
648
+ a reason, not a blanket.
649
+
650
+ ---
651
+
427
652
  ## Ownership
428
653
 
429
654
  Everything here is yours. The scaffold was generated by