@panaversity/ksor 0.0.40 → 0.0.41

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +869 -0
  2. package/README.md +11 -7
  3. package/dist/checker/check-main.mjs +14049 -0
  4. package/dist/cli.mjs +11528 -5213
  5. package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
  6. package/dist/gateway.d.mts +52 -13
  7. package/dist/gateway.mjs +2 -2
  8. package/dist/index.d.mts +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
  11. package/docs/authorization.md +8 -6
  12. package/docs/deploying.md +36 -25
  13. package/docs/index.md +26 -13
  14. package/docs/ingesting.md +70 -22
  15. package/docs/tool-surface.md +69 -16
  16. package/package.json +4 -3
  17. package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
  18. package/schema/schema.sql +77 -14
  19. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
  20. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
  21. package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
  22. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
  23. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
  24. package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
  25. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
  26. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
  27. package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
  28. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
  29. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
  30. package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
  31. package/templates/scaffold/.github/workflows/validate.yml +9 -1
  32. package/templates/scaffold/.ksor/governance.yaml +17 -0
  33. package/templates/scaffold/AGENTS.md +234 -113
  34. package/templates/scaffold/Dockerfile +5 -1
  35. package/templates/scaffold/README.md +160 -42
  36. package/templates/scaffold/env.example +37 -6
  37. package/templates/scaffold/gitignore +13 -8
  38. package/templates/scaffold/instance.md +21 -17
  39. package/templates/scaffold/knowledge/governance-ladder.md +6 -2
  40. package/templates/scaffold/knowledge/index.md +9 -0
  41. package/templates/scaffold/knowledge/surfaces/for-agents.md +7 -6
  42. package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
  43. package/templates/scaffold/knowledge/surfaces/index.md +4 -20
  44. package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
  45. package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
  46. package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
  47. package/templates/scaffold/package.json +3 -4
  48. package/templates/scaffold/pnpm-lock.yaml +3 -0
  49. package/templates/scaffold/system/gateways/content.ts +13 -0
  50. package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
  51. package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
  52. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +126 -91
  53. package/templates/scaffold/system/site/app/global.css +13 -5
  54. package/templates/scaffold/system/site/app/layout.tsx +8 -3
  55. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
  56. package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
  57. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
  58. package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
  59. package/templates/scaffold/system/site/components/governance.tsx +205 -87
  60. package/templates/scaffold/system/site/components/record-index.tsx +5 -5
  61. package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
  62. package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
  63. package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
  64. package/templates/scaffold/system/site/lib/attachments.ts +0 -28
  65. package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
  66. package/templates/scaffold/system/site/lib/audience.ts +42 -146
  67. package/templates/scaffold/system/site/lib/embed-rule.ts +9 -0
  68. package/templates/scaffold/system/site/lib/governance.ts +339 -225
  69. package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
  70. package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
  71. package/templates/scaffold/system/site/lib/lock.ts +282 -0
  72. package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
  73. package/templates/scaffold/system/site/lib/record-href.ts +68 -0
  74. package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
  75. package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
  76. package/templates/scaffold/system/site/lib/shared.ts +67 -104
  77. package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
  78. package/templates/scaffold/system/site/lib/source.ts +256 -186
  79. package/templates/scaffold/system/site/lib/stage-knowledge.ts +566 -492
  80. package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
  81. package/templates/scaffold/system/site/package.json +1 -0
  82. package/templates/scaffold/system/site/record/actor.ts +23 -0
  83. package/templates/scaffold/system/site/record/check.ts +571 -0
  84. package/templates/scaffold/system/site/record/citations.ts +312 -0
  85. package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
  86. package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
  87. package/templates/scaffold/system/site/record/hygiene.ts +320 -0
  88. package/templates/scaffold/system/site/record/index-file.ts +150 -0
  89. package/templates/scaffold/system/site/record/index.ts +103 -0
  90. package/templates/scaffold/system/site/record/instance.ts +257 -0
  91. package/templates/scaffold/system/site/record/instant.ts +43 -0
  92. package/templates/scaffold/system/site/record/ledger.ts +694 -0
  93. package/templates/scaffold/system/site/record/load.ts +129 -0
  94. package/templates/scaffold/system/site/record/lock.ts +306 -0
  95. package/templates/scaffold/system/site/record/near-miss.ts +37 -0
  96. package/templates/scaffold/system/site/record/policy.ts +414 -0
  97. package/templates/scaffold/system/site/record/profile.ts +535 -0
  98. package/templates/scaffold/system/site/record/refusal.ts +106 -0
  99. package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
  100. package/templates/scaffold/system/site/source.config.ts +77 -22
  101. package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
  102. package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
  103. package/templates/scaffold/system/site/lib/page-order.ts +0 -93
@@ -5,31 +5,33 @@ here; every coding agent reads this file first.
5
5
 
6
6
  ## The two worlds
7
7
 
8
- | Path | What it is |
9
- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
- | `knowledge/` | **the record** — governed markdown, the owner's world, the product |
11
- | `system/` | **the system** — all code that serves the record |
12
- | `instance.md` | what this SoR is authoritative for; its prose IS the agent surface's system prompt (`ksor serve` wires the body into the MCP server's instructions). Its `name:` is the machine identity (llms.txt, citations) and its body `# H1` is the DISPLAY TITLE every page leads with — both read when the server or build STARTS, so restart `pnpm dev` after changing either (found live 2026-08-18) |
8
+ | Path | What it is |
9
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | `knowledge/` | **the record** — governed markdown, the owner's world, the product |
11
+ | `system/` | **the system** — all code that serves the record |
12
+ | `instance.md` | what this SoR is authoritative for; its prose IS the agent surface's system prompt (`ksor serve` wires the body into the MCP server's instructions). Its `name:` is the machine identity (llms.txt, citations) and its `title:` is the DISPLAY TITLE every page leads with — both are frontmatter keys, both read when the server or build STARTS, so restart `pnpm dev` after changing either (found live 2026-08-18) |
13
13
 
14
14
  The record survives the system: `knowledge/` must stay readable and complete
15
15
  even if `system/` is deleted. Dependency flows one way — the system reads the
16
16
  record; the record never references the system.
17
17
 
18
- Every grouped key in `instance.md` (`ksor`, `site`, `database`, `embedding`,
19
- `retrieval`, `budgets`) is written as an indented block, never inline on one
20
- line: a group written as `site: { governance: false }` is not read as a group
21
- at all, so every setting inside it is silently dropped. `pnpm check` refuses
22
- that shape, and refuses a key repeated inside a group.
23
-
24
- `instance.md` carries a closed key set — `format`, `name`, `ksor`, `site`,
25
- the optional pair `audiences` + `default_visibility` (the record's reader
26
- audiences, ordered least- to most-restricted with `public` first, and the one
27
- a document takes when it names none — declared together or not at all), and
28
- the four serve-config blocks `database` / `embedding` / `retrieval` / `budgets`
18
+ `instance.md` (`format: 2`) carries a closed key set — `name` (the machine
19
+ identity), `title` (the display title and the root index's heading),
20
+ `description` (one sentence for `llms.txt` and the MCP discovery document),
21
+ `toolchain` (the upgrade stamp), `site`, `mcp_url`, `version`, and the four
22
+ serve-config blocks `database` / `embedding` / `retrieval` / `budgets`
29
23
  (present only once you climb to the served MCP rung — see "Serving to agents"
30
- below; a `pnpm dev`-only project declares none). Everything else that matters
31
- about the instance is the prose below the frontmatter; `pnpm check` names any
32
- other key rather than ignoring it.
24
+ below; a `pnpm dev`-only project declares none). Who may read what lives in
25
+ `.ksor/governance.yaml`, never here. The body below the frontmatter is the
26
+ MCP server's instructions in full; `pnpm check` names any other key rather
27
+ than ignoring it, and refuses a key repeated inside a group.
28
+
29
+ Authority lives in `.ksor/governance.yaml`: the audiences a document may name
30
+ (`public` is implicit), who may approve a document for `stable`, and who may
31
+ take one down. Takedowns are `.ksor/takedowns.yaml`, an append-only ledger
32
+ written by `ksor takedown` — every entry's actor is checked against the
33
+ policy, so a line appended by hand is refused exactly as the verb would refuse
34
+ it, and a deleted line is refused by `ksor build` against the file's history.
33
35
 
34
36
  ## Critical rules
35
37
 
@@ -105,9 +107,9 @@ Stand it up in this order (each step's errors explain how to fix themselves):
105
107
  3. **Bring it up.** Once, then every time:
106
108
 
107
109
  ```sh
108
- pnpm provision # schema (or migrate) + grant — the privileged acts, run once
109
- pnpm refresh # ingest the record, collect retired generations
110
- pnpm serve # the MCP server (one supervised process)
110
+ pnpm provision # schema (or migrate) + grant — the privileged acts, run once
111
+ pnpm refresh # build, ingest the record, collect retired generations
112
+ pnpm serve # the MCP server (one supervised process)
111
113
  ```
112
114
 
113
115
  `provision` is separate on purpose: applying DDL and granting ingest are acts
@@ -195,14 +197,19 @@ Stand it up in this order (each step's errors explain how to fix themselves):
195
197
  pnpm exec ksor calibrate --instance instance.md
196
198
  ```
197
199
 
198
- It prints a recommended `vector_floor` for THIS corpus in THIS embedding
199
- space. Paste the number in and restart:
200
+ It ends with a `retrieval:` block for THIS corpus in THIS embedding space,
201
+ ready to paste into instance.md's frontmatter exactly as printed. Paste it
202
+ and restart:
200
203
 
201
204
  ```yaml
202
205
  retrieval:
203
- vector_floor: 0.55 # measured by ksor calibrate on <date>
206
+ vector_floor: 0.55 # calibrated <date> on generation 3, model <model>/d1536, door: synthesized
207
+ floor_digest: 8bfb07d0e6f5
204
208
  ```
205
209
 
210
+ If instance.md already has a `retrieval:` block, merge the keys into it — a
211
+ second `retrieval:` is a duplicate key and is refused.
212
+
206
213
  Never copy a floor from another corpus — recalibrate, and record the
207
214
  measurement beside the number. Writing `vector_floor: uncalibrated` declares
208
215
  the intent to gate WITHOUT a measurement, and every serve refuses until a
@@ -319,27 +326,40 @@ ordinary zod. It is yours, and it is **deletable**: without it the door serves
319
326
  the same defaults.
320
327
 
321
328
  Edit it because an agent pays for this surface out of its context window, twice.
322
- Measured on an 81-document record:
329
+ The definitions depend only on the code, so they are exact for every record
330
+ (re-measured 2026-08-25 from the served `tools/list`); the per-call figures are
331
+ the 2026-08-23 measurement against a live 81-document record, plus the
332
+ governance block every hit now carries:
323
333
 
324
334
  | | |
325
335
  | -------------------------------- | ------------------------------ |
326
- | all three tool definitions | ~2,990 tokens, always resident |
327
- | one `search` at `k=10` (default) | ~3,541 tokens per call |
328
- | one `search` at `k=5` | ~2,002 tokens per call |
336
+ | all three tool definitions | ~4,054 tokens, always resident |
337
+ | one `search` at `k=10` (default) | ~4,196 tokens per call |
338
+ | one `search` at `k=5` | ~2,330 tokens per call |
329
339
 
330
340
  Three edits pay for themselves:
331
341
 
332
342
  - **Delete a tool nothing calls.** Removing `outline` and `read` gives back
333
- ~1,643 tokens for the whole session.
343
+ ~2,152 tokens for the whole session.
334
344
  - **Say what this record covers**, above `FLOOR.search`. It is how an agent with
335
345
  several records attached picks yours; name the subject AND the boundary.
336
346
  - **Set `k`** in the input schema — it is the lever on reply size.
337
347
 
338
348
  ```ts
339
349
  description: `Leave, benefits, conduct. Not product docs.\n\n${FLOOR.search}`,
340
- inputSchema: z.object({ query: z.string(), k: z.number().int().default(5) }),
350
+ inputSchema: z.object({
351
+ query: z.string(),
352
+ k: z.number().int().default(5),
353
+ min_trust_tier: z.enum(TRUST_TIERS).optional(),
354
+ }),
341
355
  ```
342
356
 
357
+ Keep `min_trust_tier`. Dropping it does not weaken the record — the handler
358
+ still applies `unverified` and this deployment's own floor — but it takes away
359
+ the only way a caller can ask to be answered ONLY from what a human reviewed,
360
+ and the door says so at boot with a notice naming the tool and the line to
361
+ paste, rather than failing quietly.
362
+
343
363
  You can add your own tools with `registerTool` too — but be clear-eyed: ksor
344
364
  makes no provenance claim about a tool it did not hand you a handler for.
345
365
 
@@ -356,49 +376,94 @@ surface at boot and refuses to start if a guarantee is gone:
356
376
 
357
377
  ## Withdrawing a document — `ksor takedown`
358
378
 
359
- A takedown is the one governance act that must reach EVERY surface at once.
360
- It needs the database (the denial is a row, not a file), so it belongs to the
361
- served rung.
379
+ A takedown is the one governance act that must reach EVERY surface at once. It
380
+ is written to the committed ledger `.ksor/takedowns.yaml` FIRST and to the
381
+ database denylist second, in one act — so a record with no database can
382
+ withdraw a document, and the site reads the withdrawal from the repository
383
+ rather than from anything exported.
362
384
 
363
385
  ```sh
364
- pnpm exec ksor takedown --instance instance.md <stable-id> --reason "legal request 2026-08"
365
- pnpm exec ksor takedown --instance instance.md <stable-id> --reason "..." --subtree
386
+ pnpm exec ksor takedown --instance instance.md --actor human:you <stable-id> --reason "legal request 2026-08"
387
+ pnpm exec ksor takedown --instance instance.md --actor human:you <stable-id> --reason "..." --scope subtree
366
388
  pnpm exec ksor takedown --instance instance.md --list # what is currently denied
367
389
  pnpm exec ksor takedown --instance instance.md --ledger # who denied what, when
368
- pnpm exec ksor takedown --instance instance.md --revoke <stable-id>
390
+ pnpm exec ksor takedown --instance instance.md --actor human:you --revoke <entry-id> --reason "..."
369
391
  ```
370
392
 
371
- The stable id is what a search result reports as `provenance.stable_id` for
372
- most documents that is `knowledge/<path-without-.md>`. `--subtree` withdraws a
373
- section and everything beneath it, including documents added later.
374
- `--actor NAME` names who performed the act in the ledger, and a denial or a
375
- revocation is REFUSED without it. There is no default: a name taken from the
393
+ The stable id is what a search result reports as `provenance.stable_id`, and it
394
+ is `knowledge/<path-without-.md>` always, since path is identity.
395
+ `--scope subtree` withdraws a section and everything beneath it, including
396
+ documents added later. `--revoke` takes the id of a LEDGER ENTRY, not a stable
397
+ id: the ledger is append-only, so a lift is a new entry rather than a deleted
398
+ line. The id is printed by the denial that created it, listed by `--ledger`,
399
+ and written in `.ksor/takedowns.yaml` — three ways to the same string, none of
400
+ which needs a database. `--removed <entry-id>` records that what a denial
401
+ names was deleted — a document, or at `--scope subtree` the directory — and
402
+ `--apply` writes the rows for entries that reached the database late. Record
403
+ the deletion; do not revoke instead: a revocation says the hold was LIFTED,
404
+ which is a different act, and it stops covering the path if it ever returns.
405
+
406
+ `--actor` names who performed the act, and a denial, a revocation or an
407
+ amendment is REFUSED without it. There is no default: a name taken from the
376
408
  environment reads like a person and is whatever the shell happened to be
377
409
  (`runner` under CI, `root` in a container), which is worse than no name at all
378
- in the one row that exists to record who did this. Read-only modes
379
- (`--list`, `--ledger`, `--export`) need nothing.
410
+ in the one row that exists to record who did this. It must be a well-formed
411
+ actor (`human:<handle>` or `process:<id>` — a bare name is refused) and
412
+ `takedown_authorities` in `.ksor/governance.yaml` must name it. The same check
413
+ runs over every entry in the ledger at `pnpm check`, `ksor build` and ingest, so
414
+ a line appended by hand in a pull request is refused exactly as the verb would
415
+ refuse it. The read-only modes
416
+ (`--list`, `--ledger`) need no actor — nobody is performing an act by looking.
417
+ They do not need a database either: on a record that declares none they read
418
+ the committed `.ksor/takedowns.yaml`, which is the whole record of the act
419
+ anyway.
380
420
 
381
421
  **The MCP door stops serving it immediately. The SITE stops at its next
382
- build** — the site reads a file, not the database, and `pnpm build` refreshes
383
- that file for you (`pnpm export-denylist`). So after a takedown, rebuild and
384
- redeploy the site, or the human surface keeps publishing what the agent
385
- surface already refuses.
422
+ build** — the site reads the committed ledger (`.ksor/takedowns.yaml`), not
423
+ the database, and so needs no database access to honour a takedown. After a
424
+ takedown, merge the ledger entry, rebuild and redeploy the site, or the human
425
+ surface keeps publishing what the agent surface already refuses.
386
426
 
387
427
  ## Publishing
388
428
 
389
429
  `pnpm build` emits a fully static site (`system/site/out/`) deployable to
390
430
  any host — Vercel reads the shipped `vercel.json` (deploy from the repo
391
431
  ROOT, never `system/site/`), and every other host just serves the folder.
392
- Once `instance.md` declares a `database:`, `pnpm build` needs `KSOR_DB_URL` as
393
- well: it runs `pnpm export-denylist` first, which asks the database what has
394
- been withdrawn and writes `.ksor-denylist.json`. Without the DSN the build
395
- refuses rather than publish a document someone took down.
396
- `KSOR_BASE_PATH=/repo pnpm build` targets sub-path hosting. With
397
- `audiences:` declared, plain `pnpm build` is always the public tier;
398
- `KSOR_AUDIENCE=<audience> pnpm build` builds a wider tier that belongs
432
+ `pnpm build` runs `ksor build` first — every `index.md` regenerated, the
433
+ record checked, `build.lock.json` written (commit it: it is the provenance
434
+ every machine artefact stamps) — and a refusal stops the build before a byte
435
+ is written. `ksor build --strict` additionally refuses an uncommitted input
436
+ (`ksor-build-dirty`), which is the posture for a release.
437
+ `KSOR_BASE_PATH=/repo pnpm build` targets sub-path hosting. With audiences
438
+ registered in `.ksor/governance.yaml`, plain `pnpm build` is the `public`
439
+ viewer; `KSOR_AUDIENCE=public,<audience> pnpm build` — a comma list, always
440
+ including `public` — builds for a wider viewer, and that build belongs
399
441
  behind that audience's own access control, never on a public host.
400
442
  Details in README → Deploying.
401
443
 
444
+ **The starter publishes; what the owner writes does not, until they approve
445
+ it.** The five starter documents ship `status: stable`, approved by
446
+ `ksor-starter/KSOR-STAMP-VERSION` — a PRODUCER actor, not a person, and
447
+ `.ksor/governance.yaml` authorises it so the first `pnpm build` emits a record
448
+ with something in it. Two things follow, and both are your job. The samples
449
+ describe KSoR rather than this organisation, so replacing them is the owner's
450
+ first act on this record; and when the last one goes, the producer goes with it
451
+ — delete `ksor-starter/KSOR-STAMP-VERSION` from `approval_authorities`, because
452
+ nothing the owner wrote should be approved by a tool.
453
+
454
+ Everything written after that starts at `status: draft`, and §2.5 admits a
455
+ draft to NO surface of a build — not the page, not the sidebar, not the search
456
+ index, not `llms.txt`. A build of a record whose documents are all drafts emits
457
+ a site with zero document pages and an empty `## Documents` section, reports
458
+ `0 admitted to a machine surface`, and is working correctly. `pnpm dev` shows
459
+ the drafts, which is what the preview is for. Never approve on the owner's
460
+ behalf to make a build look fuller: approving is `status: stable` plus a
461
+ `ksor.approval: { by, at }` naming an actor `.ksor/governance.yaml` authorises,
462
+ and it is the owner's act. Ask them, then write down what they said
463
+ (`.agents/skills/intake-interview/`). `KSOR_DRAFTS=show pnpm build` publishes
464
+ drafts to the HUMAN surface only, marks the build `noindex` and records itself
465
+ in `build.lock.json` — a review link, never a way to ship.
466
+
402
467
  ### The MCP door is a container
403
468
 
404
469
  The other surface is a live process, so it ships as one. `Dockerfile` and
@@ -406,9 +471,17 @@ The other surface is a live process, so it ships as one. `Dockerfile` and
406
471
 
407
472
  ```sh
408
473
  docker build -t my-record .
409
- docker run --rm -p 8080:80 --env-file .env my-record
474
+ docker run --rm -p 8080:80 --env-file .env \
475
+ -e KSOR_AUTH=disabled-public my-record
410
476
  ```
411
477
 
478
+ The `-e` is required and must not be dropped: the image sets `$PORT`, so the
479
+ door binds `0.0.0.0` — a PUBLIC bind — and the `KSOR_AUTH=disabled-local` in
480
+ `.env` refuses there, correctly. Never "fix" that refusal by editing `.env`;
481
+ put the deliberate value on the command, so the owner's ordinary `pnpm serve`
482
+ keeps its loopback posture. A real deployment sets it — or the SSO variables —
483
+ in the host's environment, since `.dockerignore` keeps `.env` out of the image.
484
+
412
485
  That image runs on Cloud Run, Fly, Render, ECS, Kubernetes or a VPS unchanged.
413
486
  `vercel.json` declares BOTH surfaces — a `site` service built from
414
487
  `system/site/out/` and a `door` service pointing at that same `Dockerfile` —
@@ -433,22 +506,49 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
433
506
  - One document per file under `knowledge/`; the path is the document's
434
507
  identity and its URL — ascii lowercase, digits, hyphens; no spaces or
435
508
  special characters; no two files differing only in case; never both
436
- `foo.md` and `foo/index.md`. The `title:` carries the document's real
437
- name in any language — the filename is the address, not the name.
509
+ `foo.md` and a folder `foo/`. The `title:` carries the document's real
510
+ name in any language — the filename is the address, not the name. Every
511
+ folder's `index.md` is GENERATED by `ksor build` (an OKF §8 index of the
512
+ folder) and committed — never edit one, never put prose in one; a folder's
513
+ own introduction is a named document inside it, such as `overview.md`.
514
+ `log.md` and `README.md` are reserved names.
438
515
  - The frontmatter `title` IS the rendered page heading — never repeat it as
439
516
  an `# h1` in the body, and quote any value containing a colon
440
517
  (`title: "Note: quoting"`).
441
- - Frontmatter: `title` and `status` (`draft | review | approved | superseded`)
442
- are required. `owner` and `provenance` (a list naming real sources) are
443
- strongly encouraged they become required as this project climbs the
444
- governance ladder. `description`, `visibility` (below), `order` (sidebar
445
- position), `effective` (the date the document takes effect — a real
446
- `YYYY-MM-DD` date and nothing else, or **quote it** to publish it as text:
447
- `effective: "Q1 2026"`. Unquoted, YAML turns `2026-06-31` into July 1st and
448
- `2026-04-01 09:00 +05:00` into the day before, without a word, and the page
449
- publishes that as fact) and `superseded` (a legacy marker prefer `status`)
450
- are available. No other keys; never
451
- `id:` or `name:` the path is the identity.
518
+ - Frontmatter is the KSoR Profile of OKF (the record spec's §2). Required on
519
+ every document: `type` (`Document` unless the knowledge is a `Policy`,
520
+ `Procedure`, `Control`, `Standard`, `Definition`, `Decision Record`,
521
+ `Example` or `Attested Computation` those reserved types also require
522
+ `sources` and `ksor.owner`), `title`, `description` (one sentence), `status`
523
+ (`draft | stable | deprecated`) and `ksor.audience` (a list `[public]`, or
524
+ audiences registered in `.ksor/governance.yaml`; never omitted, never
525
+ inferred). A `stable` document carries `generated: { by, at }` and
526
+ `ksor.approval: { by, at }` by an actor the policy authorises, with
527
+ `generated.at` no later than the approval; a `deprecated` one carries
528
+ `ksor.deprecated: { by, at }` and usually `ksor.superseded_by: <id>`
529
+ (a stable document every reader of this one may read). Optional: `order`
530
+ (reading position), `sources` (`{ id, resource, title }`, cited in the body
531
+ as GFM footnotes `[^id]`), `verified` (`[{ by, at }]` — sets the trust
532
+ tier: none → unverified, machine actors → machine-confirmed, any `human:`
533
+ → human-reviewed; unlike `ksor.approval`, the policy does not gate WHO may
534
+ appear here — it has no verification family — so a `verified` entry is a
535
+ claim the document makes about itself and the pull request that adds it is
536
+ the only thing standing behind it), `stale_after`, `ksor.effective_from`. Actors are
537
+ `human:<id>`, `process:<id>` or `<producer>/<version>` in `verified`,
538
+ `generated`, `ksor.approval` and `ksor.deprecated` — anything else there is
539
+ refused. `ksor.owner` is not checked for its shape: write an actor or
540
+ `team:<id>` by convention, but it is free text, so a bare word passes and can
541
+ then never be the `ksor.deprecated.by` that deprecates the document. Every
542
+ timestamp is an ISO 8601 instant with an offset (`2026-08-25T09:00:00Z`) —
543
+ never a bare date. Unknown keys are preserved, unless
544
+ the name is one edit from a profile key — `stale_afer:` is refused rather
545
+ than kept, because a preserved near miss is the key it meant, failing open.
546
+ The `ksor:` block's own key set is closed. The pre-profile keys
547
+ `visibility`, `owner`, `provenance`, `effective`, `superseded` and
548
+ `superseded_by` are refused by name and `ksor migrate` moves them; `id` and
549
+ `name` it deletes (the path is the identity); `sor_id` it REFUSES rather
550
+ than drops, because retiring it changes the document's stable id and any
551
+ takedown keyed on the old one must be re-denied against the new one first.
452
552
  - **Each page says how long it takes to read**, counted from the document's own
453
553
  words when the site is built. Fenced code and frontmatter do not count toward
454
554
  it, so a short page carrying a long example is not reported as a long read.
@@ -459,15 +559,29 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
459
559
  so the cost of each view is available without switching to it.
460
560
  - **The governance keys are rendered, so they are worth filling in.** Each
461
561
  page shows its owner and effective date under the title, lists every
462
- `provenance` entry separately at the foot, and — for a superseded document —
562
+ `sources` entry separately at the foot, and — for a deprecated document —
463
563
  carries a notice above the title naming its successor and linking to it. A
464
564
  key you leave off renders nothing at all: the site never invents a value, so
465
565
  a missing owner reads as missing rather than as unowned.
466
- - **The agent surface carries them too.** `llms.txt` marks a document whose
467
- status is a caveat and names the route that replaced a superseded one;
468
- `llms-full.txt` puts the keys back as frontmatter above each document. An
469
- agent reading the record therefore sees what a reader sees — a withdrawn
470
- document is never handed over as plain prose.
566
+ - **The agent surface carries them too by EXCLUDING what it must not
567
+ hand over.** `llms.txt` and `llms-full.txt` list only what the §2.5 table
568
+ admits to a machine surface: stable, effective, unexpired, undenied. A
569
+ draft, a deprecated document, and one whose `stale_after` had passed **when
570
+ the build ran**, are not entries at all, so an agent is never handed a
571
+ withdrawn document as plain prose. `llms-full.txt` serves each document's own
572
+ frontmatter intact, plus the derived `trust_tier` and this build's stamps —
573
+ so what an agent reads carries the same governance a reader sees on the page.
574
+ - **Those files are a SNAPSHOT, and keeping them true is yours.** A build
575
+ decides admission ONCE, at its own instant, and writes the answer into files;
576
+ static output cannot re-decide itself. So a document whose `stale_after`
577
+ passes AFTER a build keeps appearing in `llms.txt` and in its markdown twin
578
+ until the next build, while `ksor serve` — which evaluates the same rule per
579
+ request — already refuses it. `ksor build` prints the next instant at which
580
+ this goes out of date, and prints which documents it held back and why.
581
+ Nothing here rebuilds on a schedule: the shipped `validate.yml` runs the
582
+ checker on pull requests and `vercel.json` declares no cron. If this record
583
+ uses `stale_after` or `ksor.effective_from`, add a scheduled rebuild and
584
+ redeploy, or accept that the static half is as current as the last build.
471
585
  - **Don't want any of it on the published pages?** Set `governance: false`
472
586
  under `site:` in `instance.md`. The record keeps every key — the agent
473
587
  surface and your audit trail still read them, and `llms.txt`/`llms-full.txt`
@@ -475,38 +589,38 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
475
589
  knowing: the home page shows the agent surface VERBATIM in its panel, so the
476
590
  keys stay visible there even with this off. That panel's whole claim is that
477
591
  it is the bytes an agent is served, and editing them to match a page setting
478
- would make it lie. Remove the panel if you need the front page silent too. The supersession notice is the one thing it does not hide: a reader
479
- handed a replaced document with no word of its successor has been misled.
480
- - **`status` is shown only when it is a caveat.** `draft`, `review` and
481
- `superseded` appear as a small label; `approved` shows nothing, because a
482
- reader already assumes a document in the record is current so the label
483
- stays rare enough to be noticed on the pages where it matters.
484
- - `visibility:` names the one audience a document belongs to a single value
485
- from `instance.md`'s `audiences:`, never a list, and orthogonal to `status:`
486
- (an approved document can be restricted, and a draft is not hidden). Leave
487
- it off and the document takes `default_visibility`. Using the key WITHOUT
488
- an `audiences:` block is refused on both surfaces `pnpm build` stops with
489
- `ksor-visibility-without-audiences` and `pnpm serve` refuses to boot
490
- because a document marked restricted while nothing enforces it is the one
491
- shape where the frontmatter is the only trace of a restriction that is not
492
- happening. Once `audiences:` is declared, `pnpm check`
493
- refuses any link or `superseded_by:` pointing from a wider audience at a
494
- narrower one the leak no single build can catch, because the build that
495
- publishes the link has already dropped its target.
592
+ would make it lie. Remove the panel if you need the front page silent too.
593
+ What it does not hide is a CAVEAT: the supersession notice stays, and so does
594
+ the chip on a document the record has not brought into force yet or has not
595
+ reviewed since its `stale_after` a reader handed one of those with no word
596
+ of it has been misled, and the sidebar, the listings and the search results
597
+ say it whatever this key is set to.
598
+ - **`status` is shown only when it is a caveat.** `deprecated` appears as a
599
+ small label; `stable` shows nothing, because a reader already assumes a
600
+ document in the record is current so the label stays rare enough to be
601
+ noticed on the pages where it matters. `draft` carries the same label, but
602
+ only under `pnpm dev`: a BUILT site has no draft page to label it on.
603
+ - `ksor.audience` lists who may read a document; a viewer holds a list that
604
+ always includes `public`, and the document is visible when the two overlap.
605
+ Every identifier but `public` must be registered in
606
+ `.ksor/governance.yaml` an unregistered one is refused, because a typo
607
+ reads as a restriction. `pnpm check` refuses any link, `ksor.superseded_by`
608
+ pointer or summary that reaches a document not every reader of the source
609
+ may read (`ksor-link-widens`) the leak no single build can catch, because
610
+ the build that publishes the link has already dropped its target.
496
611
 
497
612
  **Publication, not authorship: anyone who can clone the repository reads
498
613
  every document regardless of frontmatter; if someone must not read a
499
614
  document and can clone, the answer is a second repository.**
500
615
 
501
- - A replaced document is marked `status: superseded` with `superseded_by:`
502
- pointing at its successor superseded documents are never deleted. The two
503
- keys are one statement, so `pnpm check` refuses each without the other: a
504
- successor pointer left on a document you have set back to `approved` would
505
- publish a "Superseded" banner over a live document. The pointer must name a
506
- markdown document (`./<successor>.md`), exactly as it is capitalised under
507
- `knowledge/`, and it must lead somewhere: a document that supersedes itself,
508
- or a pair that supersede each other, sends the reader in a circle and is
509
- refused.
616
+ - A replaced document is marked `status: deprecated` with
617
+ `ksor.deprecated: { by, at }` (a takedown authority, or the owner an
618
+ `ownership:` rule in `.ksor/governance.yaml` resolves `ksor.owner` on the
619
+ document is not that owner, and this record's policy declares no rule yet) and
620
+ `ksor.superseded_by:` naming its successor by id (`policies/refunds-v2`)
621
+ deprecated documents are never deleted. The successor must exist, be
622
+ `stable`, and be readable by every reader of this document, or the pointer
623
+ strands them (`ksor-supersession-strands`).
510
624
  - Images and assets live in `knowledge/` beside the document that uses them,
511
625
  referenced by relative links. A relative link must never leave `knowledge/`.
512
626
  - **Study attachments.** A document may carry four optional companions named
@@ -522,8 +636,9 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
522
636
 
523
637
  An attachment is **part of its document**, not a document. It has no URL of
524
638
  its own, no sidebar row, no line in `llms.txt`, and no identity an agent can
525
- cite — so it carries **no frontmatter at all** (the checker refuses any), and
526
- it takes its `visibility:` and any takedown from its parent. Restrict the
639
+ cite — so a summary's frontmatter is exactly `type: Summary` and nothing
640
+ else (the checker refuses any other key), and every attachment takes its
641
+ audience and any takedown from its parent. Restrict the
527
642
  document and its summary and deck go with it; there is no way to publish a
528
643
  summary more widely than the document it summarises. An attachment whose
529
644
  document is missing is refused, by `pnpm check` and by `pnpm build` alike.
@@ -584,9 +699,9 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
584
699
  from at random and offers another round; a smaller one is simply asked in
585
700
  full.
586
701
 
587
- **`pnpm check` and `pnpm build` both refuse a quiz a reader could pass
588
- without reading**, and this is worth knowing before you write one, because
589
- it is easy to trip by accident:
702
+ **`pnpm build` refuses a quiz a reader could pass without reading** (the
703
+ audit runs in the site build, not in `pnpm check`), and this is worth knowing
704
+ before you write one, because it is easy to trip by accident:
590
705
 
591
706
  | Refusal | Means |
592
707
  | -------------------------- | ----------------------------------------------------- |
@@ -718,14 +833,19 @@ SAMEORIGIN`, which forbids any other site from framing them, and a browser
718
833
  enforces that whatever this record does. Check before you rely on one.
719
834
 
720
835
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
721
- source, and name the source in `provenance`.
836
+ source, name the source in `sources`, and cite it from the claim with a GFM
837
+ footnote whose label is that source's `id`.
722
838
 
723
839
  ### Structuring the record
724
840
 
725
- - A folder per topic; its front page is `<folder>/index.md`, and the folder
726
- takes the position that page declares.
841
+ - A folder per topic. Its `index.md` is GENERATED by `ksor build` — an OKF §8
842
+ map of the folder so a folder's own introduction is a named document
843
+ inside it, such as `overview.md`, and the folder sorts where its
844
+ lowest-`order:` document does.
727
845
  - Sidebar position is the governed `order:` key: documents that declare it come
728
- first, ascending; the rest follow in name order.
846
+ first, ascending; the rest follow in name order. Folders sit in that SAME
847
+ list, not after it — so a folder can sit between two documents, and where it
848
+ sits is decided by the `order:` of the documents inside it.
729
849
  - One order drives every surface — the sidebar, `llms.txt`, the home page's
730
850
  first-document link, and the MCP `outline` tool an agent reads to decide what
731
851
  to read first. Set it once and they agree. The door picks up a reorder at the
@@ -740,8 +860,9 @@ SAMEORIGIN`, which forbids any other site from framing them, and a browser
740
860
  - `.agents/skills/add-sources/` — turn source material (documents, pages,
741
861
  notes) into governed knowledge.
742
862
  - `.agents/skills/make-slides/` — generate a presentation from one document
743
- - `.agents/skills/make-summary/` — write a document's summary and attach it
744
863
  and attach it, so it renders on that document's page.
864
+ - `.agents/skills/make-summary/` — write a document's summary and attach it,
865
+ so it renders as a second tab on that document's page.
745
866
  - `.agents/skills/format-checker/` — the rules above, as a program;
746
867
  `pnpm check` runs it and its errors explain how to fix themselves.
747
868
 
@@ -749,7 +870,7 @@ SAMEORIGIN`, which forbids any other site from framing them, and a browser
749
870
 
750
871
  You own `system/site/` outright — these are the seams, cheapest first:
751
872
 
752
- - **Display title** — `instance.md`'s body `# H1` (the intake interview
873
+ - **Display title** — `instance.md`'s `title:` key (the intake interview
753
874
  writes it). Headline, navbar, and browser title follow on restart.
754
875
  - **Accent color** — the one brand pair in `system/site/app/global.css`
755
876
  (`--primary` and `--primary-foreground`, light and dark); every accented
@@ -8,7 +8,11 @@
8
8
  #
9
9
  # Build and run it anywhere:
10
10
  # docker build -t my-record .
11
- # docker run --rm -p 8080:80 --env-file .env my-record
11
+ # docker run --rm -p 8080:80 --env-file .env -e KSOR_AUTH=disabled-public my-record
12
+ #
13
+ # The -e is not optional. ENV PORT below makes this a PUBLIC bind (0.0.0.0), and
14
+ # the KSOR_AUTH=disabled-local a local .env carries refuses there on purpose —
15
+ # see the CMD comment. Keep the deliberate value on the command, not in .env.
12
16
  #
13
17
  # This image serves; it does not publish. `ksor ingest` is a write plane that
14
18
  # runs from CI or your machine against the same database — see the deployment